在ATMEGA8上开发简单的音乐合成器

几年前,我在ATmega8微控制器上制作了一个闹钟,在那里我实现了一个单音(单声)简单旋律合成器。 Internet上有很多针对该主题的初学者文章。 通常,使用16位定时器来生成频率(音符),该频率以某种方式配置,从而迫使硬件级别在MC的特定引脚上以曲折形式发出信号。 第二个(8位)计时器用于实现音符或暂停的持续时间。 将根据众所周知的公式的音符与频率进行比较,然后将它们与某些16位数字进行比较,这些数字与指定计时器计数周期的频率成反比。

在我的设计中,我提供了三种旋律,它们以相同的调和音阶编写。 因此,我不得不使用有限数量的笔记,这使得建模更加容易。 此外,所有三个曲调都以相同的节奏演奏。 注释代码及其持续时间代码很容易装入一个字节。 该模型的唯一缺点是缺乏通用性,无法快速编辑,替换或补充旋律。 为了记录旋律,我首先在计算机上的音乐编辑器中对其进行草绘,然后复制音符及其时长,并事先确定其编号,然后形成结果字节。 我使用Excel程序进行了最后的操作。

将来,我想消除上述缺点,将设计具有一定的普遍性,并减少实施旋律的时间。 有一种想法,MK程序读取一种著名的音乐格式的字节。 最受欢迎和最常见的是MIDI格式。 更确切地说,这并不是一种可以在互联网上阅读的整体“科学”格式。 MIDI规范定义了用于通过相应的物理接口传输实时消息的协议,并描述了如何安排可存储这些消息的midi文件。 MIDI格式是面向音乐的,因此可以在相关领域找到应用。 这是声音设备,彩色音乐,音乐合成器和机器人等的同步控制。 在家庭领域,midi格式是在移动电话发展之初就遇到的。 在这种情况下,有关包含或禁用特定音符的消息,有关乐器的信息,音符的音量等信息都记录在midi文件中。 播放此类文件的手机包含一个合成器,该合成器实时解释此文件中的midi消息并播放旋律。 在最早的阶段,电话只能播放单音旋律。 随着时间的流逝,出现了所谓的复音。

在Internet上,我遇到了有关在MK上实现和弦合成器的文章,该文件可读取midi文件。 在这种情况下,至少对于存储在MK的存储器中的每种乐器使用预先形成的“波形表”(声波形的列表)。 在我的特殊情况下,我们将重点放在一个更简单的模型的实现上:单音(单声)合成器。

首先,我仔细研究了MIDI文件的结构,得出的结论是,除了有关音符的必要信息外,它还包含其他冗余信息。 因此,决定编写一个简单的程序将midi文件转换为其自己的格式。 该程序可以处理许多MIDI文件,不仅可以转换格式,还可以以某种方式组织它们。 事先,我决定在ROM存储器(EEPROM 24XX512)中组织许多乐曲的存储。 为了方便在HEX编辑器中进行可视化,我确保每个旋律均从该扇区的开头开始。 与SD卡(例如)不同,扇区的概念不适用于所用的ROM,因此我有条件地表达自己。 扇区大小为512字节。 并且ROM的第一扇区被保留用于每个旋律的开头的扇区的地址。 假定该旋律可以占用多个扇区。

当然,此处不对Midi文件格式进行完整描述。 我仅涉及最必要和必要的要点。 MIDI文件包含16个通道,通常通常对应于一个或另一个乐器。 在我们的情况下,它是哪种仪器都没有关系,只需要一个通道。 根据与在AVI容器中组织视频和音频流的存储非常相似的原理,每个通道的内容与标题一起被绘制在midi文件中。 我在我的一篇文章中较早地写了关于后者的文章。 MIDI文件头是一组一些参数。 这样的参数之一是时间分辨率。 它以每季度(tqq)的“刻度”(一种像素)的数量表示。 四分之一是演奏四分音符的时间跨度。 根据旋律的节奏,四分之一的持续时间可能会有所不同。 因此,一个“像素”的持续时间(采样周期)取决于速度和PPQN。 有关事件时间的所有信息均在此持续时间内准确确定。

