書籍購入:『micro:bitではじめるプログラミング』

あとで書く。

micro:bitではじめるプログラミング ―親子で学べるプログラミングとエレクトロニクス (Make:PROJECTS)

micro:bitではじめるプログラミング ―親子で学べるプログラミングとエレクトロニクス (Make:PROJECTS)

ALSAでMIDIをループバックさせたい――ただしALSA RawMidi APIで!

ALSA C libraryのAPIを使用してMIDIの入出力を行うLinuxアプリがあるのだが、出力したMIDIをループバックさせて入力として読み込みたい――という話である。

macOSなら標準のIACドライバを、Windowsならサードパーティの仮想MIDIケーブル*1を使うパターンである。

今回のポイントは、ALSAのRawMidi interfaceのAPIを使用しているアプリである、という点だ。

ALSAMIDI APIは、RawMidi interfaceとSequencer interfaceの2種類が存在する。RawMidi interfaceはベタにMIDIバイスのポートを読み書きするためのOSS互換のAPIだ。一方のSequencer interfaceは高水準のAPIで、MIDIバイスだけでなくTimidity++のようなアプリケーション間でもMIDIの入出力を行えるようにデザインされている。そのため、MIDIシーケンサという抽象化されたオブジェクトのポートを読み書きするようになっている。

Linux上でのMIDIの取り扱いについての解説資料――例えばThe Linux MIDI-HOWTOとかLinuxビンボ道〜明日がALSA設定編とかArchLinuxのWikiに書かれているTimidity++の話題などは、Sequencer interfaceを使用しているアプリ向けの話が大半だ。

Sequencer interfaceを使用しているアプリなら、カーネルモジュールsnd-seq-dummyをロードすることで使用可能となるMidi Throughという仮想MIDIシーケンサを使用して、MIDIをループバックさせることができる。「Midi Through」という名前の通り、入力ポートに書き込まれたMIDIをそのまま出力ポートから読み出せるMIDIシーケンサで、macOSのIACドライバやWindowsの仮想MIDIケーブルと似たような要領で扱うことができる。

# snd-seq-dummyの組み込み方。
# 末尾の「ports=1」は、ループバック用のポートの数を指定する。
sudo modprobe snd-seq-dummy ports=1

UbuntuやRaspbian(Raspberry Pi)ではデフォルトで組み込まれているので、再度ロードする必要はない。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ aconnect -i   # 入力ポートを表示
client 0: 'System' [type=カーネル]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
$
$ aconnect -o   # 出力ポートを表示
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
$ _

上記の例では、入力・出力ポートともに14:0を指定すればよい。

しかしRawMidi interfaceを使用しているアプリはMidi Throughを使えない。RawMidi interfaceで扱えるのはMIDIバイスだが、Midi Throughは仮想MIDIシーケンサだ。厄介なことに、RawMidi interfaceのMIDIバイスとSequencer interfaceのMIDIシーケンサは全く別の概念で、APIからして全く異なるのである。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ # aplaymidi -lでは、Sequencer interfaceで扱うことが可能な、
$ # MIDIシーケンサの入力ポートが表示される。
$ aplaymidi -l
 Port    Client name                      Port name
 14:0    Midi Through                     Midi Through Port-0
$ # Midi Throughが見つかる。
$
$ # amidi -lでは、RawMidi interfaceで扱うことが可能な、
$ # MIDIデバイスの入力ポートが表示される。
$ amidi -l
Dir Device    Name
$ # Midi Throughはおろか、何のMIDIデバイスの入力ポートも見つからない。
$ _

ではどうすればよいか? カーネルモジュールsnd-virmidiを組み込んで仮想MIDIバイスを使用できるようにした上で、aconnect(1)で仮想MIDIバイスの入力ポートを出力ポートを接続すればよい。これで、仮想MIDIバイスの入力ポートに書き込んだMIDIコマンドを出力ポートから読み出すことが可能となる。

# snd-virmidiの組み込み方。
#
# 末尾の「index=1」は、仮想MIDIデバイスに付与するカード番号である
# (RawMidiのデバイスは「カード番号,デバイス番号,サブデバイス番号」で指定する仕組み)。
# カード番号は0から始まる通し番号で、
# システムが認識したMIDIデバイスごとに自動的に付与されている。
# 「amidi -l」などでシステムが認識しているMIDIデバイスを確認したうえで、
# それらと重複しない番号を割り当てること。
sudo modprobe snd-virmidi index=1

