Norns SuperCollider loadRelative()

I’ve been developing a little SuperCollider script, with a view to making it in to an Engine and using it in the Norns script.

Anyone know if it’s possible to use loadRelative() to split the SC script into chunks? I have a main engine definition file based on a simple example script, and a couple of additional CS scripts, one of which defines a synthDef (synth.scd), and the other a non-realtime server for sample analysis (analyser.scd).

This seems to work fine on my Mac.

However, when I attempt to convert the scripts for Norns use, I seem to get a SuperCollider Fail error if either of the additional SC files are present in my Norns script’s “lib” folder.

I can prevent the SC Fail error by removing the “sc” or “scd” suffix from the filenames, but then the main SC script won’t load them.

Any tips?

it might help to share your code.

but:

String.loadRelative() interprets its path argument as “relative to the current document or text file.”

when called from a class, there is no such document. so if you try this in SCIDE, or check the SC tab in maiden, you will see something like

ERROR: can't load relative to an unsaved file

you probably want String.load, which accepts an absolute path. (there are also various utilities for e.g. getting the home directory if you want them.)

Loader{
	*initClass {
		postln("Loader initClass");
//		"foo.scd".loadRelative; // error
		"/home/we/foo.scd".load(); // ok, assuming the path exists
	}
}

if you’re unfamiliar with how classes work in SC, check the Guides > Language > OOP > Writing Classes helpfile in SCIDE. (all sclang code on norns must be wrapped in a class at the moment - but that class can execute arbitrary .scd files as demonstrated.)

Thanks for the super-quick reply, @zebra.

That makes sense.

I will try the String.load method!

That doesn’t solve my other problems, though - I seem to get a SuperCollider error when there are and SC files other than my engine definition in my script’s lib folder.

That sounds weird, and I can’t diagnose without seeing the code. (it’s common and recommended to split things into multiple classes / files.)

Bedtime here, but I will post some code tomorrow.

I’m a noob, so it’s entirely possible the problem is with the contents of the files.

Decided to dump the file contents here before bed :slight_smile:

Here’s one, ‘lib/analyse.scd’:

///////////////////////////////////////
// Non-Realtime FFT Analysis To File //
///////////////////////////////////////

~analyseWav = {
	arg wavIn, fftOut, pitchOut, action;

	// Define function vars
	var inBuf, fftBuf, pitchBuf, fftSize = ~fftSize, f, nrtServer, nrtScore, env;

	var cond = Condition.new;

	fork {
		f = SoundFile.openRead(wavIn);
		f.close;

		// Define Non-RealTime server
		nrtServer = Server(\nrt, NetAddr("127.0.0.1", 57110),
			ServerOptions.new
			.numOutputBusChannels_(2)
			.numInputBusChannels_(2)
			.sampleRate_(44100)
		);

		// Create buffer for input file
		inBuf = Buffer(nrtServer, 65536, 1);

		// Create output buffer for FFT analysis data
		fftBuf = Buffer.alloc(nrtServer, f.duration.calcPVRecSize(fftSize, 0.5, nrtServer.options.sampleRate), 1);
		pitchBuf = Buffer.alloc(nrtServer, (f.numFrames / nrtServer.options.blockSize).roundUp.asInteger, 2);

		// Create score for recording of FFT analysis to WAV file
		nrtScore = Score([
			[0, inBuf.allocMsg],
			[0, fftBuf.allocMsg],
			[0, pitchBuf.allocMsg],
			[0, inBuf.readMsg(wavIn, leaveOpen: true)],
			[0, [\d_recv, SynthDef(\pv_ana, {
				var sig = VDiskIn.ar(1, inBuf, f.sampleRate / SampleRate.ir);
				var fft = FFT(LocalBuf(fftSize, 1), sig);
				var pitch = Tartini.kr(sig);
				RecordBuf.kr(pitch, pitchBuf, loop: 0);
				fft = PV_RecordBuf(fft, fftBuf, run: 1);
				Out.ar(0, sig);
			}).asBytes]],
			[0, Synth.basicNew(\pv_ana, nrtServer).newMsg],
			[f.duration + (fftSize / nrtServer.options.sampleRate),
				fftBuf.writeMsg(fftOut, "wav", "float");
			],
			[f.duration + (fftSize / nrtServer.options.sampleRate),
				pitchBuf.writeMsg(pitchOut, "wav", "float");
			]

		]);

		nrtScore.score.do(_.postln);

		// Run score
		nrtScore.recordNRT(
			outputFilePath: if(thisProcess.platform.name == \windows) { "NUL" } { "/dev/null" },
			headerFormat: "wav",
			sampleRate: nrtServer.options.sampleRate,
			options: nrtServer.options,
			duration: f.duration + (fftSize / nrtServer.options.sampleRate),
			action: { "done".postln; cond.unhang }
		);

		cond.hang;

		// Free score
		nrtScore.free;

		// Free NRT server
		nrtServer.remove;

		action.value;
	};
}