此外,标题还包含MIDI文件的类型(类型0或类型1)和通道数。 无需赘述,我们将使用类型1(通道2的数量)。从逻辑上讲,具有单音旋律的midi文件包含一个通道。 但是在“类型1”的midi文件中,除了主要的文件之外,还有另一个“非音乐”通道,其中记录了不包含音符的其他信息。 这就是所谓的元数据。 也无需赘述。 我们唯一需要的信息是有关速度的信息,并且以不寻常的格式显示:每季度微秒。 将来,将展示如何与PPQN一起使用此信息来配置负责速度的MK计时器。

在带有笔记的主通道块中,我们仅对有关打开和关闭笔记的事件的信息感兴趣。 音符启用事件具有两个参数:音符编号和音量。 总共提供128个音符和128个音量级别。 我们只对第一个参数感兴趣,因为音符的音量无关紧要:播放MK旋律时,所有音符的音量都相同。 而且,当然,旋律不应包含“过度配音”的音符,也就是说,在任何时候,不应同时发出多个音符。 记下(打开)笔记的事件的代码为0x90。 注释事件代码为0x80。 但是,在将合成导出为Midi格式时,至少Cakewalk Pro Audio 9编辑器不会将事件与0x80代码一起使用。 而是在整个音乐声部发生0x90事件,并且音符关闭的音符为零音量。 即,“关闭音符”事件等同于“以零音量打开音符”事件。 也许这样做是出于经济原因。 根据规范,如果重复此事件,则无法重写事件代码。 在事件之间,有关时间间隔的信息以可变长度格式记录。 这些是上面提到的“滴答声”数量的整数值。 通常,一个字节足以记录时间间隔。 如果两个事件接连发生,那么它们之间的时间间隔显然等于零。 例如,如果它们之间没有停顿(空格),则会禁用第一个音符和紧随其后的第二个音符。

让我们尝试使用“ Cakewalk Pro Audio 9”程序编写一系列音符。 有很多编辑,但是我决定遇到的第一个。



首先,您需要配置项目设置。 在此编辑器中,您可以设置时间分辨率(PPQN)。 我选择的最小值等于48。太大的值是没有意义的,因为您必须处理超过1个字节的大数字。 但是最小值48非常令人满意。 在几乎所有旋律中,都找不到小于1/32的音符。 如果每季度的“滴答声”数量为48,则音符或暂停1/32的持续时间为48 /(32/4)= 6“滴答声”。 也就是说,理论上有可能将1/32音符完全除以2,甚至除以3。默认情况下,其余参数保留在项目属性窗口中。



接下来,打开第一首曲目的属性,并为其分配等于1的频道号。根据您的喜好,在编辑器中播放旋律时,选择与乐器相对应的音色。 当然,补丁号不会影响最终结果。



在编辑器工具栏上,以每分钟四分之一的速度设置旋律速度。 默认速度值为100 bpm。

该微控制器具有一个8位定时器,如前所述,它将用于控制发声音符和暂停声的持续时间。 决定了这种计时器的相邻操作(中断)之间的时间间隔将对应于一个“滴答声”的间隔。 根据旋律的节奏,此时间间隔的值将有所不同。 我决定使用溢出计时器中断。 并且可以根据初始计时器初始化参数来调整此相同的时间间隔,这取决于旋律的速度。 现在让我们继续进行计算。

通常,通常,歌曲的速度平均在50到200的范围内。已经说过,midi文件中的速度设置为微秒乘以四分之一。 对于速度50,此值为60,000,000 / 50 = 1,200,000,对于速度250,它将为240,000。由于根据该项目,四分之一包含48个刻度,因此最小速度的刻度长度将为1,200,000 / 48 = 25,000μs。 对于最大速度,如果以相同的方式计算,则为-5000μs。 对于石英频率为8 MHz,最大预分频器为1024的MK,我们可以获得以下信息。 对于最小速度,计时器需要计算为25000 /(1024/8)= 195次。 将结果舍入到最接近的整数值,舍入误差实际上不影响结果。 对于最大速度-5000 /(1024/8)= 39。 在这里,舍入误差不再影响,因为对于从248到253的相邻速度值也获得了舍入值39。因此,计时器必须用一个反值初始化:对于最小速度-(256-195)= 61,对于最大-(256 -39)= 217。 在当前的MK配置中,提供计时器的最小速度为39 bpm。 使用此值,计时器必须被计数250次。 且值为38-已经为257,超出了计时器的限制。 我决定以40 bpm的最小速度值,以240 bpm的最大速度值。

