オープンハードカンファレンス 2017 Tokyo/SpringにCAmiDionを出展

5月7日(日曜日)、東京・八王子で開催されたオープンハードカンファレンス 2017 Tokyo/Springに電子楽器CAmiDionを出展してきました。

八王子ともなると、JR横浜線の全区間を乗車することになります。途中でポケストップを回したり、桜木町始発に乗り換えるタイミングでポケモンジムに配置できたり。

中間の町田駅付近
さすがにここはポケストップ多め

相模原の米軍基地のあたりにポケストップ空白地帯

相原 ⇔ 八王子みなみ野間のトンネル。まじで何もなかったww
円海山の南側や、鎌倉天園の手前を思い出しました。

八王子駅に到着。
展示会場へ行く道では古本まつりが開催されていました。

昼前ぐらいに、Maker Is You Faireの会場(ひまわりビル7F・コワーキングスペースWakU2)に到着。さっそく設営。
僕はこんなのを展示してました:

  • CAmiDion 1号機から最新版まで各種
  • 自作10Wアンプ
  • 自作4chステレオミキサー
  • 音の出るJapanino信号機
  • 今回新たに登場した自作エコーマイク

他の方々の展示はこんな感じ

アナログ、デジタルシンセを展示し、CAmiDion基板も購入していただいた(ありがとうございます _o_)、ぺこさんのブース。6枚のシンセ基板が刺さっています。僕の作ったライブラリPWMDAC_Synthも6重和音出せるので、もしそれが実体化されたらこんな感じなんだろうな…って思いながら見ていました。

最初、MIDIケーブルでCAmiDionとぺこさんの自作シンセをつないで遊んでました。
近くとはいえ、通路を隔てて向こうでしたが、もし完全に隣のブースだったら陸続きでコラボできたかも…まぁ、僕がお誘いを受けたオープンハードカンファレンス組と、Maker Is You Faire 直接出展組との間が通路で区切られていたので、これは仕方ないか…。

八王子経済新聞にもこのイベントの記事が載っていました。

八王子経済新聞にあった
関連写真:1つのボタンの操作で演奏できる電子楽器を自作した人も
というのを見て、もしや、CAmiDion映ってるのかな? と思ったら…

違いました。
左のほうにさうすさんがw
(よく考えたら電子楽器を自作した人は何人もいましたよねw)

さうすさんとは8ヶ月前の僕のツイートをきっかけに知り合って、そのときにCAmiDion基板を購入していただいたのですが、その後、下記のような作品を作って活用されています。

彼のアドバイスにより、PWMDAC_Synthのさらなる改良が後押しされつつあります(ということで今後もPWMDAC_Synthのバージョンアップを予定しています)。

19:00頃から撤収開始、20:00までには完全撤収
打ち上げの会場は、八王子ボードゲームクラブでした。ここで小さな瓶ビール飲んだり、キーマカレー食べたり、ウーロン茶飲んだりしました。
貴重なボードゲームに飲み物をこぼされないよう、倒すとこぼれやすいカップ類は一切使われず、飲み物の容器はジャムの瓶のような形で、ふたにストローが差し込まれていました。(とはいえ、僕らはボードゲームやってたわけじゃなく、先ほど展示していた手持ちの電子工作品で遊んでいたんですけどねw)
料金が2時間区切りということで22:00で打ち上げ終了。

家に着いたら日付変わってましたw
帰りがけに

  • 今日、最初のポケストップ
  • 今日、最初に捕まえた1匹

のボーナスをゲットできちゃいましたw

PWMDAC_Synth更新(ピッチベンド関連のバグ修正)

連休前のこのときにCAmiDionでプログラムチェンジの受信をサポートして以来、自分のHDDに眠っているさまざまなMIDIファイルを再生し、テストを兼ねてBGMとして楽しんでいたわけですが、一つだけ、どうしても気になっていたことがありました。

それは、ニコニ・コモンズに上がっているButter-Fly(デジモンアドベンチャーOP曲)のMIDIデータを再生したときの、あのかっこいいイントロのところ。ピッチベンドで音程が一瞬上がるところで、本来なら半音しか上がらないのに、3全音近くまで上がって聞こえるのです(あぁっ、せっかくのかっこいいイントロが…どうしてこうなった)。

