2018年6月20日水曜日

SMPTEタイムコードのデコード(オフライン)

SMPTE Time Codeというレガシーな仕組みをご存知ですか?

映像の再生位置を示す「タイムコード("時間:分:秒:フレーム数"で表現される時間情報)」が,「オーディオ信号」として伝送されるという方式のものです.

SMPTEタイムコードのオーディオデータを生成してダウンロードできるサイトです.まずは,このサイトでオーディオデータを生成して聞いてみましょう.(※音量注意です.聴感上,非常に音が大きく感じられますし,結構不快な音かもしれません.

http://elteesee.pehrhovey.net/


このデータは後で使いますので,Bit-depthを"16-bit signed int"にするのをお忘れなく.

どうでしょうか?「プルルルルルルルルル・・・」という音が聞こえると思います.この音が,実はタイムコードを表しているのです.

例えば,iPhoneでSMPTEタイムコードを再生して,別のiPhoneのマイクロホンでそれを拾い,デコードしている例があります.

http://blog.livedoor.jp/cpiblog00465/archives/52419598.html

このように,音を出力し,それを拾うだけで時間情報の同期ができてしまうのです.

この技術は,現在でも音響だけでなく,照明や映像等が複雑に絡み合うシステムを構築する際によく使われます.例えば,音響は,Pro ToolsのようなDAWでマルチトラック再生し,同時にサウンドカードの別系統からSMPTEタイムコードを再生し出力,それを映像機器や照明機器に入力し,時間を同期して映像の再生や照明演出が進んでいくシステムを構築できます.

ディズニーランドではパレードやアトラクションでの演出の同期に,SMPTEタイムコードによる同期のテクニックが使われています.

https://ggsoku.com/2012/03/projection-mapping/

SMPTEタイムコードの音声信号の波形を拡大してみると,このようになっています.


矩形波のような,正弦波のような波形になっています.本当は,もっとしっかり矩形波になっている筈なんですが,上記ページから入手したものの波形はこんな感じになっています.しかし,実用上問題ありません.

この波形を見ると,幅の長い波と短い波があることがわかります.SMPTEタイムコードでは,幅の短い上下の変化2回で "1" ,幅の長い上下の変化1回で "0" とみなします.


こうして音声波形から0と1を読み取っていくと,二進数列ができます.この二進数列を以下のように読んでいきます.


このデコード処理をMATLABで実装してみたいと思います.(MATLABだと音声ファイルの扱いが簡単なので)

波形のステップ幅の検出は,ステップ時に信号の値が"0"を通過することに着目し,0通過時から次の0通過時までの幅を計測します.0通過は,今回のような矩形波の場合,現在のサンプルと.1つ前のサンプルとの積を求め,その符号を調べ,マイナスになれば0を通過したことがわかります.(プラス x プラスはプラスの値,マイナス x マイナスもプラスの値になりますが,プラス x マイナス,或いはマイナス x プラスはマイナスの値になります.)

Sync wordを基準にして,それが出現したら過去にバッファしたビット列の中から,必要な部分を参照し,タイムコードをデコードしていきます.

ゆくゆくリアルタイムで実装することを見越して,for文の中で1サンプルずつ参照しながらデコードを行う設計になっています.

clear;
s=audioread('smpte.wav');
del=0;%1サンプル前のオーディオデータ
isshort=0;%短いステップ内に入ったことを示すフラグ
count=0;%ステップ内のサンプル数を計測
bitcount=1;%ビットを記録する位置
bitbuf=zeros(16,1);%sync word検出用バッファ
synccode=[1;0;1;1;1;1;1;1;1;1;1;1;1;1;0;0];%sync word
timecode=zeros(1,16*4*2);%検出したビットのバッファ(いつまでもsync wordが検出できない場合に備えてバッファを必要数の2倍にしておく)
%行ベクトルにしているのは、bin2decの引数にする2進数の文字列をint2strによって生成するため
hour=0;
min=0;
sec=0;
frame=0;
for k=1:length(s)
    sgn=sign(s(k)*del);%現在のオーディオデータと1サンプル前のオーディオデータの積の符号
    if (sgn<0) %ステップ内に入った(信号が '0' を通過した)
        if isshort==0 %短いステップ内ではない
            for n=16:-1:2
                bitbuf(n)=bitbuf(n-1);%過去の15ビットをバッファリング
            end
            if count<11 %短いステップであれば現在のビットは '1'(閾値は適当)
                isshort=1;%次にもう一回短いステップがあるはずなのでフラグを '1' にしておく
                bitbuf(1)=1;
            else %ステップが長ければ現在のビットは '0'
                bitbuf(1)=0;
            end
            timecode(bitcount)=bitbuf(1);%ビットのバッファに現在のビットを格納
            bitcount=bitcount+1;%次回の格納先を1ずらす
            if bitcount>16*4*2 %次回の格納先がバッファサイズを超えたら一旦格納先をリセット
                bitcount=1;
            end
        else %2回目の短いステップ検知時は何もしない
            isshort=0;
        end
        if isequal(bitbuf,synccode)==1 %syncwordを検知したら、timecodeにはSMPTE規格のビット列が格納されている筈
            %各値を10進数に変換
            frame=bin2dec(int2str(timecode(4:-1:1)))+10*bin2dec(int2str(timecode(10:-1:9)));
            sec=bin2dec(int2str(timecode(20:-1:17)))+10*bin2dec(int2str(timecode(27:-1:25)));
            min=bin2dec(int2str(timecode(36:-1:33)))+10*bin2dec(int2str(timecode(43:-1:41)));
            hour=bin2dec(int2str(timecode(52:-1:49)))+10*bin2dec(int2str(timecode(58:-1:57)));
            sprintf('%d:%d:%d:%d',hour,min,sec,frame)
            bitcount=1;%バッファの格納先をリセット
        end
        count=0;%ステップ数をリセット
    end
    count=count+1;
    del=s(k);%現在のオーディオデータを保持
end

2018年6月5日火曜日

OpenMusicの基礎 番外編 〜ワンスモード〜

※この記事は「はじめての<脱>音楽 やさしい現代音楽の作曲法」における私執筆の「OpenMusic」の基礎及び,前回までのOpenMusicの基礎 番外編シリーズをお読みください.

前回はomloopの非常に基礎的な考え方をご紹介しました.少し難しい(と私は感じるの)ですが,慣れてくるとだんだん意図通りの繰り返し制御ができるようになります.ここからもう少しアルゴリズム作曲に活かせるようなomloopの使い方を紹介したいのですが,その前にご紹介しておきたい話題があります.これを知らないと,複雑なプログラムを意図通りに動作させることができません.

まず,いつも引き合いに出しているMaxで例題となる処理を作ってみます.説明のためだけに作る,全く必然性のない処理ですが,こんな処理です.

①乱数 r(0~10) を発生させる
②乱数 r の値を表示する
③r+r を計算する
④r+rの値を表示する

※「乱数 r 」というのは,勿論便宜的に設定した変数であって,MaxやOpenMusicのようなビジュアルプログラミング環境では,実際にはこのような変数を設定して代入することはしません.

乱数 r の値がいくつであっても,必ずrの2倍の値が表示されるはずです.

こんなパッチになります.Maxのrandomオブジェクトは,アーギュメントの値-1が乱数の最大値となることに注意してください.


このパッチの処理手順は図のようになります.順番が左右あちこちに飛んでいて分かりづらいですが,順番をしっかり確認しましょう.


ここで押さえておきたいのは,一回のbangで①〜⑥の全ての処理が一気に行われるということです.randomオブジェクトは,第1インレットにbangが入力されるまで乱数を新たに発生させませんから,このパッチの+オブジェクトで,まさに r+r の計算が行われることになります.

OpenMusicでも同じように作ってみましょう.これで良いでしょうか?Maxの時のように,1回のEvalBoxごとに1回乱数が発生して,その乱数の2倍の値が出力されるでしょうか?

OM+オブジェクトをEval Boxしてみましょう.


"OM >"となっている行がprintオブジェクトに入力された値,"OM =>"となっている行がOM+オブジェクトをEval Boxした処理結果です.ということは,この処理は,乱数が2回発生して,その和を求めているということになります.
これは,OpenMusicの制御構造を思い出すと,納得できると思います.OpenMusicはMaxとは逆に下から上のオブジェクトへデータを要求していく制御構造になっています.これを踏まえると,このパッチはこのような処理手順で処理を実行しています.図に書き込むと行ったり来たり,非常にややこしくなるので箇条書きにしますが,パッチと見比べながら処理手順を追ってみてください.

① OM+オブジェクトがEval Boxされる(ユーザ操作)
② OM+オブジェクトの第1インレットにデータが未到着なので,上のオブジェクトにデータを要求.
③ PRINTオブジェクトは更に上のオブジェクトにデータを要求
④ OM-RANDOMオブジェクトはデータを要求されたので,乱数を生成してアウトレットから出力
⑤ PRINTオブジェクトは乱数を表示してアウトレットからそのまま出力
⑥ 今回はあくまでOM+オブジェクトの第1インレットからの要求なので,OM+オブジェクトの第1インレットにのみ出力する
⑦ OM+オブジェクトの第2インレットにデータが未到着なので,上のオブジェクトにデータを要求.
⑧ PRINTオブジェクトは更に上のオブジェクトにデータを要求
⑨ OM-RANDOMオブジェクトはデータを要求されたので,乱数を生成してアウトレットから出力
(10) PRINTオブジェクトは乱数を表示してアウトレットからそのまま出力
(12) 今回はあくまでOM+オブジェクトの第2インレットからの要求なので,OM+オブジェクトの第2インレットにのみ出力する
(13) 両方のインレットにデータが到着したので和を計算して表示

PRINTオブジェクトのアウトレットから出ている線の数だけ同じ処理が繰り返されているのです.データが到着していないインレットの数だけ要求が発生するので当然です.そしてそのたびに,OM-RANDOMオブジェクトは乱数を生成しているのです.
これは,1つの要求が終わって,次の要求が始まるまでの間に,それまでのオブジェクトの状況が保持されていないために起こります.
だったら保持するようにしようというのが「ワンスモード」です.

OM-RANDOMオブジェクトを選択してキーボードの"1"キーを押すと,OM-RANDOMオブジェクトの左上に"1"というアイコンがオーバーラップ表示されました.これは,OM-RANDOMオブジェクトがワンスモードに入ったことを意味します.


ワンスモードになったオブジェクトは,1回のEval Boxの間,一回要求に答えたら,処理結果を保持しておき,その後新たな要求が来てもその処理結果をアウトレットから出力するだけになります.このパッチをEval Boxするとこうなります.乱数による偶然ではなく,ちゃんと意図通りの動作をしていることを示すために何度かEval Boxしてみます.

OM-RANDOMオブジェクトが2回とも同じ値を出力していて,r+r が計算できていることがわかります.この時の処理手順は以下のようになります.

① OM+オブジェクトがEval Boxされる(ユーザ操作)
② OM+オブジェクトの第1インレットにデータが未到着なので,上のオブジェクトにデータを要求.
③ PRINTオブジェクトは更に上のオブジェクトにデータを要求
④ OM-RANDOMオブジェクトはデータを要求されたので,乱数を生成してアウトレットから出力.ワンスモードなのでこの値は最後まで保持しておく.
⑤ PRINTオブジェクトは乱数を表示してアウトレットからそのまま出力
⑥ 今回はあくまでOM+オブジェクトの第1インレットからの要求なので,OM+オブジェクトの第1インレットにのみ出力する
⑦ OM+オブジェクトの第2インレットにデータが未到着なので,上のオブジェクトにデータを要求.
⑧ PRINTオブジェクトは更に上のオブジェクトにデータを要求
⑨ OM-RANDOMオブジェクトはデータを要求されたが,ワンスモードなので乱数を生成せず, ④で生成した値をアウトレットから出力
(10) PRINTオブジェクトは乱数を表示してアウトレットからそのまま出力
(12) 今回はあくまでOM+オブジェクトの第2インレットからの要求なので,OM+オブジェクトの第2インレットにのみ出力する
(13) 両方のインレットにデータが到着したので和を計算して表示

これで,r+r の値を計算することはできたのですが,OM-RANDOMオブジェクトが2回動作しているのは変わりません.そこで,PRINTオブジェクトをワンスモードにすることで,2回目の要求でOM-RANDOMオブジェクトの動作を省略することができます.

この時の手順は以下のようになります.

① OM+オブジェクトがEval Boxされる(ユーザ操作)
② OM+オブジェクトの第1インレットにデータが未到着なので,上のオブジェクトにデータを要求.
③ PRINTオブジェクトは更に上のオブジェクトにデータを要求
④ OM-RANDOMオブジェクトはデータを要求されたので,乱数を生成してアウトレットから出力.
⑤ PRINTオブジェクトは乱数を表示してアウトレットからそのまま出力.ワンスモードなのでこの値は最後まで保持しておく.
⑥ 今回はあくまでOM+オブジェクトの第1インレットからの要求なので,OM+オブジェクトの第1インレットにのみ出力する
⑦ OM+オブジェクトの第2インレットにデータが未到着なので,上のオブジェクトにデータを要求.
⑧ PRINTオブジェクトは,ワンスモードなのでOM-RANDOMオブジェクトに値を要求せず, ⑤で生成した値をアウトレットから出力
⑨ PRINTオブジェクトは乱数を表示してアウトレットからそのまま出力
(10) 今回はあくまでOM+オブジェクトの第2インレットからの要求なので,OM+オブジェクトの第2インレットにのみ出力する
(11) 両方のインレットにデータが到着したので和を計算して表示

いかがでしょうか?複雑なプログラムを作ろうとすると,ワンスモードを使いこなす必要があります.特にomloopを使う時は,非常に重要なテクニックになります.