65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (20投票s)

2007年11月23日

CPOL

14分钟阅读

viewsIcon

72724

downloadIcon

8461

这款图形化应用程序可以播放和显示频率,并将它们混合在一起,以便你分析那些我们称之为“和谐”的声音到底是怎么回事。它处理的是一个基本的DFT版本,将向我们展示哪些频率在其中起作用。它最初是用纯C语言编写的。

Screenshot - screen1.gif

引言

振动,我们无时无刻不被它们包围,并且我们不断地制造它们,通过空气的振动来交流。在音乐中,我们有一个标准,即当音调和谐一致时它们应该如何发声。我们所能做的就是听声音,看看它们是否和谐,但为什么呢?如果我们能看到听到的声音,那会更容易;我们需要将这些音调可视化,以便更容易分析它们。正弦波是计算可视化对象的良好起点。做到这一点最好的方法是使用几乎每个人家里都有的通用机器——电脑。

为了分析为什么声波听起来对我们的耳朵来说悦耳,我们需要能够至少选取两个声音并将它们可视化。这样我们就可以看到我们听到的,并看到这些声波中的共同模式。目前已知的是,我们今天使用的音阶系统建立在“二的十二次方”的公式上,而唯一真正的和谐声音是那些频率值减半或翻倍的八度音。市场上已有分析声波的软件,但这些程序的目的不是分析和学习声音为什么会和谐一致。相反,它们更多地用于录制音乐,并通过使用滤波器使声波听起来更好。这类软件会提供比你这里所需更多的功能。对于分析来说,开发一款只用于此单一任务的新软件会比较好。如前所述,已有的软件价格都不菲,不适合仅用于此目的。一个 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 制作了一个版本。

Screenshot - help1.gif

  1. 标题栏显示鼠标指针在视图中的位置(以秒为单位)。它还将显示用户通过反复单击鼠标左键所选择的时间间隔,这样会出现两个标记,一个红色,一个绿色。
  2. 这里是单独的声波叠加在彼此之上。用不同的颜色可视化,以便于跟踪单个波形。
  3. 标尺为用户提供时间测量,使人容易理解你在什么时间点。
  4. 用户选择的所有声波都混合在一起并显示在这里。
  5. 用户在此处用鼠标左键单击,将视图向右滚动约 0.0625 秒,如果用户单击左侧,则向左滚动。如果用户单击鼠标右键,则会滚动约 0.5 秒。
  6. 缩放条,可从 0 到 100 缩放,其中 50 是正常启动视图的一半。
  7. 音调板用于选择要分析的音调。通过在音调板左上角的紫色框中单击鼠标右键,该音调将静音,并停在紫色框下方。有 3 个八度音程可供选择并连接到每个音调。
  8. 音调板上要编辑的声波选择。如本文档前面所述,最多可处理四个音调。
  9. 频率域,将向用户显示混合声波包含哪些频率。

请记住 - 如果用户在声波视图中单击鼠标右键,视图将滚动到该点。这使得分析特定时间点变得容易。在较新的 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

链接

返回顶部?

历史

  • 版本 1.0:上传于?2007 年 11 月 - 版本一,没有 `Play` 函数。
  • 版本 2.0:上传于 2007 年 11 月 26 日 - 实现了一个 `Play` 函数。
  • 版本 *:更新于 2007 年 11 月 30 日 - 对文章进行了一些修改。

返回顶部?

许可证

本软件按“原样”提供,不作任何明示或暗示的保证。在任何情况下,作者均不对因使用本软件而引起的任何损害负责。允许任何人出于学习目的使用本软件。并且您绝不能声称它是您的。

© . All rights reserved.