380 lines
9.6 KiB
Python
380 lines
9.6 KiB
Python
BeatBox {
|
|
var <>basepath;
|
|
|
|
var w, <layout, <scope, <server, beats, nchannels, seq, data, recbutton;
|
|
var onsetListener, statusText, thresholdText, lagText, scope, <lowerStatus;
|
|
var fftbuf, recbuf, synth, isListening, isRecording, isJamming, recStart;
|
|
var jambutton;
|
|
|
|
*new { |basepath|
|
|
^super.new.init(basepath);
|
|
}
|
|
|
|
init { |aBasePath|
|
|
basepath = (aBasePath ? "/l/cm2/coursework/task3") ++ "/";
|
|
|
|
w = SCWindow("Beatbox Beta 1", Rect(200, 700, 520, 558), resizable: false);
|
|
|
|
// keyboard shorts
|
|
w.view.keyDownAction_{ |...args|
|
|
var shift = args[2];
|
|
var code = args.last;
|
|
|
|
code.switch(
|
|
36, { // enter
|
|
seq.setCurrentHit;
|
|
},
|
|
13, { // backspace
|
|
seq.clearCurrentHit;
|
|
},
|
|
49, {
|
|
// move to seq
|
|
seq.mainbutton.value_((seq.mainbutton.value + 1) % 2).doAction;
|
|
},
|
|
122, {
|
|
if(recbutton.enabled) {
|
|
recbutton.value_((recbutton.value + 1) % 2).doAction;
|
|
}
|
|
},
|
|
120, {
|
|
this.preview;
|
|
},
|
|
99, {
|
|
if(jambutton.enabled) {
|
|
jambutton.value_((jambutton.value + 1) % 2).doAction;
|
|
}
|
|
},
|
|
123, {
|
|
// move to seq
|
|
if(seq.xfadeval > 0.0) {
|
|
seq.xfadeslid.value_(seq.xfadeval - 0.1).doAction;
|
|
}
|
|
},
|
|
124, {
|
|
// move to seq
|
|
if(seq.xfadeval < 1.0) {
|
|
seq.xfadeslid.value_(seq.xfadeval + 0.1).doAction;
|
|
}
|
|
},
|
|
126, { // move to seq
|
|
seq.currentChannel = (seq.currentChannel - 1) % seq.numChannels;
|
|
seq.selects[seq.currentChannel].value_(1).doAction;
|
|
},
|
|
125, { // move to seq
|
|
seq.currentChannel = (seq.currentChannel + 1) % seq.numChannels;
|
|
seq.selects[seq.currentChannel].value_(1).doAction;
|
|
}
|
|
)
|
|
};
|
|
|
|
layout = FlowLayout(w.view.bounds);
|
|
w.view.decorator = layout;
|
|
|
|
nchannels = 8;
|
|
|
|
server = Server.local;
|
|
server.doWhenBooted {
|
|
recbuf = Buffer.alloc(server, server.sampleRate);
|
|
fftbuf = Buffer.alloc(server, 512);
|
|
};
|
|
|
|
isListening = false;
|
|
isRecording = false;
|
|
isJamming = false;
|
|
beats = Array.newClear(nchannels);
|
|
|
|
File.use(basepath ++ "data.sc", "r") { |file|
|
|
data = file.readAllString.interpret;
|
|
};
|
|
|
|
this.initDisplay;
|
|
}
|
|
|
|
initDisplay {
|
|
recbutton = SCButton(w, Rect(5, 5, 100, 25))
|
|
.states_([
|
|
[ "Record (F1)", Color.black, Color.green ],
|
|
[ "Stop (F1)", Color.white, Color.red ],
|
|
])
|
|
.action_{ |button| this.switch(button) }
|
|
.keyDownAction_{ nil };
|
|
|
|
SCButton(w, Rect(5, 5, 100, 25))
|
|
.states_([
|
|
[ "Preview (F2)", Color.black, Color.green ],
|
|
])
|
|
.action_{ |button| this.preview(button) }
|
|
.keyDownAction_{ nil };
|
|
|
|
jambutton = SCButton(w, Rect(5, 5, 100, 25))
|
|
.states_([
|
|
[ "Jam (F3)", Color.black, Color.green ],
|
|
[ "Stop (F1)", Color.white, Color.red ],
|
|
])
|
|
.action_{ |button| this.jam(button.value) }
|
|
.keyDownAction_{ nil };
|
|
|
|
|
|
layout.nextLine;
|
|
layout.nextLine;
|
|
|
|
SCStaticText(w, Rect(5, 5, 200, 25))
|
|
.string_("Threshold")
|
|
.font_(Font("Helvetica-Bold", 12));
|
|
|
|
|
|
SCStaticText(w, Rect(200, 5, 30, 25))
|
|
.string_("Lag")
|
|
.font_(Font("Helvetica-Bold", 12));
|
|
|
|
layout.nextLine;
|
|
|
|
SCSlider(w, Rect(5, 5, 100, 25))
|
|
.step_(0.1)
|
|
.value_(0.3)
|
|
.action_{ |slider|
|
|
var value = slider.value.clip(0.1, 1.0);
|
|
synth.set(\threshold, value);
|
|
thresholdText.string_(value.asString);
|
|
}
|
|
.keyDownAction_{ nil };
|
|
|
|
thresholdText = SCStaticText(w, Rect(5, 5, 95, 25))
|
|
.string_("0.3");
|
|
|
|
SCSlider(w, Rect(200, 5, 100, 25))
|
|
.step_(0.05)
|
|
.value_(0.1)
|
|
.action_{ |slider|
|
|
var value = slider.value.clip(0.1, 1.0);
|
|
synth.set(\lag, value);
|
|
lagText.string_(value.asString);
|
|
}
|
|
.keyDownAction_{ nil };
|
|
|
|
lagText = SCStaticText(w, Rect(5, 5, 20, 25))
|
|
.string_("0.1");
|
|
|
|
layout.nextLine;
|
|
|
|
statusText = SCStaticText(w, Rect(5, 5, 510, 25))
|
|
.background_(Color.grey);
|
|
|
|
layout.nextLine;
|
|
|
|
scope = SCUserView(w, Rect(5, 5, 510, 150))
|
|
.relativeOrigin_(true)
|
|
.background_(Color.black)
|
|
.drawFunc_({ this.drawWaveform });
|
|
|
|
layout.nextLine;
|
|
|
|
lowerStatus = SCStaticText(w, Rect(5, 5, 510, 25))
|
|
.background_(Color.grey);
|
|
|
|
layout.nextLine;
|
|
|
|
seq = Sequencer(w, Rect(5, 5, 510, 300), server, beats, this);
|
|
|
|
w.onClose = { scope.free };
|
|
w.front;
|
|
}
|
|
|
|
switch { |button|
|
|
if(button.value == 1) {
|
|
jambutton.enabled = false;
|
|
this.listen;
|
|
} {
|
|
this.silenceDetected;
|
|
this.ignore;
|
|
jambutton.enabled = true;
|
|
}
|
|
}
|
|
|
|
jam { |value| // 0 is on, 1 is off: relates to SCButton value
|
|
if(value == 1) {
|
|
recbutton.enabled = false;
|
|
isJamming = true;
|
|
if(seq.playing.not) {
|
|
seq.play
|
|
};
|
|
this.listen(amp: 0.0);
|
|
} {
|
|
isJamming = false;
|
|
recbutton.enabled = true;
|
|
jambutton.value = 0;
|
|
this.ignore;
|
|
}
|
|
}
|
|
|
|
preview {
|
|
seq.previewCurrentChannel;
|
|
}
|
|
|
|
onsetDetected {
|
|
Post << "Onset!" << $\n;
|
|
if(isJamming) {
|
|
{
|
|
seq.setCurrentHit;
|
|
}.defer;
|
|
} {
|
|
if(isRecording.not) {
|
|
isRecording = true;
|
|
synth.set(\t_resetRecord, 1);
|
|
recStart = SystemClock.seconds;
|
|
{ statusText.background_(Color.red).stringColor_(Color.white).background_(Color.red).string_("RECORDING") }.defer;
|
|
}
|
|
}
|
|
}
|
|
|
|
silenceDetected {
|
|
var dursamps, sndfile, signal, beat;
|
|
Post << "Silence!" << $\n;
|
|
|
|
if(isRecording) {
|
|
synth.run(false);
|
|
{
|
|
statusText.background_(Color.green).stringColor_(Color.black).string_("Analysing...");
|
|
}.defer;
|
|
dursamps = ((SystemClock.seconds - recStart) * server.sampleRate).asInteger;
|
|
recbuf.loadToFloatArray(0, dursamps) { |floatdata|
|
|
// check received from server OK - sometimes fails
|
|
if(floatdata.isEmpty.not) {
|
|
signal = Signal.newFrom(floatdata);
|
|
signal.normalize;
|
|
|
|
beats[seq.currentChannel] = Beat.newFromSignal(signal, server);
|
|
beats[seq.currentChannel].analyse { |beat|
|
|
this.findNearest(beat);
|
|
{
|
|
// allow new recording to begin only after completely finsihed
|
|
scope.refresh;
|
|
isRecording = false;
|
|
statusText.background_(Color.grey).stringColor_(Color.black).string_("");
|
|
this.updateLowerStatus;
|
|
recbutton.value_(0).doAction;
|
|
}.defer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
listen { |amp=1.0|
|
|
var msg, action;
|
|
|
|
if(isListening.not) { // start listening to input
|
|
synth = Synth(\beatboxlistener, [\out, 0, \in, 0, \fftbuf, fftbuf, \recbuf, recbuf, \amp, amp]);
|
|
|
|
onsetListener = OSCresponderNode(nil, '/tr') { |time, responder, msg|
|
|
action = msg[3];
|
|
action.asInteger.switch(
|
|
1, { this.onsetDetected },
|
|
2, { this.silenceDetected }
|
|
);
|
|
}.add;
|
|
|
|
statusText.background_(Color.grey).stringColor_(Color.black).string_("Waiting for input...");
|
|
isListening = true;
|
|
}
|
|
}
|
|
|
|
ignore {
|
|
if(isListening) {
|
|
var wasRecording = isRecording;
|
|
this.silenceDetected;
|
|
synth.free;
|
|
onsetListener.remove;
|
|
|
|
if(wasRecording) {
|
|
statusText.background_(Color.green).stringColor_(Color.black).string_("Analysing...");
|
|
} {
|
|
statusText.background_(Color.grey).stringColor_(Color.black).string_("");
|
|
};
|
|
|
|
isListening = false;
|
|
}
|
|
}
|
|
|
|
// TODO move to Beat.sc
|
|
findNearest { |beat|
|
|
var p, total;
|
|
var nearest = 10e10;
|
|
var neighbour;
|
|
|
|
data.getPairs.clump(2).do { |pair|
|
|
var fname, features;
|
|
# fname, features = pair;
|
|
total = 0.0;
|
|
features.size.do { |i|
|
|
p = (beat.features[i] - features[i]).squared;
|
|
total = total + p;
|
|
};
|
|
|
|
if(total < nearest) {
|
|
neighbour = fname;
|
|
nearest = total;
|
|
};
|
|
};
|
|
|
|
if(neighbour.isNil) { // FIX sometimes a strange bug causes a lot of nan's to be produced
|
|
"There was an error analysing this sound. Try another".postln;
|
|
} {
|
|
beat.nearest = neighbour;
|
|
}
|
|
}
|
|
|
|
refresh {
|
|
scope.refresh
|
|
}
|
|
|
|
updateLowerStatus {
|
|
var beat = seq.beats[seq.currentChannel];
|
|
beat !? {
|
|
beat.nearestPath !? {
|
|
lowerStatus.string_(beat.nearestPath.basename);
|
|
^this;
|
|
}
|
|
};
|
|
|
|
lowerStatus.string_("");
|
|
}
|
|
|
|
drawWaveform {
|
|
// why is SCSoundFileView so frustrating?
|
|
var sig, maximums, x2, y1, y2, p1, p2;
|
|
var beat = beats[seq.currentChannel];
|
|
|
|
beat !? {
|
|
p1 = 0 @ 75;
|
|
|
|
if(beat.sample.notNil) {
|
|
sig = (beat.sample * (seq.xfadeval)) +.s (beat.signal * (1 - seq.xfadeval));
|
|
} {
|
|
sig = beat.signal;
|
|
};
|
|
|
|
maximums = sig.clump(sig.size / 510).collect{ |window|
|
|
var posmax, negmax;
|
|
posmax = window.maxItem;
|
|
negmax = window.minItem;
|
|
|
|
if(posmax.abs > negmax.abs)
|
|
{ posmax } { negmax }
|
|
};
|
|
|
|
Pen.use {
|
|
Color(0.3, 1, 0.3).set;
|
|
maximums.do { |max, x1|
|
|
p2 = x1 @ (75 - (75 * max));
|
|
Pen.moveTo(p1);
|
|
Pen.lineTo(p2);
|
|
Pen.stroke;
|
|
|
|
p1 = p2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|