简约的四部分MIDI播放器



建议的播放器不需要存储卡;它直接在ATtiny85微控制器中存储长达6,000字节的MIDI文件(与播放WAV文件的经典设计不同,它自然需要存储卡)。 使用PWM实现具有衰减的四路回放。 这里有一个发声的例子。

该设备是根据方案制造的:



如果由于软件故障而在PB4的输出端出现逻辑单元,则微控制器和动态磁头之间的电解电容器将不会丢失常数组件。 磁头的电感不超过PWM频率。 如果决定将设备连接到放大器,为了避免后者因PWM信号而过载,您需要添加一个低通滤波器,如下所示。

MIDI文件必须以以下形式的数组放置在固件源中:

const uint8_t Tune[] PROGMEM = { 0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x03, 0xc0, 0x4d, 0x54, 0x72, 0x6b, 0x00, 0x00, 0x0a, 0x7e, 0x00, 0xff, ... 0x50, 0xb0, 0x5b, 0x00, 0x00, 0xff, 0x2f, 0x00 }; 

有一个现成的解决方案可将文件转换为类似UNIX的操作系统上的这种格式-xxd实用程序。 我们获取MIDI文件,并通过以下实用程序进行传递:

 xxd -i musicbox.mid 

控制台将显示如下内容:

 unsigned char musicbox_mid[] = { 0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x03, 0xc0, 0x4d, 0x54, 0x72, 0x6b, 0x00, 0x00, 0x0a, 0x7e, 0x00, 0xff, ... 0x50, 0xb0, 0x5b, 0x00, 0x00, 0xff, 0x2f, 0x00 }; unsigned int musicbox_mid_len = 2708; 

2708是字节长度。 原来少于6000-这意味着它适合。 通过剪贴板的十六进制数字序列将传输到草图(请记住: 在控制台中,没有Ctrl + C ),而不是默认数组。 或者,如果我们希望离开,则不要执行所有这些操作。

计时器计数器1将从PLL以64 MHz的频率工作:

  PLLCSR = 1<<PCKE | 1<<PLLE; 

我们将此定时器转换为PWM模式以用作DAC;占空比取决于OCR1B的值:

  TIMSK = 0; // Timer interrupts OFF TCCR1 = 1<<CS10; // 1:1 prescale GTCCR = 1<<PWM1B | 2<<COM1B0; // PWM B, clear on match OCR1B = 128; DDRB = 1<<DDB4; // Enable PWM output on pin 4 

矩形脉冲的频率取决于OCR1C的值,我们将其设为255(默认情况下),然后将64 MHz的频率除以256,得到250 kHz。

计时器计数器0将产生中断:

  TCCR0A = 3<<WGM00; // Fast PWM TCCR0B = 1<<WGM02 | 2<<CS00; // 1/8 prescale OCR0A = 19; // Divide by 20 TIMSK = 1<<OCIE0A; // Enable compare match, disable overflow 

16 MHz的时钟频率由8分频器分频,然后由19 + 1的OCR0A值分频,得到100 kHz。 播放器为四声,每个声音获得25 kHz。 中断后,将调用ISR处理例程(TIMER0_COMPA_vect),该例程计算并输出声音。

看门狗定时器配置为每16 ms产生一次中断,接收中断频率是必需的:

 WDTCR = 1<<WDIE | 0<<WDP0; // Interrupt every 16ms 

为了获得给定形状的振荡,使用直接数字合成。 ATtiny85中没有硬件乘法,因此我们采用矩形脉冲并将包络的幅度乘以1或-1。 幅度线性减小,并且为了在给定的时间点进行计算,足以线性减小仪表读数。

为每个通道提供三个变量:频率[]-频率,加速度[]-相电池,电流[],包络幅度值。 Freq []和Acc []的值相加。 高阶位Acc []用于获得矩形脉冲。 Freq []越多,频率越高。 完成的波形乘以包络线Amp []。 所有四个通道都被多路复用并馈送到模拟输出。

