今までのプログラムに、前回説明した楽譜データ "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 次回の予定

次回は、和音オルゴールについて考察します。