snd-virmidiを組み込むと、既定では4個の仮想MIDIバイスが使用可能となる。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ # snd-virmidiをロードする前。
$ amidi -l
Dir Device    Name
$
$ # snd-virmidiをロード。
$ sudo modprobe snd-virmidi index=1
$
$ # snd-virmidiをロードした後。
$ amidi -l
Dir Device    Name
IO  hw:1,0    Virtual Raw MIDI (16 subdevices)
IO  hw:1,1    Virtual Raw MIDI (16 subdevices)
IO  hw:1,2    Virtual Raw MIDI (16 subdevices)
IO  hw:1,3    Virtual Raw MIDI (16 subdevices)
$ # 「index=1」を指定したのでhw:1に割り当てられている
$ # (もし「index=2」だったらhw:2,0〜hw:2,3が割り当てられただろう)。
$ # 「16 subdevices」なので、例えばhw:1,0,0〜hw:1,0,15が使用できる。
$ _

興味深いことに、snd-virmidiの仮想MIDIバイスは、RawMidi interfaceで扱えるMIDIバイスであると同時に、Sequencer interfaceで扱えるMIDIシーケンサとしても振る舞う。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ aconnect -i   # 入力ポートを表示
client 0: 'System' [type=カーネル]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
client 20: 'Virtual Raw MIDI 1-0' [type=カーネル,card=1]
    0 'VirMIDI 1-0     '
client 21: 'Virtual Raw MIDI 1-1' [type=カーネル,card=1]
    0 'VirMIDI 1-1     '
client 22: 'Virtual Raw MIDI 1-2' [type=カーネル,card=1]
    0 'VirMIDI 1-2     '
client 23: 'Virtual Raw MIDI 1-3' [type=カーネル,card=1]
    0 'VirMIDI 1-3     '
$
$ aconnect -o   # 出力ポートを表示
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
client 20: 'Virtual Raw MIDI 1-0' [type=カーネル,card=1]
    0 'VirMIDI 1-0     '
client 21: 'Virtual Raw MIDI 1-1' [type=カーネル,card=1]
    0 'VirMIDI 1-1     '
client 22: 'Virtual Raw MIDI 1-2' [type=カーネル,card=1]
    0 'VirMIDI 1-2     '
client 23: 'Virtual Raw MIDI 1-3' [type=カーネル,card=1]
    0 'VirMIDI 1-3     '
$ _

上記の例では、RawMidi interfaceのデバイスhw:1,0がSequencer interfaceのポート20:0に、hw:1,1が21:0に――という具合に対応している。

仮想MIDIバイス自体はMIDIをループバックする機能を持たないが、aconnect(1)を使用してSequencer interfaceの入力ポートと出力ポートを連結させると、RawMidi interface側でも有効となる。

例えばaconnect(1)で入力ポート20:0を出力ポート20:0に連結すると:

aconnect 20:0 20:0

入力ポート20:0に書き込んだMIDIを出力ポート20:0から読み出せるだけでなく、デバイスhw:1,0の入力ポートに書き込んだMIDIをhw:1,0の出力ポートから読み出せるようになる*2

あとは、RawMidi interfaceのAPIを使用するアプリケーションにて、デバイスhw:1,0を使用すればよい。同じサブデバイス番号同士(例えば入出力ともにhw:1,0,0)にてMIDIがループバックされる。

*1:昔はMIDI YokeHubi's Loopback MIDI Driverを使っていたものだが、64bit Windowsを使うようになってからは縁遠くなった。最近はloopMIDIを使えばよいのだろうか?

*2:試してはいないが、おそらく入力ポート20:0に書き込んだMIDIをhw:1,0の出力ポートから読み出すことや、hw:1,0の入力ポートに書き込んだMIDIを出力ポート20:0から読み出すことも可能なはずだ。というのも、The Linux MIDI-HOWTOを読む限り、元々snd-virmidiはRawMidi interfaceのMIDIバイスとSequencer interfaceのMIDIシーケンサを橋渡しするために開発されたと推測できるからだ。

低速で無駄のある小規模ツール開発を実現する方法