为了计算刻度数,将使用基于上述的虚拟计时器。 如上面已经提到的那样,刻度的数量决定了音符或暂停的持续时间。

为了实现音符的回放,使用了第二个16位计时器。 根据MIDI规范,总共提供了128个音符。 但实际上,它们的使用量要少得多。 而且,最低(八次)的八度音阶和最高(八分之一的频率)八度音阶不会被微控制器和谐地再现。 但是,尽管如此,带固定分频器的16位定时器几乎覆盖了midi提供的所有音符范围,即没有前35个。 但我选择以数字37作为开头的音符(由于编码来自零,因此其代码为36)。 为方便起见,因为此数字对应于音符“ C”,是传统音阶中的第一个音符。 它的频率为65.4 Hz,对应的半周期为-1 / 65.4 / 2 = 0.00764秒。 这个时间段的MK频率为8 MHz,并且分频器1(即没有分频器)将对计时器进行整体计数,共计0.00764 /(1/8000000)= 61156次。 对于第35个音符,如果计数,则该值为68645,超出16位计时器的范围。 但是,即使需要在第36位以下弹奏音符,您也可以输入第一个可用的定时器分频器,等于8。但是实际上并不需要,就像弹最高音符一样。 但是,对于频率最高的第128个音符“ G”音符(频率为12,543.85 Hz),如果以类似的方式计数,则计时器值将为319。以上所有计算的细节都由计时器模式的特定配置确定,这将在后面显示。

现在,我有一个同样重要的问题:如何获得音符编号与计时器代码之间的关系? 有一个众所周知的公式,用于根据音符的编号来计算音符的频率。 如上面的示例所示,可以轻松计算已知频率的计时器代码。 但是12阶的根出现在频率对音符的依赖关系公式中,通常,我不希望为控制器加载此类计算过程。 另一方面,为所有音符创建计时器代码数组也是不合理的。 我决定做以下事情,选择一个中间立场。 只需为前12个音符创建一个计时器代码数组即可,这是一个八度。 接下来的八度音符的音符应通过将第一个八度音符的音符频率乘以2来获得。或者同一件事是,通过将定时器代码的值依次除以2来获得。另一个方便之处在于,八度数是巧合,是向右移( »),将其用于除以2的幂。 我选择这个运算符不是偶然的,因为它的参数反映了除数的幂(除以2的次数)的指数。 这是八度数。 对于我的一组音符,总共涉及8个八度(最后一个八度不完整)。 MIDI文件中的音符编码为一个字节,更准确地说是7位。 为了在MK中弹奏音符,根据上述想法,必须首先使用音符代码计算八度数和八度中的音符号。 在将midi文件转换为简化格式的阶段执行此操作。 八个八度可以编码为三个比特,一个八度中的12个音符可以编码为四个。 总的来说,事实证明,音符与midi文件使用相同的7位编码,但仅以方便MK的不同表示形式编码。 由于16位可以用4位编码,并且音符的八度为12,因此有未使用的字节。

最后的第八位可用作启用或禁用音符的标记。 在MK的情况下,由于旋律的一致,有关静音音符的信息将是多余的。 在旋律中直接改变音符时,音符不是“打开-打开”的,而是“切换”的。 并且在暂停的情况下,“静音已打开”,为此您可以从未使用的字节集中选择一个特殊的字节,而根本不使用有关关闭音符的信息。 这样的想法是好的,因为它节省了转换后得到的旋律的大小,但是通常会使模型复杂化。 我没有遵循这个想法,因为已经有足够的内存了。

