【工程与计算机】编程X艺术:听歌识曲

近年来,美国大学的计算机(Computer Science, CS)和电子工程(Electrical Engineering, EE)专业录取要求随着申请人数的增加逐渐水涨船高。但另一方面,国内的相关工程教育却基本是缺位的。为了弥补这种差距,陈欣老师在过去的几年里,通过我们的规划项目辅导了一批学员。通过身体力行的学习利用专业知识解决生活中的问题,他们走出了自己的舒适区。事实证明,学校对这份努力也是相当认可的。

为了帮助到更多的人,陈欣老师在此整理一些往年的成功项目经验,并同简化过的项目源代码一道与大家分享。希望能够抛砖引玉,给同学们一些思路和帮助,为之后的申请助一臂之力。毕竟短期看,CS/EE是在美国本土就业的捷径,能够较为迅速的收回教育投资;长远看,CS/EE的相关技能也能在诸多行业里起到点石成金之效。

问题

AADPS的一位学员恰巧正在同时自学吉他和编程。作为一名初学者,他遇到的一个很大的困难就是很多网上的视频教程没有对应的曲谱。在一番思索之后,他觉得可以利用编程知识,为自己开发一个趁手的专业工具,通过分析音乐音频的方式自动生成对应的曲谱。

工具

Java

Java是一种基于类的面向对象通用编程语言。藉由虚拟机技术,编译好的Java程序可以直接在不同的计算机平台上运行。目前Java是最流行的编程语言,是几乎所有类型的网络应用程序的基础,也是开发和提供嵌入式和移动应用程序、游戏、基于Web的内容和企业软件的全球标准。

TarsosDSP

TarsosDSP是一个用于音频处理的Java库,其目标是为实用的音频处理算法提供一个易于使用的纯Java接口并免去对第三方包的依赖。在设计上,TarsosDSP力图在有能力完成实际工作和足够简明以演示数字信号处理(digital signal processing, DSP)算法之间达到一个平衡。内置的算法包括打击乐音符起始检测器(percussion onset detector)、一系列音高分析算法、Goertzel双音多频解码算法(Goertzel DTMF decoding algorithm)、时域拉伸算法(time stretch algorithm)、重采样、滤波器、简单合成器、音效和变调算法。

JFreeChart

JFreeChart是一个纯Java绘图库,让程序员可以在他们的应用中嵌入专业级的图形。绘图库有一致且被充分说明的接口,支持数十种不同的图表类型,以及输出到应用程序界面、图形文件和矢量图文件的能力。

Eclipse


Eclipse是日前最流行的Java集成开发环境(integrated development environment),由一个基本的工作空间和一个发达的可定制插件系统组成。Eclipse本身也由Java编写,其前身由IBM公司的团队开发,但很快就成为了一个完全开源的项目,由非营利的Eclipse基金会主导。对于本项目,可以使用Eclipse标准版

原理

线程

线程(thread)是操作系统能够进行运算调度的最小单位。通俗的说,我们在计算机上执行的每一个程序,称为一个进程(process),而进程的实际执行则会派生出至少一个线程。同一进程中的多条线程将共享该进程中的全部系统资源,但线程会有自己的调用栈、寄存器环境和本地存储。

对于目的比较简单的程序,单线程模型是完全没有任何问题的。但是对于进行实时多媒体处理的程序,或是需要与用户进行直接且复杂交互的程序,多线程的设计模式就极有必要了。举一个最简单的例子,如果把在屏幕上不断刷新数据的操作和从文件系统里读取数据的操作放到一个循环里,一旦文件系统由于负载较大或者文件被占用的情况而无法及时完成读取,数据的更新也会一并中断。因此较为合理的思路是把这两类操作放在各自的线程里,在正确设计的前提下,它们就能并行不悖的完成各自既定的使命,不让用户有程序失灵的感觉。对于早些年单CPU的情形,不同的线程将轮流分配到CPU的计算时间。但对于现在基本已经普及的多核CPU或者实现复数逻辑处理器的Intel超线程(hyper-threading)技术,不同的线程是可以同时并行运行的。这也是单个程序能充分利用多核CPU计算资源的主要途径。

Java线程


上图展示了一个Java线程的生命周期(life cycle)。在我们自己的程序里,用UpdatePlot这个子类实现了Runnable这个接口。具体而言,唯一需要重写实现的函数是public void run(),即是线程所实际执行的内容。之后在程序实际运行时,则用UpdatePlot的一个实例myPlot来启动线程new Thread(myPlot).start()。注意在设计中非常重要的一点是,对于线程中持续不断的循环或者耗时较长的操作,一般需要定期调用Thread.sleep(time)来将本线程暂时中断。此时操作系统即能将线程挂起,把CPU资源释放给其他需要执行的线程。如果没有加入中断操作或在此处有一些不理想的设计,很容易造成线程占用全部CPU核的情况,但这在大部分时候是没有必要的。

对于Java本身来说,设计上是不推荐直接从外部终止线程,而会希望run()函数执行完既定工作并自己正常结束。示例代码里通过myPlot来派生线程进行图表数据的定期更新。但事实上所使用的TarsosDSP库同样会内部派生用于音频处理和回放的线程,下面提到的Java图形界面库还会派生用于处理GUI事件的线程。

图形用户界面

