SuperCollider+OSC+OpenFrameworksその2
SuperColliderでFFTからFreqScopeと同様の値を作成する試み。
上が自作、下がSCのFreqScope(周波数はリニアスケール)
↑の波形の確認用に使ったTidalcyclesのパターン:
d2 $ spread ($) [fast 1, rev, fast 2, slow 2]
$ n (run 8*16) # s "supersaw"
SuperColliderのコード:
RMSメーター同様に数フレーム分のバッファからスペクトルのピークを算出。
FFTの結果をいくつかの帯域に切り分けてパワースペクトルを求めている。
2019.1.21修正版
// 先にSuperDirtを起動しておく。後からだとうまくいかない。
SuperDirt.postBadValues = false // ChocolateyのSupercolliderの挙動対策
SuperDirt.start
SuperDirt.stop
s.boot;
b = Buffer.alloc(s,2048);
// 実行すると Warningが出るが支障ない。コメント入れてコード量が増えたからかも。
// SuperColliderのオーディオOUTを分析
(
// スペアナのバンド数
~size=16*8*8*2;
// 0Hz~22.1kHzまで帯域を切り分ける
~cutfreqs = Array.fill(~size,{|i| (s.sampleRate/~size*i).asInt});
c = Bus.control(s,~size);
x = {
Out.kr(c.index, // 分析結果をcで受け取る
RunningSum.kr( // RMSを求める
FFTSubbandPower.kr( // 帯域毎のpowerスペクトルを求める
FFT(b,In.ar), // serverの音声出力をFFTする
~cutfreqs, square: 1, scalemode: 1)/(~cutfreqs[1]), // 帯域当たりのサンプル数で割る。
40)/40 // RMS用のFrame数。平均をとるためFrame数で割る。
);
}.play(s,addAction: \addToTail); // serverが音声出力した後に分析をかける
)
x.free;
// グラフと数値の表示
(
p = Plotter("Freq.Scope",Rect(500,500,800,250));
t = Task({
loop{
c.getn(~size/2,{|val|
{
(val.log10*10).postln;
p.setValue(val.log10*10,findSpecs: true, minval:-96, maxval:0,defaultRange:1);
}.defer
});
0.016.wait;
};
}).start;
)
t.stop;
// テスト用音源
(
z = { SinOsc.ar(440*4,0,0.125)+SinOsc.ar(440*10,0,0.125)+SinOsc.ar(440*45,0,0.125)
}.play(s);
)
z.free;
// OSCでスペアナの結果を送信する
(
a = NetAddr.new("127.0.0.1", 8000);
v = Int8Array.fill(~size/2,0); // OSCメッセージ blob型用
o = Task({
loop{
c.getn(~size/2,{|val|
{
(val.log10*10).do({|e,i|
if(e < -96 || e.isNaN) {v[i] = -96} {v[i] = e.asInt};
});
v.postln;
a.sendMsg("/test",v);
}.defer
});
0.016.wait;
}
}).start;
)
o.stop;
OpenFrameworks側のコード
ofApp.h
const static int receivedBuffer_size = 1024;
char receivedBuffer[receivedBuffer_size];
ofApp.cpp
PIとsin cosの定義:
#include <cmath>
void ofApp::draw() の中で、スペクトルを表示
int x = 10;
ofSetColor(255,255,255);
for (char c : receivedBuffer) {
ofDrawLine(x,400,x,400-(96+c)*4.0);
x += 2;
}
ofSetColor(255, 0, 127);
int i = 0;
for (char c : receivedBuffer) {
int r2 = 150;
int r1 = r2+2 * (96 + c);
float theta = 2.0*PI* i / receivedBuffer_size;
ofDrawLine(800 + r2 * cos(theta), 400 + r2 * sin(theta), 800+ r1*cos(theta), 400 + r1*sin(theta));
i++;
}
実行結果
SupercolliderのFFTの調整
このyotube動画と同じ感じを目指してみた。https://twitter.com/_kobashi/status/1087609112127553537
まだいろいろと差がある。
周波数のメモリをlogにしてさらにsqrtする。
FFT用のバッファを8倍確保して、FFTのwindowsizeは元の半分にしてみた。
処理が重たくなるので、RMSで平均する個数は抑えた。
スペクトルの値はdB表示から値をsqrtしたものに変更した。
↑
※OSCで送信する値は、適切な精度を保ったままInt8に収まるように調整する必要がある。
s.boot;
s.stop;
b = Buffer.alloc(s,2048*8);
// 実行すると Warningが出るが支障ない。コメント入れてコード量が増えたからかも。
// SuperColliderのオーディオOUTを分析
(
// スペアナのバンド数
//~size=16*8*8*2;
~size=16*8*2;
// 0Hz~22.1kHzまで帯域を切り分ける
//~cutfreqs = Array.fill(~size,{|i| (s.sampleRate/~size*i/2).asInt}); // linearスケール
~cutfreqs = Array.fill(~size,{|i| exp(log(s.sampleRate/2)*sqrt(i/~size)).asInt}); //logスケールをsqrtしてグラフを0Hz側に寄せた
c = Bus.control(s,~size);
x = {
Out.kr(c.index, // 分析結果をcで受け取る
RunningSum.kr( // RMSを求める
FFTSubbandPower.kr( // 帯域毎のpowerスペクトルを求める
FFT(b,In.ar(2),wintype: 1,winsize: 2048/2), // serverの音声出力をFFTする
~cutfreqs, square: 1, scalemode: 1),
2)/2 // RMS用のFrame数。平均をとるためFrame数で割る。
);
}.play(s,addAction: \addToTail); // serverが音声出力した後に分析をかける
)
x.free;
// グラフと数値の表示
(
p = Plotter("Freq.Scope",Rect(500,500,800,250));
t = Task({
loop{
c.getn(~size,{|val|
{
(val.log10*10).postln;
//p.setValue(val.log10*10,findSpecs: true, minval:-96, maxval:0,defaultRange:1);
p.setValue(sqrt(val)*1,findSpecs: true, minval:0, maxval:0.025,defaultRange:1);
}.defer
});
0.016.wait;
};
}).start;
)
t.stop;
s.boot;
s.stop;
b = Buffer.alloc(s,2048);
(
// クロマグラフの音数
~size=12;
c = Bus.control(s,~size);
x = {
Out.kr(c.index, // 分析結果をcで受け取る
RunningSum.kr( // RMSを求める
Chromagram.kr(
FFT(b,In.ar(2)), // serverの音声出力をFFTする
fftsize: 2048, n: ~size, tuningbase: 32.703195662575,
octaves: 8, integrationflag: 0, coeff: 0.9, octaveratio: 2,
perframenormalize: 1),
40)/40 // RMS用のFrame数。平均をとるためFrame数で割る。
);
}.play(s,addAction: \addToTail); // serverが音声出力した後に分析をかける
)
x.free;
// グラフと数値の表示
(
p = Plotter("Freq.Scope",Rect(500,500,800,250));
t = Task({
loop{
c.getn(~size,{|val|
{
(val.log10*10).postln;
p.setValue(val*100,findSpecs: true, minval:0, maxval:10,defaultRange:1);
}.defer
});
0.016.wait;
};
}).start;
)
t.stop;
// テスト用音源
(
z = { SinOsc.ar(440*4,0,0.125)+SinOsc.ar(440*10,0,0.125)+SinOsc.ar(440*45,0,0.125)
}.play(s);
)
z.free;
追記:サウンドの可視化の際は可聴域に応じて帯域制限してからFFTを実行すると効果的の模様。
b = Buffer.alloc(s,2048);
// 実行すると Warningが出るが支障ない。コメント入れてコード量が増えたからかも。
// SuperColliderのオーディオOUTを分析
(
// スペアナのバンド数
// 0Hz~22.1kHzまで帯域を切り分ける
~size=100;
~cutfreqs = Array.fill(~size,{|i| (440*exp(log(2)/12*(i-40))).asInt});
c = Bus.control(s,~size+1);
x = {
Out.kr(c.index, // 分析結果をcで受け取る
RunningSum.kr( // RMSを求める
FFTSubbandPower.kr( // 帯域毎のpowerスペクトルを求める
FFT(b,HPF.ar(In.ar(2),440) // 可視化する帯域を制限する
,wintype: 1,hop:0.5,winsize: 2048), // serverの音声出力をFFTする
~cutfreqs, square: 1, scalemode: 2),
60)/60 // RMS用のFrame数。平均をとるためFrame数で割る。
);
}.play(s,addAction: \addToTail); // serverが音声出力した後に分析をかける
)
x.free;
// グラフと数値の表示
(
p = Plotter("Freq.Scope",Rect(500,500,800,250));
t = Task({
loop{
c.getn(~size+1,{|vals|
{
(vals.log2 + 7 * 0.08).max(0).min(1).postln;
p.setValue((vals*30+1).log10,findSpecs: true, minval:0, maxval:3,defaultRange:1);
}.defer
});
0.016.wait;
};
}).start;
)
t.stop;
// OSCでスペアナの結果を送信する
(
a = NetAddr.new("127.0.0.1", 8000);
v = Int8Array.fill(~size+1,0); // OSCメッセージ blob型用
o = Task({
loop{
c.getn(~size+1,{|val|
{
((vals*30+1).log10).do({|e,i|
if(e > 3) {v[i] = 127} {v[i] = (e*127/3).asInt};
});
v.postln;
a.sendMsg("/test",v);
}.defer
});
0.016.wait;
}
}).start;
)
o.stop;
追記2:
oF側のコードにOSCで受信したSCのFFTの値を合計して60*5(60fpsで受信できていれば約5秒間分)個記録して表示するコードを加えた。
ofApp.h
int waveBuffer[60*5];
ofApp.cpp
int sum = 0;
for (char c : receivedBuffer) {
sum += c;
}
memcpy(&waveBuffer[0], &waveBuffer[1], (60 * 5 - 1) * sizeof(int));
waveBuffer[60 * 5 - 1] = sum/20;
ofSetColor(0, 192, 64);
x = -100;
for (int e : waveBuffer) {
ofDrawLine(x, 600 + e * 0.5, x, 600 - e * 0.5);
x += 3;
}