…and the other, ‘lib/synth.scd’:

///////////////////////////////
// PV-Scrub Synth Definition //
///////////////////////////////

SynthDef(\pvscrub, {
	arg out = 0, fBuf = 0,
	noteOffset = 0, pitchBend = 0,
	amp = 0.1, gate = 1,
	ampEnvAttack = 0.01, ampEnvDecay = 0, ampEnvSustain = 1.0, ampEnvRelease = 0.5,
	grainSize = 0.2, grainPitchDispersion = 0.01, grainTimeDispersion = 0.08,
	lpCutoff = 8000, lpResonance = 1.0, hpCutoff = 50,
	scrubPos = 0.5, scrubPosInertia = 0.01,
	pvParam1 = 0, pvParam2 = 0;

	var fftSize = ~fftSize, chain, bufNum, ampEnv, pitchRatio, result, playHead;

	// Amplitude envelope ADSR type
	// Free synth after envelope finishes
	ampEnv = Env.adsr(ampEnvAttack, ampEnvDecay, ampEnvSustain, ampEnvRelease, amp).kr(2, gate);

	// Read FFT buffer
 	playHead = min(scrubPos.lag(scrubPosInertia), 1);
	bufNum = LocalBuf.new(fftSize);
	chain = PV_BufRd(bufNum, fBuf, playHead, 1);

	// FFT FX
	chain = PV_RectComb(chain, pvParam1.lag(1), 0);
	chain = PV_MagSmooth(chain, pvParam2.lag(1));

	// Resynthesise FFT
	result = IFFT(chain).dup;

	// Time-domain pitch-shift
	pitchRatio = (noteOffset + pitchBend).midiratio;
	result = PitchShift.ar(
		result,
		grainSize,
		pitchRatio,   // Pitch-shift ratio
		grainPitchDispersion.lag(2),
		grainTimeDispersion.lag(2)
	);

	// Filter //

	// Resonant 2-pole low-pass
	result = BLowPass.ar(result, lpCutoff.lag(0.5), lpResonance.lag(0.5),  1.0);

	// Non-resonant 2-pole high-pass
	result = BHiPass.ar(result, hpCutoff.lag(0.5), 1.0, 0.5);

	// Final output
	Out.ar(out, result * ampEnv);
}).add;

Nice, looks fun. I love doing this kind of weird analysis resynth stuff in SC.

I’m just looking in my phone right now, but - The one thing that gives me pause is the creation of the NRT server on what I think is the same port used by the default server. If that’s true, I’d expect that to cause an error or just not work

Also it would be good to see the class that is running these things. If, by chance, the NRT is running at boot time, (or some other long process blocking sclang) then that could cause the SC:FAIL state. (This just means that the matron pro cess timed out waiting for initial handshake from sclang. Doesn’t necessarily mean SC failed to compile class library, just usually.)

I try and give this a spin from a wrapper class on norns, but can’t promise I’ll find the time…

[update]
still haven’t tried running these on norns, but did confirm that port 57110 is the port used by the scsynth server launched by the core norns SC interface in Crone.sc. so i would use a different port for the NRT server.

in the process, i discovered that we are reporting the port number incorrectly at startup! oops. my apologies if you were bitten by this.