「低速で無駄のある」はツール本体なのか開発手法なのか、はたまた保守フェーズか。

例1:外部コマンドを何度も実行するシェルスクリプト/バッチファイルとして実装する

シェルスクリプトもバッチファイルも個人的に愛憎相半ばする気持ちを抱いてしまう言語というかツールだけど、その有効性は否定しない。自分もよく使っているし。

個々の小さなコマンドを組み合わせて目的を達成する――大抵のテキスト処理はその方法で何とかなるし、私が普段扱うデータ量は高が知れているので実行速度が問題となることもない。そして今の所はハードウェア性能は足りている。

とはいえコマンドを実行すれば(それが組み込みコマンドやシェル関数でなければ)その度にプロセスが生成されるし、各コマンドが個別にファイルにアクセスすればその度にファイルIOが発生する。気をつけないとこれらのコストが嵩んで信じられないほど低速なツールに仕上がってしまうことがある。というか覚えている限り2回ほど痛い目にあった。Windowsはセキュリティツールの影響もあってプロセスの生成コストが高いからなあ。

ループの中で外部コマンドを実行していたり、更にそこで毎回ファイルを引数指定していたりしたら要注意だ。テストデータでは気にならない時間で終了しても、現実的なデータ量ではウンザリするほど遅くなることがある。ループ数が増加すればするほど遅くなる。

該当する箇所をシェル組み込み機能に変えたり、テキストフィルタに置き換えることで済むなら良いが、それが駄目なら他の言語で書き直す羽目になるかもしれない。

例2:本格的に内部構造を変更するリスクを避けて無茶苦茶小手先の手法で既存ツールを改造する

作業コストや自分の力量の兼ね合いで、時として小手先の手法に頼らざるをえないこともある。しかし大抵は後でしっぺ返しをくらう気がする。何でだろう。

MVC (Model View Controller) や、それに微妙に似たDocument/Viewアーキテクチャなんて言葉がある。これに基づくと、例えばデータの画面表示に関して、

  • 表示したい元データはModelが保持している。
  • Modelからデータを取得し、表示用に加工してViewに突っ込む。
  • Viewには加工後のデータが存在する。

――こんな感じになると思う。実はデータの加工をどこでやるべきか分かっていない。

ある時遭遇したプログラムはViewerの類で、Model側に元データが残らない設計になっていた。Viewに加工後のデータが残っているだけだが、表示方法が1種類だけだったので何の問題もなかった……当初は

これがある時、表示方法を2種類に拡張して動的に切り替えられるように改造することになった。Model側に元データが残っているなら簡単だけど、残っていない! ピンチッ!

ここでModel側に元データを残すように修正すればよかったのに、何故か「Viewに残っている加工後のデータを使う」という力任せな方針を採ってしまった。表示方法を切り替える際にModelからデータを取得するのではなく(残っていないので不可)、Viewに残っているデータを使うのだ。この方法で何とかなってしまったのが悪かった。

その後、更に表示方法が増えたのだ! 私はこの対応の時に初めて駆り出された……。

表示を切り替える際に「Viewに残っている加工後のデータを使う」という実装の為、例えば次のような問題が噴出してしまった。

  1. 表示を切り替える度に、異なる加工済みデータが後に残る。
  2. 表示方法が3種類以上になると、表示を切り替えるときに複数の異なる種類のデータが元データとなる。つまり一つの表示方法に対して2種類以上の変換処理を実装する必要が生じる。
  3. データによっては、表示方法を色々と切り替えていくうちに誤差が生じる可能性がある。

ああ、こういうのが積み重なって「誰も弄りたがらない負債コード」と化してコールタールの沼のように踏み入れたプログラマを引きずりこんでいくのだろうなあ。

例3:よく分からんけどネット上の色々なサンプルをざっくり切り貼りして実装する

別にネット上のサンプルコードを使うことは……ライセンス絡みで問題ないなら構わないと思っている。