关于midi文件中旋律音符的信息存储在“ interval-event-interval-event ...”视图的相应通道的块中。 在转换后的格式中,原理完全相同。 如上所述,要记录事件(打开或关闭音符),将使用一个字节。 第一位(最高有效位7)编码事件的类型。 值“ 1”表示音符开,而值“ 0”表示音符开。 接下来的三位编码八度音阶,最低的四位编码八度音阶音符。 一个字节也用于记录时间间隔。 在原始的MIDI格式中,为此使用了可变长度格式。 它的小缺点是只有7位用于编码时间间隔(“滴答声”的数量),而第八位则表示连续。 也就是说,实际上,使用一个字节,您可以编码最多128个刻度的间隔。 但是,由于实际旋律和简单旋律之间的时间间隔有时会超过128,但几乎永远不会超过256,因此我放弃了可变长度格式,并以一个字节进行管理。 它编码的时间间隔最多为256个滴答声。 由于该项目每季度使用48个滴答声,或者每个周期使用48 * 4 = 192个滴答声,因此可以使用一个字节来编码256/192 = 1个持续时间的间隔(3)(一整个周期和三分之一)一个周期,即足够。

在将midi文件转换成的本机格式中,我还应用了一个16字节大小的小标头。 前14个字节包含旋律的名称。 当然,名称不能超过14个字符。 然后是零空间。 下一个最后一个字节以方便MK的角度反映旋律的节奏。 此值是在转换阶段计算的,用于初始化负责节奏的MK计时器。 上面的几段中讨论了如何计算它。

从第17个字节开始,旋律的内容随之而来。 每个奇数字节对应一个时间间隔,每个偶数字节对应一个事件(注释)。 , , , . 0xFF. . , . , , , , . . 0x0F, . 16- , , 12. . , « », , . ( ). , 36 . , ( ) , .

«Cakewalk Pro Audio 9», . , . : «Piano roll» . . .





, () , . , , .

.



, , , . , , - , «Del». , , - «». , , . , , . , : .

« 1», .



HEX . , , avi ( ), , (big endian).



. . , (1), (2) (48). . . 6 , . 6 (- 0xFF) 0x51 0x03 . – . . , . . – – . , , , , . ( ) , 48*3=144 128. , . 144 . . , . . , () , : . , 0x90, . . – , 128 .

, , , , - EEPROM. , . HEX , . .



( 16 ), , . 0xC1 (193) 154, 155 156. , 155 bpm, . ( 14-), , . – «Classic». , HEX . , , , .

( 17- ) . , , . , , . , , /. , «» , 0xB4 0x34, 0x34, . 0xB4 (0b10110100) , , 0x34 (0b00110100) , . 0x34 : 0b011, – 0b0100. , , 3 4 . , , . . , Excel, 76 (0x4C) , E6 ( «» 6- ). : .

, . , , . , . , . . , . , , - , , , . , , , 1 . «» 1 , .

(0x90), 128, , . . , , . , 0xFF, , . , .

- EEPROM. , . 8 ( 8 ). 512- . . 0x01, (, ). ( ) . . , 64, , 127 , .



, , Excel. ( ).





, , . , . , , , .

