公開日:2020/10/24
title.png

13th Season


#13-3 MMLで三和音を演奏する(仕上げ)

 前回、中途半端に終わってしまったMMLによる三和音演奏を仕上げて行きたいと思います。

MMLの拡張

 MMLも、三和音の演奏が可能な様に拡張しておきます。
 MMLの基本的な部分はUNOで実施した仕様に準じます。今回三和音に対応する必要があるのですが、前回説明した様に、play_mml()関数が受け付けるのは1チャンネル分の、和音を含まないMMLです。つまり、和音を含まないコアな部分のMML仕様(以降、コアMMLと呼びます)と、和音に対応した拡張部分のMML仕様(同じく、拡張MML)の2つを定義しておく必要があります(以下)。

 MML仕様:

 以下、拡張MMLの例です。

  # stand up - bow - take a seat
  C42,R02,G32,R02,C42,R02\  // チャンネル1
  ;E42,R02,F42,R02,E42,R02\  // チャンネル2
  ;C52,R02,B42,R02,C52,R02  // チャンネル3


 上に示した仕様と見比べながら、理解を深めるのに活用ください。後ほど実験する時にもいくつかMMLのテキストデータをリンクしてあります。

MMLの読み込み

 上で示した拡張MML仕様に従って、シリアルモニタ経由でMMLを受信するための処理について検討しておきます。

 全体像は完成版プログラム(play_triad.ino)でご確認ください。

実験で確認

 完成したプログラムを以下にリンクしておきます。

  完成版プログラム:play_triad.ino


 コンパイルしてDUEに書き込むと、シリアルモニタから以下のコマンドを受け付けるようになっています。

 DUEに三和音演奏回路を接続して、実験を進めましょう。以下、実験中の様子です。

a133_expr.jpg

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【,A44,R02;R04,A44;R02,A44】
  OK: short-cut (0)

 まずは、440Hzラ音が3回聴こえたらOKです。各チャンネルから音階を演奏できています。


 ここからは、テキストファイルの転送機能を持ったTera Termを使います。

  実験サンプル:【 】内はTera Termからの送信データ
  【「Fileメニュー ⇒ Send File」で、a133_order.txtを送信】
    : (省略)
  OK: exit play (0) 72 byte / 1 line

 三和音で、良く音楽の先生などがピアノで演奏するような「号令(起立、礼、休め)」が聞こえましたか?


 いくつか、三和音に拡張したMMLのテキストファイルをリンクしておきますので、同様に演奏して見てください(↓)。

ファイル 内容
a133_order.txt ピアノで演奏する号令(起立、礼、休め)
本来は右指2本+左指2本の計四和音のところ、
1音省いて三和音で演奏。
a133_timsig.txt 時報
秒を知らせるA4と、時刻(正時や分)を知らせるA5。
A5は長いので、次のA4と重なる。
a133_ardious.txt UNO+TFTで作ったゲームの効果音(2和音版)
a133_elise.txt 「エリーゼのために」ベートーベン

 Tera Termを使う場合には、「Fileメニュー ⇒ Send file」で上表のMMLファイルを指定すると、転送して演奏してくれるようになっています。シリアルモニタで使う場合には、面倒ですが1行づつコピー&ペーストしてください。

割り込み処理の負荷測定

 UNOでは1音演奏でしたので、PWMカウンタのオーバフロウ毎に割り込みをかけても大したことは無いだろうと思っていました。今回は三和音演奏可能と言う事で、割り込みの頻度が最大で×3倍に増加する懸念があります。そこで、割り込み処理の負荷を測定しておこうと思います。

 MML演奏には無関係な試行用の処理なので、#ifdefで無効化してありますが、完成版プログラム(play_triad.ino)の「#define DEBUG_LOAD」を有効にしてコンパイルすると、MMLを演奏した後で、その間に実行した割り込み処理の処理時間や割り込み周期等の統計情報を表示するようになっています。

  実験サンプル:【 】内はTera Termからの送信データ
  【,A41】
  DBG: cnt=5741 range=3~8 ave=3 itvl=348
  OK: short-cut (0)

  【,R41】
  DBG: cnt=40401 range=3~8 ave=3 itvl=49
  OK: short-cut (0)

 「DBG:」で始まる出力が、通常は表示されない割り込み処理の統計データです。割り込みの発生した回数cnt[回]、割り込み処理に要した時間のmin〜max時間range[μs]、平均処理時間ave[μs]、割り込みが発生する間隔の平均時間itvl[μs]です。
 割り込み処理に要した負荷率loadは、load=ave/itvl×100[%]で概算することが出来ます。loadが100%に近づくと、ほとんどのMCUタイムが割り込み処理に費やされてしまって、フォアグラウンドの処理が実行できないことを表します。十分小さいloadであれば、フォアグラウンドの処理がほとんど邪魔されずにBGMなどを演奏し続けることが出来ることになります。

 測定結果を改めてみてみると、",A41"(440Hzラ音)の負荷率が約0.9%と小さいのに対して、",R01"(休符)の負荷率が6.1%と、やや大きな値になっています。これは、前回検討した通り、無音部分では可聴域範囲外の高周波数PWMを出力する必要があり、割り込み周期が短くなっているためです。

 単音だけでなく、和音の演奏時も含めて統計データを取ってみました。以下表にまとめます。