しかしですね、何の整理もせずに大雑把に切り貼りされると困るのですよ。低速になるか否かは運次第でもソースコード自体は確実に無駄の多い代物になりますから。ええ私も経験がありますあの全体の3分の1ぐらいが全く不要なコードでバラバラなコーディングスタイルがキメラのように組み合わさっていてライブラリ関数を使わずに自前で処理を書いてたりして当然のごとく関数化なんて申し訳程度にしかされてない一枚岩でこれまたバラバラな命名スタイルの変数が山のようにあるアレですよあのたった400行足らずのコードの整理に暇を見てこつこつ実施したとはいえ2週間近く掛かったのは災難なのかそれともその程度で済んだ幸運をかみ締めるべきなのかは未だに分かりません。

世間からすればまだまだ軽い事例だとは思うけど、嫌なものは嫌だ。

ある程度まとまった量のコードを外部から持ってくる――例えば関数単位やクラス単位でサンプルコードをコピーするのならともかく、ステートメント単位で切り貼りする時に何の整理もしないのは死亡フラグだ。

  • ネット上のサンプルコードを切り貼りする時はコードのスタイルを直すべし。
    • 大概のサンプルコードのスタイルは組み込む先のコードのそれと異なる。
    • コードの一貫性を保つためにも、スタイルは直すこと。まあ後で整形ツールにかけるという前提があるのなら話は別かもしれない。
  • ネット上のサンプルコードを使う時は、自分が欲しい部分以外は徹底的に取り除くべし。
    • 残っていると、後でそのコードを解読する羽目になった人が発狂します。

あとステートメント単位でコピーする場合もまとまった量のコードをコピーする場合も、何も考えずに使うのはダメだ。

  • ネット上のサンプルコードを使う時は、その内容の是非を問うこと。必要なら書き換えるべし。
    • バグもあれば質の悪いコードだってあるさ。
    • 例えばVBScriptのサンプルコードにて文字列を格納した配列をForループで舐めて中身を連結していることに深く高度な理由があるのか、それとも単に作者が組み込み関数Joinを知らなかっただけなのか?
  • ネット上のサンプルコードを使う時は、そのコードが前提としている実行環境・開発環境・依存ライブラリ等に留意すること。
    • サンプルコード中で新しく便利なAPIを使っていて、しかしターゲット環境が古いためにまだそのAPIは使えなかった……なんてことがある。
    • あと「このクールなライブラリを組み込めば一発だぜ」という記事を鵜呑みにして実装したが、実はライブラリのライセンスが……なんてこともある。

結局のところ、ネット上のサンプルコードを読んで内容を理解してからでないと、適切に切り貼りすることは不可能なのだ。

まとめ

  1. IOの頻度に注意。
    • 例ではファイルIOを挙げたが、TCPのようにタイムアウトが起こりうる通信処理などでも注意すること。
  2. 急がば回れ。その方法は「小手先の対応」ではないか、手を付ける前に考えること。
    • ただし、本質的な対応を実施するに足るコストを賄えるか否かは別の話。
  3. ネット上のサンプルコードは、よく読んで内容を理解した上でコピペしてプログラミングすること。
    • コピペプログラミングで楽できるのは「ゼロから考えなくて済む」という点だけ。
    • 何も考えずにコピペプログラミングで一定以上の品質のモノが出来上がるはずがない。出来上がるのはウンコなコードだ。

リンク切れしたシンボリックリンクを探す

id:eel3:20121112:1352651058 でシンボリックリンクを張りなおすMakefileを書いたのだが、デッドリンク(リンク切れしたシンボリックリンク)がそのまま残ってしまう問題があった。

この問題に対処するために、デッドリンクを探す方法を――『覚えて便利 いますぐ使える!シェルスクリプトシンプルレシピ54』から持ってくることにした。ただ、ちょっと理由があって、デッドリンクを消すのではなく列挙することにした。単純に列挙するだけなら、他のツールと組み合わせて色々なことができるからだ。

Version 1

初期バージョン。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin

for i; do
    [ -L "$i" -a ! -e "$i" ] && echo "$i"
done

考え方は単純で、シンボリックリンクかつ存在しないファイルを探せばよい。-hや-Lではファイルがシンボリックリンクか否かのみチェックするだけで、シンボリックリンクを辿ることはない。一方でファイルの存在をチェックする-eはシンボリックリンクを辿る。もしデッドリンクだったなら、-hや-Lは真を返すが、-eはシンボリックリンクを辿るのに失敗して偽を返す。

