Juliaが1.0になって、徐々に環境が整ってきました。ここ数日、LibSndFile.jlやPortAudio.jlを使ってゴニョゴニョやっていたことをまとめておきます。
入出力の同期とか考えずに最低限の音再生ができればいいという場合は、
using FileIO: load, save import LibSndFile using PortAudio snd = load("hoge.wav"); stream = PortAudioStream(0, 2); write(stream, snd);
とすれば、ファイルを読み込んで、デフォルトの2チャンネル再生デバイス(内蔵スピーカとか)から再生してくれます。
パッケージのインストール
LibSndFileはオーディオファイルの入出力をするためのライブラリ、PortAudioはリアルタイムにオーディオの入出力をするためのライブラリで、〜.jlは両者をラップするパッケージです。LibSndFile.jlもPortAudio.jlもまだJulia 1.0に正式対応しているわけではないので、インストールから戸惑う部分があります。
Julia 0.7以降ではパッケージのインストールはjulia>
プロンプトで]
をタイプしてpkgモードに入って行います(プロンプトが(v1.0) pkg>
のようになります)。そこで、add SIUnits#master
、add SampledSignals#master
、add LibSndFile#master
の三つを導入します。#master
は「開発版を使用するぞ」というオマジナイです。PortAudio.jlについては、9月10日の時点で#master
ではうまくいかず、add PortAudio#julia1
とするとうまく行きました。こちらは#master
よりもさらに初期のバージョンを使用することになるかと思うので今後すぐに状況が変わると思います。
LibSndFileの使い方
Julia 0.6まではusing LibSndFile
で良かったような気がしますが、いまは以下のようにFileIO
からload
とsave
を使用し、そこにLibSndFile
を重ねます。load(ファイル名)
で、WAV、FLAC、OGGから読み込みができます。
julia> using FileIO: load, save julia> import LibSndFile julia> snd = load("recorder.wav") 352800-frame, 1-channel SampleBuf{FixedPointNumbers.Fixed{Int16,15}, 2} 8.0s sampled at 44100.0Hz ▁▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▂▁▁▁▁▁▁▁▁▁▁▁▁▅▅▅▅▅▆▆▆▆▆▆▆▆▆▆▆▆▅▁▁▁▁▁▁▁▁▁▁▁▅▅▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▅▁▁▁
snd.data
にオーディオのサンプルが、snd.samplerate
に標本化周波数が入っています。
一方、x
にオーディオデータがあるとして、それを保存するときには
using SampledSignals buf = SampleBuf(x, samplerate); save("output.wav", buf);
とします。
PortAudioの使い方
準備
PortAudioでは、まず使用するオーディオ入出力機器を確認します。
julia> using PortAudio julia> PortAudio.devices() 7-element Array{PortAudio.PortAudioDevice,1}: PortAudio.PortAudioDevice("Built-in Microphone", "Core Audio", 2, 0, 48000.0, 0) PortAudio.PortAudioDevice("Built-in Output", "Core Audio", 0, 2, 48000.0, 1) PortAudio.PortAudioDevice("USB PnP Audio Device", "Core Audio", 0, 2, 48000.0, 2) PortAudio.PortAudioDevice("USB PnP Audio Device", "Core Audio", 1, 0, 48000.0, 3) PortAudio.PortAudioDevice("Soundflower (2ch)", "Core Audio", 2, 2, 48000.0, 4) PortAudio.PortAudioDevice("Soundflower (64ch)", "Core Audio", 64, 64, 48000.0, 5) PortAudio.PortAudioDevice("Fireface UC Mac (xxxxxxxx)", "Core Audio", 18, 18, 48000.0, 6)
デバイスリストの更新はusing PortAudio
をしたときだけに行われるようなので、使用したいオーディオインターフェースはあらかじめ接続しておきます。このリストは、デバイス名、ホストAPI、最大入力チャンネル数、最大出力チャンネル数、標本化周波数のデフォルト値、デバイス番号の順になっています。(Firefaceはxxxxxxxx
の部分にシリアルナンバーが入っています)
デバイスとのやりとりをするストリームを開くことで、オーディオ信号のやりとりが可能になります。たとえば1入力・2出力を確保したければ以下のようにします。
julia> stream = PortAudioStream(1, 2) PortAudioStream{Float32} Samplerate: 48000.0Hz Buffer Size: 4096 frames 2 channel sink: "Built-in Output" 1 channel source: "Built-in Microphone"
ただ、入力・出力を別々に指定したり、チャンネル数を指定したり、標本化周波数やバッファサイズ場合を変更したりなども可能です。(ただ、ホストAPIから「このタイミングで標本化周波数は変えられないよ」などのエラーが出てくるかもしれません)
julia> stream = PortAudioStream("Built-in Microphone", "Soundflower (2ch)", 1, 2; samplerate=44100, blocksize=512, synced=true) PortAudioStream{Float32} Samplerate: 44100.0Hz Buffer Size: 512 frames 2 channel sink: "Soundflower (2ch)" 1 channel source: "Built-in Microphone"
あるいは、入出力両方に対応しているデバイス(full duplex)の場合は以下のようにデバイス名は1回指定でOKです。
julia> stream = PortAudioStream("Fireface UC Mac (xxxxxxxx)", 1, 2; samplerate=48000, blocksize=512, synced=true) PortAudioStream{Float32} Samplerate: 48000.0Hz Buffer Size: 512 frames 2 channel sink: "Fireface UC Mac (xxxxxxxx)" 1 channel source: "Fireface UC Mac (xxxxxxxx)"
ここで、Fireface UCのように入出力両方にチャンネル数が表示されているものは入出力の同期がとれますが、Built-inのMicとOutは別デバイスなので入出力の同期が取れないようです。僕の用途では入出力の遅延がわかっていることが大切なので、full duplexなデバイスを使用します。
オーディオの入出力
read()
とwrite()
でストリームから/へSampledSignals.SampleBuf
型で読み書きできます。read
はストリームと、そのブロックサイズを指定します。write
はストリーム、データ、ブロックサイズの3引数です。以下ではマイク入力を512サンプル取り込み、無音(zeros()
)を512サンプル出力しています。
julia> buf_in = read(stream, stream.blocksize) 512-frame, 1-channel SampleBuf{Float32, 2} 0.010666666666666666s sampled at 48000.0Hz ▃▃▃▃▂▁▁▂▂▂▁▁▂▂▂▃▃▃▃▃▃▂▁▁▃▃▃▂▂▁▁▂▂▁▁▂▂▂▁▁▁▂▂▁▂▃▃▃▃▂▂▂▂▂▁▁▁▁▃▃▃▃▃▂▂▁▁▁▁▂▂▂▂▂ julia> write(stream, zeros(stream.blocksize), stream.blocksize) 512
full duplexなデバイスで入出力を同期させるときには、上記のように入力と出力のデータ量を対応させる必要があります。たとえばインパルス応答測定では、測定用信号をスピーカ出力して、それに対して部屋の応答をマイク入力から受けますので、測定信号をブロックサイズぶん出力してはマイク入力からブロックサイズぶん信号を受けるということを繰り返します。
具体的な入出力部分を見てみましょう。(このあたり、アヲギリさんのNotebookがかなり参考になったはずなのですが、具体的にどのNotebookだったのかは失念……)
blksize = stream.blocksize; sig_out = load("input.wav"); sig_in = zeros(round(Int, floor(length(sig_out)/blksize + 5) * blksize)); buf_in = read(stream, blksize); for i in 0:ceil(Int, length(sig_out)/blksize)-1 # get input from microphone buf_in = read(stream, blksize); sig_in[i*blksize+1 : (i+1)*blksize] = convert(Array{Float32}, buf_in.data[:,1]); # output to loudspeaker if (i+1)*blksize > length(sig_out) buf_out = sig_out[i*blksize+1 : end] else buf_out = sig_out[i*blksize+1 : (i+1)*blksize]; end write(stream, buf_out, blksize); end recorded = SampleBuf(sig_in[floor(Int, blksize*4+1) : floor(Int, blksize*4)+length(sig_out)], sig_out.samplerate); save("output.wav", recorded); close(stream);
sig_out
には出力したい信号(インパルス応答測定であればスイープサイン音など)を入れておきます。sig_in
は最終的に録音されファイル保存される信号です。for
文でsig_out
の長さに相当する回数だけループし、各ループにおいてbuf_out
とbuf_in
はブロックサイズ(blksize
)ごとに分割された細切れの信号を入出力しています。他に色々と面倒なことをやっているのは、sig_out
の信号長がブロックサイズの整数倍になっていないときを想定したりしているからです。
特筆しておきたいのは、PortAudio.jl
では入出力のバッファとしてブロックサイズ2つぶんの長さのリングバッファを持っているようであることです。そのため、合計ブロックサイズ4つぶんの遅延が想定されます。そのため、上記コードでは、(1)sig_out
の信号長がブロックサイズの整数倍未満のとき用に1ブロック、(2)内部のリングバッファ用に4ブロック、の計5ブロックを考慮した処理を入れています。
それらの遅延などが考慮して、最終的にrecorded
にはsig_out
と同じ長さの録音信号が入ります。WAVファイルに保存し、使用が終わったストリームはclose(stream)
で閉じます。
まとめ
一応上記のコードでいくつか試しに測定してみましたが、まぁまぁうまくいっているようです。今回はオーディオ信号の出力→オーディオ信号の入力、というシンプルな部分について書きましたが、ほぼ同じやり方を使って入力に対して処理をして出力するエフェクタのようなものも書けます。
ここで紹介した内容はPortAudio#julia1
版でのものなので、今後(わりとすぐに)変わる可能性があります。そうは言っても、Juliaでオーディオを扱う人が増えるといいなということで、参考までにPinkTSP信号を用いたインパルス応答測定のコード全体をおいておきますね。play_and_record()
内のオーディオデバイス名を自身の環境に合わせて変える必要があります。
※これは現時点でのラフなコードですので、絶対に本番環境で使ってはいけません!!!
なお、PinkTSPについては以下の論文を参照して下さい。
- 藤本卓也, “低域バンドでのSN比改善を目的としたTSP信号に関する検討” 日本音響学会研究発表会講演論文集, p.433. (1999)
- 藤本卓也, “低域バンドでのSN比改善を目的としたTSP信号に関する検討 —高調波歪の除去—” 日本音響学会研究発表会講演論文集, p.555. (2000)
using FileIO: load, save using DSP, SampledSignals, LibSndFile, PortAudio, FFTW using Printf, Primes """ duration = minimum length of the signal (seconds) fs = number of samples per second (Hz) nbits = number of bits per sample (bit) reps = number of repeated measurements (times) """ function pinktsp_generate(duration=5.0, samprate=48000, nbits=32, reps=4) N = 2 ^ ceil(Int, log2(duration * samprate)); m = prevprime(N >> 2) + 1; a = 2 * m * pi / ( (N/2) * log(N/2) ); H = complex(zeros(N)); H[1] = 1; H[2:convert(Int, N/2+1)] = exp.(1im * a .* (1:N/2) .* log.(1:N/2)) ./ sqrt.(1:N/2); H[convert(Int, N/2+2):N] = conj(H[convert(Int, N/2):-1:2]); h = real(ifft(H)); mi = argmin(abs.(h)); h = [h[mi:end] ; h[1:mi-1]]; h = reverse(h); hh = [repeat(h, outer=reps) ; zeros(length(h))]; # write to an audio file hh = hh / maximum(abs.(hh)) * sqrt(1/2); # peak at -3 dBFS if nbits==16 hh2 = map(PCM16Sample, hh); elseif nbits==24 hh2 = map(PCM24Sample, hh); elseif nbits==32 hh2 = map(PCM32Sample, hh); else nbits = 32; hh2 = map(PCM32Sample, hh); error("unsupported bitrate (falls back to 32bits)"); end buf = SampleBuf(hh2, samprate); return(buf); end ## playback and record function play_and_record(snd) fs = snd.samplerate; stream = PortAudioStream("Fireface UC Mac (xxxxxxxx)", 1, 1; samplerate=fs, blocksize=512, synced=true); blksize = stream.blocksize; data_in = zeros(round(Int, floor(length(snd)/blksize + 5) * blksize)); buf_in = read(stream, blksize); for i in 0:ceil(Int, length(snd)/blksize)-1 # get input from microphone buf_in = read(stream, blksize); data_in[i*blksize+1 : (i+1)*blksize] = convert(Array{Float32}, buf_in.data[:,1]); # output to loudspeaker if (i+1)*blksize > length(snd) buf_out = snd[i*blksize+1 : end] else buf_out = snd[i*blksize+1 : (i+1)*blksize]; end write(stream, buf_out, blksize); end close(stream); buf = SampleBuf(data_in[floor(Int, blksize*4+1) : floor(Int, blksize*4)+length(snd)], fs); return(buf); end ## deconvolve recorded signal with tsp signal function pinktsp_analyze(tspsignal, recorded, reps) k = convert(Array{Float64}, tspsignal.data[:,1]); k = sum(reshape(k, (round(Int, length(k)/(reps+1)), reps+1)), dims=2); x = convert(Array{Float64}, recorded.data[:,1]); x = sum(reshape(x, (round(Int, length(x)/(reps+1)), reps+1)), dims=2); y = real(ifft(fft(x) ./ fft(k))); outir = SampleBuf(y, snd.samplerate); return(outir); end dur = 5.0; nbits = 32; reps = 4; fs = 48000; snd = pinktsp_generate(dur, fs, nbits, reps); save("sweep.wav", snd); buf = play_and_record(snd); save("recorded.wav", buf); outir = pinktsp_analyze(snd, buf, reps); save("ir.wav", outir);