1.cpp
#include <stdio.h> #include <windows.h> #include <string.h> #define SPACE 1 HANDLE openInputFile(const char * filename) { return CreateFile ( filename, // Open Two.txt. GENERIC_READ, // Open for writing 0, // Do not share NULL, // No security OPEN_ALWAYS, // Open or create FILE_ATTRIBUTE_NORMAL, // Normal file NULL); // No template file } HANDLE openOutputFile(const char * filename) { return CreateFile ( filename, // Open Two.txt. GENERIC_WRITE, // Open for writing 0, // Do not share NULL, // No security OPEN_ALWAYS, // Open or create FILE_ATTRIBUTE_NORMAL, // Normal file NULL); // No template file } void filepos(HANDLE f, unsigned int p){ LONG LPos; LPos = p; SetFilePointer (f, LPos, NULL, FILE_BEGIN); //FILE_CURRENT //https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-setfilepointer } DWORD wr; DWORD ww; unsigned long int read32(HANDLE f){ unsigned char b3,b2,b1,b0; ReadFile(f, &b3, 1, &wr, NULL); ReadFile(f, &b2, 1, &wr, NULL); ReadFile(f, &b1, 1, &wr, NULL); ReadFile(f, &b0, 1, &wr, NULL); return b3<<24|b2<<16|b1<<8|b0; } unsigned long int read24(HANDLE f){ unsigned char b2,b1,b0; ReadFile(f, &b2, 1, &wr, NULL); ReadFile(f, &b1, 1, &wr, NULL); ReadFile(f, &b0, 1, &wr, NULL); return b2<<16|b1<<8|b0; } unsigned int read16(HANDLE f){ unsigned char b1,b0; ReadFile(f, &b1, 1, &wr, NULL); ReadFile(f, &b0, 1, &wr, NULL); return b1<<8|b0; } unsigned char read8(HANDLE f){ unsigned char b0; ReadFile(f, &b0, 1, &wr, NULL); return b0; } void message(unsigned char e){ printf("Error %d: ",e); switch(e){ case 1: // -   -; printf("In track0 event is not FF\n"); break; case 2: // -  127 printf("Len of FF >127\n"); break; case 3: //  ; printf("Midi is incorrect\n"); break; case 4: //   ; printf("Delta>255\n"); break; case 5: //    RPN  NRPN; printf("RPN or NRPN is detected\n"); break; case 6: //   ; printf("Note in 1...35 range\n"); break; case 7: //    ; printf("Long of name of midi file >18\n"); break; } system("PAUSE"); } int main(){ HANDLE in; HANDLE out; unsigned int i,j; unsigned int inpos; unsigned int outpos=0; unsigned char byte; // ; unsigned char byte1; //  1  ; unsigned char byte2; //  2  ; unsigned char status; //- ( ); unsigned char sz0; // -; unsigned long int bsz0; //    -; unsigned short int format, ntrks, ppqn; //  ; unsigned long int bsz1; //    ; unsigned long int bpm; // ( .  ); unsigned long int time=0; //    ( ); unsigned char scale; //    ,  ; unsigned char oct; //    ; unsigned char nt; // ; unsigned char outnote; //      ; unsigned char prnote=0; //  ; unsigned char tdt; // ()   ; unsigned int dt; //    ( ); unsigned int outdelta=0; //    ( ); unsigned char prdelta=0; //  ; char fullname[30]; //    ; char name[16]; // ; WIN32_FIND_DATA fld; //   mid; HANDLE hf; unsigned short int csz; //  ; unsigned char nfile=0; // ; unsigned char adr[128]; //    ; out=openOutputFile("IMAGE.out"); outpos=512; //   ; filepos(out,outpos); hf=FindFirstFile(".\\midi\\*.mid",&fld); do{ printf("\n***** %s *****\n",fld.cFileName); if(strlen(fld.cFileName)>18){ //   ; message(7); } sprintf(name,"%s",fld.cFileName); name[strlen(fld.cFileName)-4]=0; // ; sprintf(fullname,".\\midi\\%s",fld.cFileName); //    ; WriteFile(out, name, strlen(name), &ww, NULL); //    ; in=openInputFile(fullname); //    ; #include "process.cpp" //     ; outpos+=((csz/512)+1)*512; //    ; adr[nfile]=(outpos/512)-((csz/512)+1); //  ()   ; filepos(out,outpos); CloseHandle(in); nfile+=1; }while(FindNextFile(hf,&fld)); //   ,    ; FindClose(hf); WriteFile(out, &outnote, 1, &ww, NULL); outpos=0; //   ; filepos(out,outpos); WriteFile(out, adr, nfile, &ww, NULL); outpos=511; //  ; filepos(out,outpos); WriteFile(out, &nfile, 1, &ww, NULL); CloseHandle(out); system("PAUSE"); return 0; } 