ただ、この方法は「シンボリックリンクを指し示すシンボリックリンク」では意図しない挙動となるかもしれない。xxxxがyyyyを指し示すシンボリックリンクで、yyyyがzzzzを指し示すシンボリックリンクで、zzzzが存在しなかったと仮定する。zzzzが存在しないのでyyyyはデッドリンクだ。xxxxは、yyyyが存在するのでデッドリンクではないはずだが、シンボリックリンクを辿った先の最終地点であるzzzzが存在しないので、このスクリプトアルゴリズムではデッドリンクと見なされてしまう。

もっとも今回の最終目的はあくまで「デッドリンクを消す」だ。デッドリンクであるyyyyが消された時点でyyyyを指し示していたxxxxはデッドリンクになる――削除対象となるのだから、最初の時点でxxxxまでデッドリンクとして列挙されても問題はないだろう。

ちなみに使い方はこんな感じ。

$ deadlink *
xxxx
yyyy
$ deadlink * | xargs -d '\n' rm
$ _

引数として渡したファイル名に対してデッドリンクか否かチェックを行い、1行1ファイル名の形式で標準出力に表示する。この例ではカレントディレクトリのファイルをチェックした結果、デッドリンクとしてxxxxとyyyyが列挙されている。もしデッドリンクを削除したいのなら、xargs(1)を使ってrm(1)の引数に渡せばよい。

ところで、なぜ「引数に指定したフォルダの中身をチェックしてデッドリンクを列挙する」みたいな仕様にしなかったのか? 理由は、それをやろうとすると面倒だから。再帰的にチェックするか否かとか、再帰レベルを調整できるように云々とか、そんな機能は他のツールに任せれば十分だ。具体的にはfind(1)とか。

Version 2

最初に書いたdeadlinkの実装では、チェック対象のファイルを引数として渡す仕様になっていた。

仮にあるディレクトリ以下にて再帰的にデッドリンクをチェックしたい場合、どうするのか? find(1)とxargs(1)を使えばよい。

find ./work -type l -print0 | xargs -0 deadlink

シンボリックリンクか否かのチェックを二重に行っているが、気にしない方向で。find(1)は列挙したファイルを標準出力に書き出すので、xargs(1)を使ってdeadlinkの引数に渡すようにする。

ただxargs(1)を使う方法はファイル数が多くなったときに効率の面が気になる。というのも、引数として許される上限までファイルが列挙されないとdeadlinkが実行されないのだ。なので下手をすればfind(1)によるファイルの列挙が完了してからdeadlinkが実行されるだろうし、そうなる可能性は高いと思う。

しかし、もしdeadlinkが標準入力からファイル名を読み込みつつデッドリンクか否かのチェックを行うのなら、find(1)などによるファイルの列挙とdeadlinkによるチェックが並行して実行される。マルチコア環境では高速化が期待できる。実際にはfind(1)もdeadlinkもファイルシステムにアクセスするので、効果は低いかもしれないが。

そんな訳で標準入力からもファイル名を読み込むことが可能なように改造することにした。ただ元々の仕様も捨て難い。元の仕様を残すのなら「引数が無かったら標準入力から読み込む」という仕様では都合が悪い(メタキャラクタであるアスタリスクを引数に指定したら、ファイルが無かったので引数ゼロになった――なんて可能性があるので)。そこで引数が `-' の1つのみだった場合にのみ標準入力から読み込むようにしてみた。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    # Trims IFS character from the beginning and end of the $i.
    while read i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

実はチェック対象のファイルが1つだけで、且つファイル名が `-' だった場合に困るのだが……そこは運用でカバー、ということで。

Version 3

標準入力からもファイル名を読み込むことが可能なようにしたものの、今度はwhile readを使っている点が気になる。高速化のために追加した部分なのに、低速な機能を使うというのも妙な話だ。

そこでwhile readによるループの部分をスクリプト言語で書くことにした。

最初はawkを使おうとしたのだが、system()を使ってtest(1)を呼び出す部分の外部コマンドの組み立てが面倒だった(特にファイル名のエスケープ処理が……)。そこでPerl5にしてみた。Perlが入っている環境は結構多いはずなので、問題になることは少ないだろう。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin:/usr/local/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    perl -n -e 'chomp; -l $_ && ! -e $_ && print "$_\n";'
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

うん、非常に短い。もういい加減sayを使うべきなんだろうけどprintを使用している。

