📜 ⬆️ ⬇️

Minimal four-part MIDI player



The proposed player does not require a memory card, it stores a MIDI file up to 6000 bytes in length directly in the ATtiny85 microcontroller (unlike this classic design, which plays WAV files, and the memory card, of course, requires). Four-voice playback with attenuation using PWM is implemented by software. An example of sounding is by reference .

The device is made according to the scheme:
')


An electrolytic capacitor between the microcontroller and the dynamic head will not miss the DC component if a logical unit appears at the output of the PB4 as a result of a software failure. Inductive resistance of the head does not pass the PWM frequency. If you decide to connect the device to the amplifier, in order to avoid overloading the latter with a PWM signal, you need to add a low-pass filter like here .

The MIDI file must be placed in the firmware source as an array of the form:

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 }; 

To convert a file to such a format in UNIX-like OS there is a ready-made solution - the xxd utility. We take the MIDI file and skip this utility like this:

 xxd -i musicbox.mid 

The console will display something like:

 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 is the length in bytes. It turned out less than 6000 - so fit. The sequence of hexadecimal numbers through the clipboard is transferred to the sketch (just do not forget: in the console - no Ctrl + C ) instead of the default array. Or do not do all this if we want to leave it.

The timer counter 1 will operate at a frequency of 64 MHz from the PLL:

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

We translate this timer into the PWM mode to work as a DAC, the duty cycle will depend on the OCR1B value:

  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 

The frequency of the square-wave pulses depends on the OCR1C value, we will keep it equal to 255 (by default), then the frequency of 64 MHz will be divided by 256, and we will get 250 kHz.

Timer Counter 0 will generate interrupts:

  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 

The clock frequency of 16 MHz is divided by a divider by 8, and then by the OCR0A value equal to 19 + 1, and 100 kHz is obtained. A four-voice player, for each voice is obtained at 25 kHz. By interruption, the call is processed by the ISR (TIMER0_COMPA_vect), which calculates and outputs sounds.

The watchdog timer is configured to generate an interrupt every 16 ms, which is required to receive note frequencies:

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

To obtain oscillations of a given form, direct digital synthesis is used. There is no hardware multiplication in ATtiny85, so we take rectangular pulses and multiply the amplitude of the envelope by 1 or -1. The amplitude decreases linearly, and in order to calculate it at a particular time, it is sufficient to linearly decrease the meter readings.

Three variables are provided for each channel: Freq [] - frequency, Acc [] - phase battery, Amp [], envelope amplitude value. The values ​​of Freq [] and Acc [] are added together. The high bit Acc [] is used for receiving rectangular pulses. The larger the freq [], the greater the frequency. The finished waveform is multiplied by the Amp [] envelope. All four channels are multiplexed and fed to the analog output.

An important part of the program is the interrupt processing procedure from timer counter 0, which outputs the oscillations to the analog output. The call to this procedure occurs at a frequency of about 95 kHz. For the current channel c, it updates the values ​​of Acc [c] and Amp [c], and also calculates the value of the current note. The result goes to the OCR1B comparison register of the OCR1B timer-counter to get the analog signal at pin 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; } 

Line

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

adds to the accumulator Acc [c] the value of the frequency Freq [c]. The more Freq [c], the faster Acc [c] value will change. Then string

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

decreases the amplitude value for this channel. The fragment (Amp [c]! = 0) is needed so that after reaching zero amplitude it does not decrease further. Now string

 Temp = Acc[c] >> 8; 

transfers the higher 9 bits of Acc [c] to Temp. And string

 Temp = Temp & Temp<<1; 

leaves the high bit of this variable equal to one if the two high bits are equal to one, and sets the high bit to zero if it is not. Square pulses are obtained with an on / off ratio of 25/75. In one of the previous constructions, the author applied the meander, with the same method of harmonics it turns out a little more. Line

 Mask = Temp >> 7; 

transfers the remaining bits of the byte to the value of the high one, for example, if the most significant bit was 0, then we get 0x00, and if 1 - then 0xFF. Line

 Env = Amp[c] >> Volume; 

transfers the Amp [c] bit to Env, which is set to the value of Volume, the high by default, since Volume = 8. String

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

it all unites. If Mask = 0x00 then Note is assigned the value Env. If Mask = 0xFF, then Note is assigned a value that is additional to Env + 1, that is, Env with a minus sign. Now, the Note contains the current mode of oscillation, varying from positive to negative values ​​of the current amplitude. Line

 OCR1B = Note + 128; 

adds 128 to Note and writes the result to OCR1B. Line

 c = (c + 1) & 3; 

outputs four channels on the corresponding interrupts, multiplexing voices at the output.

Twelve frequencies of notes are given in the array:

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

The frequencies of the notes of the other octaves are obtained by dividing by 2 n . For example, we divide 10973 by 2 4 and we get 686. The upper bit Acc [c] will switch at a frequency of 25000 / (65536/685) = 261.7 Hz.

The sound is influenced by two variables: Volume - volume, from 7 to 9 and Decay - attenuation, from 12 to 14. The greater the Decay value, the slower the attenuation.

The simplest MIDI interpreter pays attention only to the values ​​of the note, tempo and division factor, and ignores other data. The readIgnore () subroutine skips the specified number of bytes in the array obtained from the file:

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

The readNumber () routine reads a number from the specified number of bytes with an accuracy of 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; } 

The readVariable () subroutine reads a number with a variable adopted in MIDI. The number of bytes in this case can be from one to four:

 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; } 

From each byte, seven bits are taken, and the eighth is equal to one, if you need to read one more byte, or zero, if not.

The interpreter calls the subroutine noteOn () to play a note in the following available channel:

 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; } 

The variable Ptr indicates the following read byte:

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

The first block in the MIDI file is a header indicating the number of tracks, tempo and division factor:

 // 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; 

The division factor is usually 960. Now we read the specified number of blocks:

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

Read sequential events until the end of the block:

  // 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); 

In each event, delta is specified - the delay in units of time determined by the division factor, which should occur before this event. For events that should occur here de, delta is zero.

Meta events are events of type 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); 

The only kind of meta-events we are interested in is Tempo, a tempo value in microseconds. By default it is 500,000, that is, half a second, which corresponds to 120 beats per minute.

The remaining events are MIDI events defined by the first hexadecimal digit of their type. We are only interested in 0x90 - Note On, playing a note on the next available channel:

  // 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); } } } 

The velocity value is ignored, but if you wish, you can set the initial amplitude of the note on it. We skip the rest of the events, their length may be different. When an error occurs in the MIDI file, the LED turns on.

The microcontroller operates at a frequency of 16 MHz, so that quartz is not needed, you need to configure the built-in PLL accordingly. In order for the microcontroller to become Arduino-compatible, this practice of Spence Konde is applied. In the Board menu, select the ATtinyCore submenu, and there - ATtiny25 / 45/85. In the following menus choose: Timer 1 Clock: CPU, BOD Disabled, ATtiny85, 16 MHz (PLL). Then select Burn Bootloader, then fill the program. The programmer is used like SpinyFun's Tiny AVR Programmer Board.

The firmware for CC-BY 4.0, which already has a Bach fug in D minor, is here , the original MIDI file is taken here .

Source: https://habr.com/ru/post/454394/


All Articles