Windows内蔵のMIDI音源で再生するとこの現象は出ませんでした(CAmiDionに再生させたときにだけ発生する現象)。

そこで、6月の初めくらいから本格的に調査。
PWMDAC_Synthのピッチベンド周りの処理が怪しいと思い、桁あふれや桁落ちか?と思って型キャストの方法を見直してみたり、計算順を見直してみたり、いろいろやってみたのですが、改善せず。

じゃあ、処理が重いせい?
と思って問題のパートだけをソロ演奏してみたものの、現象は変わらず。

で、たまたま休みだった6月10日(金)に、問題の切り分けをしているうちに気づいたこと。

それは、ベンド幅(Pitch Bend Sensitivity)を変数から持ってくると上記の現象が再現し、12で固定すると再現しないことです。

もしかして、ベンド幅が間違って代入されてしまっている…!?

さらに調べていると…
あっ!これかもしかして!?

気づいたところでさっそくツイート:

ベンド幅が、最大12半音(= 1オクターブ)と指定されていたわけですが、そのあとにも Data Entry の項目で64と指定されているのを、MIDIエディタソフトDominoで見ていて発見。

RPN MSB=0 LSB=0 と来て、Pitch Bend Sensitivity だと判断して Data Entry 12 だからベンド幅12と解釈されたはいいが、その後にNRPNの Data Entry もやってきて、こっちは64だけど、ベンド幅を後から上書きしたものとプログラムが誤認し、結果としてベンド幅が64/12=5.3倍に。

うん、たしかに5.3半音ぐらいに聞こえた気が。
本来半音になるはずのところが、ドを基準にしてファのちょっと高めぐらいになるくらいの音程。どうりで音程が大きく外れた状態になるわけです。

ベンド幅に対応したのは2015年9月頃ですが、NRPNが来た場合を想定せずに実装して9か月近くそのままになっていたようです。

さっそくバグフィックスしました。

その後、動画もniconicoに上げました

PWMDAC_Synthの配列生成マクロ

少し前のブログ記事で、PWMDAC_Synthライブラリにおける音階の作り方を解説しましたが、そのためにどんな関数を使っているか、といった計算式の説明が中心でした。

この計算式は、AVRマイコン上でランタイム(実行時)に動かすためではなく、PC上でのコンパイル時にC++のプリプロセッサで字面の置き換えをして配列を生成するためのものです。

従来は

#define F(x) …
PROGMEM const byte table[] = {F(0), F(1), ... , F(127)};

のように0~127まで書き並べる方法で波形テーブルを作っていましたが、新しいPWMDAC_Synthではよりスマートなこの方法に切り替えました。

#define FX2(f,x) f(x), f(x + 1)
#define FX4(f,x) FX2(f,x), FX2(f,x + 2)
#define FX8(f,x) FX4(f,x), FX4(f,x + 4)
#define FX16(f,x) FX8(f,x), FX8(f,x + 8)
#define FX32(f,x) FX16(f,x),FX16(f,x + 16)
#define FX64(f,x) FX32(f,x),FX32(f,x + 32)
#define FX128(f,x) FX64(f,x),FX64(f,x + 64)
#define ARRAY128(f) {FX128(f,0)}
#define ARRAY256(f) {FX128(f,0),FX128(f,128)}
:
#define PWMDAC_CREATE_WAVETABLE(table, function) PROGMEM const byte table[] = ARRAY256(function)

FXn(f,x) で、n/2 個の要素を2倍に増やし、n 個の要素を持つf(x)の並びを作る、ということを7回繰り返します。たったこれだけで 27 = 128 個のカンマ区切り数値の並びができてしまいます。

これを活用して作ったマクロ ARRAY256(function) や ARRAY128(function) を使えば、その引数 function にマクロ関数名を入れるだけで、関数の結果を使った配列が簡単に作れます。

それをさらにマクロにしてまとめれば、ライブラリを呼び出す側でそのマクロを指定するだけで、インスタンス(実体)を重複なく必要な分だけ作れるようになります。タイマー割り込み処理に欠かせない ISR() (Interrupt Service Routine) の定義もインスタンスとみなして一緒に定義することすら可能になります。