该程序的重​​要部分是处理来自定时器0的中断的过程,该过程将振荡输出到模拟输出。 以大约95 kHz的频率调用此过程。 对于当前通道c,它更新Acc [c]和Amp [c]的值,并计算当前音符的值。 结果被发送到OCR1B计时器的OCR1B比较寄存器,以在引脚4上获得模拟信号:

 ISR(TIMER0_COMPA_vect) { static uint8_t c; signed char Temp, Mask, Env, Note; Acc[c] = Acc[c] + Freq[c]; Amp[c] = Amp[c] - (Amp[c] != 0); Temp = Acc[c] >> 8; Temp = Temp & Temp<<1; Mask = Temp >> 7; Env = Amp[c] >> Volume; Note = (Env ^ Mask) + (Mask & 1); OCR1B = Note + 128; c = (c + 1) & 3; } 

弦乐

 Acc[c] = Acc[c] + Freq[c]; 

将频率freq [c]添加到电池Acc [c]。 频率[c]越大,Acc [c]值变化得越快。 然后行

 Amp[c] = Amp[c] - (Amp[c] != 0); 

降低给定通道的幅度值。 需要片段(Amp [c]!= 0),以便在振幅达到零后不会进一步减小。 现在行

 Temp = Acc[c] >> 8; 

将Acc [c]的高9位传送到Temp。 和线

 Temp = Temp & Temp<<1; 

如果两个高阶位等于1,则使此变量的高阶位等于1;如果不相同,则将其设置为零。 结果是开/关比为25/75的矩形脉冲。 在以前的一种构造中,作者施加了曲折,而采用新方法时,谐波得到了更多。 弦乐

 Mask = Temp >> 7; 

将最高有效位的值传输到字节的其余位,例如,如果最高有效位为0,则将获得0x00,如果为1-则为0xFF。 弦乐

 Env = Amp[c] >> Volume; 

由于Volume = 8,因此将Volume值指定的Amp [c]位传输到Env,默认情况下是优先级1。

 Note = (Env ^ Mask) + (Mask & 1); 

所有这些结合在一起。 如果Mask = 0x00,则为Note分配值Env。 如果Mask = 0xFF,则为Note分配Env +1以外的值,即带有负号的Env。 现在注意包含电流波形,电流波形从正值变为负值。 弦乐

 OCR1B = Note + 128; 

将128加到Note并将结果写入OCR1B。 弦乐

 c = (c + 1) & 3; 

根据相应的中断输出四个通道,多路复用输出端的声音。

数组中给出了十二个音符频率:

 unsigned int Scale[] = { 10973, 11626, 12317, 13050, 13826, 14648, 15519, 16442, 17419, 18455, 19552, 20715}; 

其他八度音阶的音符频率通过除以2 n获得 。 例如,将10973除以2 4得到686。高位Acc [c]将以25000 /(65536/685)= 261.7 Hz的频率进行切换。

有两个变量会影响声音:音量-音量(从7到9)和衰减-衰减(从12到14)。“衰减”值越高,衰减越慢。

最简单的MIDI解释器仅关注音符,速度和除法系数的值,而忽略其他数据。 readIgnore()例程跳过从文件接收的数组中指定数量的字节:

 void readIgnore (int n) { Ptr = Ptr + n; } 

readNumber()例程从给定的字节数中读取一个数字,精度为4:

 unsigned long readNumber (int n) { long result = 0; for (int i=0; i<n; i++) result = (result<<8) + pgm_read_byte(&Tune[Ptr++]); return result; } 

readVariable()例程读取MIDI变量精度的数字。 在这种情况下,字节数可以为1到4:

 unsigned long readVariable () { long result = 0; uint8_t b; do { b = pgm_read_byte(&Tune[Ptr++]); result = (result<<7) + (b & 0x7F); } while (b & 0x80); return result; } 

