公開日:2019/05/18
title.png

5th Season


#5-8 MMLで音楽を奏でる

 MML(Music Macro Language)を使って、Arduinoに音楽を演奏させようと思います。

MMLの仕様(Arduino用)

 「鍵盤ハモニカで演奏する」というCGを作った時に使ったMMLを使いますので、詳しい仕様については、「鍵盤ハモニカで演奏する」のページをご参照ください。
 ただし、Arduinoで認識する音階は、「A〜G」(ASCII)の音階コードのみとし、「ド〜シ」(S-JIS)の音階コードは対象外とします。

 Arduinoで音を鳴らす時、音階+オクターブから周波数に変換する必要が生じるのですが、以下式で計算することができます。

frequency.png

 音階番号dについては、「鍵盤ハモニカで演奏する」を参照。

mark.png

 上図は、音の長さを表す記号です。全音符が基準となって、4分音符なら全音符の1/4の長さ、16分音符なら全音符の1/16の長さで演奏することを示しています。
 時々、楽譜の上の方に「♩=120」などの表示がありますが、この場合、「4分音符が1分間に120個入る長さ」と、なります。全音符に換算すると、全音符を2秒で演奏するスピードです。

 今回Arduino用に起こす必要があるのは、MMLの記載された文字列を引数として受け取って、音楽を奏でる関数play_mml()です。MMLから、音階に相当する周波数と、音符の長さを計算して、実際に鳴らす音のパラメータに変換します。

※具体的な方法については、完成版プログラム(play_mml.ino)と「鍵盤ハモニカで演奏する」を照らし合わせてご確認ください。

※「#13-3 MMLで三和音を演奏する(仕上げ)」にて、DUEのplay_mml()関数を三和音演奏対応としたのに伴い、MML仕様をブラッシュアップしました。新しいMML仕様はこちらをご覧ください。基本的には後方互換性を保っていますので、今までのMMLはそのまま使用可能です。