最初の頃はExcelのワークシート関数で配列初期化のソースを作らせていたのですが、このような計算を#defineでC++のプリプロセッサに任せることだってできるのです。

ただし、一つだけ注意点が。
この方法を使ってノイズ波形を作るために random() を使った乱数表を作ろうとすると、なぜかうまくいかないようです。コンパイルして実行したら動きませんでした。
random() は呼び出すたびに値が変わるため、実行時に値を作るようにコンパイルされてしまったのでしょうか…?
このような特殊なデータはExcelに作らせるか、エディタを使って手動で作るしかなさそうです。

電子楽器CAmiDion ソフトウェア更新・その2:パフォーマンスチューニング

前回もこのブログでCAmiDionとPWMDAC_Synthの更新をアナウンスしましたが、あのあとまた更新が色々入りました。

チューニングの考え方

今回の主な更新は、リファクタリングはもちろん、それによって判明したパフォーマンスチューニングすべき箇所の修正による、使用RAM領域の節約が中心です。Flashメモリであるプログラム領域については、削減できればいいなと思いつつ、余裕があることや速度を犠牲にしたくないこともあり、それほど重視はしませんでした。

節約の際は、速度を犠牲にしないよう、以下の3つの処理を常に意識しておく必要があります。

  • 割り込み処理ISR()でその瞬間の波形ポイントをPWMのパルス幅にして出力(最高頻度・最優先:できる限り手早く処理しなければならない)
  • エンベロープ音量更新(loop()から定期的に呼ばれるので、あまり遅い処理は禁物だが、頻度が高くないので上記ほどシビアではない)
  • 通常の処理(これもシビアではないが、あまり遅いと全体の処理に影響するかも)

主なリファクタリング/パフォーマンスチューニング項目

PWMDAC_Synth

  • ADSRのenumを外出ししたうえで、エンベロープパラメータをよりコンパクトな形式に変更
  • 音量を16bit/8bit両方持たせていたのを共用体に置き換えた
  • 位相速度(dphase)を構造体にまとめてリファクタリングした。これによりオリジナル値を保持するのが無駄なことが判明したので、保持するのをやめた
  • Voice(発声子)の優先度(「温度」と呼ぶことにした)を見直し、音量をより重視した

CAmiDion

  • ソースを整理し、使えそうな箇所でtemplateを使うようにしたり、ボタンIDのenumを外出しすることで型の明確化に役立てた

エンベロープパラメータ

これまではbyteとunsigned intを両方使っていましたが、16ビット分という細かい値までMIDIチャンネル16個分保持するのはRAMの無駄になることなどから、すべて8ビット(= 1 byte)に統一しました。これに伴って配列化も容易になったので、ADSRのenumを使ったbyte配列として参照や更新が出来るようにしました。これに伴い、値だけでなくメソッドを持たせられるよう、struct から class への「昇格」も行いました。

音量、位相速度のコンパクト化

発声状態(VoiceStatus)には、現在の音量と位相速度(phase speed、音の高さに比例)が常に保持されていますが、これらを保持する変数のサイズ削減も行いました。発声状態はデフォルトで6重和音分あるので、ここのサイズ削減もRAMの節約に役立ちます。

【音量】最終的には高頻度割り込み処理ISR()にて8bitで掛け算されますが、エンベロープ状態の更新はそれに比べて低頻度なので、より細かい16bitで保持しています。今まではこの2つの値をvolume8とvolume16という別々の変数で持たせていたので3バイト占有していました。
しかしよく見ると、8bitで音量を得るには16bitの上位8bitをそのまま参照すればいいことに気づきます。そこで、この部分を共用体(union)で定義し直しました。これにより2バイトの占有で済むようになるだけでなく、8bitシフトしようとして >> 8 とか書かなくても共用体メンバーを参照すればよくなり、ソースが読みやすくなります。

【位相速度】ピッチベンド前の値(dphase original)はNoteOnやピッチベンドのタイミングでその都度wavetableから読み出せばいいことがわかったので、なくしました。位相速度はunsigned longで4バイトも食っているので、これも削減できました。ISRの割り込みに比べて低頻度なので、その都度読み出しても処理速度への影響は少なく、RAMの節約効果が大きいです。