每个字节取7位,如果需要进一步读取另一个字节,则第八位等于1;否则,则等于0。

解释器调用noteOn()例程在以下可用通道中播放笔记:

 void noteOn (uint8_t number) { uint8_t octave = number/12; uint8_t note = number%12; unsigned int freq = Scale[note]; uint8_t shift = 9-octave; Freq[Chan] = freq>>shift; Amp[Chan] = 1<<Decay; Chan = (Chan + 1) & 3; } 

Ptr变量指示要读取的下一个字节:

 void playMidiData () { Ptr = 0; // Begin at start of file 

MIDI文件中的第一个块是标头,它指示音轨的数量,速度和分割比率:

 // Read header chunk unsigned long type = readNumber(4); if (type != MThd) error(1); unsigned long len = readNumber(4); unsigned int format = readNumber(2); unsigned int tracks = readNumber(2); unsigned int division = readNumber(2); // Ticks per beat TempoDivisor = (long)division*16000/Tempo; 

除法系数通常等于960。现在,我们读取给定的块数:

  // Read track chunks for (int t=0; t<tracks; t++) { type = readNumber(4); if (type != MTrk) error(2); len = readNumber(4); EndBlock = Ptr + len; 

读取顺序事件,直到块结束:

  // Parse track while (Ptr < EndBlock) { unsigned long delta = readVariable(); uint8_t event = readNumber(1); uint8_t eventType = event & 0xF0; if (delta > 0) Delay(delta/TempoDivisor); 

在每个事件中,都指定了增量-由除法系数确定的时间单位的延迟,该延迟必须在此事件之前发生。 对于应该在此处发生的事件,delta为零。

元事件是类型为0xFF的事件:

  // Meta event if (event == 0xFF) { uint8_t mtype = readNumber(1); uint8_t mlen = readNumber(1); // Tempo if (mtype == 0x51) { Tempo = readNumber(mlen); TempoDivisor = (long)division*16000/Tempo; // Ignore other meta events } else readIgnore(mlen); 

我们感兴趣的唯一元事件类型是Tempo,即拍速的值(以微秒为单位)。 默认情况下为500,000,即半秒,相当于每分钟120次。

其余事件是由其类型的第一个十六进制数字定义的MIDI事件。 我们只对0x90感兴趣-注意开启,在以下可用频道上播放笔记:

  // Note off - ignored } else if (eventType == 0x80) { uint8_t number = readNumber(1); uint8_t velocity = readNumber(1); // Note on } else if (eventType == 0x90) { uint8_t number = readNumber(1); uint8_t velocity = readNumber(1); noteOn(number); // Polyphonic key pressure } else if (eventType == 0xA0) readIgnore(2); // Controller change else if (eventType == 0xB0) readIgnore(2); // Program change else if (eventType == 0xC0) readIgnore(1); // Channel key pressure else if (eventType == 0xD0) readIgnore(1); // Pitch bend else if (eventType == 0xD0) readIgnore(2); else error(3); } } } 

我们忽略了力度值,但是如果您愿意,可以在其上设置音符的初始幅度。 我们跳过其余事件,它们的长度可以不同。 如果MIDI文件中发生错误,则LED点亮。

微控制器的工作频率为16 MHz,因此不需要石英,您需要正确配置内置PLL。 为了使微控制器变得与Arduino兼容,应用 Spence Konde的这种经验 。 在Board菜单中,选择ATtinyCore子菜单,然后显示ATtiny25 / 45/85。 在以下菜单中,选择:计时器1时钟:CPU,禁用BOD,ATtiny85、16 MHz(PLL)。 然后选择Burn Bootloader,然后填写程序。 像SpinyFun的Tiny AVR编程器板一样使用该编程器。

CC-BY 4.0的固件在这里 ,已经在D小调中具有巴赫赋格了,原始MIDI文件在这里获取

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


All Articles