الحد الأدنى أربعة الجزء ميدي لاعب



لا يحتاج المشغل المقترح إلى بطاقة ذاكرة ؛ فهو يخزن ملف MIDI يصل طوله إلى 6000 بايت مباشرة في متحكم ATtiny85 (على عكس هذا التصميم الكلاسيكي ، الذي يشغل ملفات 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 بتردد 64 ميجاهرتز من PLL:

  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 ميجا هرتز على 256 ، وسنحصل على 250 كيلو هرتز.

عداد 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 ميغاهرتز بواسطة مقسم على 8 ، ثم على قيمة OCR0A 19 + 1 ، ويتم الحصول على 100 كيلو هرتز. المشغل أربعة صوت ، يتم الحصول على 25 كيلو هرتز لكل صوت. عند المقاطعة ، يتم استدعاء روتين معالجة ISR (TIMER0_COMPA_vect) ، والذي يحسب ويخرج الأصوات.

تم تكوين مؤقت الوكالة الدولية للطاقة لإنشاء مقاطعة كل 16 مللي ثانية ، وهو أمر مطلوب لاستقبال ترددات الملاحظة:

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

للحصول على تذبذبات من شكل معين ، يتم استخدام التوليف الرقمي المباشر. لا يوجد أي مضاعفة للأجهزة في ATtiny85 ، لذلك نأخذ نبضات مستطيلة ونضرب سعة الظرف بمقدار 1 أو -1. تنخفض السعة خطيًا ، ومن أجل حسابها في وقت معين ، يكفي تقليل قراءة العداد خطيًا.

يتم توفير ثلاثة متغيرات لكل قناة: Freq [] - التردد ، Acc -] - بطارية الطور ، Amp [] ، قيمة السعة المغلف. يتم تلخيص قيم Freq [] و Acc []. يتم استخدام بت عالية الترتيب Acc [] للحصول على نبضات مستطيلة. كلما زاد التكرار [] ، كلما زاد التردد. يتم ضرب الموجي النهائي بواسطة المغلف Amp []. يتم مضاعفة جميع القنوات الأربع وتغذيتها إلى الإخراج التناظرية.

جزء مهم من البرنامج هو الإجراء الخاص بمعالجة المقاطعة من عداد المؤقت 0 ، والذي يخرج التذبذبات إلى المخرجات التناظرية. يسمى هذا الإجراء على تردد حوالي 95 كيلو هرتز. للقناة الحالية 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]; 

يضيف تردد التكرار [c] إلى البطارية Acc [c]. كلما زاد التكرار [c] ، كلما تغيرت القيمة Acc [c] بشكل أسرع. ثم خط

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

يقلل من قيمة السعة لقناة معينة. الجزء (Amp [c]! = 0) ضروري حتى بعد أن تصل السعة إلى الصفر ، لا تنخفض أكثر. خط الآن

 Temp = Acc[c] >> 8; 

ينقل 9 بتات عالية من Acc [c] إلى Temp. والخط

 Temp = Temp & Temp<<1; 

يترك البت ذو الترتيب العالي لهذا المتغير مساويًا واحدًا إذا كان عدد البتتين المرتبتين بالترتيب العالي مساويا للواحد ، ويعين بت البتات ذات الترتيب العالي على الصفر إذا لم يكن كذلك. والنتيجة هي نبضات مستطيلة مع نسبة تشغيل / إيقاف 25/75. في أحد الإنشاءات السابقة ، طبق المؤلف تعرجًا ، بينما باستخدام الطريقة الجديدة ، تم الحصول على التوافقيات أكثر من ذلك بقليل. صف

 Mask = Temp >> 7; 

ينقل قيم الأكثر أهمية إلى البتات المتبقية للبايتة ، على سبيل المثال ، إذا كانت البتة الأكثر أهمية هي 0 ، فسيتم الحصول على 0x00 ، وإذا كان 1 - ثم 0xFF. صف

 Env = Amp[c] >> Volume; 

ينقل Amp [c] bit المحددة بواسطة قيمة Volume إلى Env ، بشكل افتراضي الأقدم ، لأن Volume = 8. String

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

كل هذا يوحد. إذا قناع = 0x00 ثم يتم تعيين ملاحظة القيمة Env. إذا كان Mask = 0xFF ، فسيتم تعيين Note لقيمة إضافية لـ Env + 1 ، أي Env بعلامة ناقص. ملاحظة تحتوي الآن على الشكل الموجي الحالي ، متغيرة من القيم الموجبة إلى السالبة للسعة الحالية. صف

 OCR1B = Note + 128; 

يضيف 128 إلى Note ويكتب النتيجة إلى OCR1B. صف

 c = (c + 1) & 3; 

إخراج أربع قنوات وفقا لمقاطعات المقاطعات ، مضاعفة الأصوات في الإخراج.

ترد 12 ملاحظة تردد في الصفيف:

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

يتم الحصول على ترددات ملاحظة الأوكتافات الأخرى بقسمة 2 ن . على سبيل المثال ، نقسم 10973 على 2 4 ونحصل على 686. سيتم تبديل البت العلوي Acc [c] بتردد 25000 / (65536/685) = 261.7 هرتز.

يؤثر متغيرين على الصوت: حجم - الصوت ، من 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. يمكن أن يكون عدد البايتات في هذه الحالة من واحد إلى أربعة:

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

تؤخذ سبعة بتات من كل بايت ، والثامن يساوي واحد إذا كنت بحاجة إلى قراءة بايت آخر بشكل أكبر ، أو صفر إن لم يكن.

يقوم المترجم باستدعاء روتين 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); 

في كل حالة ، يتم تحديد دلتا - التأخير في وحدات الوقت التي تحددها معامل القسمة ، والتي يجب أن تحدث قبل هذا الحدث. بالنسبة للأحداث التي يجب أن تحدث هنا ، فإن دلتا هي صفر.

أحداث التعريف هي أحداث من نوع 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 ، أي نصف ثانية ، وهو ما يعادل 120 نبضة في الدقيقة.

الأحداث المتبقية هي أحداث MIDI المعرّفة من قِبل الرقم الست عشري الأول من نوعها. نحن مهتمون فقط بـ 0x90 - ملاحظة 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); } } } 

نحن نتجاهل قيمة السرعة ، ولكن إذا كنت ترغب في ذلك ، يمكنك ضبط السعة الأولية للملاحظة عليها. نحن نتخطى بقية الأحداث ، يمكن أن يكون طولها مختلفًا. في حالة حدوث خطأ في ملف MIDI ، يتم تشغيل المصباح.

يعمل المتحكم الدقيق بتردد 16 ميجاهرتز ، وبالتالي فإن الكوارتز غير مطلوب ، تحتاج إلى تهيئة PLL المدمجة بشكل صحيح. من أجل أن يصبح متحكم Arduino متوافقًا ، يتم تطبيق تجربة Spence Konde هذه. في قائمة اللوحة ، حدد القائمة الفرعية ATtinyCore ، وهناك - ATtiny25 / 45/85. في القوائم التالية ، حدد: Timer 1 Clock: CPU ، تعطيل BOD ، ATtiny85 ، 16 MHz (PLL). ثم حدد Burn Bootloader ، ثم املأ البرنامج. يتم استخدام المبرمج مثل SpinyFun's Tiny AVR Programmer Board.

البرامج الثابتة لـ CC-BY 4.0 ، التي تحتوي بالفعل على fugue Bach في D قاصر ، هنا ، يتم أخذ ملف MIDI الأصلي هنا .

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


All Articles