频率分析器 V2.0 - 你是否曾想过是什么让音调听起来和谐一致?






4.91/5 (20投票s)
这款图形化应用程序可以播放和显示频率,并将它们混合在一起,以便你分析那些我们称之为“和谐”的声音到底是怎么回事。它处理的是一个基本的DFT版本,将向我们展示哪些频率在其中起作用。它最初是用纯C语言编写的。
引言
振动,我们无时无刻不被它们包围,并且我们不断地制造它们,通过空气的振动来交流。在音乐中,我们有一个标准,即当音调和谐一致时它们应该如何发声。我们所能做的就是听声音,看看它们是否和谐,但为什么呢?如果我们能看到听到的声音,那会更容易;我们需要将这些音调可视化,以便更容易分析它们。正弦波是计算可视化对象的良好起点。做到这一点最好的方法是使用几乎每个人家里都有的通用机器——电脑。
为了分析为什么声波听起来对我们的耳朵来说悦耳,我们需要能够至少选取两个声音并将它们可视化。这样我们就可以看到我们听到的,并看到这些声波中的共同模式。目前已知的是,我们今天使用的音阶系统建立在“二的十二次方”的公式上,而唯一真正的和谐声音是那些频率值减半或翻倍的八度音。市场上已有分析声波的软件,但这些程序的目的不是分析和学习声音为什么会和谐一致。相反,它们更多地用于录制音乐,并通过使用滤波器使声波听起来更好。这类软件会提供比你这里所需更多的功能。对于分析来说,开发一款只用于此单一任务的新软件会比较好。如前所述,已有的软件价格都不菲,不适合仅用于此目的。一个 otherwise 好的软件是来自 Synthrillium 的 Cool Edit(Adobe audition),它们有一个免费试用版,但对于在此级别进行所需的声波分析来说仍然不够实用。
有很多人已经尝试过制作这个,但他们也没有完全为这个目的而精确地制作。这里 Code Project 网站上就有一个例子。这个例子接收一个输入信号并将其转换为均衡器,显示从输入信号中提取并由 FFT(快速傅里叶变换)转换的频率。我在这篇文章中描述的软件基于一个假设:声波在时间尺度上相互干涉的程度越高,听起来就越好。为了证伪或证实这个假设,我们需要一个工具并寻找一些理论。这个项目基于这样的问题:当看不到音调之间的模式时,为什么 C 就是 C,为什么 E 就是 E,以及为什么它们听起来和谐一致。我在研究中发现,存在一个常数乘以给定音调的频率,就可以得到音阶中的下一个音调。这个常数,如前所述,源自“二的十二次方”公式,其中十二是八度音中的音调数量,而二是一半八度音。以 440 Hz 的 A 作为音阶的起始位置标准音。在这里,我们可以通过乘以或除以一个已知音调来实现这个常数,以获得半音阶上行或下行的下一个音调。这个项目以及本文档中包含的示例代码都是用 C 语言编写的。我还为 Visual Studio C++ .NET 2003 和 2005 添加了各自的项目。此应用程序的 2.0 版本有一个 `Play` 函数,而第一个版本没有。
使用代码
该软件是用 DevC++ (BloodShed) 环境以 C 语法开发的,具有 Win32 API 外观。没有菜单,只有基于用户在屏幕区域点击位置的鼠标事件。下图显示了演示版本的截图;箭头将指出其含义。我还为 VS 2003 和 2005 制作了一个版本。
- 标题栏显示鼠标指针在视图中的位置(以秒为单位)。它还将显示用户通过反复单击鼠标左键所选择的时间间隔,这样会出现两个标记,一个红色,一个绿色。
- 这里是单独的声波叠加在彼此之上。用不同的颜色可视化,以便于跟踪单个波形。
- 标尺为用户提供时间测量,使人容易理解你在什么时间点。
- 用户选择的所有声波都混合在一起并显示在这里。
- 用户在此处用鼠标左键单击,将视图向右滚动约 0.0625 秒,如果用户单击左侧,则向左滚动。如果用户单击鼠标右键,则会滚动约 0.5 秒。
- 缩放条,可从 0 到 100 缩放,其中 50 是正常启动视图的一半。
- 音调板用于选择要分析的音调。通过在音调板左上角的紫色框中单击鼠标右键,该音调将静音,并停在紫色框下方。有 3 个八度音程可供选择并连接到每个音调。
- 音调板上要编辑的声波选择。如本文档前面所述,最多可处理四个音调。
- 频率域,将向用户显示混合声波包含哪些频率。
请记住 - 如果用户在声波视图中单击鼠标右键,视图将滚动到该点。这使得分析特定时间点变得容易。在较新的 2.0 版本中还有一个“播放”按钮,它将播放混合声音,以便你可以听到音调的混合效果。
混合声波
为了计算声波,有一个函数
double CalculationZenit(double i, double step, double fq, int type)
{
if(type==0) return -sin(( i + ( step * HUNDREDS ) * fq ) * DEGREES);
else if(type==1)
return -sin(((i*fq) + ( step * HUNDREDS ) * fq ) * DEGREES);
else if(type==2)
return sin(i*(int)fq * DEGREES);
return 0;
}
此函数接收时间值(`i`)、滚动步长(`step`)、频率(`fq`),并根据变量(`type`)中检索到的内容返回一个值。`HUNDREDS` 是一个重新计算到百分之一秒的定义。为了混合单独的波形,我们使用这个算法
for(i=0;i<(DEG_SEC*2)/zoom;i=i+(1/zoom/calib))
{
double temp1 = CalculationZenit(i,step,wH1.freq,1);
double temp2 = CalculationZenit(i,step,wH2.freq,1);
double temp3 = CalculationZenit(i,step,wH3.freq,1);
double temp4 = CalculationZenit(i,step,wH4.freq,1);
wHM.rY = temp4 + temp3 + temp2 + temp1; wHM.rX = i*zoom;
WavePainter (dc, &wHM, RGB(250, 200, 120));
}
这将调用 `WavePainter`,它会在屏幕上绘制混合效果。
void WavePainter(HDC dc, struct WaveHolder *wH, COLORREF rgb)
{
//Check if we need to paint pixel, if we moved one pixel side up or ...
if((int)wH->rX!=wH->pX||(int)(wH->amp * wH->rY)!=wH->pY)
{
wH->pX = (int)(wH->rX);
wH->pY = (int)(wH->amp * wH->rY);
SetPixel (dc, wH->sX + wH->rX, wH->sY + wH->amp * wH->rY, rgb );
}
}
此算法从 0 到 2 秒的时间循环,计算每个声波的差值,将值相加,然后通过 `SetPixel` 命令将其显示在屏幕上。`wHM` 是一个名为 `WaveHolder` 的结构,它保存有关声波的信息。
DFT - 离散傅里叶变换
以下函数是 DFT,它基于用于信号处理的离散傅里叶变换公式,该公式可以找到波形中的所有频率,其公式如下:
int DFT ( int lenght, double *input, double *output)
{
long i,ii = 1;
if (NULL==input)
return ( FALSE );
for ( i = 1; i < lenght; i++)
{
for(ii = 1; ii < lenght; ii++)
{
output[i]+= (input[ii]*-sin(i*ii*2*PI/lenght))/length;
}
}
return ( TRUE );
}
从 DFT 公式中剥离出来,但仍然可以分割声波。它接收要处理的数据的长度和包含该数据的输入缓冲区。它还接收一个输出缓冲区,频率域将存储在该缓冲区中。它的工作方式是,以变量 `i` 为步长 1 遍历整个数据缓冲区,然后以变量 `ii` 为步长 1 在嵌套循环中再次遍历。这里,当 `i` 为 1 且 `ii` 也为 1 时,它会将输入缓冲区字段 1 的内容相加,并乘以输入缓冲区的负正弦(i*ii*2*PI/length),然后将此总和再次除以长度。因此,输入值乘以一个完整旋转的负正弦除以长度,完成后,这就会给我们输出缓冲区中的频率域。我们将所有内容除以长度值以将其归一化到可接受的长度。如果我们仔细查看算法,我们会看到 `i` 乘以 `ii`,这在步进时会给我们以下值:
i,ii |
ii = 1 |
ii = 2 |
ii = 3 |
ii = 4 |
i = 1 |
1 |
2 |
3 |
4 |
i = 2 |
2 |
4 |
6 |
8 |
i = 3 |
3 |
6 |
9 |
12 |
I = 4 |
4 |
8 |
12 |
16 |
现在如果我们加上负正弦函数内的其余值,我们将得到
i,ii |
ii = 1 |
ii = 2 |
ii = 3 |
ii = 4 |
i = 1 |
6.28 / length |
12.57/ length |
18.85 / length |
25.13/ length |
i = 2 |
12.57 / length |
25.13/ length |
37.71 / length |
50.27/ length |
i = 3 |
18.85 / length |
37.71/ length |
56.55 / length |
75.41/ length |
i = 4 |
25.13 / length |
50.27/ length |
75.41 / length |
100.50/length |
我们必须计算 length 的值;在这个项目中,这个值设置为 360,对我们来说,这意味着时间尺度上的 1 秒。如果我们假设 length 是 4 并重新计算先前的值,我们将得到
i,ii |
ii = 1 |
ii = 2 |
ii = 3 |
ii = 4 |
i = 1 |
1.57 |
3.1425 |
4.7125 |
6.2825 |
i = 2 |
3.1425 |
6.2825 |
9.4275 |
12.5675 |
i = 3 |
4.7125 |
9.4275 |
14.1375 |
18.8525 |
i = 4 |
6.2825 |
12.5675 |
18.8525 |
25.125 |
如果我们使用这些值,我们可以根据选择的采样率创建最快的频率。根据奈奎斯特定理,频率只能是采样率/2;4 Hz 采样率的一半,在我们的情况下结果是 2 Hz。当我们步进时,第一个值为 1,第二个值为 -1,第三个值为 1,依此类推。这是 2 Hz 的频率。我们这样做是为了缩小规模,使其能够计算和观察这些值会发生什么。
i,ii |
ii = 1 |
ii = 2 |
ii = 3 |
ii = 4 |
i = 1 |
-0.25 |
-0.000227 |
0.25 |
-0.000171 |
i = 2 |
0.000227 |
-0.000171 |
0.000681 |
0.000282 |
i = 3 |
0.25 |
-0.000681 |
-0.25 |
0.000736 |
i = 4 |
0.000171 |
0.000282 |
-0.000736 |
-0.001935 |
现在如果我们把每一行加起来,我们将在输出数组的每个字段中得到最终结果,这将是
i |
|
i = 1 |
-0.000398 |
i = 2 |
0.001019 |
i = 3 |
0.000055 |
i = 4 |
-0.001935 |
由于奈奎斯特定理,这个结果的一半是必需的。当 `i` 为 1 时,我们有 1 Hz 的值。当 `i` 为 2 时,我们有最大可能频率 2 Hz。另外,请注意,当 `i` 为 1 时,值为负;当 `i` 为 2 时,值为正,这意味着 2 Hz 是占主导地位的。这证明并确定了我们创建的波形是由 2 Hz 频率组成的。`i` 值周围显示的值越高,该点周围的赫兹就越占主导地位。我的精简版 DFT 就是这样工作的,并且是一种快速简便的分析频率的方法。关于代码没有太多要解释的了。其余函数在附件的伪代码中进行了解释。它们可以在本文档的末尾,参考文献部分之前找到。
播放功能
这里是我们为 2.0 版本实现的一个函数,它可以创建所选频率的混合并播放它们。
int PlayIt(struct WaveHolder *wH1,struct WaveHolder *wH2,
struct WaveHolder *wH3,struct WaveHolder *wH4)
{
HWAVEOUT hWOut; //Handle for sound card
WAVEHDR WHeader;//WAVE header
WAVEFORMATEX WFormat;//The wave format
char info<BUFFERSIZE>; //Sound data
HANDLE init_done;
double x1; //Waves ...
double x2;
double x3;
double x4;
//Set format
WFormat.wFormatTag = WAVE_FORMAT_PCM; //Uncompressed
WFormat.nChannels = 1; //1=Mono 2=Stereo
WFormat.wBitsPerSample = 8; //8 Bits per sample
WFormat.nSamplesPerSec = 44100; //Sample per sec
WFormat.nBlockAlign = WFormat.nChannels * WFormat.wBitsPerSample / 8;
WFormat.nAvgBytesPerSec = WFormat.nSamplesPerSec * WFormat.nBlockAlign;
WFormat.cbSize = 0;
init_done = CreateEvent (0, FALSE, FALSE, 0);
if (waveOutOpen(&hWOut,0,&WFormat,(DWORD) init_done, 0,CALLBACK_EVENT) != MMSYSERR_NOERROR)
return 0;
double mix;
//Create the mix!
for(int i=0;i<BUFFERSIZE; i++)
{
x1 = sin(i*2.0*PI*((wH1->freq)*OCTAS)/(double)WFormat.nSamplesPerSec);
x2 = sin(i*2.0*PI*((wH2->freq)*OCTAS)/(double)WFormat.nSamplesPerSec);
x3 = sin(i*2.0*PI*((wH3->freq)*OCTAS)/(double)WFormat.nSamplesPerSec);
x4= sin(i*2.0*PI*((wH4->freq)*OCTAS)/(double)WFormat.nSamplesPerSec);
mix = 128+((x1+x2+x3+x4)*30); //Mix them!
info[i] = (char)mix;
}
//Put header and sound data together
WHeader.dwFlags=0;
WHeader.lpData=info;
WHeader.dwBufferLength=BUFFERSIZE;
WHeader.dwFlags=0;
if (waveOutPrepareHeader(hWOut,&WHeader,sizeof(WHeader))!= MMSYSERR_NOERROR)
return 0;
ResetEvent(init_done);
if (waveOutWrite(hWOut,&WHeader,sizeof(WHeader)) != MMSYSERR_NOERROR)
return 0; //Can't write to card!
if (WaitForSingleObject(init_done,INFINITE) != WAIT_OBJECT_0)
return 0; //Could not wait anymore!
//Disconnect.
if (waveOutUnprepareHeader(hWOut,&WHeader,sizeof(WHeader))!= MMSYSERR_NOERROR)
return 0;
//Shut down device.
if (waveOutClose(hWOut) != MMSYSERR_NOERROR)
return 0;
//Close handle!
CloseHandle(init_done);
}
该项目主要在 3.2 GHz 英特尔奔腾 4 处理器上运行进行测试,但也可以在 Windows 2000 和 NT 等其他环境中运行。所有测试均在安装了 Windows 操作系统的 PC 上进行。C 语言的 BloodShed 版本使用约 2 MB 的 RAM 内存和大约 400 KB 的磁盘空间。大部分 RAM 使用量是由图形造成的,图形占用了 372 KB 的磁盘空间。`SetPixel` GDI 函数在被调用时会占用机器时间,使用此函数的算法必须重新排列/优化,以便我们可以在绘制更高频率时减少此时间。
关注点
如果这个软件有下一个版本,代码结构会更好,使用更多的结构和函数。其中一个发现是,在没有恒定问题的情况下,要制作一个如此规模的软件是很困难的,即在修改一小部分代码时需要更改整个代码。当涉及到使用该软件时,测试对象必须投入时间和精力。如果完整的混音在时间上有节奏感,它会显示出比与之相反的声音更和谐的结果。至于软件本身,目前可能存在一些内存泄漏,但这些问题将在完成之前得到解决。
关于这一点最令人鼓舞的是,你可以快速更改频率,这在分析时节省了时间。本项目中的傅里叶变换在这里有点不必要,因为它没有被充分利用,频率从一开始就已经知道了。但是,当出现加载功能允许用户分析 `*.wav` 文件时,这是一个可以实现和开发的函数。该项目作为学习工具最有用,可以快速分析你在吉他或钢琴等乐器上演奏的音调。
此版本在同时分析的音调数量方面功能有限;在吉他合奏中,如果吉他有 6 根弦,您可以使用多达 6 个音调。在这里,您只能处理 4 个音调,但无论如何,如果屏幕上绘制了 4 个以上的音调,看起来都会像迷宫。另一件事要提的是,该软件需要一个带有滚轮按钮的三键鼠标才能工作;滚轮用于放大或缩小声波。该软件的目的在能够可视化声音的这一点上已经实现,但您应该亲自尝试一下,以找出它是否按照预期工作。
附录
这是该项目的伪代码,从结构开始。
STRUCTURE CLICKER
s1Marked;
s1Tone;
s2Marked;
s2Tone;
s3Marked;
s3Tone;
s4Marked;
s4Tone;
STRUCTURE – END
STRUCTURE WAVEHOLDER
freq;
amp;
sX;
sY;
rX;
rY;
pX;
pY;
STRUCTURE – END
FUNCTION INITWINDOW
Input variables - hInstace
Local variables - hwnd, wincl.
SET - A winddowclass
IF - it fails
RETURN - 0
IF - END
CREATE – A window.
RETURN – hwnd.
FUNCTION – END
FUNCTION STEERINGMOTOR
Input variables – hwnd, msg,zoom,step,x,y,cSTRUCTURE clicko,done
Local variables – r,sender
IF – mouse move
Calculate and transform mouse coordinates into time.
DISPLAY – coordinates in titelbar.
IF – END
IF – mousewheel
Transform wheel delta into zoom value.
END _ IF
IF – left mouse button down
IF – mouse pointer is within the left scroll area.
DECREASE – variable step by 6.25.
ELSE – IF – mouse button is within the right scroll area.
INCREASE – variable step by 6.25
ELSE – IF – mouse pointer is within first frequency button.
SELECT – frequency or deselect.
ELSE – IF – mouse pointer is within second frequency button. I
SELECT – frequency or deselect.
ELSE – IF – mouse pointer is within third frequency button.
SELECT – frequency or deselect.
ELSE – IF – mouse pointer is within fourth frequency button.
SELECT – frequency or deselect.
ELSE - IF – END
IF – mouse is within the toneboard
SET – a new higheer position on the marker by one half tone step.
To indicate that a new tone has been given.
IF – END
IF – right mouse button down
IF – mouse pointer is within the left scroll area.
DECREASE – variable step by 50.0
ELSE – IF – mouse pointer is within the right scroll area.
INCREASE – variable step by 50.0
ELSE – IF –END
END – IF
IF – mouse pointer is within the scroll view
INCREASE – variable step as mutch as needed to make the view scroll
to the chosen point.
END – IF
IF – mouse is within tone board area
SET – the tone board marker to a new lower tone by a half tone step.
IF – END
FUNCTION – END
FUNCTION – PAINTRECT
Input variables – dc,x1,y1,x2,y2,rgb
Local variables – old,br
CREATE – create a brush with the color stored in the rgb variable.
DISPLAY – The box on the screen at given cordinates.
FUNCTION - END
FUNCTION WAVEPAINTER
Input variable – dc,STRUCTURE wH, rgb
IF – it's a new point, then it´s time to paint.
DISPALY – pixel on the screen.
IF – END
FUNCTION – END
FUNCTION PAINTRULER
Input variable – dc, zoom,step,discount
Local variable – meter,o
LOOP – While o is less than 2 seconds of time.
IF – One second of time has elapsed.
DISPLAY – a line over this point in vertical direction.
IF - END
LOOP – END
FUNCTION – END
FUNCTION – PAINTGRAPHIX
Input variable – hwnd,zoom,step,fout,foutimg,done,discount,STRUCTURE clicko
Local variable – dc,ps, wH1,wH2,wH3,wH4.wHM,o,i,time,c0,calib
SET – wH1 to wH4 by a tone frequency and amplitude.
IF – a frequency is unselected.
SET – this frequency to 0. II
IF – END
SET – the highest frequency to the variable calib.
LOOP – trough 2 seconds of time.
FUNCTION CALL – Call the CALCULATIONZENTIT function.
SET – wH1 to wH4 whit the return value from previous function call.
DISPLAY – all four waveforms.
LOOP – END
LOOP – trough 2 seconds of time.
FUNCTION CALL – Call the CALCULATIONZENIT function.
CALCULATE – the complete mix by the four wasveforms.
DISPALY – te mixdown of waves.
LOOP – END
IF – if this is not yet done.
SET – variable done to 1 to indicate it's done.
LOOP – one half second of time.
SET – DFT field with wave info.
LOOP – END
IF – END
DISPLAY – all bitmap graphics and frequency domain, and the ruler.
FUNCITON – END
FUNCTION – TONEBOARDMARKER
Input variables – dc,clicko
Local variables – x,y,temp,cR
DISPLAY – the four frequency buttons and the tone board marker.
FUNCTION – END
FUNCTION – DFT
Input variable – length,input,output
Local variable – i,ii
IF – input field is NULL
RETURN – FALSE
IF – END
LOOP – from 1 to variable length by the step of one in variable i.
LOOP - from 1 to variable length by the step of one in variable ii.
CALCULATE – the DFT values in the output buffer.
LOOP – END
RETURN – the value TRUE.
LOOP – END
FUNCTION – END
FUNCTION – CACULATIONZENIT
Input variables – i,step,fq,type
IF – type is 0
RETURN - -sin((i+(step*HUNDREDS)*fq)*DEGREES)
IF – END
IF type is 1
RETURN - -sin(((i*fq)+(step*HUNDREDS)*fq)*DEGREES)
IF – END
IF – type is 2
RETURN – sin (i*(int)fq*DEGREES)
IF – END
RETURN – 0
FUNCTION – END
Input variables – hBitmap
LOAD – a bitmap and store it.
RETURN – hBitmap
FUNCTION – END
FUNCTION – PAINTBITMAP
Input variables – dc,hBitmap,x1,y1,width,height
DISPLAY – bitmap on the screen.
FUNCTION – END
参考文献
参考作品
- Bilting, Skansholm "Vagen till C" – ISBN: 91-44-01460-6
- Kochan G. S. "Programming in C" – 第三版 – ISBN: 0-672-32666-3
- LaMothe, André "Tricks of the windows programming gurus" – ISBN: 0-672-31361-8
- Svardstrom, Anders "Tillampad signalanalys" – ISBN: 91-44-25391-5
链接
- Adobe Audition 2.0。
- Fred Ackers 编写的 FFT of waveIn audio signals。
- 由 Paul Bourke 于 1993 年 6 月撰写。
- Paul Guy 著《A = 440Hz - men varfor?》
- Catherine Schmidt-Jones 著《半音和全音》
- 抗锯齿线绘制
- C 编程语言。
历史
- 版本 1.0:上传于?2007 年 11 月 - 版本一,没有 `Play` 函数。
- 版本 2.0:上传于 2007 年 11 月 26 日 - 实现了一个 `Play` 函数。
- 版本 *:更新于 2007 年 11 月 30 日 - 对文章进行了一些修改。
许可证
本软件按“原样”提供,不作任何明示或暗示的保证。在任何情况下,作者均不对因使用本软件而引起的任何损害负责。允许任何人出于学习目的使用本软件。并且您绝不能声称它是您的。