今までのプログラムに、前回説明した楽譜データ "gakufu.h" を読み込み曲を演奏する機能を追加します。
楽譜データを読込み内容を把握し、プログラムの流れを変更するのは、多少複雑な制御が必要です。そんな複雑な流れの制御には、「ステート制御」を使用するとプログラムしやすくなります。
1 使用する回路
Criosiry開発ボードの RC5 (PWM) に圧電スピーカーまたは、アンプ付きスピーカーを接続します。簡易的には、圧電スピーカを接続して動作を確認できますが、良い音を出すにはローパスフィルターを経由させたアンプ付きの外部スピーカを利用します。
2 プログラム概要
プログラムのメインループは、1ms 毎に実行される「タイミング」ブロックと常時実行される「ステート制御」ブロックから構成されます。
タイミングブロックでは、オルゴールの音に近づけるため音を徐々に小さくするプログラムと、音符の演奏時間を決めるプログラムが書かれています。
ステート制御ブロックでは、SWをモニターし楽譜から音符を取得して演奏を指示するプログラムが書かれています。
「ステート制御」ブロック
各ステートの詳細は以下のとおりです。
- newPlay
- 動作に必要となる各パラメータの初期化をします。
出力はゼロ、楽譜の演奏位置を示すポインタを、先頭に戻します。 - ckSW
- プッシュSWが押されたかを監視し、押されると次のステートに移行します。
- getTempo
- 楽譜データの最初のデータを読み込みTempoの1単位が何msになるか計算します。
- getOnp
- 楽譜データの次の音符データを読み込み、その音符の演奏時間と音の高さを計算し演奏の準備をします。準備だけで実際の演奏はまだ行いません。
- なお、読み込んだデータが楽譜の終了を示す 0xFFFF だった場合には、長さFF単位の休符に変換されます。
- waitEnd
- 直前の音符の演奏終了を待ちます。音符休符の長さが演奏範囲であれば次の startOnp ステートに進みます。楽譜の終了を示す長さFF単位の場合 newPlay ステートに分岐します。
演奏終了を待つのと同時に、プッシュSWのモニターも行い、もしSWが押されたら、全てを初期化する newPlay ステートに分岐します。 - startOnp
- 演奏準備したパラメータを書き込み、演奏を開始します。書き込んだ後は、次に演奏する音符情報の準備のため getOnp に戻ります。
3 楽譜データの保存
前回の記事の方法で作成した楽譜データ gakufu.h をプロジェクトに取り込みます。
4 main.c の変更
main.c ファイルを開き、内容を下のリストの様に変更します。
、MPLAB X プロジェクトを下のリンクからダウンロードですることができます。
---> 1619_orgel_6.zip
// ###################################### // オルゴール プログラム 6 // 楽譜を演奏 // 2021-04-20 // ###################################### // 音が減衰する程度 指定値(ms)で1/16減少 #define EnvConst 30 #include "mcc_generated_files/mcc.h" #include "gakufu.h" #define LED LATA5 #define SWpressed LC1OUT // クリーンなSW出力 #define SWreset() C1POL=0;C1POL=1 // SW出力のリセット #define newPlay 0 // 制御のステート名 #define ckSW 1 #define getTempo 2 #define getOnp 3 #define waitEnd 4 #define startOnp 5 // 音の高さを決める定数 for 8MHz Fosc PWM Freq 15.625kHz const unsigned int dW_tbl[] = { // Do, Do#, Re, Re#, Mi, Fa, Fa#, So, So#, Ra, Ra#, Si, 0,274, 291, 308, 326, 346, 366, 388, 411, 435, 461, 489, 518, 549, 581, 616, 652, 691, 732, 776, 822, 871, 923, 978, 1036, 1097,1163,1232, 1305, 1383, 1465, 1552, 1644, 1742, 1845, 1955, 2071, 2195,2325,2463, 2610, 2765, 2930, 3104, 3288, 3484, 3691, 3910, 4143, 4389,4650,4927, 5220, 5530, 5859, 6207, 6577, 6968, 7382, 7821, 8286, 8779,9301,9854,10440,11060,11718,12415,13153,13935,14764,15642,16572 }; // 0x892(index 37) が中心のド(60 0x3C) ,3691 0xD9B はラ (440Hz) // 共通変数 ####################################### uint16_t accWave = 0; // 波形周期計算 積算値 uint16_t incWave = 0; // 波形周期計算 増加値 uint8_t amp = 0; // 波形振幅 bool wave = false; // 波形フェーズ // ユーザーの割り込み関数のプロトタイプ void MyTMR2_ISR(void); /****** Main application ******/ void main(void) { uint16_t nextIncWave; // 波形周期計算 増加値 uint16_t nextRemnTime; // 演奏時間残り ms uint16_t onpWord; // 音符データ uint16_t remnTime = 0; // 音符演奏残り時間 uint16_t tempo; // テンポ時間 ms uint8_t nextAmp; // 次の波形振幅 uint8_t EnvCnt = EnvConst; // 音量減衰時間 uint8_t onpLEN; // 音符 長さ uint8_t onpMIDI; // 音符MIDI番号 uint8_t ptrOnp; // 音符データポインタ uint8_t state = newPlay; // 制御ステート SYSTEM_Initialize(); // ユーザーの割り込み関数を指定する TMR2_SetInterruptHandler(MyTMR2_ISR); INTERRUPT_GlobalInterruptEnable(); // 割込を有効化 INTERRUPT_PeripheralInterruptEnable(); while (1) { // 1ms毎に TimeUp (タイミング) ########################## if(TMR6IF){ // 1ms毎に TimeUp TMR6IF = 0; // TimeUp flag クリア // --------------------------------------------- if(EnvCnt-- == 0){ // 30ms毎に EnvCnt = EnvConst; // amp = (int)amp * 15/16; // 音量を -6% する if(amp > 100)LED = 1; // 音量大で LED On else LED = 0; // 音量小で LED Off } // ----------------------------------------------- if(remnTime) remnTime--; // 残り時間を1ms減少 } // Main controls (ステート制御) ####################### switch(state){ case newPlay: // 新しく演奏準備 ----- incWave = 0; // 各設定値を初期化 wave = false; amp = 0; ptrOnp = 0; state = ckSW; break; case ckSW: // SWが押されるのを待つ----- if(SWpressed){ // SWが押されたか SWreset(); // SWフラグクリア state = getTempo; } break; case getTempo: // テンポ値を取得 ----- tempo = 7500 / p1gkf[ptrOnp++]; // Tempo単位をmsに変換 state = getOnp; break; case getOnp: // 音符情報を取得 ----- onpWord = p1gkf[ptrOnp++]; // 音符情報を取得 onpLEN = onpWord & 0xFF; // 長さ取得 nextRemnTime = onpLEN * tempo; // 演奏時間ms計算 onpMIDI = onpWord >> 8; // MIDI番号取得 if(onpMIDI < 24) onpMIDI = 0; // 演奏不可音符は if(onpMIDI > 95) onpMIDI = 0; // 休符に変換 if(onpMIDI){ // 音符なら nextIncWave = dW_tbl[onpMIDI-23]; // 波形加算値を取得 nextAmp = 0x7F; // 音量最大 }else{ // 休符なら nextAmp = 0; // 音量ゼロ } state = waitEnd; break; case waitEnd: // 前の音符演奏終了をを待つ----- if(SWpressed){ // SW押されたら SWreset(); // SWリセット state = newPlay; // 先頭に戻る break; } if(remnTime) break; // 前の音符演奏終了を待つ if(onpLEN <= 32) // 音符休符なら state = startOnp; // 演奏する else // 曲終わりなら state = newPlay; // 先頭に戻る break; case startOnp: // 新たな音符炎症の情報をセット----- amp = nextAmp; // 各設定値を remnTime = nextRemnTime; // セットすると incWave = nextIncWave; // 演奏が始まる state = getOnp; break; } // end swtich ===== } } // ユーザーの割り込み関数 void MyTMR2_ISR(void){ accWave += incWave; // 周期計算 if(STATUSbits.CARRY)wave ^= 1; // キャリが出れば反転 if(wave)PWM3DCH = amp; // 波形 Hi ならamp else PWM3DCH = 0; // 波形 Low ならゼロを } /** End of File **/ /** End of File **/
5 プロジェクトのコンパイルと書き込み
Program アイコンをクリックし、PICにプログラムを書き込みます。
6 動作の確認
書き込み終了後に、スピーが、RC5(PWM)端子に接続されているのを確認し、Curiosity開発ボードの S1 を押します。
夕焼け小焼けがスピーカーから聞こえるでしょう。
曲の途中でSWを押すとその時点で演奏が終了します。
7 次回の予定
次回は、和音オルゴールについて考察します。