発声優先度の見直しで「温度」の概念を導入

同時発音数いっぱいに発声中でも、受信したNoteOnを無視するわけにはいきません。反応が鈍ったように感じてしまうからです。

これを解決するのが優先度の概念ですが、内部的にはこれを「温度」(temperature)と呼ぶことにしました。ADSRのAttackでは音量が上がるほど下がり、Decay以降は音量が下がるほど下がる、そのような値を「温度」としています。発声が始まったときが最高温度(最も熱い状態)で、ADSRのReleaseが終わって音が止まったときが最低温度(最も寒い状態)になる、という考え方です。こうすると、同時発音数が足りなくなったとき、温度の低い(=寒い)ものを優先的に「横取り」することで、急に音が止まったような感じを抑えることができます。

最初はADSRの状態を優先していましたが、Attackとそれ以外で分け、あとは音量を重視するようにしました。このほうが急に音が止まったような感じがしにくいからです。

templateの活用

CAmiDionのソースでは、ノート番号(byte型)とコード(Chordクラス)が違うだけのメソッドがあるので、この違いを吸収すべくC++のtemplateを初めて活用しました。

同時に、ボタンIDをbyte型からButtonIDというenum型にし、これを広く活用する形にして型を明確化しました。これによりソースが読みやすくなったと思います。

手持ちのCAmiDionハードにも反映しました

以前よりチューニングの効果が出たので、頒布待ちの在庫10セットを含め、ファームウェアを更新しました。基板頒布のリクエストは引き続き受け付けています。欲しいという方はぜひお知らせください。

PWMDAC_Synthライブラリにおける音程を決める式

PWMDAC_Synthライブラリのソースには、さまざまな計算式が、主にマクロで記述されています。

ここでは、音の高さを決めるマクロ、PHASE_SPEED_OF() について解説します。

#define PHASE_SPEED_OF(note_number) ( \
pow( 2, ((double)note_number - 69)/12 + BitSizeOf(unsigned long) ) \
* PWMDAC_NOTE_A_FREQUENCY * 0xFF * 2 / F_CPU )

これは「位相速度」、すなわち単位時間あたり音波の位相をどれくらい進めるかを計算するマクロです。この式では、位相は最終的には unsigned long の整数値で表わされます。整数値がオーバーフローして一周すると 0 == 2π [rad] の位相ということになります。

単位時間

ここで「単位時間」は、PWMのパルス幅を更新するための割り込みサービスルーチン ISR() が呼ばれる周期です。最高速の位相基準PWM(Phase-correct PWM)の設定にすることにより、カウンタがCPUクロックごとに 0, 1, 2, … ,254, 255, 254, … 2, 1, のように往復しながら繰り返されることで、0 → 254の255個(0xFF個)、255 → 1 の255個、計510個ごとに割り込みがかかります。つまり、

255 * 2 / CPUクロック周波数

ということになります。CPUクロック周波数は F_CPU としてすでに定義済みで、16MHzになっています。これにより

0xFF * 2 / F_CPU

という、割り込み周期(単位時間)を表す式ができました。

音階の周波数

PHASE_SPEED_OF(note_number) の引数 note_number は MIDI のノート番号(0 ~ 127)ですが、これをもとに周波数を決める式が MIDI Tuning Standard として次のように決められています。

2(note_number - 69)/12・440Hz

これは、オクターブが周波数2倍、平均律に基づく半音がその1/12乗(12乗根)であることに由来します。69 は、A=440Hz の音階に対応するノート番号です。

440Hz を PWMDAC_NOTE_A_FREQUENCY として別途 #define し、これを使ってマクロで書くと

pow( 2, ((double)note_number - 69)/12 ) * PWMDAC_NOTE_A_FREQUENCY

のようになります。指数部分は12で割る段階で実数でなければならないので(double)でキャストしています。

周波数を位相速度へ

周波数は1秒当たりのサイクル数なので、これに単位時間を掛けると、その単位時間あたりのサイクル数に変わります。