最近はデフォルトでPerl5が入っていない(!)事態も起こりそうなので、頑張ってPythonでも書いてみた。何が「頑張って」なのかというと、インデント位置である。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin:/usr/local/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    python -c '
import os, sys
for line in sys.stdin:
  i = line.strip()
  if os.path.islink(i) and not os.path.exists(i):
    print i'
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

インデント位置で怒られたので、仕方なくPythonのコードを行頭から書き始めることにした。うーん、見栄えが良くない……。なおPython 2.7にてそれっぽく動作することは確認したが、Python 3で動作するか否かは不明だ。

(念のため書いておくと、Pythonのインデントに関する仕様は特に気にしていない。少なくとも、Python単体でコードを書く分には好ましいと思っている。シェルスクリプトに埋め込むという使い方が邪道気味なのだ)

個人的にはRubyが好みなのだが――PerlPythonは入っていないがRubyは入っている、という環境は考えにくい。むしろ逆にRubyが入ってない環境の方が多そうなので、Ruby版は書いていない。

しかし、ここまでくると、シェルスクリプトPerlPythonのコードを埋め込むのではなく、全てPerlPythonで書いてしまった方がスッキリするような気が……。

終わりに

最終的には、上記のPerl5版に手を加えて、こんな感じになった。

https://github.com/eel3/eel3-scripts/blob/master/bin/deadlink

初めて生PCMを触る人には『WAVプログラミング C言語で学ぶ音響処理 増補版』を推薦します

久しぶりに生のPCMデータを弄る機会があった。生PCMまわりは独学で、割と知識が歯抜けだったので、基本的なところを学び直そうとしたところ、意外とまとまった情報が見つからなかった。

色々と探し回って、ようやくたどり着いたのがこの本だった。

WAVプログラミング―C言語で学ぶ音響処理

WAVプログラミング―C言語で学ぶ音響処理

本書を見せた同僚の感想は「生PCMを取り扱う業務にアサインされた若手・新人に自習用に渡すのにちょうど良い本」だった。

この本は生PCMを触る際のものすごく基本的なことがサンプルコード付きで説明されているというか、むしろサンプルコードで説明されている。そのため、読み解くにはC/C++系統の言語知識が必要となるのだが、逆に考えれば、その辺の言語が分かっている若手のプログラマに参考資料として渡しやすい本だと言える。

内容自体も「ステレオのLとRを反転」とか「モノラル化」とか「16bitから8bitに変換」とか「ボリューム変更」とか、そういう簡単なところから始まっている。意外なことに、この辺の内容がまとまっている資料があまり無いのである。そういう意味では、本書はあまり類書の無い本だと言える(ニッチすぎるのか絶版寸前っぽい感じである)。

おそらく本書の次にやさしい本は『C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理』だが、こちらの本はサウンドエフェクトの入門書なので、生PCMの入門書としては厳しいというか、そもそも分野が少々異なるといえる。

本書のサンプルコードはWAVファイルの生PCMを加工するものが大半で、最後の1章のみWAVファイルからPCMを取り出して加工してWindows APIで再生させるものである。

なのでリアルタイム処理まわりはサッパリなのだが、その辺になると各OSのマルチメディアAPIによって流儀が全く異なってくるので、1冊の本にまとめるのは厳しいだろう。しかも実のところ、どんなAPIを使おうとも、生PCMを直接加工する部分は、WAVファイルで生PCMを加工する場合と基本的な考え方は同じである(そのAPIに用意されている便利な機能を使う場合は別だけど)。

あとサンプルコードが16bitよりも大きな分解能には対応していないのだが……「8bitがOffset Binaryで16bit以上が2の補数」という点さえ押さえておけば、サンプルコードを片手に自前でコードを書くのは難しくないはずだ。というかサンプルコードは中身を理解してから流用するもの(※ただしライセンス等の問題が無い場合に限る)で、何も考えずに流用しちゃうのはプログラマとしてアウトだ。

ともかく、PCMまわりの知識ゼロの状態で生PCMを触るのなら、1冊目として本書はオススメだ。後は、実際に使うAPIのドキュメントとか、サウンドエフェクト系の作業ならそちらの入門書とか、もう1〜2冊ほど併読すれば何とかなるだろう。

