
Pemain yang diusulkan tidak memerlukan kartu memori; ia menyimpan file MIDI hingga 6.000 byte secara langsung di mikrokontroler ATtiny85 (tidak seperti desain klasik
ini , yang memutar file WAV, secara alami membutuhkan kartu memori). Pemutaran empat arah dengan atenuasi menggunakan PWM diimplementasikan dalam perangkat lunak. Contoh terdengar di
sini .
Perangkat ini dibuat sesuai dengan skema:

Kapasitor elektrolit antara mikrokontroler dan kepala dinamis tidak akan kehilangan komponen konstan jika unit logis muncul pada output PB4 sebagai akibat dari kegagalan perangkat lunak. Induktansi head tidak melewati frekuensi PWM. Jika Anda memutuskan untuk menghubungkan perangkat ke amplifier, untuk menghindari overloading yang terakhir dengan sinyal PWM, Anda perlu menambahkan filter low-pass, seperti di
sini .
File MIDI harus ditempatkan di sumber firmware sebagai larik formulir:
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 };
Ada solusi siap pakai untuk mengonversi file ke format ini pada sistem operasi mirip UNIX - utilitas xxd. Kami mengambil file MIDI dan melewati utilitas ini seperti ini:
xxd -i musicbox.mid
Konsol akan menampilkan sesuatu seperti:
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 adalah panjang dalam byte. Ternyata kurang dari 6000 - itu berarti cocok. Urutan angka heksadesimal melalui clipboard ditransfer ke sketsa (ingat saja:
di konsol - tidak ada Ctrl + C ) alih-alih array default. Atau jangan lakukan semua ini jika kita ingin meninggalkannya.
Penghitung waktu 1 akan beroperasi pada frekuensi 64 MHz dari PLL:
PLLCSR = 1<<PCKE | 1<<PLLE;
Kami mentransfer timer ini ke mode PWM untuk bekerja sebagai DAC, siklus tugas akan tergantung pada nilai 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
Frekuensi pulsa persegi panjang tergantung pada nilai OCR1C, kita biarkan sama dengan 255 (secara default), maka frekuensi 64 MHz akan dibagi dengan 256, dan kami mendapatkan 250 kHz.
Penghitung waktu 0 akan menghasilkan interupsi:
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
Frekuensi clock 16 MHz dibagi dengan pembagi dengan 8, dan kemudian dengan nilai OCR0A 19 + 1, dan 100 kHz diperoleh. Pemain empat suara, 25 kHz diperoleh untuk setiap suara. Setelah terputus, rutin pemrosesan ISR disebut (TIMER0_COMPA_vect), yang menghitung dan mengeluarkan suara.
Pengawas waktu diatur untuk menghasilkan interupsi setiap 16 ms, yang diperlukan untuk menerima frekuensi catatan:
WDTCR = 1<<WDIE | 0<<WDP0; // Interrupt every 16ms
Untuk mendapatkan osilasi dari bentuk yang diberikan, sintesis digital langsung digunakan. Tidak ada perkalian perangkat keras dalam ATtiny85, jadi kami mengambil pulsa persegi panjang dan mengalikan amplitudo amplop dengan 1 atau -1. Amplitudo berkurang secara linear, dan untuk menghitungnya pada waktu tertentu, itu cukup untuk secara linear mengurangi pembacaan meter.
Tiga variabel disediakan untuk setiap saluran: Frek [] - frekuensi, Acc [] - baterai fase, Amp [], nilai amplitudo amplop. Nilai-nilai Freq [] dan Acc [] dijumlahkan. Bit orde tinggi Acc [] digunakan untuk mendapatkan pulsa persegi panjang. Semakin banyak Frekuensi [], semakin tinggi frekuensinya. Bentuk gelombang yang sudah selesai dikalikan dengan amplop Amp []. Keempat saluran multiplexing dan diumpankan ke output analog.
Bagian penting dari program ini adalah prosedur untuk memproses interupsi dari penghitung waktu 0, yang mengeluarkan osilasi ke keluaran analog. Prosedur ini disebut pada frekuensi sekitar 95 kHz. Untuk saluran saat ini c, itu memperbarui nilai Acc [c] dan Amp [c], dan juga menghitung nilai catatan saat ini. Hasilnya dikirim ke register perbandingan OCR1B dari penghitung waktu OCR1B untuk mendapatkan sinyal analog pada 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; }
Tali
Acc[c] = Acc[c] + Freq[c];
menambahkan frekuensi freq [c] ke baterai Acc [c]. Semakin besar Frekuensi [c], semakin cepat nilai Acc [c] akan berubah. Kemudian berbaris
Amp[c] = Amp[c] - (Amp[c] != 0);
mengurangi nilai amplitudo untuk saluran tertentu. Fragmen (Amp [c]! = 0) diperlukan sehingga setelah amplitudo mencapai nol, tidak berkurang lebih jauh. Sekarang berbaris
Temp = Acc[c] >> 8;
mentransfer 9 bit Acc [c] tinggi ke Temp. Dan garis
Temp = Temp & Temp<<1;
meninggalkan bit orde tinggi dari variabel ini sama dengan satu jika dua bit orde tinggi sama dengan satu, dan menetapkan bit orde tinggi ke nol jika tidak. Hasilnya adalah pulsa persegi panjang dengan rasio on / off 25/75. Dalam salah satu konstruksi sebelumnya, penulis menerapkan berliku-liku, sedangkan dengan metode baru, harmonik diperoleh sedikit lebih banyak. Tali
Mask = Temp >> 7;
mentransfer nilai yang paling signifikan ke bit byte yang tersisa, misalnya, jika bit paling signifikan adalah 0, maka 0x00 akan diperoleh, dan jika 1 - lalu 0xFF. Tali
Env = Amp[c] >> Volume;
mentransfer bit Amp [c] yang ditentukan oleh nilai Volume ke Env, secara default yang senior, karena Volume = 8. String
Note = (Env ^ Mask) + (Mask & 1);
Semua ini menyatukan. Jika Mask = 0x00 maka Note diberikan nilai Env. Jika Mask = 0xFF, maka Note diberi nilai tambahan untuk Env + 1, yaitu, Env dengan tanda minus. Catatan sekarang berisi bentuk gelombang saat ini, berubah dari nilai positif ke negatif dari amplitudo saat ini. Tali
OCR1B = Note + 128;
menambahkan 128 ke Note dan menulis hasilnya ke OCR1B. Tali
c = (c + 1) & 3;
output empat saluran sesuai dengan interupsi yang sesuai, multiplexing suara pada output.
Dua belas frekuensi note diberikan dalam array:
unsigned int Scale[] = { 10973, 11626, 12317, 13050, 13826, 14648, 15519, 16442, 17419, 18455, 19552, 20715};
Frekuensi nada oktaf lain diperoleh dengan membaginya dengan 2
n . Sebagai contoh, kita membagi 10973 dengan 2
4 dan kita mendapatkan 686. Bit atas [c] akan beralih dengan frekuensi 25000 / (65536/685) = 261,7 Hz.
Dua variabel memengaruhi suara: Volume - volume, dari 7 hingga 9 dan Decay - redaman, dari 12 menjadi 14. Semakin tinggi nilai Decay, semakin lambat redamannya.
Penerjemah MIDI yang paling sederhana hanya memperhatikan nilai-nilai catatan, tempo dan koefisien pembagian, dan mengabaikan data lainnya. Rutin readIgnore () melewatkan jumlah byte yang ditentukan dalam array yang diterima dari file:
void readIgnore (int n) { Ptr = Ptr + n; }
Rutin readNumber () membaca angka dari jumlah byte tertentu dengan akurasi 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; }
Rutin readVariable () membaca angka dengan presisi variabel MIDI. Jumlah byte dalam kasus ini dapat dari satu hingga empat:
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; }
Tujuh bit diambil dari setiap byte, dan yang kedelapan sama dengan satu jika Anda perlu membaca byte lain lebih lanjut, atau nol jika tidak.
Interpreter memanggil noteOn () rutin untuk memainkan note di saluran yang tersedia berikut ini:
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; }
Variabel Ptr menunjukkan byte berikutnya untuk dibaca:
void playMidiData () { Ptr = 0; // Begin at start of file
Blok pertama dalam file MIDI adalah header yang menunjukkan jumlah trek, tempo, dan rasio pembagian:
// 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;
Koefisien pembagian biasanya sama dengan 960. Sekarang kita membaca jumlah blok yang diberikan:
// Read track chunks for (int t=0; t<tracks; t++) { type = readNumber(4); if (type != MTrk) error(2); len = readNumber(4); EndBlock = Ptr + len;
Baca peristiwa berurutan sampai akhir blok:
// 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);
Dalam setiap peristiwa, delta ditentukan - penundaan dalam satuan waktu yang ditentukan oleh koefisien pembagian, yang harus terjadi sebelum peristiwa ini. Untuk peristiwa yang seharusnya terjadi di sini, delta adalah nol.
Meta-events adalah acara bertipe 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);
Satu-satunya jenis meta-event yang menarik minat kami adalah Tempo, nilai tempo dalam mikrodetik. Secara default, itu adalah 500.000, yaitu, setengah detik, yang setara dengan 120 denyut per menit.
Peristiwa yang tersisa adalah peristiwa MIDI yang ditentukan oleh digit heksadesimal pertama dari tipenya. Kami hanya tertarik pada 0x90 - Note On, memainkan catatan pada saluran yang tersedia berikut:
// 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); } } }
Kami mengabaikan nilai kecepatan, tetapi jika Anda mau, Anda dapat mengatur amplitudo awal not di atasnya. Kami melewatkan sisa acara, panjangnya bisa berbeda. Jika kesalahan terjadi pada file MIDI, LED menyala.
Mikrokontroler beroperasi pada frekuensi 16 MHz, sehingga kuarsa tidak diperlukan, Anda harus mengkonfigurasi PLL bawaan dengan benar. Agar mikrokontroler menjadi kompatibel dengan Arduino,
pengalaman Spence Konde ini diterapkan. Di menu Papan, pilih submenu ATtinyCore, dan di sana - ATtiny25 / 45/85. Dalam menu berikut, pilih: Timer 1 Jam: CPU, BOD Disabled, ATtiny85, 16 MHz (PLL). Kemudian pilih Burn Bootloader, lalu isi program. Programmer digunakan seperti Tiny AVR Programmer Board SpinyFun.
Firmware untuk CC-BY 4.0, yang sudah memiliki fugue Bach di D minor, ada di
sini , file MIDI asli diambil di
sini .