MML cnt min max ave itvl load ply_MUTE 備考
,A41 5,741 3 8 3 348 0.9 20kHz A4全音符
10,853 3 8 3 184 1.6 40kHz
,A41;A41 11,549 3 8 3 173 1.7 20kHz A4全音符を2チャンネル
同時演奏
21,720 3 9 3 92 3.3 40kHz
,A41;A41;A41 12,239 3 12 3 163 1.8 20kHz A4全音符を3チャンネル
同時演奏
22,487 3 13 4 88 4.5 40kHz
,R01 40,401 3 8 3 49 6.1 20kHz 全休符
82,069 3 8 3 24 12.5 40kHz
,R01;R01 80,812 3 7 3 24 12.5 20kHz 全休符、2チャンネル同時
164,137 3 8 3 12 25.0 40kHz
,R01;R01;R01 80,802 3 13 3 24 12.5 20kHz 全休符、3チャンネル同時
164,142 3 13 3 12 25.0 40kHz
a133_ardious.txt 96,109 2 182 3 72 4.2 20kHz  
210,728 2 315 3 33 9.1 40kHz
a133_elise.txt 172,128 2 172 3 194 1.5 20kHz  
330,722 2 163 3 101 3.0 40kHz
a133_order.txt 206,505 3 313 3 29 10.3 20kHz  
406,476 3 315 3 14 21.4 40kHz

 以下、雑感です。

 当初、無音部分のPWM周波数を、確実に聞こえない高周波数にするため、40kHzに設定してあったのですが、実際に回路を組んで試したところ、20kHzでも全く聞こえないことが判ったので、現在のプログラムではply_MUTEを20kHzに設定してあります(以下コード参照)。

  const int ply_MUTE=F_CPU/64/20e3;  // ply: mute pwm period ~20kHz

 ただ、20kHzの音が聞こえなかった理由は不明です。恐らく、以下の様な理由(いずれか、または複合要因)でしょう。

 環境(回路)によって異なる可能性もあるので、各自の環境で実際に聞いてみて、ply_MUTEの値を調整して見てください。例えば、ply_MUTEを13kHz程度まで下げたところ、筆者の耳にも「キーーー」という高音が聞こえました。聞こえてしまうようだと周波数を下げ過ぎなので、聞こえなくなるまで周波数を上げる必要があります。
 ply_MUTEの周波数を上げると、確実に聞こえなくなる代わりに、play_mml()関数の負荷が上がり、フォアグラウンドの処理を圧迫します。上表の20kHzと40kHzの負荷率の違いを見比べていただくと一目瞭然です。

 ply_MUTEを20kHz程度まで落とせたとするなら、通常の音楽演奏では10%未満の負荷率なので、フォアグラウンドの処理にも十分なMCUパワーを費やすことが出来るでしょう。休符の演奏時が一番高負荷であるということは念頭に置いておいてください。
 なお、MMLの演奏が終了した時の無音部分では、PWMは出力し続けていますが、割り込みは禁止しているので、MCUに負荷はかかりません。注意すべきは、休符の演奏時だけです。

音が消える?

 単音演奏の場合には問題なく、全ての音が演奏されるのですが、今回の様に3音の同時演奏が可能な場合、異なる2チャンネルが同じ音階を演奏した場合に、音が打ち消し合って消えてしまう場合があります。
 下図をご覧ください。

