Confused as to how you should do SoundIn.ar for the Right and Left channels separately.

I’ve been using SoundIn.ar([0, 1]), but the UGen I’m working with expects two separate inputs. So are the mono inputs just SoundIn.ar(0) and SoundIn.ar(1) ?

Yup. You could also do

var inputs, left, right;
inputs = SoundIn.ar([0, 1]);
left = inputs[0];
right = inputs[1];

(also, I believe on Norns context.in_b[0].index and context.in_b[1].index are preferred to a simple 0, 1 respectively)

Probably worth reading a bit about Multichannel Expansion – definitely one of SC’s harder bits to wrap your mind around, but super helpful once you get it in your head

1 Like

its sort of a long story but FWIW: it’s no longer actually needed to use these, and they’ll be removed (or at least deprecated) in next major version (soon i promise)

2 Likes

I’ve built the Fates/Norns device and it’s working perfectly.
Managing the device and running scripts from Maiden is fantastic.

Now, I’d like to convert some supercollider scripts to run on the Norns platform.
Can supercollider scripts be uploaded to Norns through the Maiden interface? Do they have to be treated with Lua?

Yup, SC files are uploaded / edited same as Lua files in maiden. Big difference is you have to restart the device after changing any SC code. For that reason, I recommend developing your SC using the official desktop SC IDE, then uploading to norns once you’re pretty confident in it

If you haven’t gone through the official studies yet I definitely recommend starting there. It doesn’t get too deep into custom SC code, but you can dig into the PolyPerc engine it uses for most studies if you want something to crib off of.

In short: you need to wrap your SC code in an “engine” subclass so that norns knows how to use it, and then your Lua code needs to declare that when your script launches, it should also launch that particular engine.

1 Like

I found this highly useful!

3 Likes

Thanks for the help!
It’s a little daunting trying to find out where to start with something like this.

1 Like

norns supercollider tutorial is in the works

22 Likes

If a SC class method takes a parameter with -inf as a possible value - how would you pass that from lua?

use -math.huge, or equivalently -(1/0)

norns lua:

osc.send({"127.0.0.1", 57120}, "/neginf", {-math.huge})

SC output:

OSC Message Received:
	time: 33290.255541006
	address: a NetAddr(127.0.0.1, 8888)
	recvPort: 57120
	msg: [ /neginf, -inf ]
1 Like

While working on SC engines, I noticed some changes seem to require a full restart of norns while others do not. Is there a simple heuristic around this?

can you give an example?

any changes to SC class code require a restart of sclang to take effect, and at the moment this also initiates a restart of scsynth. (all norns SC code is class code.)

at that point it’s best to restart the whole stack, because stopping the SC processes can cause various things to get weird on the matron side.

for example:

  • if running the Glut engine or similar, you will get a flood of timeouts from the position polls when SC goes away
  • engine descriptor list could be stale
  • commands will be lost
  • &c, idk
Engine_DroneSine : CroneEngine {
  var hz=440;
  var amp=1;
... 

Seems like I can update this variables like these, save, and rerun with no problem but if I’m adding a new SynthDef or something it doesn’t work until I restart.

I guess what I really want to know is what is a good development workflow? Restarting norns takes a long time. Should I be doing most development in SC standalone?

none of the code changes you make there will have an effect until SC is restarted. i promise!

Should I be doing most development in SC standalone?

yes. i would do it like this:


// the "kernel" or "business logic" class, implementing the synth
DroneSine { 
    var hz; 
    var amp;
    var synth;

    *new { arg server;
        synth = Synth.new(\someSynthDef, target: server);
    }

    setHz { arg val; hz = val; .... }
}

// a "wrapper" class exposing the synth to norns
Engine_DroneSine : CroneEngine { 
   var droneSine; // a DroneSine

    *new { arg context, doneCallback;
        ^super.new(context, doneCallback);
   }

    alloc { 
        droneSine = DroneSine.new(context.server);
        addCommand("hz", "f", { droneSine.setHz(msg[1]); });
   }
}

[ed: ok, i think that is now basically correct and reasonably complete.]

the DroneSine “kernel” class can do all the heavy lifting and is not dependent on norns stuff.

5 Likes

Beautiful. Thank you so much.

Is there a way to restart just SC?

you can enter ;restart in the SC REPL window in maiden to restart the supercollider systemd service on norns.

thisProcess.platform.recompile in sclang should also work to recompile the class library including engine code, and thiat will also cause the norns system classes to reboot scsynth and so on. (i haven’t actually tried this on norns and the Platform interface might be wonky since we are running headless.)

again, be aware that forcibly restarting the SC services or the sclang interpeter, without also restarting matron, will lead to some things not working (like running polls), and could consequently hang the norns UI for some scripts.

2 Likes

Thank you so much. I read the docs a bunch but I always learn best when I can talk about it with others. :slight_smile:

I’m getting a silly attempt to call a nil value (field 'hz') error that I can’t figure out. The Engine_Dronecaster.sc is Engine_TestSine.sc renamed. What am I doing wrong? Just trying to get K1 and K2 to update amp and hz.

Error Message
lua: 
/home/we/dust/code/dronecaster/dronecaster.lua:85: attempt to call a nil value (field 'hz')
stack traceback:
	/home/we/dust/code/dronecaster/dronecaster.lua:85: in global 'updateHz'
	/home/we/dust/code/dronecaster/dronecaster.lua:53: in field 'action'
	/home/we/norns/lua/core/params/control.lua:85: in function 'core/params/control.bang'
	/home/we/norns/lua/core/params/control.lua:67: in function 'core/params/control.set_raw'
	/home/we/norns/lua/core/params/control.lua:75: in function 'core/params/control.delta'
	/home/we/norns/lua/core/paramset.lua:266: in function 'core/paramset.delta'
	/home/we/dust/code/dronecaster/dronecaster.lua:115: in function 'enc'
	/home/we/norns/lua/core/encoders.lua:60: in function 'core/encoders.process'
dronecaster.lua
-- DRONECASTER
-- 
-- k1: exit  e1: amp
--
--            e2: hz    e3: drone
--         k2: alt     k3: cast
--

engine.name = 'Engine_Dronecaster'

drones = {'Sine', 'Eno', 'Belong', 'Hecker', 'Gristle', 'Starlids', 'GY!BE', 'V/Vm', 'Canada'}


function init()

  -- ui
  altKey = false
  castKey = false
  
  -- time
  seconds = 0
  counter = metro.init()
  counter.time = 1
  counter.count = -1
  counter.event = theSandsOfTime
  counter.play = 0
  
  -- draw
  screen.aa(0)
  screenO = 0
  screenL = 5
  screenM = 10
  screenH = 15
  screen.level(screenH)
  screen.font_face(0)
  screen.font_size(8)
  
  -- animation
  frame = 1
  driftMaxX = 5
  driftMaxY = 3
  joeHomeX = 25
  joeHomeY = 25
  bethHomeX = 20
  bethHomeY = 30
  alexHomeX = 32
  alexHomeY = 29
  
  -- params
  params:add_control("amp","amp",controlspec.new(0,1,'amp',0,0.5,'amp'))
  params:set_action("amp", function(x) updateAmp(x) end)
  params:add_control("hz","hz",controlspec.new(0,20000,'lin',0,440,'hz'))
  params:set_action("hz", function(x) updateHz(x) end)
  params:add_control("drone","drone",controlspec.new(1,9,'lin',0,1,'drone'))
  params:set_action("drone", function(x) updateType(x) end)
  
      counter:start()
      counter.play = 1  
end

function theSandsOfTime(c)
  seconds = c
  frame = frame + 1
  redraw()
end

function count()
  units = units+1
  ms = units / 100
  seconds = ms % 60
  redraw()
end

function redraw()
  screen.clear()
  drawLandscape()
  drawBirds()
  drawClock()
  drawPlayStop()
  drawTopMenu()
  screen.update()
end

function updateHz(x)
  engine.hz(x)
end

function updateAmp(x)
  -- engine.amp(x)
end

function updateType(x)

end



-- enc & keys - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



function enc(n,d)
  
  -- amp
  if n == 1 and altKey then
     params:delta("amp", d)
  elseif n == 1 then
    params:delta("amp", d * .1)     
  end
  
  -- hz
  if n == 2 and altKey then
    params:delta("hz", d)
  elseif n == 2 then
    params:delta("hz", d * .01)
  end
  
  -- drone
  if n == 3 then
     params:delta("drone", d) 
  end
  
  redraw()
  
end



function key(n,z)
  
  -- k2 alt
  if n == 2 and z == 1 then
    altKey = true
  elseif n == 2 and z == 0 then
    altKey = false
  end

  -- k3 start/stop  
  if n == 3 and z == 1 then
    if counter.play == 1 then
      counter:stop()
      counter.play = 0
      frame = 1
    else
      counter:start()
      counter.play = 1
      units = 0
    end
  end
  
  redraw()
  
end



-- utils - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



function round(num, places)
 if places and places > 0 then
    local mult = 10 ^ places
    return math.floor(num * mult + 0.5) / mult
  end
  return math.floor(num + 0.5)
end



-- draws - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



function drawBirds()
  
  screen.level(screenL)
  
  birdFrame = frame % 3

  -- drifting works but need to implement boundaries  
  -- driftX = math.random(0, 1)
  -- driftY = math.random(0, 1)
  -- operator = math.random(0, 1)
  -- if operator == 0 then
  --   driftX = driftX * -1
  --   driftY = driftY * -1
  -- end
  
  -- joeHomeX = joeHomeX + driftX
  -- joeHomeY = joeHomeY + driftY
  -- bethHomeX = bethHomeX + driftX
  -- bethHomeY = bethHomeY + driftY
  -- alexHomeX = alexHomeX + driftX
  -- alexHomeY = alexHomeY + driftY

  if birdFrame == 0 then
    -- joe
    screen.move(joeHomeX, joeHomeY)
    screen.line_rel(2, 2)
    screen.move(joeHomeX, joeHomeY)
    screen.line_rel(-2, 2)
    -- beth
    screen.move(bethHomeX, bethHomeY)
    screen.line_rel(2, -2)
    screen.move(bethHomeX, bethHomeY)
    screen.line_rel(-2, -2)
     -- alex
    screen.move(alexHomeX, alexHomeY)
    screen.line_rel(2, 1)
    screen.move(alexHomeX, alexHomeY)
    screen.line_rel(-2, 1)   
  end
  
  if birdFrame == 1 then
    -- joe
    screen.move(joeHomeX, joeHomeY)
    screen.line_rel(2, 1)
    screen.move(joeHomeX, joeHomeY)
    screen.line_rel(-2, 1)
    -- beth
    screen.move(bethHomeX, bethHomeY)
    screen.line_rel(2, 2)
    screen.move(bethHomeX, bethHomeY)
    screen.line_rel(-2, 2)
     -- alex
    screen.move(alexHomeX, alexHomeY)
    screen.line_rel(2, -2)
    screen.move(alexHomeX, alexHomeY)
    screen.line_rel(-2, -2)       
  end
  
  if birdFrame == 2 then
    -- joe
    screen.move(joeHomeX, joeHomeY)
    screen.line_rel(2, -2)
    screen.move(joeHomeX, joeHomeY)
    screen.line_rel(-2, -2)
    -- beth
    screen.move(bethHomeX, bethHomeY)
    screen.line_rel(2, 1)
    screen.move(bethHomeX, bethHomeY)
    screen.line_rel(-2, 1)
     -- alex
    screen.move(alexHomeX, alexHomeY)
    screen.line_rel(2, 2)
    screen.move(alexHomeX, alexHomeY)
    screen.line_rel(-2, 2)       
  end

end



function drawLandscape()
  
  screen.level(screenL)
  
  -- antenna sides
  screen.move(62, 52)
  screen.line(66, 20)
  screen.move(70, 53)
  screen.line(66, 20)
  
  -- antenna horizontals
  screen.move(64, 34)
  screen.line_rel(3, 0)
  screen.move(64, 39)
  screen.line_rel(3, 0)
  screen.move(64, 45)
  screen.line_rel(3, 0)

  -- antenna supports
  screen.move(62,52)
  screen.line(70,44)
  screen.move(70,52)
  screen.line(62,44)
  screen.move(70,44)
  screen.line(63,37)
  
  -- antenna details
  screen.move(65, 19)
  screen.line_rel(2, 0)
  screen.move(65, 17)
  screen.line_rel(1, 0)
  screen.move(62, 30)  
  screen.line_rel(2,0)
  screen.move(67, 28)  
  screen.line_rel(2, 0)
  screen.move(62, 27)
  screen.line_rel(1, 2)
  screen.move(62, 25)
  screen.line_rel(1, 1)
  screen.move(69, 25)
  screen.line_rel(1, 2)
  screen.move(69, 23)
  screen.line_rel(1, 1)
  
  -- distant horizon
  screen.move(0,48)
  screen.line_rel(60, 0)
  screen.move(72,48)
  screen.line_rel(50, 0)
  
  -- second horizon
  screen.move(1, 50)
  screen.line_rel(1, 0)
  screen.move(4, 50)
  screen.line_rel(40, 0)
  screen.move(46, 50)
  screen.line_rel(9, 0)
  screen.move(57, 50)
  screen.line_rel(1, 0)
  screen.move(74, 50)
  screen.line_rel(40, 0)
  screen.move(116, 50)
  screen.line_rel(2, 0)
  
  -- third horizon
  screen.move(5, 55)
  screen.line_rel(3, 0)
  screen.move(10, 55)
  screen.line_rel(40, 0)
  screen.move(55, 55)
  screen.line_rel(20, 0)
  screen.move(80, 55)
  screen.line_rel(41, 0)
  screen.move(80, 55)
  
  -- closest horizon
  screen.move(39, 62)
  screen.line_rel(71, 0)
  
  
end



function drawTopMenu()

screen.level(screenL)

  if altKey then
    for i=11,0,-1 do
      screen.move(0, i)
      screen.line_rel(40,0)
      screen.move(44,i)
      screen.line_rel(40,0)
    end
  end

  
  screen.move(0,12)
  screen.line_rel(40,0)
  screen.move(44,12)
  screen.line_rel(40,0)
  screen.move(88,12)
  screen.line_rel(40,0)
  
  
  -- todo: understand why commenting out this breaks the landscape
  screen.stroke()

  screen.level(screenH)
  screen.move(2,8)
  screen.text(round(params:get("amp"),2) .. " amp")
  screen.move(45,8)
  screen.text(round(params:get("hz")) .. " hz")
  screen.move(89,8)
  screen.text(drones[round(params:get("drone"))])

end



function drawClock()
  screen.level(screenL)
  screen.move(2,64)
  local display_time = util.s_to_hms (seconds)
  screen.text(display_time)
end



function drawPlayStop()

  screen.level(screenL)

  if counter.play == 1 then
    -- play
    screen.move(33, 59)
    screen.line(33, 64)
    screen.move(34, 60)
    screen.line(34, 63)
    screen.move(35, 61)
    screen.line(35, 62)
  else
    -- stop
    screen.move(33, 59)
    screen.line(33, 64)
    screen.move(34, 59)
    screen.line(34, 64)
    screen.move(35, 59)
    screen.line(35, 64)
    screen.move(36, 59)
    screen.line(36, 64)
  end

end
lib/Engine_Dronecaster.sc
 Engine_Dronecaster : CroneEngine {
	var <synth;

	*new { arg context, doneCallback;
		^super.new(context, doneCallback);
	}


	alloc {
		synth = {
			arg out, hz=220, amp=0.5, amplag=0.02, hzlag=0.01;
			var amp_, hz_;
			amp_ = Lag.ar(K2A.ar(amp), amplag);
			hz_ = Lag.ar(K2A.ar(hz), hzlag);
			Out.ar(out, (SinOsc.ar(hz_) * amp_).dup);
		}.play(args: [\out, context.out_b], target: context.xg);

		this.addCommand("hz", "f", { arg msg;
			synth.set(\hz, msg[1]);
		});

		this.addCommand("amp", "f", { arg msg;
			synth.set(\amp, msg[1]);
		});
	}

	free {
		synth.free;
	}
	
}

you want engine.name = DroneCaster

1 Like

Ahhhhhh!! Yes!! Thank you so much. Brutal. I wonder why I thought that was correct. I was poring over at the norns studies pages for so many hours too. My brain just didn’t see it. Thank you.