Last active
October 11, 2021 19:06
-
-
Save schollz/5dc0e978b36fe4ff89899d7d9012eee0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// top-level processor abstraction, defining all our sea-wolf functionality | |
// it doesn't depend on norns | |
PirateRadio { | |
//--------------------- | |
//----- class variables | |
//----- these are global - best used sparsely, as constants etc | |
// the `<` tells SC to create a getter method | |
// (it's useful to at least have getters for everything during development) | |
classvar <numStreams; | |
classvar <defaultFileLocation = "/home/we/dust/audio/pirates"; | |
//------------------------ | |
//----- instance variables | |
//----- created for each new `PirateRadio` object | |
// where all our loot is stashed | |
var <fileLocation; | |
// all the loots | |
var <filePaths; | |
//--- busses | |
// array of busses for streams | |
var <streamBusses; | |
// strength busese for output the strength of the stream | |
var <strengthBusses; | |
// a bus for noise | |
var <noiseBus; | |
// final output bus. effects can be performed on this bus in-place | |
var <outputBus; | |
// a control bus for the dial position | |
var <dialBus; | |
//--- child components | |
var streamPlayers; | |
var noise; | |
var dial; | |
var selector; | |
var effects; | |
//--- synths | |
// final output synth | |
var outputSynth; | |
//-------------------- | |
//----- class methods | |
// most classes have a `*new` method | |
*new { | |
arg server, fileLocation; | |
// this is a common pattern: | |
// construct the superclass, then call our init function on it | |
// (beware that the superclass cannot also have a method named `init`) | |
^super.new.init(server, fileLocation); | |
} | |
//---------------------- | |
//----- instance methods | |
// initialize a new `PirateRadio` object / allocate resources | |
init { | |
arg server, fileLocationPath; | |
if (fileLocation.isNil, { fileLocation = fileLocationPath; }); | |
this.scanFiles; | |
//-------------------- | |
//-- create busses | |
// audio buses are stereo | |
numStreams=2; | |
streamBusses = Array.fill(numStreams, { | |
Bus.audio(server, 2); | |
}); | |
noiseBus = Bus.audio(server, 2); | |
outputBus = Bus.audio(server, 2); | |
// control buses are mono | |
dialBus = Bus.control(server,1); | |
// each station outputs its own strength | |
strengthBusses = Array.fill(numStreams, { | |
Bus.control(server, 1); | |
}); | |
//------------------ | |
//-- create synths and components | |
// since we are routing audio through several Synths in series, | |
// it's important to manage their order of execution. | |
// here we do that in the simplest way: | |
// each component places its synths at the end of the server's node list | |
// so, the order of instantation is also the order of execution | |
dial = PradDialController.new(server, dialBus); | |
streamPlayers = Array.fill(numStreams, { arg i; | |
PradStreamPlayer.new(server, streamBusses[i], strengthBusses[i], dialBus); | |
}); | |
noise = PradNoise.new(server, noiseBus, dialBus); | |
streamBusses.postln; | |
streamBusses.postln; | |
noiseBus.postln; | |
outputBus.postln; | |
selector = PradStreamSelector.new(server, streamBusses, strengthBusses, noiseBus, outputBus); | |
effects = PradEffects.new(server, outputBus); | |
outputSynth = { | |
arg threshold=0.99, lookahead=0.2; | |
var snd; | |
snd = In.ar(outputBus, 2); | |
snd = Limiter.ar(snd, threshold, lookahead).clip(-1, 1); | |
Out.ar(0, snd); | |
}.play(target:server, addAction:'addToTail'); | |
} | |
// refresh the list of sound files | |
scanFiles { | |
fileLocation.postln; | |
filePaths = PathName.new(fileLocation).files; | |
filePaths.postln; | |
///... update the streamPlayers or whatever | |
} | |
// set the dial position | |
setDial { | |
arg value; | |
dial.setDial(value); | |
// now... there is a little issue/question here. | |
// the simplest way to manage the multiple streams is to just have them all running all the time. | |
// but this won't be feasible if there are many streams | |
// it may be better, but def. more complicated, | |
// to have the selector hold references to the streamPlayers themselves, | |
// and pause un-selected streams as appropriate. | |
} | |
// set an effect parameter | |
setFxParam { | |
arg key, value; | |
effects.setParam(key, value); | |
} | |
// stop and free resources | |
free { | |
// by default i tend to free stuff in reverse order of alloctaion | |
outputSynth.free; | |
effects.free; | |
selector.free; | |
noise.free; | |
streamPlayers.do({ arg player; player.free; }); | |
outputBus.free; | |
noiseBus.free; | |
streamBusses.do({ arg bus; bus.free; }); | |
strengthBusses.do({ arg bus; bus.free; }); | |
} | |
} | |
//------------------------------------ | |
//-- helper classes | |
// | |
// supercollier doesn't have namespaces unfortunately (probably in v4) | |
// so this is a common pattern: use a silly class-name prefix as a pseudo-namespace | |
PradDialController { | |
// a simple controller that sets the global "dial" | |
var <synth; | |
*new { | |
arg server, outBus; | |
^super.new.init(server, outBus); | |
} | |
init { | |
arg server, outBus; | |
synth = { | |
arg dial; | |
Out.kr(outBus, Lag.kr(dial,0.1)); | |
}.play(target:server, addAction:\addToTail); | |
} | |
setDial { | |
arg value; | |
synth.set(\dial, value); | |
} | |
free { | |
synth.free; | |
} | |
} | |
PradStreamPlayer { | |
// streaming buffer(s) and synth(s).. | |
// (probably want 2x of each, to cue/crossfade) | |
var <bufs; | |
var <synths; | |
*new { | |
arg server, outBus, outStrengthBus, inDialBus; | |
^super.new.init(server, outBus, outStrengthBus, inDialBus); | |
} | |
init { | |
////////////////// | |
// this one could get a little involved.. | |
/////////////////// | |
arg server, outBus, outStrengthBus, inDialBus; | |
synths = Array.fill(1,{ | |
arg band=100, bandwidth=0.5; | |
var snd, strength, dial; | |
// dial is control by one | |
dial = In.kr(inDialBus, 1); | |
// strength emulates the "resonance" of a radio | |
// strength is function of the dial position | |
// and this stations band + bandwidth | |
strength=exp(0.5.neg*(((dial-band)/bandwidth)**1).abs); | |
// dummy sound | |
snd = SinOsc.ar(band); | |
Out.kr(outStrengthBus, strength); | |
Out.ar(outBus,snd); | |
}.play(target:server, addAction:\addToTail) | |
); | |
} | |
///////////////// | |
// will need various methods to manage playlist, cue sound files | |
// playlist / cued file management might be better done in the owning `PirateRadio`... | |
// .. in which case this guy will also need a reference to its owner, | |
// to inform when current soundfile is runnning out, etc | |
fileFinished { | |
} | |
pickFromLootPile { | |
} | |
outOfLoot { | |
} | |
//////////////// | |
free { | |
synths.do({ arg synth; synth.free; }); | |
bufs.do({ arg buf; buf.free; }); | |
} | |
} | |
// noise generator | |
PradNoise { | |
var <synth; | |
*new { | |
arg server, outBus, dialBus; | |
^super.new.init(server, outBus, dialBus); | |
} | |
init { | |
arg server, outBus, dialBus; | |
synth = { | |
var snd, dial,moving; | |
dial = In.kr(dialBus,1); | |
moving = EnvGen.kr(Env.perc(0.1,1),Changed.kr(dial)+Dust.kr(0.1)); | |
/////////////// | |
//// radio static | |
snd = BrownNoise.ar(0.2).dup + LPF.ar(Dust.ar(1), LinExp.kr(LFNoise2.kr(0.1),0,1,100, 4000)); | |
snd = snd + SinOsc.ar(LFNoise2.kr(LinExp.kr(LinLin.kr(LFNoise1.kr(0.5),0,1,50, 100),60,666)),mul:moving); | |
snd = SelectX.ar(moving,[snd,snd.ring1(SinOsc.ar(LFNoise2.kr(LinExp.kr(LinLin.kr(LFNoise1.kr(0.5),0,1,1, 100),60,666))).dup)]); | |
// commented because this is very very high pitched | |
// snd = snd + HenonC.ar(a:LFNoise2.kr(0.2).linlin(-1,1,1.1,1.5).dup); | |
//... or whatever | |
//////////////// | |
// force everything down to stereo | |
snd = Mix.new(snd.clump(2)); | |
Out.ar(outBus, snd); | |
}.play(target:server, addAction:\addToTail); | |
} | |
setDial { | |
arg value; | |
synth.set(\dial, value); | |
} | |
free { | |
synth.free; | |
} | |
} | |
// effects processor | |
// applies effects to stereo bus | |
PradEffects { | |
var synth; | |
*new { | |
arg server, bus; | |
^super.new.init(server, bus); | |
} | |
init { arg server, bus; | |
// also could define the SynthDef explicitly | |
synth = { | |
// ... whatever args | |
arg chorusRate=0.2, preGain=1.0; | |
var signal; | |
signal = In.ar(bus, 2); | |
//////////////// | |
signal = DelayC.ar(signal, delayTime:LFNoise2.kr(chorusRate).linlin(-1,1, 0.01, 0.06)); | |
signal = Greyhole.ar(signal); | |
signal = (signal*preGain).distort.distort; | |
//... or whatever | |
/////////// | |
// `ReplaceOut` overwrites the bus contents (unlike `Out` which mixes) | |
// so this is how to do an "insert" processor | |
ReplaceOut.ar(bus, signal); | |
}.play(target:server addAction:\addToTail); | |
} | |
free { | |
synth.free; | |
} | |
} | |
// this will be responsible for selecting / mixing all the streams / noise | |
PradStreamSelector { | |
var <synth; | |
*new { | |
arg server, streamBusses, strengthBusses, noiseBus, outBus; | |
^super.new.init(server); | |
} | |
init { arg server, streamBusses, strengthBusses, noiseBus, outBus; | |
var numStreams = streamBusses.size; | |
// also could define the SynthDef explicitly | |
synth = { | |
arg dial; // the selection parameter | |
var streams, strengths, noise, mix, snd; | |
strengths = strengthBusses.collect({ arg bus; | |
In.kr(bus.index, 1) | |
}); | |
streams = streamBusses.collect({ arg bus; | |
In.ar(bus.index, 2) | |
}); | |
noise = In.ar(noiseBus, 2); | |
// weight sound by strength | |
mix = Mix.new(streams.collect({ arg snd, i; | |
snd * strengths[i] | |
})); | |
// noise is attenuated by inverse of total strength | |
noise = (1-Clip.kr(Mix.new(strengths.collect({arg s; s}))))*noise; | |
snd = mix + noise; | |
Out.ar(outBus.index, snd); | |
}.play(target:server, addAction:\addAfter); | |
} | |
free { | |
synth.free; | |
} | |
} | |
//////////////////// | |
//////////////////// | |
/// little bonus... | |
PradStereoBitSaturator { | |
classvar <compressCurve; | |
classvar <expandCurve; | |
var <compressBuf, <expandBuf, <synth; | |
*initClass { | |
var n, mu, unit; | |
n = 512; | |
mu = 255; | |
unit = Array.fill(n, {|i| i.linlin(0, n-1, -1, 1) }); | |
compressCurve = unit.collect({ |x| | |
x.sign * log(1 + mu * x.abs) / log(1 + mu); | |
}); | |
expandCurve = unit.collect({ |y| | |
y.sign / mu * ((1+mu)**(y.abs) - 1); | |
}); | |
} | |
*new { | |
arg server, target, bus; | |
^super.new.init(server, target, bus); | |
} | |
init { | |
arg server, target, bus; | |
compressBuf = Buffer.loadCollection(server, Signal.newFrom(compressCurve).asWavetableNoWrap); | |
expandBuf = Buffer.loadCollection(server, Signal.newFrom(expandCurve).asWavetableNoWrap); | |
synth = { | |
arg steps = 256, compAmt=1, expAmt=1; | |
var src, comp, x, crush, exp; | |
src = In.ar(bus.index, 2); | |
comp = Shaper.ar(compressBuf.bufnum, src); | |
x = SelectX.ar(compAmt, [src, comp]); | |
crush = (x.abs * steps).round * x.sign / steps; | |
exp = Shaper.ar(expandBuf.bufnum, crush); | |
ReplaceOut.ar(bus.index, SelectX.ar(expAmt, [crush, exp])); | |
}.play(target:target, addAction:\addAfter); | |
} | |
setParam { | |
arg key, value; | |
synth.set(key, value); | |
} | |
free { | |
synth.free; | |
expandBuf.free; | |
compressBuf.free; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment