// osk -- offener schaltkreis // 2010 mu eindhoven // christoph haag, martin rumori, franziska windisch, ludwig zeller // http://osk.openkhm.de ( // globals q = q ? (); // print status messages // q.verboselevel = 0; // quiet // q.verboselevel = 1; // activity change messages q.verboselevel = 2; // + speaker status messages // q.verboselevel = 3; // + scene change messages // q.verboselevel = 4; // + algorithmic status messages // q.verboselevel = 5; // + low level measurement messages // base path q.basepath = "/opt/osk"; // global volume q.volume = 0.dbamp; // first output channel q.out = 0; // output offset for metering busses q.meteroutoffset = 12; // limiter gain q.limitgain = -9.dbamp; // initial global activity level q.act = 0.5; // activity increase per action q.actincrease = 0.02; // activity trigger speedlim q.actspeedlim = 0.5; // activity trigger penalty time q.actpenalty = 3.0; // activity release delay q.actreleasedelay = 1.0; // activity release time q.actrelease = 120.0; // periodic activity monitor update interval q.actinterval = 5.0; // initial sonic scene q.scene = 0; // scene parameters q.scenelength = 30.0; q.scenes = [ 0.0, 30.0, 60.0, 90.0 ]; // logging q.loglevel = 4; q.logfile = q.basepath +/+ "log/osk_eindhoven_%.osklog"; q.maxlogfilesize = 65536 * 1024; q.logstring = ""; // fun gadgets q.tmpdir = q.basepath +/+ "tmp"; q.tmpfile = nil; q.fungadget1pidfile = q.tmpdir +/+ "fungadget1pid"; q.fungadget2pidfile = q.tmpdir +/+ "fungadget2pid"; // measurement settings q.samplerate = 48000; q.measurefreq = 22000; q.measurefftsize = 1024; q.measurefftbin = (q.measurefreq / q.samplerate * q.measurefftsize).ceil; // calib data file q.calibfile = q.basepath +/+ "cfg/calibration_eindhoven.oskcfg"; // load calib data from file if existing if (File.exists(q.calibfile), { q.measurecalib = Object.readArchive(q.calibfile); q.logstring = "osk: calibration data loaded from file %\n".format(q.calibfile); if (q.verboselevel >= 1, { q.logstring.post; }); if (q.loglevel >= 1, { q.log(q.logstring); }); }, { q.measurecalib = [ // [ gain, polynomial coeffs 5, 4, 3, 2, 1, 0 ] [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 1 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 2 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 3 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 4 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 5 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 6 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 7 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 8 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 9 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 10 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ], // 11 [ 0.dbamp, 0.036985, -0.164907, 0.470179, 0.030720, 1.744055, -0.339426 ] // 12 ]; }); // soundfile location q.sfpath = q.basepath +/+ "snd"; q.files = [ [ q.sfpath +/+ "1.wav", -6.dbamp ], [ q.sfpath +/+ "2.wav", -6.dbamp ], [ q.sfpath +/+ "3n.wav", -6.dbamp ], [ q.sfpath +/+ "4n.wav", -6.dbamp ], [ q.sfpath +/+ "5n.wav", -6.dbamp ], [ q.sfpath +/+ "6.wav", -6.dbamp ], [ q.sfpath +/+ "7n.wav", -6.dbamp ], [ q.sfpath +/+ "8n.wav", -6.dbamp ], [ q.sfpath +/+ "9.wav", -6.dbamp ], [ q.sfpath +/+ "10.wav", -6.dbamp ], [ q.sfpath +/+ "11.wav", -6.dbamp ], [ q.sfpath +/+ "12.wav", -6.dbamp ] ]; // vars q.speakers = Array.fill(q.files.size, 0); q.lastactivities = Date.getDate.bootSeconds.dup(q.files.size); q.lastactivity = q.lastactivities[0]; q.lastactivityact = q.act; ); ( // synthdefs // grain player SynthDef.new(\oskgrain, { arg out, gain, playgain, limitgain, bufnum, start, transp, attack = 0.1, sustain = 0.1, release = 0.1, hpfreq = 60, lpfreq = 18000, meteroutoffset = 12; var env, sig, envsig, filtsig, outsig; env = EnvGen.ar(Env.linen(attack, sustain, release), doneAction: 2); sig = playgain * PlayBuf.ar(1, bufnum, transp * BufRateScale.kr(bufnum), 1.0, start, loop: 1, doneAction: 0); envsig = env * sig; filtsig = LPF.ar(HPF.ar(envsig, hpfreq), lpfreq); outsig = gain * Limiter.ar(filtsig, limitgain, 0.1); OffsetOut.ar(out, outsig); // for metering Out.ar(meteroutoffset + out, outsig); }).store; // loop player SynthDef.new(\oskloop, { arg out, gain = 0.5, bufnum; Out.ar(out, gain * PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum), loop: 1)); }).store; // constant measurement tone SynthDef.new(\oskmeasuretone, { arg out, gain = 0.125, freq = q.measurefreq; Out.ar(out, gain * FSinOsc.ar(freq, mul: gain)); }).store; // measurement and notification SynthDef.new(\oskmeasure, { arg in, id = 0, gain = 0.5, poly = #[ 0, 0, 0, 0, 0, 0 ]; var buf, insig, fftsig, filtsig, sone, sone2, sone3, sone4, sone5, speakers; buf = LocalBuf.new(q.measurefftsize); insig = SoundIn.ar(in, mul: gain); fftsig = FFT.new(buf, insig, wintype: 1); filtsig = fftsig.pvcollect(q.measurefftsize, { arg mag, phase; [ mag, phase ] }, frombin: q.measurefftbin, tobin: q.measurefftbin, zeroothers: 1); sone = Loudness.kr(filtsig); sone2 = sone * sone; sone3 = sone2 * sone; sone4 = sone3 * sone; sone5 = sone4 * sone; speakers = (poly[0] * sone5) + (poly[1] * sone4) + (poly[2] * sone3) + (poly[3] * sone2) + (poly[4] * sone) + poly[5]; SendReply.kr(fftsig >= 0, \oskmeasure, [ speakers, sone ], id); }).store; ); ( // startup s.waitForBoot({{ "osk startup sequence initiated...".postln; // load files "loading soundfiles...".postln; q.buffers = q.files.collect({ arg f; Buffer.read(s, f[0], action: { arg buf; "finished loading %\n".postf(buf.path); }); }); s.sync; "% soundfiles loaded.\n".postf(q.files.size); // algorithmic osk processes // osk pattern gen func q.oskgen = { arg e, i; Pbind.new( \instrument, \oskgrain, \playgain, q.files[i][1], \limitgain, q.limitgain, \meteroutoffset, q.meteroutoffset, \finish, { ~gain = q.volume * q.act.linexp(0.0, 1.0, 1.0, 2.0); ~bufnum = [ [ q.buffers[i] ], q.buffers ].wchoose([ 1 - q.act, q.act ]).choose; ~start = [ // scene based ((q.scenes[q.scene] + rrand((q.scenelength * 0.333).neg, q.scenelength * 0.667)) * ~bufnum.sampleRate).round.mod(~bufnum.numFrames), // random rrand(0, ~bufnum.numFrames) ].wchoose([ 1 - q.act, q.act ]); ~laststart = ~start; ~dur = q.act.linexp(0.0, 1.0, q.scenelength, 0.1) * rrand(0.66, 1.33); ~transp = 1.0 * rrand(q.act.linlin(0.0, 1.0, 0.5, 0.5), q.act.linlin(0.0, 1.0, 1.0, 2.0)); ~attack = ~dur * 0.1; ~sustain = ~dur - ~attack; ~release = max(0.4, ~dur * 0.1); ~out = q.out + i; // permanent activity loss q.act = q.lastactivityact * (cos((Date.getDate.bootSeconds - q.actreleasedelay - q.lastactivity / q.actrelease * pi).clip(0.0, pi)) + 1 * 0.5); // switch to new scene? if (1.0.rand < 0.05, { q.scene = (q.scene + rrand(1, q.scenes.size - 1)).mod(q.scenes.size); q.logstring = "osk%: triggered scene switch to scene %\n".format(i, q.scene); if (q.verboselevel >= 3, { q.logstring.post; }); if (q.loglevel >= 3, { q.log(q.logstring); }); }); // status message q.logstring = "osk%: act % start at %s dur %s transp % from file %, envelope: a: % s: % r: %\n".format(i, q.act.round(0.01), (~start / s.sampleRate).round(0.1), ~dur.round(0.01), ~transp.round(0.1), ~bufnum.path.basename, ~attack.round(0.01), ~sustain.round(0.01), ~release.round(0.01)); if (q.verboselevel >= 4, { q.logstring.post; }); if (q.loglevel >= 4, { q.log(q.logstring); }); }); }; // generate local patterns q.osks = q.files.collect({ arg f, i; q.oskgen(i); }); // osk players q.players = q.osks.collect({ arg p; p.play; }); // measurement tones q.measuretones = q.players.collect({ arg p, i; Synth.new(\oskmeasuretone, [ \out, q.out + i, \gain, -12.dbamp, \freq, q.measurefreq ]); }); // measurement synths q.measures = q.players.collect({ arg p, i; Synth.new(\oskmeasure, [ \in, q.out + i, \id, i, \gain, q.measurecalib[i] ]); }); // measurement data responder q.measureresp = OSCresponder.new(s.addr, '/oskmeasure', { arg time, responder, msg; var osk, speaker, diff, deviation, added, diffsuffix, speakersuffix; osk = msg[2]; if (q.measures[osk].nodeID == msg[1], { speaker = msg[3].round; diff = speaker - q.speakers[osk]; deviation = (msg[3] - speaker).abs; q.logstring = "osk: track %: speakers: %, deviation %, sones: %\n".format(osk, speaker, deviation.round(0.001), msg[4].round(0.001)); if (q.verboselevel >= 5, { q.logstring.post; }); if (q.loglevel >= 5, { q.log(q.logstring); }); if (diff != 0, { // status changed if (diff.isStrictlyPositive && (Date.getDate.bootSeconds - q.lastactivity > q.actspeedlim), { // remember activity q.lastactivity = Date.getDate.bootSeconds; // increase activity q.act = min(q.act + q.actincrease, 1.0); q.lastactivityact = q.act; // trigger new event stream on action if (q.lastactivity - q.lastactivities[osk] > q.actpenalty, { q.players[osk].stop; q.osks[osk] = q.oskgen(osk); q.players[osk] = q.osks[osk].play; q.lastactivities[osk] = q.lastactivity; }); // status message q.logstring = "osk activity change: new activity is %\n".format(q.act.round(0.01)); if (q.verboselevel >= 1, { q.logstring.post; }); if (q.loglevel >= 1, { q.log(q.logstring); }); }); // status message if (diff.isPositive, { added = "added to" }, { added = "removed from" }); if (diff.abs != 1, { diffsuffix = "s" }, { diffsuffix = ""; }); if (speaker != 1, { speakersuffix = "s" }, { speakersuffix = ""; }); q.logstring = "osk status change: % speaker% % track %, % speaker% on track %, measurement deviation %)\n".format(diff.abs, diffsuffix, added, osk, if (speaker < 8, { speaker }, { "many" }), speakersuffix, osk, deviation.round(0.01)); if (q.verboselevel >= 2, { q.logstring.post; }); if (q.loglevel >= 2, { q.log(q.logstring); }); // remember state and time q.speakers[osk] = speaker; }); }); }); q.measureresp.add; // periodic activity monitor thisThread.clock.sched(5.0, { q.logstring = "osk activity monitor: current activity is %.\n".format(q.act.round(0.001)); q.logstring.post; q.log(q.logstring); q.actinterval; }); // notify "% algorithmic osk players started.\n".postf(q.players.size); "verbose level is %.\n".postf(q.verboselevel); "log level is %.\n".postf(q.loglevel); "global volume set to % dB.\n".postf(q.volume.ampdb.round(0.1)); "limiter set to % dB.\n".postf(q.limitgain.ampdb.round(0.1)); "current activity is %.\n".postf(q.act(0.01)); // logging prep q.logprep = { arg e; if (q.logf.notNil, { if (q.logf.isOpen, { q.logf.close; }); q.logf = nil; }); q.logfpath = q.logfile.format(Date.getDate.bootSeconds.round); q.logf = File.open(q.logfpath, "w+"); // (re)start fun gadgets if (File.exists(q.fungadget1pidfile), { q.tmpfile = File.open(q.fungadget1pidfile, "r"); "kill %".format(String.readNew(q.tmpfile)).unixCmd; q.tmpfile.close; q.tmpfile = nil; File.delete(q.fungadget1pidfile); }); if (File.exists(q.fungadget2pidfile), { q.tmpfile = File.open(q.fungadget2pidfile, "r"); "kill %".format(String.readNew(q.tmpfile)).unixCmd; q.tmpfile.close; q.tmpfile = nil; File.delete(q.fungadget1pidfile); }); "urxvt -T \"osk activity\" -geometry 96x16+672+596 -e /opt/osk/scripts/oskactivityactivity.sh % %".format(q.logfpath, q.fungadget1pidfile).unixCmd; "urxvt -T \"sed s\/activity\/fun\/g\" -geometry 96x16+672+780 -e /opt/osk/scripts/oskactivityfun.sh % %".format(q.logfpath, q.fungadget2pidfile).unixCmd; }; // logging function q.log = { arg e, logmsg; var now = Date.getDate; if (q.logf.isNil, { q.logprep.value }); if (q.logf.isOpen.not, { q.logprep.value }); if (q.logf.isOpen, { q.logf.write(now.bootSeconds.asString ++ ":" ++ now.asString ++ "> " ++ logmsg); q.logf.flush; if (q.logf.length > q.maxlogfilesize, { q.logprep.value }); }); }; // public volume meter thisThread.clock.sched(6.0, { g.waitForBoot({ var win, inBus, outBus, fntSmall, viewWidth, inMeterWidth, outMeterWidth, inMeter, outMeter, inGroup, outGroup, chanWidth = 18, meterHeight = 320, fLab, fBooted, numIn, numOut; numIn = 12; numOut = 12; inMeterWidth = numIn * chanWidth + 20; outMeterWidth = numOut * chanWidth + 20; viewWidth = inMeterWidth + outMeterWidth + 11; win = JSCWindow("osk level monitor", Rect(720, 640, viewWidth, meterHeight + 26), false); win.view.background = Color.black; inMeter = JSCPeakMeter(win, Rect(4, 4, inMeterWidth, meterHeight)).border_(true).caption_(false).font_(JFont("Helvetica", 10)); outMeter = JSCPeakMeter(win, Rect(inMeterWidth + 8, 4, outMeterWidth, meterHeight)).border_(true).caption_(false); fntSmall = JFont("Helvetica", 10); fLab = { arg name, numChannels, xOff; var comp; comp = JSCCompositeView(win, Rect(xOff, meterHeight + 4, numChannels * chanWidth + 28, 18)).background_(Color.black); JSCStaticText(comp, Rect( 0, 0, 22, 18)).align_(\right).font_(fntSmall).stringColor_(Color.white).string_(name); numChannels.do({ arg ch; JSCStaticText(comp, Rect(3 + (ch * (chanWidth + 1.05)), 0, 20, 18)).align_(\center).font_(fntSmall).stringColor_(Color.white).string_((ch + 1).asString); }); }; fLab.value("", numIn, 4); fLab.value("", numOut, 8 + inMeterWidth); fBooted = { inGroup = Group.head(RootNode(s)); outGroup = Group.tail(RootNode(s)); outBus = Bus(\audio, 12, numOut, s); inBus = Bus(\audio, 24, numIn, s); inMeter.group = inGroup; inMeter.bus = inBus; outMeter.group = outGroup; outMeter.bus = outBus; }; win.front; win.onClose_({ ServerTree.remove(fBooted); inGroup.free; inGroup = nil; outGroup.free; outGroup = nil; }); ServerTree.add(fBooted); if(s.serverRunning, fBooted); // otherwise starts when booted }); }); }.fork}); ); // stop players // q.players.do({ arg p; p.stop; }); // q.measureresp.remove; // q.measureresp.add; // thisThread.clock.clear; // ( // simple loop players // q.synths = q.buffers.do({ arg buf, i; // Synth.new(\oskloop, [ \out, i, \bufnum, buf ]); }); // ) // stop simple loop players // q.synths.do({ arg syn; syn.free; }); // free buffers // q.buffers.do({ arg buf; buf.free; }); // EOF