// title: SpacePan - A Spatial Audio Environment // author: audioholic // description: // This is a basic spatial audio tool using source oriented vector based // delay and amplitude panning as well as basic room simulation. // // This code was written using SC3.11 / OSX10.13.6 // Dependencies: Quarks wslib, mathlib and SC3plugins package. // // As I cannot submit the complete folder, there is no helpfile in here, so I copied it to the tail of the code!! // Copy the readme text to a new file, name it "spacepan_help.txt and save it to the spacepan project folder. Additionally, you´ll need to create a subfolder "Snapshots" to your spacepan project folder. You can download the complete folder here: https://www.dropbox.com/s/vx591sp2sh4ps52/SpacePanV1.zip?dl=0 // Tutorial: https://www.youtube.com/watch?v=4k96AOfATIs&feature=youtu.be // code: /* SpacePan V1.0 This is a basic spatial audio tool using source oriented vector based delay and amplitude panning as well as basic room simulation. This code was written using SC3.11 / OSX10.13.6 Dependencies: Quarks wslib, mathlib and SC3plugins package. It is crucial to read the helpfile! Check out the tutorial on youtube as well. dominik.wegmann@tonmeister.de - www.sccode.org/audioholic */ (// ----- code starts here. set main parameters first! var sources, statsources, currpath, makeBusses, makeSynths, makeGUI, makeNodes; var srcgroup, fxsgroup, fxrgroup, mstrgroup, master, testsig, reverb, early, delay, aux, revbus, dlybus, auxbus, fxretbus, masterbus, tracklevel; var newsrc, removesrc, editsrc, srcmute, srcsolo, srcon, srcstate, srclevel, makesnap, recallsnap, mirrorXYC; var pos2pan, pos2point, rect2pos, pos2angle, pos2rect, pos2drive, autorota, autorand, saveprj, loadprj, gethelp, autorefreshview, cleanup; var gui, guipoints, srcsize, btnsize, fdrsize, txtsize, clicked, selected, mousepoint, oldmspoint, mousedist, roomdim, roomscale, lspos, statpos, scolors, colors, fontsml, fontlrg, rotaspeedSpec, randspeedSpec, randrangeSpec, rotastepSpec, focusSpec; var speedofsound = 343, autorefreshrate = 0.03, showlbl = true, showls = true, showroomsens = false, solostate = 0; var masterout = 0, roomsens = 0.5, roomdamp = -12, roomfreq = 4000, fxpan = 1, xfade = 1, xfadetime = 2, xstep = 0.02; // ----- server options s = Server.local; s.options.device = "Babyface Pro (71967432)"; //your audio device. s.options.numOutputBusChannels = 12; //n outputs. use "masterout" to offset s.options.numInputBusChannels = 4; //n inputs s.options.sampleRate = 48000; s.options.memSize = 2.pow(16); s.newBusAllocators; // ----- set room & loudspeakers roomdim = 16; //virtual room size in meters (always square shape) roomscale = roomdim/2; //factor to reduce divisions in some functions lspos = [ //your real loudspeaker positions [x, y] You can offset, but be precise with speaker-to-speaker relation! [-1.5, 2], //!!!set arraysize of amps#[] and delays#[] in synthdefs "spacepan" & "staticsrc" to number of speakers! [0, 2], [1.5, 2], [2, 0], [2, -2], [0, -3], [-2, -2], [-2, 0] ]; statpos = [ //relative pos of static sources (fx returns), always 4 [-0.4, 0.4], [0.4, 0.4], [-0.4, -0.4], [0.4, -0.4] ] * roomscale; lspos.size.do{arg i, it; lspos[i] = lspos[i].asPoint}; //convert pos to point array statpos.size.do{arg i, it; statpos[i] = statpos[i].asPoint}; currpath = PathName(thisProcess.nowExecutingPath).parentPath; //project path sources = Dictionary.new; //create dict for sound sources rotaspeedSpec = ControlSpec(0.4, 0.01, 'exp', 0.001, 0.1); rotastepSpec = ControlSpec(0.01, 0.02, 'exp', 0.001, 0.015); randspeedSpec = ControlSpec(3.0, 0.1, 'exp', 0.1, 1.0); randrangeSpec = ControlSpec(0.1, roomscale/2, 'exp', 0.1, 1); focusSpec = ControlSpec(0, 2, 'lin', 0.1); gui = (); //the gui environment guipoints = 800; srcsize = 15; txtsize = Point(40, 24); btnsize = Point(65, 24); fdrsize = Point(135, 24); fontlrg = Font("Helvetica", 18); fontsml = Font("Helvetica", 13); scolors = [ //source colors Color.new255(238, 238, 53), //yellow, default Color.new255(110, 159, 247), //blue, grp1 Color.new255(0, 230, 114), //green, grp2 Color.new255(215, 140, 149), //red, grp3 ]; colors = [ //some colors to use somehow Color.new255(245, 222, 179), //wheat Color.new255(171, 130, 255) //orange ]; // ----- init all the main stuff Routine.run({ s.bootSync; "\n--- busses ---".postln; makeBusses.value; s.sync; "\n--- synthdefs ---".postln; makeSynths.value; s.sync; "\n--- groups, fx and master ---".postln; makeNodes.value; s.sync; "\n--- draw the gui ---".postln; {makeGUI.value}.defer; s.sync; autorefreshview.start; }); makeBusses = { masterbus = Bus.audio(s, lspos.size); revbus = Bus.audio(s, 4); dlybus = Bus.audio(s, 4); auxbus = Bus.audio(s, 4); fxretbus = Bus.audio(s, 4); ("Master Bus Index: "++masterbus.index).postln; ("Reverb Bus Index: "++revbus.index).postln; ("Delay Bus Index: "++dlybus.index).postln; ("Aux Bus Index: "++auxbus.index).postln; ("FX Return Bus Index: "++fxretbus.index).postln; ("Sending default test signal on bus 100").postln; }; makeNodes = { // ----- groups srcgroup = Group(s); //source fxsgroup = Group.after(srcgroup); //fx sends fxrgroup = Group.after(fxsgroup); //fx returns mstrgroup = Group.after(fxrgroup); //master // ----- test signal testsig = Synth("testsig", [\out, 100]); // ----- 4 channel reverb reverb = 2.collect{arg it; Synth("reverb", [ \in, revbus.index+(it*2), \out, fxretbus.index+(it*2), \rt, 1.7, \stereo, 0, \lf, 200, \hf, 4000, \lfratio, 1.0, \hfratio, 1.0, \early, 0.5, \late, 0.7, \modRate, 0.5, \modDepth, 0.3, \amp, 0.5 ], fxsgroup)}; // ----- 4 channel delay delay = 4.collect{arg it; Synth("delay", [ \in, dlybus.index+it, \out, fxretbus.index+it, \dly, 0.3, \fb, 0.7, \amp, 0.5 ], fxsgroup)}; // ----- static sources (fx returns) statsources = 4.collect{arg it; var pos, amps; pos = statpos[it]; d = lspos.collect{arg i; pos.dist(i)}; a = d.minItem/d; amps = a / a.squared.sum.sqrt; Synth("staticsrc", [ \inbus, fxretbus.index+it, \out, masterbus, \amps, amps ], fxrgroup)}; // ----- master master = Synth("master", [\in, masterbus, \out, masterout, \amp, 1, \gate, 1], mstrgroup); // ----- source input metering tracklevel = Synth("tracklevel", addAction:'addToTail'); }; // ----- main gui window makeGUI = { gui.win = Window(" SPACEPAN ", Rect(0, 0, 1040, 810).center_(Window.availableBounds.center), resizable: false, border: true).front; //gui.win.alwaysOnTop_(true).userCanClose_(true); gui.win.onClose = {cleanup.value}; gui.win.view.background = Color.gray(0.2); // ----- subviews gui.srctop = View(gui.win, Rect(5, 5, 225, 40)).backColor_(Color.gray(0.85)); gui.srctop.addFlowLayout(5@5, 5@5); gui.src = View(gui.win, Rect(5, 50, 225, 540)).backColor_(Color.gray(0.85)).enabled_(false); gui.src.addFlowLayout(5@5, 5@5); gui.opt = View(gui.win, Rect(5, 595, 225, 100)).backColor_(Color.gray(0.85)); gui.opt.addFlowLayout(5@5, 5@5); gui.mstr = View(gui.win, Rect(5, 700, 225, 105)).backColor_(Color.gray(0.85)); gui.mstr.addFlowLayout(5@5, 5@5); gui.fxpanel = Window("FX SETTINGS", Rect(0, 0, 285, 350), resizable: false, scroll: true).visible_(false).userCanClose_(false); gui.fxpanel.addFlowLayout(5@5, 5@5); gui.snaplist = Window("SNAPSHOTS", Rect(0, 0, 285, 450), resizable: false).visible_(false).userCanClose_(false); gui.meters = Window("METERING", Rect(0, 0, ServerMeterView.getWidth(0, s.options.numOutputBusChannels), ServerMeterView.height), resizable: false).visible_(false).userCanClose_(false); gui.space = UserView(gui.win, Rect(235, 5, guipoints, guipoints)).backColor_(Color.black); // ----- source panel top StaticText(gui.srctop, Point(70, 30)).string_("Source: ").font_(fontlrg); gui.srcID = StaticText(gui.srctop, Point(40, 28)).string_("0").font_(fontlrg).align_(\center); gui.srcPrev = Button(gui.srctop, Point(27, 27)).string_(" < ").font_(fontlrg) .action_({arg view; if(selected > 1) { selected = selected -1; editsrc.value(selected)}; }); gui.srcNext = Button(gui.srctop, Point(27, 27)).string_(" > ").font_(fontlrg) .action_({arg view; if (sources.keys.maxItem > selected){ selected = selected +1; editsrc.value(selected)}; }); gui.srcAdd = Button(gui.srctop, Point(27, 27)).font_(fontlrg) .states_([[" + ", Color.black, Color.green(0.9)]]) .action_({arg view; newsrc.value}); // ----- source edit panel StaticText(gui.src, txtsize).string_("Label").font_(fontsml); gui.srcLabel = TextField(gui.src, btnsize).string_("Name").font_(fontsml); gui.srcGrp = PopUpMenu(gui.src, btnsize). items_(["NoGrp", "Grp1", "Grp2", "Grp3"]).font_(fontsml); StaticText(gui.src, txtsize).string_("Input").font_(fontsml); gui.srcInput = NumberBox(gui.src, btnsize).value_(0).font_(fontsml); gui.srcMeter = LevelIndicator(gui.src, btnsize).style_(\led).stepWidth_(1).warning_(0.8).critical_(0.95); StaticText(gui.src, txtsize).string_("").font_(fontsml); gui.srcSolo = Button(gui.src, btnsize).font_(fontsml) .states_([["Solo", Color.black, Color.gray(0.9)], ["Solo", Color.black, Color.yellow]]); gui.srcMute = Button(gui.src, btnsize).font_(fontsml) .states_([ ["Mute", Color.black, Color.red ], ["Mute", Color.black, Color.gray(0.9)] ]) .value_(1); StaticText(gui.src, txtsize).string_("Level").font_(fontsml); gui.srcVol = Slider(gui.src, fdrsize); gui.srcVolnum = StaticText(gui.src, Point(30,24)).string_("-inf").font_(fontsml); StaticText(gui.src, txtsize).string_("Focus").font_(fontsml); gui.srcFocus = Slider(gui.src, fdrsize).step_(0.1); gui.srcFocusnum = StaticText(gui.src, Point(30,24)).string_("0.0").font_(fontsml); StaticText(gui.src, txtsize).string_("Mode").font_(fontsml); StaticText(gui.src, Point(115,24)).string_("Apply Delay").font_(fontsml); gui.srcPanmode = CheckBox(gui.src, Point(24,24)).value_(0); StaticText(gui.src, txtsize).string_("").font_(fontsml); StaticText(gui.src, Point(115,24)).string_("Apply Room FX").font_(fontsml); gui.srcRoom = CheckBox(gui.src, Point(24,24)).value_(1); // ----- fx sends StaticText(gui.src, txtsize).string_("Rev").font_(fontsml); gui.srcRev = Slider(gui.src, fdrsize); gui.srcRevnum = StaticText(gui.src, Point(30,24)).string_("-inf").font_(fontsml); StaticText(gui.src, txtsize).string_("Dly").font_(fontsml); gui.srcDly = Slider(gui.src, fdrsize); gui.srcDlynum = StaticText(gui.src, Point(30,24)).string_("-inf").font_(fontsml); StaticText(gui.src, txtsize).string_("Aux").font_(fontsml); gui.srcAux = Slider(gui.src, fdrsize).enabled_(false); gui.srcAuxnum = StaticText(gui.src, Point(30,24)).string_("-inf").font_(fontsml); // ----- automation (rota/rand) StaticText(gui.src, txtsize).string_("Rota").font_(fontsml); gui.srcRota = Button(gui.src,Point(35,24)) .states_([["ON", Color.black, Color.gray(0.9)], ["ON", Color.black, Color.cyan]]).font_(fontsml); gui.srcRotaDir = PopUpMenu(gui.src, Point(95, 24)).items_(["Clockwise","Anticlock"]).font_(fontsml); StaticText(gui.src, txtsize).string_("Speed").font_(fontsml); gui.srcRotaSpeed = Slider(gui.src, fdrsize); gui.srcRotaSpeedString = StaticText(gui.src, Point(30,24)).string_("0.0").font_(fontsml); StaticText(gui.src, txtsize).string_("Rand").font_(fontsml); gui.srcRand = Button(gui.src,Point(35,24)).font_(fontsml) .states_([["ON", Color.black, Color.gray(0.9)], ["ON", Color.black, Color.cyan]]).font_(fontsml); gui.src.decorator.nextLine; StaticText(gui.src, txtsize).string_("Step").font_(fontsml); gui.srcRandRange = Slider(gui.src, fdrsize); gui.srcRandRangeString = StaticText(gui.src, Point(30,24)).string_("0.0").font_(fontsml); StaticText(gui.src, txtsize).string_("Speed").font_(fontsml); gui.srcRandSpeed = Slider(gui.src, fdrsize); gui.srcRandSpeedString = StaticText(gui.src, Point(30,24)).string_("0.0").font_(fontsml); // ----- position mirroring StaticText(gui.src, txtsize).string_("Mirror").font_(fontsml); Button(gui.src, Point(41,24)).states_([[" x "]]).font_(fontsml) .action_({arg view; mirrorXYC.value(1)}); Button(gui.src, Point(41,24)).states_([[" y "]]).font_(fontsml) .action_({arg view; mirrorXYC.value(2)}); Button(gui.src, Point(41,24)).states_([[" c "]]).font_(fontsml) .action_({arg view; mirrorXYC.value(3)}); // ----- show src position StaticText(gui.src, txtsize).string_("Pos").font_(fontsml); gui.srcposX = StaticText(gui.src, btnsize).string_("X: 0.0 m").font_(fontsml); gui.srcposY = StaticText(gui.src, btnsize).string_("Y: 0.0 m").font_(fontsml); StaticText(gui.src, txtsize).string_("").font_(fontsml); gui.srcposDeg = StaticText(gui.src, btnsize).string_("A: 0.0 °").font_(fontsml); gui.srcposDist = StaticText(gui.src, btnsize).string_("D: 0.0 m").font_(fontsml); // ----- duplicate/remove /*gui.srcCopy = Button(gui.src, Point(105,24)) .string_("Duplicate Src").font_(fontsml) .action_({arg view; copysrc.value(selected)}); gui.srcRemove = Button(gui.src, Point(105,24)) .string_("Remove Src").font_(fontsml) .action_({arg view; removesrc.value});*/ // ----- space view for source positioning gui.space.drawFunc_({ arg view; var bounds = view.bounds.moveTo(0,0); // cross at center: Pen.width = 1; Pen.strokeColor = colors[0]; Pen.line(Point(guipoints/2 - 15, guipoints/2), Point(guipoints/2 + 15, guipoints/2)); Pen.line(Point(guipoints/2, guipoints/2 - 15), Point(guipoints/2, guipoints/2 + 15)); Pen.stroke; // roomfx threshold: if(showroomsens){ Pen.width = 1; Pen.strokeColor = colors[0]; Pen.strokeOval(Rect.aboutPoint(pos2point.value(Point(0, 0)), roomsens*guipoints/2, roomsens*guipoints/2)); }; // loudspeakers: if(showls){ Pen.fillColor = Color.red(0.9);//colors[0]; lspos.size.do{ arg i; Pen.fillRect(Rect.aboutPoint(pos2point.value(lspos[i]), 8, 8)); Pen.stringCenteredIn((i+1).asString, Rect.aboutPoint(pos2point.value(lspos[i]), 8, 8), Font(size: 10), Color.black); }; }; // sources: if(sources.size > 0){ sources.pairsDo { arg key, src, i; Pen.fillColor = src[\state].if( { scolors[src[\grpid]] }, { Color.grey(0.7) } ); Pen.fillOval(src[\rect]); if(showlbl){ Pen.stringAtPoint(src[\label], src[\rect].rightBottom - Point(4, 4)); }; Pen.fillColor = Color.black; Pen.stringCenteredIn(src[\id].asString, src[\rect]); }; }; // source marker: if(selected.notNil) { Pen.width = 4; Pen.strokeColor = Color.red; Pen.strokeOval(sources[selected][\rect]); }; }) // action when source is clicked: .mouseDownAction_({ arg view, x, y, modifiers, buttonNumber, clickCount; mousepoint = Point(x,y); oldmspoint = mousepoint; clicked = nil; sources.pairsDo { arg key, src, i; if (src[\rect].containsPoint(mousepoint)) { clicked = key } }; if(clicked.notNil) { mousedist = (mousepoint - (sources[clicked][\rect].origin)); if(clicked != selected) { selected = clicked; editsrc.value(selected); }; if(sources[clicked][\grpid] > 0 && modifiers.isShift){ sources.pairsDo{ arg key, src, i; if(src[\grpid] == sources[clicked][\grpid]){ if(modifiers.isAlt){ srcstate.value(key) }; if(modifiers.isCtrl){ srcsolo.value(key) }; }; }; editsrc.value(clicked); }{ if(modifiers.isAlt) { srcstate.value(clicked); editsrc.value(clicked) }; if(modifiers.isCtrl) { srcsolo.value(clicked); editsrc.value(clicked) }; }; }; }) // action when source is moved: .mouseMoveAction_({ arg view, x, y, modifiers; var bounds = view.bounds.moveTo(0,0); if (clicked.notNil){ mousepoint = Point(x.clip(bounds.left, bounds.right), y.clip(bounds.top, bounds.bottom)); if(sources[clicked][\grpid] > 0 and: modifiers.isShift){ //move group sources.pairsDo{ arg key, src, i; if(src[\grpid] == sources[clicked][\grpid]){ src[\rect] = src[\rect].translate(mousepoint - oldmspoint); rect2pos.value(key); pos2drive.value(key); }; }; oldmspoint = mousepoint; }{ //move single source sources[clicked][\rect] = sources[clicked][\rect].moveToPoint(mousepoint - mousedist); rect2pos.value(clicked); pos2drive.value(clicked); }; view.action.value(this, x, y, modifiers); }; }) // action when mousebutton is released: .mouseUpAction_({ arg view, x, y, modifiers; clicked = nil; }); // end of spaceview // ----- option panel gui.sessnMenu = Menu( MenuAction("Load", { loadprj.value }), MenuAction("Save", { saveprj.value }), MenuAction("Help", { gethelp.value }), MenuAction("Quit", { gui.win.close }), ); gui.optnMenu = Menu( MenuAction("Show Loudspeakers") .checkable_(true) .checked_(showls) .action_({arg a, checked; if(checked){ showls = true; }{ showls = false; }; }), MenuAction("Show Labels") .checkable_(true) .checked_(showlbl) .action_({arg a, checked; if(checked){ showlbl = true; }{ showlbl = false; }; }), MenuAction("Show Roomsens") .checkable_(true) .checked_(showroomsens) .action_({arg a, checked; if(checked){ showroomsens = true; }{ showroomsens = false; }; }); ); gui.sessn = Button(gui.opt, Point(105, 24)).string_("Session..").font_(fontsml) .action_({arg view; gui.sessnMenu.front; }); gui.optn = Button(gui.opt, Point(105, 24)).string_("View..").font_(fontsml) .action_({arg view; gui.optnMenu.front; }); gui.showsnaplist = Button(gui.opt, Point(105, 24)).string_("Snapshots").font_(fontsml) .action_({arg view; if(gui.snaplist.visible == false) { gui.snaplist.visible = true; gui.snaplist.bounds = gui.snaplist.bounds.moveToPoint(Point(gui.win.bounds.rightBottom.x, gui.win.bounds.rightBottom.y - gui.snaplist.bounds.extent.y)); }{ gui.snaplist.visible = false }; }); gui.showsettings = Button(gui.opt, Point(105, 24)).string_("FX Settings").font_(fontsml) .action_({arg view; if(gui.fxpanel.visible == false) { gui.fxpanel.visible = true; gui.fxpanel.bounds = gui.fxpanel.bounds.moveToPoint(gui.win.bounds.rightTop); }{ gui.fxpanel.visible = false }; }); gui.showmeters = Button(gui.opt, Point(105, 24)).string_("Out Meters").font_(fontsml) .action_({arg view; if(gui.meters.visible == false) { m = ServerMeterView.new(s, gui.meters, Point(0,0), 0, s.options.numOutputBusChannels); gui.meters.visible = true; gui.meters.bounds = gui.meters.bounds.moveToPoint(gui.win.bounds.rightTop); }{ gui.meters.visible = false; m.free; }; }); gui.na2 = Button(gui.opt, Point(105, 24)).string_("n/a").font_(fontsml) .action_({arg view; }); // ----- master StaticText(gui.mstr, txtsize).string_("Master").font_(fontsml); gui.mstrlevel = Slider(gui.mstr, fdrsize).value_(1) .action_({arg view; master.set(\amp, view.value); gui.mstrlevelnum.string_(view.value.ampdb.round(0.1).asString); }); gui.mstrlevelnum = StaticText(gui.mstr, Point(30, 24)).string_("0").font_(fontsml); StaticText(gui.mstr, txtsize).string_("Out").font_(fontsml); gui.masterBus = NumberBox(gui.mstr, btnsize).value_(masterout).font_(fontsml) .action_({arg view; view.value = view.value.clip(0, 63); master.set(\out, view.value); }); gui.mstrMute = Button(gui.mstr, btnsize).font_(fontsml) .states_([ ["Mute", Color.black, Color.red ], ["Mute", Color.black, Color.gray(0.9)] ]) .value_(1) .action_({arg view; master.set(\gate, view.value); }); // ----- fx settings window StaticText(gui.fxpanel, 260@20).string_("::: GENERAL :::").align_(\center); gui.roomsens = EZSlider.new(gui.fxpanel, 260@20 ,"Rm Sens",ControlSpec(0.1, 0.9,\lin, 0.1, roomsens), {arg view; roomsens = view.value; if(sources.size > 0){ sources.pairsDo({arg key, src, i; src[\node].set(\roomsens, view.value); }); }; }); gui.roomfreq = EZSlider.new(gui.fxpanel, 260@20 ,"Rm LPF",ControlSpec(200, 12000, 'exp', 1, roomfreq), {arg view; roomfreq = view.value; if(sources.size > 0){ sources.pairsDo({arg key, src, i; src[\node].set(\roomfreq, view.value); }); }; }); gui.roomdamp = EZSlider.new(gui.fxpanel, 260@20 ,"Rm Damp",ControlSpec(-3, -24, \lin, 1, roomdamp), {arg view; roomdamp = view.value; if(sources.size > 0){ sources.pairsDo({arg key, src, i; src[\node].set(\roomdamp, view.value.dbamp); }); }; }); // ----- reverb StaticText(gui.fxpanel, 260@20).string_("::: REVERB :::").align_(\center); gui.revlevel = EZSlider.new(gui.fxpanel, 260@20 ,"Rev Level",ControlSpec(-60, 0, \lin, 1, -6), {arg view; reverb.do{arg it; it.set(\amp, view.value.dbamp) } }); gui.revtime = EZSlider.new(gui.fxpanel, 260@20 ,"Time",ControlSpec(1.0, 5.0, \lin, 0.1, 1.7), {arg view; reverb.do{arg it; it.set(\rt, view.value) } }); gui.revlf = EZSlider.new(gui.fxpanel, 260@20 ,"LF Ratio",ControlSpec(0.5, 1.5, \lin, 0.1, 1.0), {arg view; reverb.do{arg it; it.set(\lfratio, view.value) } }); gui.revhf = EZSlider.new(gui.fxpanel, 260@20 ,"HF Ratio",ControlSpec(0.5, 1.5, \lin, 0.1, 1.0), {arg view; reverb.do{arg it; it.set(\hfratio, view.value) } }); // ----- delay StaticText(gui.fxpanel, 260@20).string_("::: DELAY :::").align_(\center); gui.dlylevel = EZSlider.new(gui.fxpanel, 260@20,"Dly Level",ControlSpec(-60, 0, \lin, 1, -6), {arg view; delay.do{arg it; it.set(\amp, view.value.dbamp) } }); gui.dlytime = EZSlider.new(gui.fxpanel, 260@20,"Dly T",ControlSpec(0.1, 1.1, \lin, 0.01, 0.3), {arg view; delay.do{arg it; it.set(\dly, view.value) } }); gui.dlyfb = EZSlider.new(gui.fxpanel, 260@20,"Dly FB",ControlSpec(0.1, 1, \lin, 0.1, 0.7), {arg view; delay.do{arg it; it.set(\fb, view.value) } }); // ---- snapshot window gui.snapshots = ListView(gui.snaplist, Rect(5, 5, 275, 320)) .background_(Color.gray(0.9)) .font_(fontsml) .hiliteColor_(Color.yellow(alpha: 0.6)) .selectedStringColor_(Color.black) .items_([ ]) .selectionMode_(\single) .action_({arg view; //recallsnap.value; }); gui.activeSnap = StaticText(gui.snaplist, Rect(5, gui.snapshots.bounds.leftBottom.y+5, gui.snapshots.bounds.width, 35)) .backColor_(Color.white) .string_(" >> ") .font_(fontlrg); gui.deleteSnap = Button(gui.snaplist, Rect(5, gui.snapshots.bounds.leftBottom.y+45, 65, 35)).string_("Delete") .action_({arg view; if(gui.snapshots.items.size > 0){ gui.snapshots.items = gui.snapshots.items.removing(gui.snapshots.item) }; }); gui.makeSnap = Button(gui.snaplist, Rect(75, gui.snapshots.bounds.leftBottom.y+45, 65, 35)).string_("Make") .action_({arg view; makesnap.value; }); gui.recallSnap = Button(gui.snaplist, Rect(145, gui.snapshots.bounds.leftBottom.y+45, 65, 35)).string_("Recall") .action_({arg view; recallsnap.value; gui.activeSnap.string = " >> "++gui.snapshots.item; }); gui.nextSnap = Button(gui.snaplist, Rect(215, gui.snapshots.bounds.leftBottom.y+45, 65, 35)).string_("Next") .action_({arg view; gui.snapshots.value = (gui.snapshots.value + 1).clip(0, gui.snapshots.items.maxIndex); recallsnap.value; gui.activeSnap.string = " >> "++gui.snapshots.item; }); gui.xactive = Button(gui.snaplist, Rect(5, gui.snapshots.bounds.leftBottom.y+85, 65, 25)) .states_([ ["X [sec]", Color.black, Color.grey(0.9) ], ["X [sec]", Color.black, Color.cyan] ]) .value_(1) .action_({arg view; xfade = view.value}); gui.xtime = NumberBox(gui.snaplist, Rect(75, gui.snapshots.bounds.leftBottom.y+85, 30, 25)) .value_(xfadetime) .align_(\center) .action_({arg view; view.value = view.value.clip(1, 10); xfadetime = view.value }); };// ----- end of GUI // ----- create new source newsrc = { var id, src, point; if(sources.size < 1){ gui.src.enabled_(true); id = 1}{ id = sources.keys.maxItem + 1}; src = Dictionary[ \id -> id, \label -> id.asString, \inbus -> 100, \rect -> nil, \pos -> Point(0.2.rand2, 0).round(0.01), \grpid -> 0, \room -> 1, \dodelays -> 0, \focus -> 0, \amp -> 0, \solo -> false, \rotastate -> false, \rota -> nil, \rotaspeed -> 0.04, \rotadir -> -1, \randstate -> false, \rand -> nil, \randrange -> 1, \randspeed -> 1, \aux -> 0, \rev -> 1, \dly -> 0, \node -> nil, \state -> true ]; src[\rect] = pos2rect.value(src[\pos]); sources.add(id -> src); selected = id; editsrc.value(selected); srcon.value(selected); }; // ----- start the src processing synth srcon = { arg id; var src; src = sources[id]; src[\node] = Synth("spacepan", [ \inbus, src[\inbus], \out, masterbus, \revbus, revbus, \dlybus, dlybus, \auxbus, auxbus, \amp, src[\amp], \dodelays, src[\dodelays], \rev, src[\rev], \dly, src[\dly], \aux, src[\aux], \room, src[\room], \roomsens, roomsens, \roomdamp, roomdamp.dbamp, \roomfreq, roomfreq ], srcgroup); pos2drive.value(id); }; // ----- edit selected source settings editsrc = {arg selected; var src; src = sources[selected]; srclevel.value; tracklevel.set(\inbus, src[\inbus]); gui.srcID .string_(src[\id].asString); gui.srcLabel .value_(src[\label]) .action_({arg view; src[\label] = view.value[..7]; }); gui.srcGrp .background_(scolors[src[\grpid]]) .value_(src[\grpid]) .action_({ arg view; view.background = scolors[view.value]; src[\grpid] = view.value; }); gui.srcInput .value_(src[\inbus]) .action_({arg view; src[\inbus] = view.value; src[\node].set(\inbus, src[\inbus]); tracklevel.set(\inbus, src[\inbus], \vol, src[\vol]); }); gui.srcVol .value_(src[\amp]) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\amp] = view.value; sc[\node].set(\amp, sc[\amp]); }; }; }{ src[\amp] = view.value; src[\node].set(\amp, src[\amp]); }; gui.srcVolnum.string = src[\amp].ampdb.round(0.1).asString; }); gui.srcVolnum .string_(src[\amp].ampdb.round(0.1).asString); gui.srcMute .value_(src[\state]) .action_({arg view, mod; if(src[\grpid] > 0 && mod.isShift){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ srcstate.value(sc[\id]) }; }; }{ srcstate.value(selected) } }); gui.srcSolo .value_(src[\solo]) .action_({arg view, mod; if(src[\grpid] > 0 && mod.isShift){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ srcsolo.value(sc[\id]) }; }; }{ srcsolo.value(selected) } }); gui.srcPanmode .value_(src[\dodelays]) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\dodelays] = view.value; sc[\node].set(\dodelays, sc[\dodelays]) }; }{ src[\dodelays] = view.value; src[\node].set(\dodelays, src[\dodelays]) }; }; }); gui.srcFocus .value_(focusSpec.unmap(src[\focus])) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\focus] = focusSpec.map(view.value); pos2drive.value(sc[\id]); }; }; }{ src[\focus] = focusSpec.map(view.value); pos2drive.value(selected); }; gui.srcFocusnum.string = view.value.asString; }); gui.srcFocusnum.string = focusSpec.unmap(src[\focus]).asString; gui.srcRoom .value_(src[\room]) .action_({arg view; src[\room] = view.value; src[\node].set(\room, src[\room]); }); gui.srcRev .value_(src[\rev]) .action_({arg view; src[\rev] = view.value; gui.srcRevnum.string = src[\rev].ampdb.round(0.1).asString; src[\node].set(\rev, src[\rev]); }); gui.srcRevnum.string_(src[\rev].ampdb.round(0.1).asString); gui.srcDly .value_(src[\dly]) .action_({arg view; src[\dly] = view.value; gui.srcDlynum.string = src[\dly].ampdb.round(0.1).asString; src[\node].set(\dly, src[\dly]); }); gui.srcDlynum.string_(src[\dly].ampdb.round(0.1).asString); gui.srcAux .value_(src[\aux]) .action_({arg view; src[\aux] = view.value; gui.srcAuxnum.string = src[\aux].ampdb.round(0.1).asString; src[\node].set(\aux, src[\aux]); }); gui.srcAuxnum.string_(src[\aux].ampdb.round(0.1).asString); gui.srcRota .value_(src[\rotastate].asInteger) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\rotastate] = view.value.asBoolean; if(sc[\randstate]){ sc[\randstate] = false; autorand.value(key); gui.srcRand.value_(0); }; autorota.value(key); }; }; }{ src[\rotastate] = view.value.asBoolean; if(src[\randstate]){ gui.srcRand.value_(0); src[\randstate] = false; autorand.value(selected); }; autorota.value(selected); }; }); gui.srcRotaSpeed .value_(rotaspeedSpec.unmap(src[\rotaspeed])) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\rotaspeed] = rotaspeedSpec.map(view.value); }; }; }{ src[\rotaspeed] = rotaspeedSpec.map(view.value); }; gui.srcRotaSpeedString.string = view.value.round(0.01).asString; }); gui.srcRotaSpeedString.string = gui.srcRotaSpeed.value.round(0.01).asString; gui.srcRotaDir .value_(src[\rotadir].linlin(-1, 1, 0, 1)) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\rotadir] = view.value.linlin(0, 1, -1, 1); }; }; }{ src[\rotadir] = view.value.linlin(0, 1, -1, 1); }; }); gui.srcRand .value_(src[\randstate].asInteger) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\randstate] = view.value.asBoolean; if(sc[\rotastate]){ sc[\rotastate] = false; autorota.value(key); gui.srcRota.value_(0); }; autorand.value(key); }; }; }{ src[\randstate] = view.value.asBoolean; if(src[\rotastate]){ gui.srcRota.value_(0); src[\rotastate] = false; autorota.value(selected); }; autorand.value(selected); }; }); gui.srcRandRange .value_(randrangeSpec.unmap(src[\randrange])) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\randrange] = randrangeSpec.map(view.value); }; }; }{ src[\randrange] = randrangeSpec.map(view.value); }; gui.srcRandRangeString.string = view.value.round(0.01).asString; }); gui.srcRandRangeString.string = gui.srcRandRange.value.round(0.01).asString; gui.srcRandSpeed .value_(randspeedSpec.unmap(src[\randspeed])) .action_({arg view; if(src[\grpid] > 0){ sources.pairsDo{arg key, sc, i; if(sc[\grpid] == src[\grpid]){ sc[\randspeed] = randspeedSpec.map(view.value); }; }; }{ src[\randspeed] = randspeedSpec.map(view.value); }; gui.srcRandSpeedString.string = view.value.round(0.01).asString; }); gui.srcRandSpeedString.string = gui.srcRandSpeed.value.round(0.01).asString; }; // ----- mute source srcstate = {arg id; var src; src = sources[id]; if(src[\state]){ src[\node].set(\gate, 0); src[\state] = false} { src[\node].set(\gate, 1); src[\state] = true }; }; // ----- solo source srcsolo = {arg id; if(sources[id][\solo] == false){ sources[id][\solo] = true; solostate = solostate +1; ("!!! SOLO SOURCE "++id.asString).postln; }{ sources[id][\solo] = false; solostate = solostate -1; }; if(solostate > 0){ sources.do{arg src; if(src[\solo] == true && src[\state] == true){ src[\node].set(\gate, 1) }{ src[\node].set(\gate, 0) }; }; }{ sources.do{arg src; if(src[\state] == true){ src[\node].set(\gate, 1) } }; ("!!! SOLO OFF").postln; }; }; // ----- mirror source pos mirrorXYC = {arg mode; var src; src = sources[selected]; if(mode == 1){src[\pos] = src[\pos].mirrorX}; if(mode == 2){src[\pos] = src[\pos].mirrorY}; if(mode == 3){src[\pos] = src[\pos].mirrorO}; src[\rect] = pos2rect.value(src[\pos]); pos2drive.value(selected); }; // ----- update spacepan synth according to src gui position pos2drive = {arg id; var src, dists, distance, amps, delays, pan; src = sources[id]; //s.bind{ dists = lspos.collect{arg i; src[\pos].dist(i).clip(0.1, roomdim)}; //clip dist to prevent div by zero delays = dists / speedofsound; //delays src->ls amps = dists.minItem / dists; //amp factors based on dist amps = amps.lincurve(amps.minItem, 1, 0, 1, src[\focus]); //focus windowing amps = amps / amps.squared.sum.sqrt; //equal power distance = src[\pos].rho / roomscale; //distance for roomfx pan = pos2pan.value(src[\pos]); //4ch fx send pan //s.sync; src[\node].set( \delays, delays, \amps, amps, \distance, distance, \pan, pan, \fxdlycomp, delays.minItem ) //} }; // ----- convert position to pan pos2pan = {arg pos; [pos.x.linlin(roomscale.neg, roomscale, fxpan.neg, fxpan), pos.y.linlin(roomscale.neg, roomscale, fxpan.neg, fxpan)]; }; // ----- convert meters to pixels pos2point = {arg pos; Point(pos.x.linlin(roomscale.neg, roomscale, 0, guipoints), pos.y.linlin(roomscale.neg, roomscale, guipoints, 0)).round(1.0); }; // ----- calculate rect for spacegui pos2rect = {arg pos; Rect.aboutPoint(pos2point.value(pos), srcsize, srcsize); }; // ----- convert pixels to meters rect2pos = {arg id; var src = sources[id]; p = src[\rect].center; src[\pos] = (Point(p.x.linlin(0, guipoints, roomscale.neg, roomscale), p.y.linlin(0, guipoints, roomscale, roomscale.neg))).round(0.01); }; // ----- convert pos to normalized degrees (-180..+180) pos2angle = {arg pos; pos = pos.rotate(-0.5pi); pos.theta.raddeg.neg.round(0.1); }; // ----- osc function to feed src levelmeter srclevel = { OSCFunc({arg msg; { gui.srcMeter.value = msg[3].ampdb.linlin(-60, 0, 0, 1); }.defer; },'/level', s.addr); }; // ----- automation: rotation autorota = {arg id; var src = sources[id]; if(src[\rotastate]){ src[\rota] = Task({ inf.do({arg i; {src[\pos] = src[\pos].rotate(rotastepSpec.map(rotaspeedSpec.unmap(src[\rotaspeed])) * src[\rotadir]); //src[\pos] = src[\pos].rotate(0.015 * src[\rotadir]); pos2drive.value(src[\id]); src[\rect] = pos2rect.value(src[\pos]); }.defer; (src[\rotaspeed]).wait; }); }).start }{ src[\rota].stop; }; }; // ----- automation: random walk autorand = {arg id; var src = sources[id]; if(src[\randstate]){ src[\rand] = Task({ inf.do({arg i; {src[\pos] = (src[\pos].translate(Point(src[\randrange].rand2, src[\randrange].rand2).round(0.01))).clip(roomscale.neg, roomscale); src[\rect] = pos2rect.value(src[\pos]); pos2drive.value(src[\id]); }.defer; (src[\randspeed]).wait; }); }).start }{ src[\rand].stop; }; }; // ----- autorefresh gui autorefreshview = Task({ loop { { gui.space.refresh; if(selected.notNil){ gui.srcposX.string_("X: "++sources[selected][\pos].x.round(0.01)++" m"); gui.srcposY.string_("Y: "++sources[selected][\pos].y.round(0.01)++" m"); gui.srcposDeg.string_("A: "++pos2angle.value(sources[selected][\pos])++" °"); gui.srcposDist.string_("D: "++sources[selected][\pos].rho.round(0.01)++" m"); }; }.defer; autorefreshrate.wait; } }); // ----- remove src removesrc = {arg id; var src; if(sources.size > 0){ src = sources[id]; if(src[\rotastate]){src[\rota].stop}; if(src[\randstate]){src[\rand].stop}; if(src[\state]){src[\state] = false}; src[\node].free; sources.removeAt(id); }; }; // ----- make snapshot makesnap = { var path, stamp, array, filename, file; path = currpath++"Snapshots/"; stamp = Date.getDate.format("%d%m%y_%H%M%S"); filename = "Snapshot_"++stamp; file = File(path ++ filename ++".txt", "w"); sources.pairsDo{arg i; array = array.add([sources[i][\id], sources[i][\pos]])}; file.write(array.asCompileString); file.close; {gui.snapshots.items = gui.snapshots.items.add(filename)}.defer; }; // ----- recall snapshot recallsnap = {arg view; var path, file, filename, array, steps, env; path = currpath++"Snapshots/"; filename = gui.snapshots.item; steps = (xfadetime/xstep).asInteger; sources.do{ arg src; if(src[\rotastate]){ src[\rota].stop; src[\rotastate] = false }; if(src[\randstate]){ src[\rand].stop; src[\randstate] = false }; editsrc.value(selected); }; { file = File(path ++ filename ++".txt", "r"); array = file.readAllString.interpret; if(xfade == 0){ sources.do{arg src, i; src[\pos] = array[i][1]; src[\rect] = pos2rect.value(src[\pos]); pos2drive.value(src[\id]); }; }{ { sources.do{arg src, i; env = env.add(Env.new( [src[\pos].asArray, array[i][1].asArray], [xfadetime]).discretize(steps) ); }; sources.do{arg src, it; Task({ steps.do({arg i; { src[\pos].x = env[it][0][i]; src[\pos].y = env[it][1][i]; src[\rect] = pos2rect.value(src[\pos]); pos2drive.value(src[\id]); }.defer; xstep.wait; }); }).start; }; }.defer; }; }.defer; }; // ----- save current session saveprj = { var array, snaps, fxparas, file; Dialog.savePanel( { arg path; file = File(path ++ ".txt", "w"); fxparas = [ gui.roomsens.value, gui.roomfreq.value, gui.roomdamp.value, gui.revlevel.value, gui.revtime.value, gui.revhf.value, gui.revlf.value, gui.dlylevel.value, gui.dlytime.value, gui.dlyfb.value ]; snaps = gui.snapshots.items; { sources.do{ arg src; if(src[\rotastate]){ src[\rota].stop; src[\rotastate] = false }; if(src[\randstate]){ src[\rand].stop; src[\randstate] = false }; src[\node].free; src[\node] = nil; }; array = array.add(sources); array = array.add(fxparas); array = array.add(snaps); file.write(array.asCompileString); file.close; }.defer; sources.pairsDo{ arg key, src, i; srcon.value(key); if(src[\state] == false){ src[\node].set(\gate, 0) }; }; }, cancelFunc: {"canceled".postln}); }; // ----- load a session loadprj = { var array, file; { Dialog.openPanel({ arg paths; autorefreshview.stop; gui.src.enabled_(true); sources.keys.copy.do{ arg id; removesrc.value(id) }; file = File(paths, "r"); array = file.readAllString.interpret; { gui.roomsens.value = array[1][0]; gui.roomfreq.value = array[1][1]; gui.roomdamp.value = array[1][2]; gui.revlevel.value = array[1][3]; gui.revtime.value = array[1][4]; gui.revhf.value = array[1][5]; gui.revlf.value = array[1][6]; gui.dlylevel.value = array[1][7]; gui.dlytime.value = array[1][8]; gui.dlyfb.value = array[1][9]; roomsens = array[1][0]; roomfreq = array[1][1]; roomdamp = array[1][2]; reverb.do{ arg it; it.set(\amp, gui.revlevel.value.dbamp, \rt, gui.revtime.value, \hfratio, gui.revhf.value, \lfratio, gui.revlf.value) }; delay.do{ arg it; it.set(\amp, gui.dlylevel.value.dbamp, \dly, gui.dlytime.value, \fb, gui.dlyfb.value) }; array[0].pairsDo{ arg key, dict, i; sources.add( key -> dict); srcon.value(key); }; sources.pairsDo{ arg key, src, i; if(src[\state] == false){ src[\node].set(\gate, 0) }; rect2pos.value(src[\id]); }; selected = sources.keys.choose; editsrc.value(selected); gui.snapshots.items = array[2]; }.defer; autorefreshview.start; }, cancelFunc: {"canceled".postln}); }.defer; }; // ----- close session clear stuff cleanup = { autorefreshview.stop; sources.do{ arg src; if(src[\rotastate]){src[\rota].stop}; if(src[\randstate]){src[\rand].stop}; src[\node].free; }; statsources.do{arg it; it.free}; reverb.do{arg it; it.free}; delay.do{arg it; it.free}; testsig.free; tracklevel.free; master.free; masterbus.free; revbus.free; auxbus.free; dlybus.free; srcgroup.free; fxsgroup.free; fxrgroup.free; mstrgroup.free; if(gui.meters.visible == true) {gui.meters.close}; if(gui.fxpanel.visible == true) {gui.fxpanel.close}; if(gui.snaplist.visible == true) {gui.snaplist.close}; }; // ----- open helpfile gethelp = { var path = currpath ++ "/spacepan_help.txt"; Document.open(path); }; // ----- add synths to server makeSynths = { SynthDef("spacepan", { arg inbus = 100, out = 0, delays = #[0,0,0,0,0,0,0,0], //always set this array to lspos size! amps = #[0,0,0,0,0,0,0,0], //always set this array to lspos size! dodelays = 1, distance = 0, pan = #[0,0], room = 1, roomsens = 0.5, roomfreq = 4000, roomdamp = 0.5, fxdlycomp = 0.02, rev = 1, dly = 0, aux = 0, auxbus = 0, revbus = 0, dlybus = 0, amp = 0, gate = 1; var sig, dirlevel, fxlevel, fxsig, predly; sig = In.ar(inbus, 1) * amp; sig = sig * Linen.kr(gate, 0.1, 1, 0.1, doneAction: 0); distance = distance * room; sig = LPF.ar(sig, distance.lincurve(roomsens, 1, 20000, roomfreq, 1)); fxsig = sig; sig = sig * amps.lag(0.05); sig = Select.ar(dodelays, [ sig, DelayC.ar(sig, 0.1, delays.lag(0.05)) ]); fxsig = Select.ar(dodelays, [ fxsig, DelayC.ar(fxsig, 0.1, fxdlycomp) ]); dirlevel = distance.lincurve(roomsens, 1, 1, roomdamp, 1); fxlevel = distance.lincurve(roomsens, 1, 0, 1, 1); predly = distance.lincurve(roomsens, 1, 0.03, 0.003, 1); Out.ar(out, sig * dirlevel); Out.ar(revbus, Pan4.ar(DelayC.ar(fxsig, 0.1, predly), pan[0], pan[1], fxlevel * rev)); Out.ar(dlybus, Pan4.ar(fxsig, pan[0], pan[1], fxlevel * dly)); }).add; SynthDef("staticsrc", { arg inbus, out, amps = #[0,0,0,0,0,0,0,0]; //set this array to lspos size! var sig = In.ar(inbus, 1); Out.ar(out, sig * amps); }).add; SynthDef("tracklevel", { arg inbus = 100; var sig = In.ar(inbus, 1); SendReply.kr(Impulse.kr(20), '/level',[Amplitude.kr(sig)]); }).add; SynthDef("master", { arg in, out, amp = 1, gate = 1; var sig = In.ar(masterbus, lspos.size); sig = sig * Linen.kr(gate, 0.1, 1, 0.1, doneAction: 0); sig = Limiter.ar(sig, 0.9, 0.01); Out.ar(out, LeakDC.ar(sig, mul: amp)); }).add; SynthDef("reverb", { arg in, out, rt, stereo, lf, lfratio, hf, hfratio, early, late, modRate, modDepth, amp; var sig = In.ar(in, 2); sig = NHHall.ar(sig, rt, stereo, lf, lfratio, hf, hfratio, early, late, modRate, modDepth); Out.ar(out, sig * amp); }).add; SynthDef("delay", { arg in, out, dly, fb, amp; var sig = In.ar(in, 1); var fdbck = LocalIn.ar(1); sig = fdbck * fb + sig; sig = BPF.ar(sig, 1000, 2.0); LocalOut.ar(DelayC.ar(sig.tanh, 2, dly)); Out.ar(out , LeakDC.ar(sig, mul: amp)); }).add; SynthDef("testsig", { arg out; var sig = Decay2.ar(Impulse.ar(0.25), 0.1, 1, PinkNoise.ar, Decay.ar(Impulse.ar(1), 0.3, SinOsc.ar(500))) ; Out.ar(out, sig); }).add; }; ) //EOF /*SPACEPAN V1 README Note: I will convert the spacepan code to classes and write a proper helpfile within the next months, for now it is just a single .scd file and this lousy .txt ! You may also watch the video tutorial on youtube. *** BEFORE YOU START *** - You will need the "SC3plugins" extensions and the "wslib" and "mathlib" quarks (type "Quarks.gui") to run the code! - You will need at least 4 loudspeakers in a front-back configuration. For proper results use >= 8 loudspeakers - Create a dedicated spacepan folder in your SC working folder. This folder must contain SpacePanV1.scd, spacepan_help.txt and a subfolder named "Snapshots" *** RUNNING SPACEPAN *** - Open SpacePanV1.scd - Define your audio device, samplerate and the number of in/outs - Set the size of the virtual room. Depends on your real space, reasonable size will be between 10..50 meters - Set your loudspeaker positions array lspos = [x, y] meters. You can choose your reference point (from where you measure x y) but it makes sense to define the middle of your loudspeaker arrangement to be the reference (0, 0). Try to be precise. - VERY IMPORTANT: You have to set the arrays named amps = #[...] and delays = #[...] in the synthdefs "spacepan" and "staticsrc" to the number of loudspeakers! For example for 6 speakers set arrays to #[0,0,0,0,0,0]. There will be a more convenient solution in the next version of this code... - Select the code (cursor to first bracket to autoselect all) and evaluate (cmd+enter) - The gui shows up. No source by default. Click the green "+" in the upper left corner of the source panel to add a source. - If needed, change your master out index (number box bottom left) - Turn up the volume of your source. You should hear the ping sound. - Check your outputs by clicking "Outmeters" *** SOURCE PANEL FUNCTIONS *** - "Label" : label the source - "Group" : assign source group. use shiftkey to apply actions to group (mute = shift+alt+click, solo = shift+ctrl+click, move=shift+move). - "Input" : input bus. input metering is always pre fader. - "Solo" : solo selected source. you can do that in spaceview using "ctrl" + click. In group mode use "shift+ctrl" to solo the group. - "Mute" : mute selected source. you can do that in spaceview using "alt" + click. In group mode use "shift+alt" to solo the group. - "Level" : source level -inf...0dB. Group levels are always linked - "Focus" : spread of the source. always use 1 if you have a stereo input source routed to group to minimize spill and phasing. focus is always linked. - "Apply delay" : if position algo uses time+amplitude or amplitude only - "Apply room fx" : apply distance based lowpass filtering and fx sends - "Reverb" : reverb send level. - "Delay" : delay send level. - "Aux" : aux send level. not active yet. - "Rota" : autorotation. adjust speed and direction as you like. always linked in groups. - "Rand" : randomwalk. set stepsize and speed as you like. always linked in groups. - "Mirror": mirror current source position. not linked in groups. - "Position" : x and y position, distance and angle of selected source. *** OPTION PANEL *** - "Session.." : load & save a session to .txt. if you want to overwrite a file, remove the .txt extension - "Out Meters" : output metering - "View.." : view options - "Snapshots" : open snapshot window. always use same number of sources. do not make a snapshot and then add sources, this will fail in recall. - "FX Settings" : open fx settings panel. *** MASTER PANEL *** - "Level" : master out volume - "Offset": offset master out Code : sccode.org/audioholic / Contact: dominik.wegmann@tonmeister.de */