|
产生波形 MIDI播放器使用DDS(Direct Digital Synthesis)产生波形。为了给这个项目一个音乐盒的声音我想给波形衰减。 ATtiny85不提供硬件乘法,因此为了避免需要乘法,基础波形采用的是矩形波,所以我们只需要将包络的幅度乘以1或-1。此外,我使包络线性衰减,因此我们可以简单地通过递减计数器来计算幅度值。
对于每个通道,有三个变量; Freq [] - 当前音符值,Acc [] - 相位累加器和Amp [] - 包络幅度值。对于每个样本,Freq []值被添加到相位累加器Acc []。 Acc []的最高位用于生成通道的方波。 Freq []的值越大,顶部位产生的频率越高。最后,波形乘以包络Amp []。四个通道复用在一起,结果输出到模拟输出。
中断服务程序 该程序的关键部分是T / C0中断服务程序,它将波形样本输出到模拟输出,并以约95kHz的速率调用。对于当前通道c,它更新频率累加器Acc [c]和幅度Amp [c],并计算当前音符的值。然后输出到T / C1的比较寄存器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]。 Freq [c]的值越大,Acc [c]的变化越快。 - Amp[c] = Amp[c] - (Amp[c] != 0);
复制代码减小通道的幅度值。 (Amp [c]!= 0)部分确保一旦达到零,它就保持为零。 将Temp设置为频率累加器的前8位。 如果前两位为1,则该行将顶部位设置为1,否则设置为0,这将给出具有25/75标记空间比的矩形波。对于这个项目,我决定使用一个矩形波,它具有更丰富的谐波和更好的声音。 由于Temp和Mask是有符号值,因此在整个字节中将顶部位向下扫描。如果最高位为0,则得到0x00,如果为1则得到0xFF。 默认情况下,Volume为8,将Env设置为幅度的顶部字节。 - Note = (Env ^ Mask) + (Mask & 1);
复制代码最后,把它们放在一起。如果Mask为0x00,则将Note设置为Env值。如果Mask为0xFF,则将Note设置为Env + 1的补码,或减去Env值。因此,Note现在包含一个在正负电流幅度之间变化的波形。 输出寄存器设置为Note + 128,以提供无符号的8位值。 四个通道在连续中断时输出,在输出端多路复用通道。
生成比例 良好的调节比例由数组Scale []中的常量生成: - unsigned int Scale[] = {
- 10973, 11626, 12317, 13050, 13826, 14648, 15519, 16442, 17419, 18455, 19552, 20715};
复制代码
第一个数字10973对应于C0。为了获得C4,中间C,即四个八度音高,我们将其除以24得到686.因此,Acc [c]的最高位将以25000 /(65536/685)或261.7 Hz,中间C的频率变化。 。
改变声音 两个变量可让您更改音符的声音。您可以在7到9之间改变音量以尝试不同的音量设置。你可以改变14到12之间的腐烂来改变信封; 14给出了长衰减,12给出了最短的衰减。超出这些范围的值可能不会有用。
MINI解释器 MIDI解释器读取在音乐盒合成器上播放曲调所需的MIDI子集。它假定只有一个MIDI通道,并读取速度和分度(每拍节拍)设置,但忽略大多数其他设置。
读取数据 MIDI解释器使用以下函数来读取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变量精度格式读取数字。这可以包含一到四个字节: - 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位;如果顶部位置位,则表示后面跟着另一个字节。
播放音符 解释器调用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;
- }
复制代码
播放MIDI数据 最后,这是播放MIDI数据的主要例程。变量Ptr是指向要读取的下一个字节的指针: - void playMidiData () {
- Ptr = 0; // Begin at start of file
复制代码MIDI文件中的第一个块是标题,它指定了音轨的数量和division(节拍): - // 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;
复制代码division是节拍中的细分数量;通常为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,事件发生之前的时间延迟应该生效。对于同时发生的事件,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元事件,它指定以微秒为单位的节拍持续时间。默认情况下,这是500000;即半秒,相当于120bpm。
其余事件是MIDI事件,由事件类型中的第一个十六进制数字标识。我们感兴趣的唯一一个是0x90,Note On,它在下一个可用的音乐盒频道上播放音符: - // 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);
- }
- }
- }
复制代码
我目前忽略了速度值velocity,因为它不适用于音乐盒,但您可以使用它来设置音符的初始幅度。
我们必须识别其他MIDI事件以允许我们跳过它们,因为它们的长度会有所不同。最后,任何其他信号都会在错误LED上发出错误信号。
编译程序 为了能够支持四个通道,ATtiny85需要以16MHz时钟运行;幸运的是,它提供了16MHz时钟选项,无需晶体,使用内部PLL将内部8MHz时钟提升至16MHz。
我使用Spence Konde的ATTiny Core 编译了程序。 在Board菜单上的ATtinyCore标题下选择ATtiny25/45/85选项。 然后在后续菜单中选择 Timer 1 Clock: CPU, B.O.D. Disabled, ATtiny85, 16 MHz (PLL) 。 选择Burn Bootloader以适当设置保险丝。 然后使用ISP上传程序(系统内编程); 我使用的是Sparkfun的Tiny AVR Programmer Board。
以上就是整个MIDI数字音乐盒的程序。 |