書籍購入:『WAVプログラミング C言語で学ぶ音響処理 増補版』

あとで書く。

WAVプログラミング―C言語で学ぶ音響処理

WAVプログラミング―C言語で学ぶ音響処理

分野に関係なく『組込みソフトウェア開発のための構造化プログラミング』を読んでおかなきゃあかん

『組込みソフトウェア開発のための構造化プログラミング』を軽く読んだのだが、この本は組込み系以外の分野でコードを書いている人も読むべきだ。

全部で8章で、そのうちC言語成分が含まれているのは2章と5章だけ。だから残りの6章はC言語とは縁遠い分野のプログラマでも読めるはず。

意外と忘れがちなのだけど、オブジェクト指向プログラミングが必須であるような大規模な開発であっても、分割統治を繰り返した結果として、個々のクラス/モジュールといった粒度においては小規模なプログラミングの世界となる。

で、構造化プログラミングは元々高々3000行程度のプログラムが大規模プログラムと呼ばれていた時代に原典が考案された上で、頭の良い人たちが10年以上もの間に寄ってたかってそういった粒度の世界において高品質なコードを書くにはどうすればよいのかを考えて散々揉みしだいた結果として導き出された結晶な訳だ。

構造化プログラミングに関する文献を探せば分かるが、1968年の最初の論文以降、10年以上もの間、原典を継承・派生させた文献がコンスタントに出現している。それだけ色々な人たちに揉まれているのだ。例えば『ソフトウェア作法』の原著である『Software Tools』が出版されたのは1976年だ。

だから出たばかりの頃と散々揉まれた後では割と別物というか、ダイクストラ本人の論文だって1968年の「Go To Statement Considered Harmful」と1970年の「Structured Programming」とでは随分と内容が異なる。

散々揉まれた後の構造化プログラミングは今でも十二分に通じる。むしろ構造化プログラミングを古臭いと言われると「じゃあ、クラス/モジュールの中身としてメソッドを書くときに、何を指標とすればよいの?」と途方に暮れるものだ。

手続き型の系譜に連なる言語を使っているなら(それがオブジェクト指向プログラミング言語だろうと関数型言語とのハイブリットだろうとも)、現代においても「小規模なプログラミング」――クラスやモジュールの中身を実装する際には非常に有効な技法だ。

構造化プログラミングを軽視するのは、考え方によっては「質の悪い建材で家を建てる」に等しいものだ。設計品質が同じならば、出来上がる家(クラス/モジュール/コンポーネント)の品質は建材の質に左右されるものである。

書籍購入:『組込みソフトウェア開発のための構造化プログラミング』

あとで書く。

優秀なエンジニアは勉強しない

もう1ヶ月経つから、そろそろ書いてもよいだろう。

個人的な好悪より「エンジニア」ではなく「プログラマ」と記述するが、元来プログラマは勉強などしないものだ。

プログラマの三大美徳を思い出すこと。

プログラマは、技術的課題にたいして怠惰・短気・傲慢をもって取り組む*1。このいずれかに触れる何かがあった場合のみ、プログラマは意地になって、ドキュメントを漁り、コードを読み、昼夜を問わず解決方法を探る。

これは傍から見ると、特に職業プログラマの場合、私生活をかなぐり捨てて仕事をしている社畜だと思われがちだ。しかし実態は、単に「筋の悪い力業」で課題を解決することによる気持ち悪さから逃れようとする、プログラマ特有の生理現象に過ぎない。

もとより、課題を無理矢理力でねじ伏せて解決するなど、目先の売上としては正しくとも、技術的には後々に禍根を残すことが多いものだ。今日の「筋の悪い力業」は、明日の死亡フラグ。そのことを身をもって知っているからこそ、もっとマシな解決方法を得るためにプログラマはあがき続ける。

しかし、これらの行為は、決して「勉強」とは言わない。終業間際のコーディング中に何かしら引っかかるものを感じて、帰宅後に一風呂浴びた後に一杯飲みながらデバイスのデータシートを読んだりフレームワークの公式リファレンスを眺めたりしていようとも、プログラマはそれをもって「プライベートに勉強している」とは言わない。

なぜなら、それはプログラマ特有の習性ないし生理現象であり、不快さから逃れようとした結果に過ぎないからだ。気になったからちょっと調べただけで、別段大したことはしていない――というのが、当人の答えだ。