a133_addsin.png

 周期4ms、周波数にして250Hzの正弦波が2つ(青線V1と赤線V2)表示されています。緑線はV1+V2の加算結果です。V1,V2は0Vを中心とした交流波形で、振幅は1Vです。さらに、V2に限り、少しづつ位相がずれるような数式を与えてあるため、V1とV2の位相は左端から右端までの間でちょうど1周期分ずれています。
 注目は緑線V1+V2で、V1とV2の山と山/谷と谷が重なっている、左端と右端ではV1+V2は互いに強め合って振幅が2倍の2Vに広がっています。
 一方、V1+V2の中央付近に目を移すと、振幅が振れず、ほぼ0になってしまっています。これは、V1とV2の山と谷/谷と山が重なったことで互いに打ち消し合ってしまったためです。
 複数チャンネルの演奏を可能としたことで、上図の様に2チャンネルから同じ音階を演奏すると、音が消えてしまう場合があります。以下の実験で試してみましょう。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【tmpo,2000】
  OK: tempo, whole note 2000[ms]

  【,R00,B31_,B31;B32,R02000,B32,R02000,B32,R02000,B32】
  OK: short-cut (0)

 ↑チャンネル1でB3(オクターブ3のシ)を4秒間(全音符×2)演奏し、その間にチャンネル2で同じB3を4回に分けて演奏(2分音符×4)しています。チャンネル2の方は、"R02000"という、見慣れないMMLが間に挟まっています。"R02000"は、2,000分休符です。【tmpo,2000】によって、全休符の長さを2,000msに設定していますので、2,000分休符は1msの休符となります。
 一方で、B3と言う音階の周波数は約247Hzなので、PWM周期は約4msとなります。"B34"を演奏する度に、休符"R02000"によって1ms=1/4周期の時間をずらしているので、4回鳴らす間に、1回は強め合い、1回は弱め合い、残り2回はその中間で聞こえるはずです。……期待通りなら、「強→中→弱→中」の順で聞こえるはず。

 どうでしたか?実は、筆者の実験では、「弱」のパートでも音は(完全には)消えませんでした。「消そうとして合わせ込んだつもりだったのに消せなかった」が、正しいです。PWM周期約4msに対して、ミリ秒オーダでタイミングをずらせたつもりなのですが、そこまで時間分解能が無いので、ぴったり山と谷/谷と山が重ならなかったのでしょう。
 逆に言うと、故意に1/2波長ずらせて音を消そうとしても消えなかったくらいですから、演奏していて、「たまたま」消えてしまう確率は相当に低いと考えて良いと思います。

PWMカウンタのリセット(同期化)

 前節の実験では、チャンネル1の演奏開始時点で、"R00"という特殊なMMLを実行しています。"R00"が実行されると、その時点で全チャンネルのPWMカウンタがリセットされます。全チャンネルで同時にカウンタをリセットすることで、位相を合わせることが可能となる仕様です。
 "R00"を実行した時のカウンタリセットコードは以下の様になります。

  // disable all channels
  REG_PWM_DIS=PWM_DIS_CHID3 | PWM_DIS_CHID2 | PWM_DIS_CHID1;

  // polling counter==0
  while (REG_PWM_CCNT3 || REG_PWM_CCNT2 || REG_PWM_CCNT1)
    ;

  // enable all channels
  REG_PWM_ENA=PWM_ENA_CHID3 | PWM_ENA_CHID2 | PWM_ENA_CHID1;


 PWMチャンネルを無効(disable)にして、カウンタがオーバフロウするまで待ちます。PWM波形が途中で崩れてしまわないように、カウンタがオーバフロウするまではカウントを続ける仕様になっているためです。オーバフロウすると、カウンタPWM_CCNTxが0で停止しますので、その時点で再び有効(enable)化すると、全チャンネル揃って1からカウントを再開します。
 PWMユニットのカウンタはリードオンリで書き込めないので、この方法しかリセットする手段はありません。

 "R00"実行直後であれば別ですが、普通に演奏している時の任意2チャンネル間の位相差は、その時のPWMユニットのカウンタ値次第なので、「必ず消える」とも、「必ず鳴る」とも、言い切ることが出来ません。とは言えど、2チャンネルから同じ音を鳴らしたいケースは少ないはずなので、ひとまず気にせずに普通に使って、万一、音が消えてしまうケースに遭遇したら、その時は"R00"等を使って位相を制御して見てください。

音量アップ

 この特性を前向き(肯定的)にとらえると、うまく3チャンネルの位相を合わせて同じ音を鳴らせば、強め合って振幅が×3倍となり、通常よりも大きな音で演奏することが出来るので、警告などのアラーム音を出す時に有効かもしれません。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【,R00,A41_;R03,A43_,A43_;R03,R03,A43_】
  OK: short-cut (0)

 どうですか?段々と音が大きくなって聞こえましたか?

まとめ

 筆者が一番最初に使ったパソコンFM-7は、PSG(Programmable Sound Generator)を搭載していて、三和音の演奏が可能でした。今回のチャレンジで、ようやく30年以上昔の8ビットパソコンと同じ、三和音の演奏に対応することが出来ました。
 そもそもDUEにはDA変換器が搭載されていて、WAVファイルの演奏が可能なので、その点では遥かにFM-7を凌駕しているのだけど、パフォーマンス的にはDUEのMCUパワーのほとんどを使ってしまうため、他の処理と並行してWAVファイルを演奏することは困難です。
 PWMを使って音を鳴らすこの方法(play_mml()関数)なら、バックグラウンドで演奏しながらフォアグラウンドでは別な処理を継続することが可能なので、いろいろと便利に使えると思いませんか?


 DUEにはPWMユニットに8チャンネルのPWM出力が有りますので、最大8和音まで実現可能ですが、外部のオペアンプ回路を作るのが大変だし、MMLだって、8和音分のデータを作るのはしんどくなってしまいますから、三和音くらいがちょうど使い易くて良いと思います(1^ー^)v。


btn_arduino.png

btn_prev.png btn_next.png

home