Process.cpp文件附件
 time=0; inpos=8; //  ; filepos(in,inpos); format=read16(in); ntrks=read16(in); ppqn=read16(in); if(format!=1 || ntrks!=2 || ppqn!=48){ message(3); } inpos+=10; filepos(in,inpos); //    -; bsz0=read32(in); inpos+=4; while(inpos<22+bsz0){ //      ; tdt=read8(in); inpos+=1; //   ; dt=(unsigned int)(tdt&0x7F); while(tdt&0x80){ tdt=read8(in); inpos+=1; dt=(dt<<7)|(tdt&0x7F); } byte=read8(in); inpos+=1; if(byte==0xFF){ //  ,  -    -; byte=read8(in); //  -; sz0=read8(in); //  , ,     127 ( ); if(sz0&0x80){ message(2); } inpos+=2; switch(byte){ case 0x51: //   "Set Tempo"; bpm=read24(in); scale=256-(bpm/(ppqn*128)); printf("scale=%d\n",scale); filepos(out,outpos+15); // ; WriteFile(out, &scale, 1, &ww, NULL); csz=16; break; default: break; } inpos+=sz0; filepos(in,inpos); // ,     0x51; }else{ message(1); } } //    ; outdelta=0; inpos+=4; filepos(in,inpos); bsz1=read32(in); inpos+=4; while(inpos<30+bsz0+bsz1){ tdt=read8(in); inpos+=1; //   ; dt=(unsigned int)(tdt&0x7F); while(tdt&0x80){ tdt=read8(in); inpos+=1; dt=(dt<<7)|(tdt&0x7F); } outdelta+=dt; //  ; // ,      , ; time+=dt; //  ; byte=read8(in); //    ,  ; inpos+=1; if(byte&0x80){ //  ; status=byte; // ; if(byte==0xFF){ //   -; byte=read8(in); //    ,    ; sz0=read8(in); inpos+=(2+sz0); filepos(in,inpos); }else{ //    ; byte1=read8(in); inpos+=1; } }else{ //    ,        ; byte1=byte; } switch(status&0xF0){ // ,      ; case 0xF0: //   ,  -; break; case 0x80: // ; byte2=read8(in); //     ( ); inpos+=1; //     ,    ; if(byte1>1&&byte1<36){ //         ; message(6); } if(byte1>1){ // ; oct=((byte1-36)/12); //  ; nt=(byte1-36)%12; //    ; }else{ //   ; oct=0; nt=15; } outnote=(oct<<4)|nt; //  ; prnote=outnote; prdelta=outdelta; if(outdelta>255){ //     255 (  ); message(4); } WriteFile(out, &outdelta, 1, &ww, NULL); WriteFile(out, &outnote, 1, &ww, NULL); csz+=2; outdelta=0; //  ; break; case 0x90: //   ; byte2=read8(in); //    ( ); inpos+=1; //     ,    ; if(byte1>1&&byte1<36){ //         ; message(6); } if(byte1>1){ // ; oct=((byte1-36)/12); //  ; nt=(byte1-36)%12; //    ; }else{ //   ; oct=0; nt=15; } if(byte2){ //  ,   ; outnote=0x80|(oct<<4)|nt; //  = 1; //   ; if(!outdelta && (outnote&0x7F)==prnote){ //     ; prdelta-=SPACE; // -; filepos(out,outpos+csz-2); //    ; WriteFile(out, &prdelta, 1, &ww, NULL); // ; filepos(out,outpos+csz); outdelta=SPACE; //  -  ; } }else{ //  ,    ; outnote=(oct<<4)|nt; prnote=outnote; //  ; prdelta=outdelta; //  -; } if(outdelta>255){ //   -    ; message(4); } WriteFile(out, &outdelta, 1, &ww, NULL); WriteFile(out, &outnote, 1, &ww, NULL); csz+=2; outdelta=0; // -   ; break; //   () ; case 0xA0: // ; byte2=read8(in); inpos+=1; break; case 0xB0: //   ; if(byte1>=98&&byte1>=101){ //     NRPN  RPN; message(5); //  ; } byte2=read8(in); inpos+=1; break; case 0xC0: //  (.  ); // , ,    ; break; case 0xD0: //; break; case 0xE0: // ; byte2=read8(in); inpos+=1; break; default: //  (   ); break; } } //     0xFFFF,    ; outdelta=255; outnote=255; WriteFile(out, &outdelta, 1, &ww, NULL); WriteFile(out, &outnote, 1, &ww, NULL); csz+=2; //   ,     ; printf("Length: %i (%i:%02i)\n",time,time/192,time%192); 