pow( 2, ((double)note_number - 69)/12 ) * PWMDAC_NOTE_A_FREQUENCY * 0xFF * 2 / F_CPU

単位時間は音波の周期よりもはるかに短いため、サイクル数は1以下の小数になるはずです。これを位相速度にするには unsigned long の最大値+1を掛ける必要があります。unsigned long は 4 バイトなので、4 * 8 = 32 ビット。つまり unsigned long の最大値+1は、232 です。

が、当然のことながらこの数値は大きすぎて unsigned long で表すことはできません。ではどうするか?

ここで音階の周波数計算で使っている pow(2, …) に注目してください。

232 を掛けるということは、pow(2, n) の n に 32 を足すことと同じであることに気づきましたか?

そうです。
指数 n のところにビット数 32 を足すだけでよいのです!

これで、以下のように計算式ができあがりました。

#define BitSizeOf(type) (8 * sizeof(type))
#define PHASE_SPEED_OF(note_number) (pow( 2, ((double)note_number - 69)/12 + BitSizeOf(unsigned long) ) * PWMDAC_NOTE_A_FREQUENCY * 0xFF * 2 / F_CPU )

位相速度の活用

ではこの「位相速度」はどう活用されるか?

位相速度は unsigned long(32ビット)ですが、波形テーブルのインデックスはbyte(8ビット)です。これを変換するには 24 ビット右にシフトする必要があります。

変換できれば、あとはインデックスをもとに波形テーブル(値もインデックスも0~255の範囲)から現在あるべきパルス幅を読み出し、パルス幅を決めるレジスタを設定するだけです。この処理を割り込みサービスルーチンの中で行うことで、一定の単位時間でパルス幅を更新し続けることができるのです。
6重和音の場合は、その数だけこのパルス幅を合算します。
エンベロープでボリュームがコントロールされる場合は、そのボリュームを掛けたりします。

割り込みサービスルーチンでは、あまり時間のかかる処理を行うとそれ以外の処理を行う暇がなくなってしまうので、割り算などクロック数を食うような処理はできません。しかし、クロック数を最小限に抑えれば、6重和音も平気で出せるようになるのです。

電子楽器CAmiDion ソフトウェア更新

電子楽器CAmiDionと、シンセライブラリPWMDAC_Synthのソフトウェアを更新しました。

主な修正点

CAmiDion

  • クラス構造の見直し
  • ソースファイルの分割
  • 新たな#defineパラメータ(同時発音数、A音の周波数など)を追加し、CAmiDionConfig.hへ集約
  • ボタンのチャタリング対策(一定回数以上ボタンOFFが検出されるまで「ボタンが離された」ことにしない)
  • 最新のPWMDAC_Synthに対応

PWMDAC_Synth

  • 処理を見直し、多くの処理を*.cppから*.hに移行
  • 同時発音数、A音の周波数をライブラリを変えることなく#defineで変更できるようにした
  • 波形や位相速度テーブルに設定する値をべた書きせず#defineを駆使してプリプロセッサに計算させるようにした
  • 波形テーブルの実体定義をマクロ化し、必要な波形テーブルだけを実体定義できるようにした
  • ピッチベンド・センシティビティに対応

これに伴い、先ほど、更新したソフトウェアを手持ちの全CAmiDionと、未頒布の基板部品付セットに付属のATMEGA328に反映しました。

現時点で部品付セットの在庫が10セットあります。
欲しいという方はこちらを参照してください。

DMM.Make AKIBA で開催のNT東京2015に出展してきた

7月25日~26日にDMM.Make AKIBAで開催されたニコニコ技術部イベントNT東京2015に電子楽器CAmiDionを2日間とも出展してきました。

翌週末にはMaker Faire Tokyo 2015 (MFT2015) がありますが、今回は初めからこちらには出展申込せず、NT東京のほうにしようと思っていました。
人が多い中、一人で出展して基板を頒布するにしても、昼食などに出るのもままならない。これは過去のMake:のイベントでもしばしば経験していました。それでいて、有償頒布がある場合は出展料が無料ではなくいきなり跳ね上がる。でもDMM.Make AKIBAで行われるNT東京なら出展料はそれよりも大幅に安い。というわけでNT東京のほうにしました。

