SSブログ

音楽を演奏したい - 楽譜編 [Arduino]

久しぶりです。
notegure.jpg
Arduinoで和音を使った音楽を演奏したいと思いました。
さて、データの形式をどうしましょうか?
マイコンでの実装なので極力データは少なく、扱いやすい形式がいいです。
また、わかりやすいという面ではいわゆる西洋音楽の楽譜をベースにしたいと思います。
一つ一つの音を音符で表していくのがよさそう。
音符については wikipedia によると、、

「音符は五線譜などの中で、相対的な音の長さ(音価)と時間的な位置、および高さ(音高)を表す。また、音価によってその形が異なる。」

ということです。時間的な位置については、データの位置ということになるので、音の長さ(音価)と高さ(音高)について考えていきます。
オルゴールでは、音の長さ(音価)はどの音も同じですが、時間的な位置というものを音の長さ(音価)として扱ってもいいような気がします。

◆音の長さ(音価)
表し方としては、
(1) 絶対的な長さ(マイクロ秒とか、ミリ秒とか)
(2) 相対的な長さ(ある音符を1として長さを比較)
(3) 使用する音の長さ(音価)にIDを振る

さて、それぞれの特徴から、
(1)はわかりやすが、データが大きくなる、楽譜という法則と疎遠な感じ
そもそも一つの曲で使われる音符の長さの種類は限られているので(3)の方法だと少ないデータで済むと思います。ただそのIDと結びつくデータベースを別に持っていないといけません。
ということで、(2)です。テンポの情報は必要ですが、プログラムからの操作は容易になります。

◆音の高さ(音高)
表し方としては、
(1) 絶対的な値(周波数や周期)
(2) 相対的な値(ある音高から順番にID)
(3) 使用する音の高さ(音高)にIDを振る
さて、それぞれの特徴から、
(1)これは場合によってはプログラム上扱いやすいかもしれないが、音高ごとの周波数の比率は2の12乗根倍の等比数列であり、使われる周波数はきまっているためきわめて無駄が多い。(3)は、最小のデータになる可能性が高いが、音の長さ(音価)の場合と同じでデータベースが必要。ということで、(2)です。

◆音符データの形式
音価、音高ふくめて1バイトにおさめるとなると両者とも(3)の方法しかないのですが、曲ごとにデータベースを作るのは面倒なので、とりあえず2バイト(16ビット、uint16_t)でなんとかしたい。上位8ビットを音高、下位8ビットを音値だとわかりやすいだろうと考えました。
・音の長さ(音価)
一番長い音符はwikipediaによると倍全音符ですが、短い音符にはきりがありません。
口でタタタとか、トゥクトゥクとかで表現できるのは16~32分音符くらい、ボカロの曲とかだと256分音符などもあると聞きましたが、マイコンなのである程度の切り捨てが必要です。
あと、3連符、5連符、7連符とか2で割り切れないものや、付点音符、複付点音符、3重付点音の扱いもある程度の切り捨てが必要です。
ということで考えました。
水戸黄門のテーマや、ドラえもんのオープニングなどでも3連符はでてくるので3連符までは押さえておく。一般的な曲ではせいぜい16分音符。余裕をもって32分音符まで対応する。付点は1個まで。
ということで96分音符というものの長さを1とします。32分音符は3となり、4分音符は24となり、付点4分音符は36となり、一番長い倍全音符は192となり255までに収まります。通常の音符はすべて3の倍数になるので、3で割って3連符を表現できるというわけです。
・音の高さ(音高)
MIDI番号というのがあって、C-1, 8.2Hzを0番として、A4, 440.0Hz を69番、G9, 12543.9Hzの127番までとなっています。これを使うのが一見よさそうに見えますが、2の12乗根のべき乗計算や割り算や剰余計算はマイコンには酷です。そこで音高をオクターブと音名(C, C#, D, D#, … ,Bの12個)にわけます。1オクターブごとに周波数は2倍(周期は半分)になることを利用し、あらかじめ12個だけ周波数(あるいは周期)をデータとして持っておき、音名からデータを取り出し、オクターブ情報でビットシフト計算するというのがいいんじゃないかなぁと考えました。それぞれ4ビットごと使用します。音名は12個のデータに4ビット(16個のデータ)を割り当てるので多少の無駄はあります。オクターブも0~7までなら3ビットで済み1bit余ります。多少の無駄はありますが、データ処理が容易になるので現実的な方法だと思います。

例:A4(ラ 440Hz)、4分音符の場合

(MSB) 0100 1001 0001 1000 (LSB)   = 0x4918
* 上位4ビットがオクターブ(4)
* 次の4ビットが音階スケール(ド~シ; C, C#, D, D#, … ,B)(9)
* 下位8ビットが音の長さ0x18(=24)(1/96音符を1とする)
#define H_A4  0x4900
#define L_4    24

と定義すれば、

= H_A4 | L_4

というように、ORでくっつけることで音符が完成です。

トータルで2バイトなので pgm_read_word_near で読みだせるし、ビットマスクとビットシフト、配列操作がメインで高速動作が期待できるんじゃないかと。

ここまでは理論の話。
実際に楽譜(ためしに鬼滅の刃の主題歌「紅蓮華」)を入れてみたところ、、
PROGMEM const uint16_t Gurenge[] = {
  135 , 0 ,     // テンポ 135
  H_G4 | L_8d, H_FS4| L_8d , H_G4 | (L_8+L_2),
  H_G4 | L_8d, H_FS4| L_8d , H_G4 | (L_8+L_2),
  H_G4 | L_8d, H_FS4| L_8d , H_E4 | L_4d, H_D4 | L_8, H_D4 | (L_8+L_2),
  H_RST| L_8 , H_B3 | L_4, H_D4 | L_8,
  H_E4 | L_2 , H_RST| L_8 , H_E4 | L_4, H_G4 | L_8,
  H_A4 | L_2 , H_RST| L_8 , H_G4 | L_4, H_A4 | L_8,
  ...

と、面倒でした。
はじめから、すべての組み合わせを定義したところ、ちょっとすっきりし、入力も楽になりました。
PROGMEM const uint16_t Gurenge[] = {
  135 , 0,                    // テンポ 135
  G4_8d , FS4_8d, G4_8+L_2, 
  G4_8d , FS4_8d, G4_8+L_2,
  G4_8d , FS4_8d, E4_4d , D4_8  , D4_8+L_2,
  RST_8 , B3_4  , D4_8  ,
  E4_2  , RST_8 , E4_4  , G4_8  ,
  A4_2  , RST_8 , G4_4  , A4_8  ,
  ...


・音符から楽譜へ

音符を連ねて楽譜にしていくのですが、データの形式としては単音の音符(音高と音価)を連続していくのですが、そうすると和音がだせません。和音は別のパート、トラックとして扱おうと思います。
パート毎に配列を作ると2次元配列とした場合には無駄が出るし、、。ということで、1次元の配列で、区切りを入れることとしました。
配列名[] = { テンポ, 0, パート1のデータ … , 0, パート2のデータ … , 0, パートnのデータ … , 0, 0 }

0を区切りとして、0が2個続いたらデータ終了という感じ。

以下、作成した定義ファイル「notes.h」

notes.h


nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。