实际上,MK程序的基本部分非常简单。 考虑实施它的选项之一,更确切地说,是其主要部分。

用于产生音符的计时器1的配置如下。 要启用和禁用注释,分别使用以下替换。

 #define ENT1 TCCR1B=0x09;TCCR1A=0x40 #define DIST1 TCCR1B=0x00;TCCR1A=0x00;PORTB.1=0 

在启动定时器之前,您需要为OCR1A寄存器分配一个16位的值,该值将对应于所播放的频率。 稍后将显示。 当定时器打开时,TCCR1B寄存器被分配给波形产生模式,定时器分频器为1,并且在比较匹配时TCCR1A寄存器设置为Toggle OC1A。 在这种情况下,信号将从MK“ OC1A”的特殊指定输出中删除。 在SMD封装的ATmega8中,这是引脚13,与PORTB.1相同。 当定时器关闭时,两个寄存器均复位,并且PORTB.1的输出强制为零。 为了防止在静音期间输出恒定电压,这对于ULF的输入是不希望的,这是必要的。 虽然,您可以在电路中放置一个电容器,但是您也可以以编程方式禁用输出。 如果在信号的相应相位时刻关闭音符,则此输出上可能会出现恒定电压,这种情况发生在50%的情况下。

为第一个八度的12个音符创建一个计时器值数组。 这些值是预先计算的。

 freq[]={61156,57724,54484,51426,48540,45815,43244,40817,38526,36364,34323,32396}; 

正如我所说,其他八度音阶的音符将通过除以二获得。

定时器0的配置更加简单。 它会持续工作,并带有溢出中断,每次都使用与旋律的节奏相对应的值重新初始化。 定时器分频器为5:TCCR0 = 0x05。 基于此计时器,将创建一个虚拟计时器,用于对旋律中的抽动(时间)进行计数。 处理该计时器的响应位于主程序循环中。

定时器0中断功能如下。

 interrupt [TIM0_OVF] void timer0_ovf_isr(void){ if(ent01){ vt01+=1; } TCNT0=top0; } 

此处,变量ent01负责激活虚拟计时器。 通过此变量,可以根据需要打开或关闭它。 vt01变量是虚拟计时器的可计数主变量。 TCNT0 = top0行表示将计时器0初始化为所需值top0,该值是在播放旋律之前从旋律的标题中读取的。