鳴らし方

 周波数と長さがわかったので、後は、どうやって、Arduinoを使って音を鳴らすか?ですね。何らかの方法で繰り返し波形を作り、それを指定された時間だけ出力すれば良いのですよね?

 どこに出力しますか?

 アクティブブザーやパッシブブザーでは(「#3-7 ブザーとサウンドセンサ」)、音階の演奏は無理そうでしたよね?

※後日、パッシブブザーを使って、音階の演奏が可能なことを確認したので、記事をアップデートしておきました。パッシブブザーを使う場合、多少問題があるので、こちらのアップデート記事をご参照ください。

 スピーカ?Arduinoから、直接スピーカとかを駆動したいけれども、何だか大変そうじゃないですか?そもそも、出力(電力)が足りなさそうだし、インピーダンスやらアンプやら、どういった回路を作って良いものやら、見当もつきません(1^^;)。
 でも、イヤホンくらいなら、鳴らせるかもしれません。耳に入れている時には聞こえている音も、耳から外してしまうと、イヤホンからはほとんど音が聞こえませんので、それだけ小さいエネルギで鳴っているのでしょう。

 イヤホンに出力する信号は、無音状態が0Vで、+/−に振れる交流波形です。VP-P(交流波形の山から谷までの電圧範囲)が0.5〜1[V]程度の信号らしいです。ArduinoのI/Oは5Vなので、VP-P=5Vと、ちょっと振幅が大きすぎですね。しかも、0〜5[V]のDC波形ですので、0Vセンタの「交流波形」とも、異なります。

 ここで、「#5-4 デジタルオシロスコープ(STM32対応)」の時に、PWM波形をLPF(Low Pass Filter、CR回路)に通した場合の波形を観測したことを思い出してみましょう(下図)。

ar54_wavep.png

 目測ですが、VP-P≒0.3V程度の波形になっていることがわかります。上図を測定した時のPWMデューティは64/255でした。三角形の波形は、CR回路の充放電に起因するので、デューティを50%にした時が一番振幅が大きくなります。その状態でDC成分をカットしてあげたら、イヤホンに入力する信号として、丁度良いのではないでしょうか?
 三角波の周波数は、PWMの周波数ですので、PWM周波数を音階に相当する周波数に変更すれば、イヤホンからその音階の音が聞こえるはずです。

回路図

 回路図は、以下の様になるでしょうか?

ar58_schem.png

 ArduinoのPWM出力(D9)をLPF(R1、C1)に通して所望の周波数と振幅を持った波形に変換します。その信号のDC成分をカットするために、カップリングコンデンサ(C2)を通して、イヤホンに接続します。
 たぶん、この回路で良いはずですが、筆者の専門分野ではないので、正直、自信はありません。

 よって、真似して実験する時は、壊れても惜しくないようなイヤホンを使ってください。

 ありませんか?何かのおまけについてきた形ばかりのイヤホン。音も安っぽいので、とても音楽を聴く気になれずに、しまい込んであるような……。筆者宅でちょっと探したら、こんなに(↓)ありました。

ar58_earphone.jpg

 今回使用したイヤホンは、ステレオタイプだったので、ミニプラグは以下図左側のように3つに分かれています(根本側からGND/R/L)。

ar58_mini.jpg

 ミニプラグの部分に針金を巻き付けたり、オーディオ延長コードを切断してメスのミニプラグから配線を引き出したりと、方法は色々ですが、上図のミニプラグとコンタクトして、回路図のear_plとear_miに接続します。
 筆者は、上写真右側の様にワニ口クリップでコンタクトしました。

※ワニ口クリップを使う時は、ワニ口の歯の部分でミニプラグに傷をつけてしまう場合があるので、ご注意を。傷がついてメッキが剥げると、そこから錆びて使い物にならなくなります。

 今回使用したのはステレオイヤホンだったので、左右から別な音を出して和音にしてみたりとか、そういうことも可能(PWM出力を2本使う)ですが、今回のMMLがステレオや和音には対応していないため、モノラルのイヤホンでも良いです。その場合は、ミニプラグが2つに分かれていて、根本側がGND、先側が信号入力です。

音量

 音量を決めるのは、LPF通過後の三角波の波高値(VP-P)です。これは、CR回路の充放電電圧波形の一部であり、そのスピードは時定数C×Rで決まっているのでしたね。元は5V振幅のPWMですので、時定数CRが小さくなると、急速に5Vおよび0Vへ向かって充放電が進み、結果としてVP-Pが大きくなります。逆に時定数CRが大きくなると、充放電に起因する波形が鈍って、電圧の上昇・下降が緩やかになるため、VP-Pが小さくなります。実験で確かめてみましょう。
 以下図は、LPFの容量値(C1)を固定して、抵抗値(R1)を変化させたときの出力波形をLogiscopeで測定した物です。

ar58_lpf.png

※「A21.」を鳴らした時の波形(PWM周波数=110Hz)。解り易い様に、330Ωの波形も取ってみましたが、たぶん、音量が大きすぎなので、ご注意ください(VP-P≒3Vくらいある)。

 デューティは50%に固定してあるので、平均電圧は2.5V程度となっています。
 イヤホンに入力する交流波形のVP-Pは、音量となって認知されます。VP-Pが小さいと小さな音、VP-Pが大きいと大きな音として聞こえるわけです。すなわち、前節で示した回路では、LPFのパラメータ、時定数CRが音量を決めていることになります。
 実際に聞いてみて、お好みの音量になるように時定数CRを調整してみてください。

波の形(音色)

 あと、見ての通り、PWMのLPF通過波形は、三角形の波型(鋸歯状)波形です。正弦波ではないので、音叉を鳴らした時のような、綺麗な音では鳴りません。

 正弦波と聞いて、「#5-6 正弦波を生成する」を思い出す(下図)かもしれませんが、

ar56_sin2.png

 上図の正弦波には、ギザギザの波形が残ってしまうため、ざらざらした汚い音になって、使い物になりません。返って、LPF出力そのままの三角波の方が、波形は綺麗(ノイズは乗っていない)なので、そこそこの音が鳴ります。

※実際に上の正弦波で音を鳴らしてみると、「ブ〜〜〜〜」という連続音ではなく、一定の周期を持って、「ブ〜ンブ〜ンブ〜ン」と、鳴っていました。これは、PWM周期と正弦波の周期が一致していないために起こるものと推測できます。いずれにしても、上図の正弦波を使って音を鳴らすという目論見は、外れです。

カップリングコンデンサ

 出力のDC成分をカットするために入れたカップリングコンデンサC2=470μFですが、ちょっと大きいですよね……。でも、計算したらこうなってしまいました。一般に、イヤホンのインピーダンスとして、約32Ωと言う値が使われるのですが、この値を使うと、イヤホンを繋いだ時の回路モデルは以下図の様になります。

ar58_schem2.png

 青破線から右の回路は、青破線より左の回路と比べて、CとRの位置が逆転しています。このような回路をHPF(High Pass Filter)と呼びます。HPFは、その名の通り、高い周波数成分を通し、低い周波数成分は通さない回路のことで、DC成分をカットしようとした本来の目的には合致しています。

 一方、ここを通るのは20〜20k[Hz]という可聴域の信号なのですが、20Hzという低い音も、十分低周波数成分に分類されてきます。つまり、HPFのカットオフ周波数fc次第では、低音域が遮断され、イヤホンから聞こえなくなってしまうことが考えられます。
 以下図は、横軸に周波数、縦軸に出力ゲインを取った時のHPFの周波数特性の模式図です。

ar58_hpf.png

 図中に記載した通り、カットオフ周波数fc=1/(2πRC)ですので、R=32Ω、fc=10Hz(可聴域下限の20Hz+マージン)を代入してCについて解くと、C≒498μFとなったわけです。

※実際のところ、イヤホンの等価回路はもっと複雑で、L、C成分も入っています。単純にRだけの等価回路と比べると、特に交流信号に関してはだいぶ雰囲気が違ってくるのではないか?と思われます。C≒498μFの計算結果は、抵抗だけの等価回路&DC信号に対する計算から導かれたものだということに留意してください。

 手元には、こんな大きなコンデンサは無く、一番大きいもので100μFでしたので、これを使いました。この場合fc≒50Hzなので、50Hzを下回る低音は、ゲインが低下して、音量が小さく聞こえるかもしれません。かなり低い音なので、普通の音程を奏でるだけなら、問題ないと思います。

 もっと言うと、普通に「A4(440Hzラ音)」辺りの音を鳴らすだけなら、1μFもあれば十分かと思います。この場合、HPFでかなりゲインが落ちますので、その分、LPF出力のVP-Pを上げて相殺してしまえば、十分聞こえる音量に調整可能です。

※ただし、ゲインの落ちた低音域で音量を調整してしまうと、ゲインの落ちない高音域の音量が大きくなってしまうので、高音を鳴らす際には音量にご注意ください。

音階を鳴らす

 ハードウェアの方は固まりましたね……。次はソフトウェア。Tone.hと言うライブラリがあるらしいです。それを使ったら終わりだけれども、他人の力を借りるほどのことでもないので、自分で起こしてみます。
 play_mml()が、音階から周波数を求めてあるし、音を鳴らす時間も求めてあるので、あとは、指定した周波数で、指定した時間だけPWMを出力できたらそれでおしまい。もう何回もPWMは作って来たので、16ビットタイマ(TC1)を使って、デューティ50%のPWMを所望の周波数で所望の時間だけ出力します。
 PWM出力には、16ビットタイマ(TC1)を使います。

レジスタ名 アドレス 設定値 説明
TCCR1A 0x0080 0x41 波形モード:Phase and Frequency Correct PWM Mode
出力モード:toggle OC1A
TCCR1B 0x0081 0x12 波形モード:Phase and Frequency Correct PWM Mode
クロック選択:clkio/8
OCR1A 0x0088 0x0000 周波数に合わせて設定する。
0x0000にセットすると、PWM出力が停止する。

 波形モードを「Phase and Frequency Correct PWM Mode」で、TOP(カウンタの上限値)指定をOCR1Aで行うモードにします。このモードでコンペアマッチ出力OC1Aの出力モードを「トグル」にしておくと、タイマ周期の2倍の周期を持ったデューティ50%のPWM波形を出力することが可能です。
 タイマのカウントクロックをclkio/8にします。clkioは16MHzなので、PWM周波数fPWMは、fPWM=clkio/8/(2×TOP)/2[Hz]となります。TOPはOCR1Aの設定値なので、1〜65,535の間で指定できます。この時、fPWM≒500k〜7.6[Hz]となります。可聴域は20〜20k[Hz]ですので、カウントクロックを変更することなく、OCR1Aの値を変更するだけで、可聴域の周波数をカバー(出力)することができます。
 OCR1Aについては、所望の周波数に合わせて毎回設定します。上表の値は初期値として設定した値で、PWM出力を停止する設定です。出力モードがトグルなので、止めた瞬間のレベル次第で、0Vで停止するか、5Vで停止するかコントロールできませんが、DCカットして出力するので、どちらで止まっても問題はないはずです。

 実際に音を鳴らす関数は以下の様になります。

  void play_scale (
  float frq,
  uint32 len)
  {
    // play frq [Hz] tone len [ms]
    uint16 peri;

    if (frq<20. || 20e3<frq)
    {
      // rest
      delay (len);
      return;
    }
    peri=uint16 ((F_CPU/(8*2*2))/frq + (0.5 - 1.));

    // set PWM
    noInterrupts ();
    OCR1A=peri;
    interrupts ();

    delay (len*7/8);

    // PWM off
    noInterrupts ();
    OCR1A=0x0000;
    interrupts ();

    delay (len*1/8);
  }


 指定時間lenの間、ずっと音を出し続けてしまうと、「C44,C44」などの様に同じ音階が続いた時に音がつながってしまって聞こえが良くないので、len時間の7/8まで音を鳴らしたら、残りの1/8期間は無音にしています。

 それと、frq=0など、可聴域外の周波数が渡された場合には、「休符」として扱い、指定時間lenだけ待って戻るようにしてあります。

MMLを作って演奏させてみる

 完成版プログラムをリンクしておきます。

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

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

 では、鳴らしてみましょう。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【play】
  OK+ receiving mml... ('end' to exit receive)
    【A42】
    【end】
  OK: exit play (0)

 イヤホンから、調音の基準となる440Hzラ音が聞こえたら成功です。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【,A41,A31,A21,A11,A01】 …… ラ音をオクターブを下げながら演奏
  OK: short-cut (0)

  【,A41,A51,A61,A71,A81,A91】 …… ラ音をオクターブを上げながら演奏
  OK: short-cut (0)

 ちゃんと低音〜高音まで聞こえましたか?筆者の環境では、「A91」は全く聞こえませんでした(周波数にして約14kHz)。そもそも、交流波形を作るために使っているLPFで遮断されてしまったのか、筆者の耳で聞こえなかっただけなのかは定かではありませんが……。

 では、MMLで1曲分のデータを記述して、鳴らしてみましょう。

曲名 備考
きらきら星 鍵盤ハモニカで演奏する」で作ったデータ。
Arduino用にASCII文字に変換したもの。
カエルの合唱 WAVファイルのミキシング」で作ったデータ。
上のページでは、ステレオ三輪唱になっています。
めだかの学校 今回起こしてみました。

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


 出力が弱いので、イヤホンくらいしか鳴らせませんが、意外と簡単に鳴りました。もし、スピーカに出力できるなら、アクティブブザーでアラームを鳴らすよりは、聞こえの良い効果音を使えるので、応用利くのですけど……。勿論、アンプ内蔵の外付けスピーカとかに繋げば、一応目的は果たせます。「Arduinoにわざわざアンプ内蔵スピーカ繋ぐの?」ってことになりますけどね……。安価なスピーカ(別ウインドウが開きます)も販売されているので、ソリューションの一つではあります。


後日のアップデート

 「#6-7 Arduinoはサイコロを振る」にて、play_mml()関数をノンブロッキングに変更しました。
 旧バージョンでは、play_mml()を実行すると全てのMMLを演奏終了するまで、制御が戻ってきませんでしたが、新バージョンでは、演奏を開始した時点で制御が戻ってきますので、BGM等演奏中にも、並行して他の(演奏以外の)処理を実行することができるようになりました。
 詳細はこちらをご覧ください。

 本ページからダウンロードできるプログラム(play_mml.ino)も、新バージョンに差し替えてあります。ただし、本文中の解説やコードは旧バージョンのままとなっていることにご注意ください。

タイマの選択オプション

 「#11-3 ゲームで遊ぼう」にて、本ページで使用した16ビットタイマ(TC1)の端子が使用できない事態に遭遇しました。この時、8ビットタイマ(TC2)の端子は空いていて使用可能だったため、演奏に使用するタイマをTC2に変更する必要に迫られました。

 8ビットタイマ(TC2)に変更することで、以下の様な懸念点が有りましたが、許容範囲と判断しました。

 プログラム(play_mml.ino)先頭付近に、「#define USE_TC2」がコメントしてあります。USE_TC2が定義されていなければ、今まで通りTC1を使うので、D9(OC1A)端子がサウンド出力です。USE_TC2が定義された時はTC2を使うので、D11(OC2A)端子がサウンド出力となります。

レジスタ名 アドレス 設定値 説明
TCCR2A 0x00b0 0x41 波形モード:Phase Correct PWM Mode (OCR2A)
出力モード:toggle OC2A
TCCR2B 0x00b1 0x0e 波形モード:Phase Correct PWM Mode (OCR2A)
クロック選択:clkio/256
OCR2A 0x00b3 0x00 周波数に合わせて設定する。
PWMを停止する時は、OC2Aをnormal portに設定する。

 「#6-7 Arduinoはサイコロを振る」で追加した【bgm】コマンドも追加してあります。【bgm】コマンドの使い方についてはこちらをご参照ください。


 本ページからダウンロードできるプログラム(play_mml.ino)と回路図は、新バージョンに差し替えてあります。ただし、本文中の解説やコードは旧バージョンのままとなっていることにご注意ください。


btn_arduino.png

btn_prev.png btn_next.png

home