顾名思义,图形用户界面(Graphical User Interface, GUI)是相对于早期终端命令行界面(Command Line Interface, CLI)而言更加友善的人机交互模式,也是现在个人电脑普及的最重要因素。图形用户界面可以让用户直观的通过鼠标甚至触屏与屏幕上的可视化元素互动。当然为实现这一目标,无论是在硬件上还是软件上都提出了更高的要求。

Swing

Swing是目前Java最主要的图形组件库(GUI widget toolkit),基于更早的awt(abstract window toolkit)之上、但纯粹由Java实现而非利用本地操作系统所提供的图形组件。这些特性让采用Swing的程序可以更加方便的跨平台运行,且系统里还内置了一些主题,让组件的风格可以与本地系统大致保持一致。

相对于其他一些采用标记语言的GUI解决方案(比如微软早年的MFC和新的WPF乃至UAP,以及安卓和苹果应用开发),Swing则采取了相对而言比较抽象、但不引入额外成本的基于组件嵌套的设计模式。一般来说,程序员不会精确设定每个组件的实际坐标,而是将这一任务托付给系统的布局管理器(layout manager)。以下是我们用到的一些重要Swing组件:

  • JFrame:通俗的说就是我们运行程序时见到的窗口。注意窗口本身其实是和程序的执行没有关联的,但在setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)以后,关闭该窗口也意味着终止程序。
  • JPane:是加载图形组件的容器,也可以嵌套子容器。对于复杂的布局,我们需要利用容器的组合来实现所期待的效果。其他还有JScrollPane、JSplitPane、JTabbedPane等来实现滚动条、分屏、标签页等效果。
  • BorderLayout:因为我们的窗口相对简单,所以只用到了这个最基本的布局管理器。可以把组件/容器安插到布局管理器预制的各个槽位里。在程序运行时,布局管理器将会根据窗口的实际大小来调整各组件/容器的大小。其他的布局管理器还有BoxLayout、GridLayout、FlowLayout等,下图展示了它们的效果。
  • JRadioButton:单选按钮。在程序里组合起来用于选择输入音源。
  • JCheckBox:复选框。在程序里用于确认是否回放。

当然,我们的图表本身也会生成ChartPanel来插入到上级的JPane里。

事件与监听器

之前已经大致说明了如何用图形组件组合成为应用的用户界面。不过用户界面的最重要一点就是响应用户输入的能力。事实上,相当大一部分Swing组件是可以接受事件(event),并注册动作监听器(ActionListener)来进行对应的处理。事实上awt本身就是一直在执行一个不间断的循环,不断获取用户的交互事件并执行对应的监听器。

对于组件内嵌在主类里的简单程序,直接用监听器一般就能执行期望的操作。但我们的应用里是把音源选择的部分单独封装成一个类。这个类和主类的组件间通讯是通过firePropertyChange和对应的PropertyChangeListener来实现的。

主音高检测

主音高检测其实是一个相对复杂的数学问题,在这边仅做一些定性的、概念上的说明。对于一般乐器实际的发音过程,是由和所演奏音符一致的基频以及一系列以基频为整数倍的倍频所组合而成。基频和倍频的独特组合比例从人的主观感受上来看就是乐器的音色。获取音乐的主音高,就是要在排除倍频和音乐中其他干扰的前提下,把这个基频的频率确认出来。

获取基频的主要方式有两大途径。对于频域(frequency domain)途径主要的思路是将波形进行傅里叶变换(Fourier transformation),直接分解成由不同频率简谐波所组成的频谱,再通过基频倍频之间的比例关系推算出基频。对于时域(temporal domain)途径主要的思路是利用自相关函数(autocorrelation function)提取出声音波形里有效的周期性信息作为基频。两类方法各自适用于不同的情形,在实际应用中也有将它们组合起来的做法。

我们当前采用了基于时域的McLeod法(McLeod Pitch Method, MPM)。比较有意思的地方在于归一化方差函数最大值阈值k的设定,以及方差函数计算的优化,有兴趣可以细读这篇论文

示例


上图展示了项目源码实际执行的场景。在选择对应的音源输入后,分析器就自动开始工作。对于笔记本一般可以选择内置的麦克风,而台式机可能需要外接麦克风或者带麦克风的摄像头。下面可以勾选是否回放麦克风的输入,如果只有外放而没有耳机的话,建议关闭这一选项。

窗口中部和底端的两张实时绘制的折线图分别展示音强(loudness)和主音高(pitch)随时间的变化,单位是分贝(dB)和赫兹(Hertz)。

思考

  1. 如何根据音强和主音高的数据生成曲谱?
  2. 在实际分析音乐时,数据会因为各方面的干扰和音乐本身的特性有一定的波动。如何在硬件和软件层面排除这些波动?
  3. 如何处理并正确分解和弦?
  4. 如何在分析一段音频后能迅速在一个曲库中找出对应的乐曲?

以上就是我们今天分享的案例,欢迎大家登陆网站后下载项目源码,通过实践来加深理解。简单的疑问可以评论在文章下,陈欣老师将会在有空时予以解答。

重要通知

本文为AADPS原创,原始发布地址是https://aadps.net/2022/11671.html。如发现其他自媒体盗用文章,欢迎粉丝告知或协助我们举报。

发表回复