いざ展示してみたところ、6年前の川崎と同程度、いやもっと小さいかな、っていうくらいの規模で、MFTみたいにすごい人混みというのもなかったです。

展示ではCAmiDion 6年間の進化の軌跡を物語るよう、右から古いもの順に並べるようにしていました。

元々はCAmiDionで6重和音を出したくて作ったPWMDAC_Synthライブラリですが、今回のNT東京では別の区画でこのライブラリを活用した電子オルゴールが展示されていました。

togatterまとめ(ハッシュタグ #nttokyo)

CAmiDionは部品付セットが各日4セットずつ売れて好評でした。夏休み工作に、と言いながら買っていった方が多く、やはり今月初めに部品付セットを10セット増やしておいてよかったです。増やしてなかったら在庫が1セットしかなかったところでした。

1日目の夜は新宿(ってか隣の駅である大久保に近い)で新宿 AKI PARTYなるMaker系クラブイベントが開催されるということで、こちらにも参加してきました。Pepper君がDJをやったり、扇風機で放熱されていたり、終了後は生首が取れたり…なかなか楽しかった。

LED + CR2032できれいに光るTENGA EGGケースを持って振りながらノリノリで踊っていたりしましたw いい腕の運動になりました。気分はエアロビw

だが、片方はハンダ付けがしっかりしていなくて、急遽裏についていた両面テープでCR2032にくっつけてなんとかしました。

ということでNT東京2015楽しかったです!
みなさんお疲れ様でした!

作業部屋Wikiにプリント基板情報と部品表を移動しました

作業部屋Wikiを大幅に整理しました。

基板頒布情報や部品表、Arduinoスケッチ、回路図に至るまで、この作業部屋Wikiにて主要な情報を網羅する形にするための総合的な目次を作りました。

それまではこのブログの固定ページに部品表などを書いていましたが、作業部屋にWikiシステムができたことで、部品表や基板の情報もここに集約しておけば、ハードウェアからソフトウェア、ブログのタグによってできる最新記事までカバーできることに気づいたからです。こうすることで、ブログは時系列の記録用、作業部屋Wikiはまとめ用、といった使い分けができるようになります。

さらに、先日注文した基板が届いたこともあり、これを機会に、基板に添付する紙を最小化してWebを見てもらう形に切り替えていくことにしました。

CAmiDionのURLは今までアナウンスしたものを踏襲したく、一番短いURLとして当ブログのCAmiDionのトップも窓口ページの一つとして存続する形にしています。

Gitによるバージョン管理を始めました

これまで作業部屋(PersonalForge)はCAmiDionだけでしたが、PWMDAC_Synthの分も作りました。
両者にはGitのリポジトリが1個ずつあるので、ここにpushしておきました。

ソースがGit管理下に置かれたことで、README.txtが作業部屋Wikiのトップに表示されたり、ファイル置き場からダウンロードしなくてもブラウザ上でGitリポジトリにあるソースを直接読むことができて便利になりました。
今後は変更点もGitリポジトリに蓄積されていくので、ある時点でのソースを参照するのも容易になってくるものと思います。

なお、CAmiDionの回路図はGit管理下にはまだ入っていません。
また、MIDI Chord Helper プロジェクトのほうは Subversion ですでに管理されていますが、Git に乗り換えるかどうかは検討中です。→ (1月5日)なんとか移行できました。

Tips

最初、SSHのキーを登録するなどの作業で手間取りました。
GitのGUIツールの一つであるSourceTreeから、SSHのキーを生成するのにPuTTY Key Generatorを起動するわけですが、ここでファイル保存すると改行が入ったりして、SourceForgeのユーザ設定に公開鍵を設定したときに「OpenSSHの形式じゃない」みたいなメッセージが出て拒否されてしまいました。
PuTTY Key Generator の画面に表示された公開鍵をコピペしたらうまくいきました。

Windows 7 の場合、ArduinoのスケッチやライブラリをGit管理下に入れるには下記のディレクトリを対象に git init するとよいようです。

  • C:/Users/ユーザ名/Documents/Arduino/libraries/ライブラリ名/
  • C:/Users/ユーザ名/Documents/Arduino/スケッチ名/

剰余を高速に求める

MIDIや音階の処理をしていると、% 演算子で剰余を計算したくなるときがあります。
例えば x % 12 はMIDIノート番号 0 ~ 127 からオクターブ分を抜いて 0 ~ 11 の値に置き換えたいときによく使います。

ところが、C言語の剰余演算子を負数と一緒に使うと、結果も負数になってしまいます。
MIDIノート番号には負数が出てこないので影響はないのですが、五度圏を文字盤に見立てた「時刻」の値(調号の♯の数)など、一時的に負数が出てくる場合、結果が負数になったら +12 するといった対応が必要になります。

ここでふと思ったのですが、割る数を固定にした剰余の計算なら高速化できるのではないか?
そう、例えば x & 3 のようにビットAND演算子を使って二進数の11でマスクすれば、どんな値でも下2ビットだけになって、結果的に4で割った余りが出せますよね。しかもこの方法なら割られる数が負数の場合であっても結果は負数にならず 0 ~ 3 の値が返されます。10進数でも、100で割った余りは下2桁を取り出すだけで簡単に求まりますよね!あれと全く同じ原理です。

では、他の値はどうか? CAmiDion では 12 の剰余、7 の剰余が使われますが、これはどうやって実現すればよいのでしょう?

調べてみたら、特定の剰余演算(mod)をトリッキーに行う方法という記事を発見。これを参考にしてみたところ、案外簡単に高速化できそうなことがわかりました。

まずは 7 の剰余。

byte PWMDACSynth::musicalMod7(char x) {
  while( x & 0xF8 ) x = (x >> 3) + (x & 7);
  if(x==7) return 0;
  return x;
}

これは、かけ算九九の 9 の段で十の位と一の位を足すといつも 9 になるという10進数の性質と全く同じ原理です。99や999でも桁が2桁や3桁に増えるだけで全く同じ性質を持ちます(例:99 * 2 = 198 → 1 + 98 = 99)。これを二進数に置き換えると、ビット1が全部並ぶ 3、7、15、31、… のような数値が、10進数でいう9とか99とか999に相当するわけです。このような数の剰余はビットシフトとビットANDと加算の繰り返しだけで求まり、しかも繰り返し回数は極めて少なくて済みます。

負数の場合も、while の条件で2の補数だからいつまでもループ抜けない?…と思いきや、実際は加算が行われるので必ずいつか正数になり、ビット0に転じてループを抜けるので永久ループにはなりませんし、結果に負数が返されることもありません。

7 の剰余は、五度圏の「時刻」値から FCGDAEB(ファドソレラミシ)の法則で並んだ音階を導き出すのに役立ちます。

使用例:五度圏での位置を音名(例:C、F#、Bb)に変換。F# と Gb の違いもはっきり区別できますし、B#、Cb、のような音名や、ダブルシャープ、ダブルフラットのつく音名にも対応できます。

    void setNote(char co5) {
      *bufp++ = "FCGDAEB"[PWMDACSynth::musicalMod7(++co5)];
      if( co5 < 0 ) *bufp++ = 'b'; // flat or double flat
      if( co5 >= 14 ) *bufp++ = 'x'; // double sharp
      else if( co5 >= 7 ) *bufp++ = '#'; // sharp
      if( co5 < -7 ) *bufp++ = 'b'; // double flat
    }

次に 12 の剰余。

byte PWMDACSynth::musicalMod12(char x) {
  char n = x >> 2;
  while( n & 0xFC ) n = (n >> 2) + (n & 3);
  x &= 3;
  if(n==3||n==0) return x;
  return x + (n << 2);
}

これは 12 = 3 * 4 であることを利用し、下2ビットはANDでマスクをかけて4の剰余として求め、その上の2ビットは 3 の剰余を求めた結果を継ぎ足しているだけです。もちろんこれも負数を引数に与えても常に0または正数が返されます。

この関数は最近作業部屋にあるPWMDAC_Synthのユーティリティとして実装しました。

Arduino のコンパイラとして使われていると思われる gcc はここまで最適化してくれるのかわかりませんが、少なくとも、剰余の結果として負数を返さないようにしたいときに有効な手段の一つだと思います。