(fix: post correct scsynth port on boot by catfact · Pull Request #1389 · monome/norns · GitHub)

of course the issue may be something else entirely!

Making some progress!

Now I don’t get a SuperCollider Fail, but so far my file isn’t being analysed when the script is loaded, and I get an SC error. I have a feeling it’s something simple, though…

Current version:

Main SC file (‘engine_pvscrub.sc’):

Engine_PVScrub : CroneEngine {
  
    var pg;
  
    var fftSize = 1024;
    var wavPath = "/home/we/dust/code/pvscrub/analysis/demo.wav";
    var fftPath = "/home/we/dust/code/pvscrub/analysis/demo-fft.wav";
    var pitchPath = "/home/we/dust/code/pvscrub/analysis/demo-pitch.wav";
  
    var amp=0.3;
    var release=0.5;
    var pw=0.5;
    var cutoff=1000;
    var gain=2;

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

    alloc {
    
        // NRT Analysis
        "/home/we/dust/code/pvscrub/lib/analyser.scd".load();
        
        analyseWav(~wavPath, ~fftPath, ~pitchPath).value;
        
  
        pg = ParGroup.tail(context.xg);
            SynthDef("PolyPerc", {
                arg out, freq = 440, pw=pw, amp=amp, cutoff=cutoff, gain=gain, release=release;
                var snd = Pulse.ar(freq, pw);
                var filt = MoogFF.ar(snd,cutoff,gain);
                var env = Env.perc(level: amp, releaseTime: release).kr(2);
                //      out.poll;
                Out.ar(out, (filt*env).dup
            );
        }).add;

        this.addCommand("hz", "f", { arg msg;
            var val = msg[1];
            Synth("PolyPerc", [\out, context.out_b, \freq,val,\pw,pw,\amp,amp,\cutoff,cutoff,\gain,gain,\release,release], target:pg);
        });

        this.addCommand("noteOn", "f", { arg msg;
            var val = msg[1];
            Synth("PolyPerc", [\out, context.out_b, \freq,val,\pw,pw,\amp,amp,\cutoff,cutoff,\gain,gain,\release,release], target:pg);
        });

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

        this.addCommand("pw", "f", { arg msg;
            pw = msg[1];
        });
        
        this.addCommand("release", "f", { arg msg;
            release = msg[1];
        });
        
        this.addCommand("cutoff", "f", { arg msg;
            cutoff = msg[1];
        });
        
        this.addCommand("gain", "f", { arg msg;
            gain = msg[1];
        });
    }
}

Analyser function:

///////////////////////////////////////
// Non-Realtime FFT Analysis To File //
///////////////////////////////////////

analyseWav = {
	arg wavIn, fftOut, pitchOut, action;

	// Define function vars
	var inBuf, fftBuf, pitchBuf, fftSize = ~fftSize, f, nrtServer, nrtScore, env;

	var cond = Condition.new;

	fork {
		f = SoundFile.openRead(wavIn);
		f.close;

		// Define Non-RealTime server
		//nrtServer = Server(\nrt, NetAddr("127.0.0.1", 57110),
		nrtServer = Server(\nrt, NetAddr("127.0.0.1", 57111),
			ServerOptions.new
			.numOutputBusChannels_(2)
			.numInputBusChannels_(2)
			.sampleRate_(44100)
		);

		// Create buffer for input file
		inBuf = Buffer(nrtServer, 65536, 1);

		// Create output buffer for FFT analysis data
		fftBuf = Buffer.alloc(nrtServer, f.duration.calcPVRecSize(fftSize, 0.5, nrtServer.options.sampleRate), 1);
		pitchBuf = Buffer.alloc(nrtServer, (f.numFrames / nrtServer.options.blockSize).roundUp.asInteger, 2);

		// Create score for recording of FFT analysis to WAV file
		nrtScore = Score([
			[0, inBuf.allocMsg],
			[0, fftBuf.allocMsg],
			[0, pitchBuf.allocMsg],
			[0, inBuf.readMsg(wavIn, leaveOpen: true)],
			[0, [\d_recv, SynthDef(\pv_ana, {
				var sig = VDiskIn.ar(1, inBuf, f.sampleRate / SampleRate.ir);
				var fft = FFT(LocalBuf(fftSize, 1), sig);
				var pitch = Tartini.kr(sig);
				RecordBuf.kr(pitch, pitchBuf, loop: 0);
				fft = PV_RecordBuf(fft, fftBuf, run: 1);
				Out.ar(0, sig);
			}).asBytes]],
			[0, Synth.basicNew(\pv_ana, nrtServer).newMsg],
			[f.duration + (fftSize / nrtServer.options.sampleRate),
				fftBuf.writeMsg(fftOut, "wav", "float");
			],
			[f.duration + (fftSize / nrtServer.options.sampleRate),
				pitchBuf.writeMsg(pitchOut, "wav", "float");
			]

		]);

		nrtScore.score.do(_.postln);

		// Run score
		nrtScore.recordNRT(
			outputFilePath: if(thisProcess.platform.name == \windows) { "NUL" } { "/dev/null" },
			headerFormat: "wav",
			sampleRate: nrtServer.options.sampleRate,
			options: nrtServer.options,
			duration: f.duration + (fftSize / nrtServer.options.sampleRate),
			action: { "done".postln; cond.unhang }
		);

		cond.hang;

		// Free score
		nrtScore.free;

		// Free NRT server
		nrtServer.remove;

		action.value;
	};
}

SuperCollider error:

a CroneAudioContext
ERROR: Variable 'analyseWav' not defined.
  in interpreted text
  line 80 char 1:
   
   
-----------------------------------
ERROR: Message 'analyseWav' not understood.
RECEIVER:
   nil
ARGS:
   nil
   nil
CALL STACK:
	DoesNotUnderstandError:reportError
		arg this = <instance of DoesNotUnderstandError>
	Nil:handleError
		arg this = nil
		arg error = <instance of DoesNotUnderstandError>
	Thread:handleError
		arg this = <instance of Thread>
		arg error = <instance of DoesNotUnderstandError>
	Thread:handleError
		arg this = <instance of Routine>
		arg error = <instance of DoesNotUnderstandError>
	Object:throw
		arg this = <instance of DoesNotUnderstandError>
	Object:doesNotUnderstand
		arg this = nil
		arg selector = 'analyseWav'
		arg args = [*2]
	Engine_PVScrub:alloc
		arg this = <instance of Engine_PVScrub>
	< FunctionDef in Method CroneEngine:init >  (no arguments or variables)
	Routine:prStart
		arg this = <instance of Routine>
		arg inval = 28.834867233
^^ The preceding error dump is for ERROR: Message 'analyseWav' not understood.
RECEIVER: nil

analyseWav needs to be declared as a variable before its assigned. (or, it can be made a global environment variable by prefixing it with ~, but you probably don’t want that.)

analyse_wav.scd:

var analyseWav = {
  ...
};
analyseWav // <- last statement will be return value of executing the script

this variable will go out of scope when the script is finished.

(technically, you could just have nothing but the bare function in the .scd, but maybe this is clearer.)

then in the engine, or wherever, assign it to a variable in the scope of the class method (or wherever):

var analyseWav = (Platform.userHomeDir ++ "/dust/code/analyse_wav.scd").load;

(incidentally, you don’t actually need/want the parens on the .load method call since it takes no arguments. i also used the Platform home dir helper so i can do this on non-norns machine.)


now, personally i would just put the analysis routine in a class as well. it can be a “static” class with no instance methods or constructor:

AnalyseWav.sc

AnalyseWav {

	*run {
		arg wavIn, fftOut, pitchOut, action;
		// ...
	}
}

and then you can do AnalyseWav.run(...) from wherever with no additional fuss.

that way you don’t have to worry about scopes, getting the path right, any syntax errors will definitely be caught at class lib compilation time, its tidy and reusable. (only reason i see to put put stuff in .scd is if you want to tweak it at runtime.) but that’s just my preference.

the other point of structure i’d make is to consider putting the actual PV scrubbing resynthesis stuff outside of the engine as well. so you have a PVScub, an AnalyseWav, and your Engine_PVScrub whose only job is to provide the OSC command glue for norns. benefit of doing this is not requiring the norns stuff to use the core functionality in other contexts.

but again, totally up to you. hope that helps

1 Like

Thanks you very much for these tips. @zebra. I really appreciate you taking the time to help me. I’m at work at the moment, and don’t have access to my Shield, but will work through your advice above when I get back home.