一方で、三大美徳にかすりもしない課題には、プログラマは熱意を抱けない。だから、そんなものをわざわざ勉強しようとなど思わない。

そんなことするぐらいなら、趣味に費やす方がマシだ。

興味深いことに、理由は分からないがプログラマには古い意味でのおたく・マニア的気質の持ち主も多くて、そのためか趣味についても割と深掘りする傾向にあることが多い。英語でいうなら「like to do」よりも「hobby」に近い。どちらかといえば凝り性だ。

ついでに言えば、三大美徳を重んじるプログラマには、プログラミングという行為そのものや、プログラミングから派生した物事を趣味とする人も多い。この辺り、金田一秀穂氏の説に沿うとするなら、おたく的気質の人だと割とプログラミング系の趣味しか持たず、マニア的気質の人だとプログラミング関連以外の趣味も持っている。

私の周囲という非常に狭い観測範囲となるが、技術面で一日の長がある人は、割とコンピュータ関連の趣味を持っている(もしくは過去に持っていた)ケースが多い。実際には、並行して他の趣味も持っている人が大半で、コンピュータ・プログラミング系に趣味が偏っている人は少数派である。

こういった趣味は、その時々の時流と一致すると、職業プログラマの仕事に大いに役立つ。

例えば、私の本業は組み込み系Cプログラマなのだが、趣味で全く畑違いの言語を触ることがある。それらのうち、JavaScriptLuaXSLTあたりは、なぜか数年後に業務で触れることになった。趣味レベルとはいえ事前に経験済みだった点は、仕事にプラスになった。

また、ライトなPC自作も趣味で、しかしお金がないのでOSとしてLinuxディストリビューションを入れている(特に最近は、手間をかけるのが面倒なのでUbuntuに統一している)のだが、これも業務絡みでUbuntu LTSやDebian仮想マシンを構築・運用したり、実験機としてRaspberry Pi(のRaspbian)を使ったりする場合に若干の応用が効くものだ。

こういったケースは、実のところ少ない。趣味の一環でプログラミング言語を30言語近く触ってきたが、その大半は本業では使うことがない。業務で使うことがあったとしても、シェルスクリプトPythonPowerShellのように(私の仕事では)内製ツールや自分用ツールの開発に使う程度であったり、Makefileのように「納品物には含まれるけど、実際のソフトウェア製品そのものではないよね」という具合に留まることが多い。

最近だとちょっとしたArduino用ライブラリを書いたりしたが、これも仕事とはほぼ無関係だ。そもそも仕事でArduinoを使うことなどないし、今のところその予定もない。

あくまでも趣味なのだ。プライベートの趣味は自由だ。だから、趣味がその時々の時流と一致するとは限らない。むしろ「一致しない」という前提で考えるべきだろう。

というか、趣味に無理矢理「仕事のエッセンス」をぶち込まれることほど無粋で興醒めなことはない。

実のところ、「お勉強」という視点では、Windows Serverの構築・運用・管理だとか、Office 365のクラウド機能の評価とか、やったほうがよいことは色々ある*2。あるのだが、それは私の趣味に合わないので、手を出していない。

結局、会社的にみて「優秀なプログラマ」というのは、単にその個人の趣味が時々の時流に偶然合致しているに過ぎないのだ。そしておそらく、その会社では偶然にも今までコンピュータ関連の趣味の持ち主が多くて、さらに偶然にも彼らの趣味が会社の方向性と重なる部分がそこそこ多かっただけなのだ。

だから、職業プログラマにたいして「業務時間外に勉強を」云々と述べるのは筋違いだ。アレは趣味なのだ。趣味にケチをつけるな。

よく分からないけど、アメリカのような雇用の流動性が高い地域なら、会社のその時々の方向性と合致する趣味の人を数年単位で雇用するなんてことがあまり無理なく可能じゃないのかしら。ただし方向性の違いで離職したり、逆に方向転換のためにレイオフしたり、なんてことも割とある感じで。

*1:「技術的課題にたいして」という点に留意すること。同僚/チームメンバーといった「人」というファクターにたいしては謙虚・尊敬・信頼をもって臨むのが正しい。

*2:最近、時々自分の本業が分からなくなる。