要播放的旋律编号对应于变量alm。 它也充当复制开始的标志。 根据任务的不同,她需要以一种方式分配旋律编号。 之后,主循环的下一个程序段将被激活。

 if(alm){ //     ; adr=eepr(alm-1)<<9; //     (<<9    512); adr+=15; //   ,      ; top0=eepr(adr); //  ; adr+=1; //     ; adr0=adr; //      (  ); top01=eepr(adr); //      " "  ; adr+=1; //   ; note=eepr(adr); // ; adr+=1; //    -; vt01=0; //    ; ent01=1; //  ; TCNT0=0; //  ; alm=0; //        ,   ; } 

从音符到音符的进一步切换在虚拟计时器的处理单元中进行,该虚拟计时器也位于主循环中。

 if(vt01>=top01){ //   ,    ; vt01=0; //  ; if(note&0x80){ //     ""; nt=note&15; //    ; oct=(note&0x7F)>>4; //  ; if(nt!=15){ //       15,   ; OCR1A=freq[nt]>>oct; //     ; //         ; ENT1; // ; }else{ //  " "   ; DIST1; // ; } }else{ //     ""; DIST1; // ; } top01=eepr(adr); //      " "; adr+=1; //   ; note=eepr(adr); //   ; adr+=1; // ; if(note==255 && top01==255){ //      ; top01=eepr(adr0); //   ,   ; note=eepr(adr0+1); //   ; adr=adr0+2; //   ; } } 

根据程序文本中的注释,所有内容都应该非常清晰易懂。

要停止旋律,请使用以下主循环插入。

 if(stop){ //  ; DIST1; //  ; ent01=0; //  ; vt01=0; //  ; } 

关于旋律播放的实现,有一点评论。 在每个新音符开始发声之前,微控制器会花费少量时间将音符的读取字节转换为计时器值。 实际上,这次的时间相对较小,并且不会影响播放质量。 但是我怀疑该操作是否仍然不可见。 在这种情况下,每个音符之前会出现额外的停顿,并且旋律的节奏会中断。 但是这个问题也可以解决。 在当前音符发声时,预先计算下一个音符的定时器值就足够了。 此过程必须与使用特殊指定的标志的主程序循环中的虚拟计时器处理分开进行。 由于计算时间不可能超过最短音符的演奏时间,因此这种解决方案是合适的。

现在让我们继续测试程序。

除了上述代码片段外,我还在MK程序中添加了按钮处理功能,通过这些功能,我可以控制特定旋律的包含或禁用。 EEPROM通过I2C总线连接到MK,并在软件级别实现工作。 该项目是在“ CodeVisionAVR”和“ CodeWizardAVR”的帮助下完成的。 我通过分频器将MK从引脚13输出到PC声卡,并在声音编辑器中记录旋律的声音。 我在固件的帮助下刷新了EEPROM存储器,这是我在之前的一篇文章中写的。 由于并非图像文件的所有字节都是有用的事实,因此只能通过有用的字节(直到旋律的结尾标记)来实现内存固件,以节省录制时间和芯片资源。 为此,您可以制作一个单独的程序,或在转换过程中直接将字节写入芯片,从而添加到主程序中。

在这八种音调中,有三个测试音调,在它们的帮助下,我将评估耳朵的频率范围,合并相同音符的声音,最短音符的声音,快速过渡等。 让我提醒您,合并相同的音符实际上听起来会停顿一格,而合并中的第一个音符会少一格。

其中一种测试音调是从头到尾的音符序列,其持续时间为四分之一,音调速度为40 bpm。



在这种情况下,一个音符听起来要多一秒钟,因此您可以详细聆听整个音符范围的声音。 在音频编辑器“ Adob​​e Audition”中的频谱上,由于有相应的锯齿波形,因此可以观察到主要频率成分及其高次谐波。 音符编号与频率之间的对数关系令人震惊。



通过分析时间间隔,可以清楚地看到连续音符之间的实际停顿平均约为145个样本(在音频记录44100 Hz的采样频率下),约为3毫秒。 这是MK执行必要计算的时间。 这些插页经常出现在每个音符之前。 我专门在示例中写了含义,因为此信息虽然不是很重要,但更原始,更准确。



而且,以120 bpm的平均旋律节奏的一tick的长度约为10毫秒。 因此,原则上,当两个相同的音符不停地接连传来时,可能不会在1个滴答中引入相同的校正。 我认为在音符之间定期插入3毫秒就足够了。 聆听旋律时,这些常规插入音根本听不到,而且旋律均匀。 因此,在播放当前音符时,不需要特别计算下一个音符的计时器值。

另一种速度为200 bpm的测试旋律连续包含来自中间音域的相同1/32音符,没有暂停。 在这种情况下,经过处理后,在它们之间进行播放时,会有1个停顿的停顿,在此停顿状态下,记录信号的采样速度为310个样本(约6 ms)。



顺便说一下,此暂停的时间长度与信号的周期相当,这表明旋律的节奏很高。 它的声音让人联想起颤音。

原则上,这可以完成。 我对设备的结果感到满意,它超出了所有期望。 大多数时候,我致力于研究Midi格式并调试程序以进行转换。 在以下文章之一中,我还将致力于与MIDI相关的主题,该主题将讨论此格式在其他有趣的应用程序中的应用。

Source: https://habr.com/ru/post/zh-CN454514/


All Articles