Norns and ES-8

I was curious if it was possible to use an Expert Sleepers ES-8 as a soundcard for Norns and as it turns out it appears to work with only a few changes necessary, but will need some development to really make use of the ES-8.

It’s an exciting idea to me, having something like FM7 modulated by and being output to multiple channels into the modular sounds potentially really fun!

Digging into Norns I found a systemd unit file that manages jack at
/etc/systemd/system/norns-jack.service
with the startup command:
/usr/bin/jackd -R -P 95 -d alsa -d hw:0 -r 48000 -n 3 -p 128 -S -s

After a bit of googling I found what looks to be a simple way to tell jack to start up and connect to an ES8:
/usr/bin/jackd -R -P 95 -d alsa -d hw:ES8,0 -r 48000 -n 3 -p 128 -S -s

So I edited the startup command and restarted norns.

I don’t know much about jack yet, but after a bit of googling I found the command:

jack_lsp

This will list the in/out ports known to jack, and if we pass the flag -c it will show the connections that are configured:

Normally:

192.168.99.171 ~ $ jack_lsp -c
system:capture_1
   crone:input_1
system:capture_2
   crone:input_2
system:playback_1
   crone:output_1
system:playback_2
   crone:output_2
crone:input_1
   system:capture_1
crone:input_2
   system:capture_2
crone:input_3
   softcut:output_1
crone:input_4
   softcut:output_2
crone:input_5
   SuperCollider:out_1
crone:input_6
   SuperCollider:out_2
crone:output_1
   system:playback_1
crone:output_2
   system:playback_2
crone:output_3
   softcut:input_1
crone:output_4
   softcut:input_2
crone:output_5
   SuperCollider:in_1
crone:output_6
   SuperCollider:in_2
softcut:input_1
   crone:output_3
softcut:input_2
   crone:output_4
softcut:output_1
   crone:input_3
softcut:output_2
   crone:input_4
SuperCollider:in_1
   crone:output_5
SuperCollider:in_2
   crone:output_6
SuperCollider:out_1
   crone:input_5
SuperCollider:out_2
   crone:input_6

We can see that system:capture_1,2 and system:playback_1,2 are mapped to crone ins and outs normally.

After starting jack with ES8:

system:capture_1
   crone:input_1
system:capture_2
   crone:input_2
system:capture_3
system:capture_4
system:capture_5
system:capture_6
system:capture_7
system:capture_8
system:capture_9
system:capture_10
system:capture_11
system:capture_12
system:playback_1
   crone:output_1
system:playback_2
   crone:output_2
system:playback_3
system:playback_4
system:playback_5
system:playback_6
system:playback_7
system:playback_8
system:playback_9
system:playback_10
system:playback_11
system:playback_12
system:playback_13
system:playback_14
system:playback_15
system:playback_16
crone:input_1
   system:capture_1
crone:input_2
   system:capture_2
crone:input_3
   softcut:output_1
crone:input_4
   softcut:output_2
crone:input_5
   SuperCollider:out_1
crone:input_6
   SuperCollider:out_2
crone:output_1
   system:playback_1
crone:output_2
   system:playback_2
crone:output_3
   softcut:input_1
crone:output_4
   softcut:input_2
crone:output_5
   SuperCollider:in_1
crone:output_6
   SuperCollider:in_2
softcut:input_1
   crone:output_3
softcut:input_2
   crone:output_4
softcut:output_1
   crone:input_3
softcut:output_2
   crone:input_4
SuperCollider:in_1
   crone:output_5
SuperCollider:in_2
   crone:output_6
SuperCollider:out_1
   crone:input_5
SuperCollider:out_2
   crone:input_6

It’s a lot more output, but to summarize: crone still starts up and maps the first two device inputs and the first two device outputs :smiley:

So I started Awake, made sure the output was set to audio and… it works!

The output levels were really hot, so they probably need some trimming, and I found some settings in /etc/systemd/system/norns-init.service that may indicate a way of configuring this (though that assumes some similarity between the ES8 and the Norns soundcard).

Inputs seem to work as well, but I noticed a lot of lag but I suspect that was due to how the input was being processed… I know almost nothing about tape/softcut and the internal routing at the moment.

Next steps I think will be to modify Crone so that it creates a set of jack clients* and makes them available to Supercollider to noodle around with it some more, then if that all still works look at expanding the crone mixer to include the extra I/O. Ideally, Crone would be able to detect the device type, and if it’s an ES-8 then configure the jack environment accordingly.

Looking at the command running scsynth I think I’ll need to modify that as well, it appears it’s passed some flags setting input and output ports to 2 each

scsynth -u 57110 -a 1024 -i 2 -o 2 -b 1026 -R 0 -C 0 -l 1

*is Client the right term here? Need to do some more reading.

13 Likes

good god. just when i thought i was gonna sell my es-8 and replace it with two crows. as usual the answer is “keep all of it”

3 Likes

do you want to use all es-8 io for audio IO?

I’ve been using an ES-8 a lot with Organelle/Orac/PD for a while now, and I’ve found the setup I lean towards is 2 audio in/out and then the rest for (audio rate) CV IO.
I’d guess this should work ‘out of the box’ - no? id assume SC has access to the additional ports as normal.

but yeah, the ES-8 is a lot of fun with these kind of setups.


(*) my main outputs come from eurorack, so the 2 audio io allow for a simple send/return setup.

To be honest I’ve not gotten so far as to think about the end configuration, but my default stance is that “everything is audio rate”.

Currently my blocker is figuring out where scsynth is called. I’ve searched the norns codebase and only found crone.sh which has the commented out line:
# scsynth -u 57122 -i 2 -o 2

Looks like the same command I see running… but seems like it’s started somewhere else now.

I noodled about with getting sc to output to the additional I/O on the ES-8 in maiden last night, but had no success and I still suspect the -i 2 -o 2 above has something to do with that, unless my approach was just plain wrong: something like this:

Out.ar(0, SinOsc(440) * 0.5);

and adjusting the output index to see if things started happening on the es8

sure… my point was, isn’t crone only interested in audio?
does it need to get involved with audio rate modulation? can’t SC just get this directly?

yes, the script are just for debugging. sclang and other process are handled by systemd:
[ https://github.com/monome/norns-image/blob/master/config/norns-sclang.service ]

the server itself is booted from the Crone class in sclang. (this is a pretty normal way to start a server process in supercollider.) we use the default I/O bus count. (which is 2 in, 2 out.) if you want to explicitly set that or other options i would do it with a ServerOptions here:
[ https://github.com/monome/norns/blob/master/sc/core/Crone.sc#L68 ]

something like

Server.scsynth;
server = Server.local;
server.options.numOutputBusChannels = 8;
//.... etc
server.waitForBoot { Crone.finishBoot; };

you could also launch scsynth explicitly from another service and set the Crone.useRemoteServer classvar. but i doubt there’s any need or benefit to doing this and it is more complicated.

i would not get into modifying the mixer client, but up to you. (for what it’s worth, bus channel count is a template specialization, softcut should actually work fine for manipulating DC buffers, &c.)

1 Like

Had to put this aside for a week while I travelled for work, but managed to find time to check out more of the code and the SC docs to get an idea of where to go next.

First is the good news, the changes to book scsynth and connect it to the remaining ES-8 I/O is indeed quite simple! See the diff here: https://github.com/monome/norns/compare/master...moogah:jefarr_es8_support

Now I’m looking into the best method for adding this code without creating something that just crashes if I’ve not connected a powered on ES-8 before I boot Norns.

Unfortunately jack doesn’t seem to expose much info about devices to a Client, or at least the scsynth Server and ServerOptions classes don’t have methods to access this. So I’ll need to maybe get matron aware of this info and send in flags to crone engine so it’ll start up accordingly… and this is probably good, because I also need to figure out how to manage the systemd stuff.

The end UX I think will be some extra options in System -> Devices that will reboot the appropriate services in “es8 mode”. This may be wonky still, since currently jack doesn’t appear to know about the existence of the ES-8 without first restarting the service.

After playing around a bit, I do think that connecting the extra I/O directly to SC makes more sense than trying to extend Crone.

because the jack server connects to a single specific device when it is launched - in this case by systemd - and that is the device available to clients when they are created / connected. device discovery is totally separate (in fact i believe jack relies on the driver backend for this - viz., ALSA.)

you already found the correct invocation to pick your soundcard when starting jack. so, edit your norns-jack.service appropriately. presently, there is no convenient way to change this on the fly and still use systemd to manage norns process dependencies. if we wanted to extend this, i would use an environment variable for the device name, change the var, and restart the service.

but of course, in developing norns stack we were (/are) not generally worried about supporting dynamic audio device changes.

sure it does. the jack API function is jack_get_ports(). see the API docs. this returns a NULL-terminated lists of ports matching specified flags.

for example to get physical ADC ports, as we do in crone::Client::connectADCPorts():

 const char **ports = jack_get_ports (client, nullptr, nullptr,
                                                 JackPortIsPhysical | JackPortIsOutput);

(potential point of confusion: JackPortIsOutput means the port is a source - viz, an ADC - and JackPortIsInput means it is a sink (DAC)).

and then loop over ports until you see a NULL. (we don’t check system port count at present in crone because we are only asking for stereo I/O.)

and if you want, here is a tiny standalone C program that reports ADC and DAC channel counts.

compile with -ljack

#include <jack/jack.h>
#include <stdio.h>

int count_ports(const char **ports) {
    int count=0;
    const char *port;
    do {
	port = ports[count];
	if (port == NULL) { break; }
	count++;
    } while (1);
    return count;
}

int main() {
    jack_status_t status;
    jack_client_t *client = jack_client_open("counter", JackNullOption, &status, NULL);
    if (client == NULL) { return 1; }
    
    const char **adc_ports = jack_get_ports (client, NULL, NULL,
					     JackPortIsPhysical|JackPortIsOutput);

    const char **dac_ports = jack_get_ports (client, NULL, NULL,
					     JackPortIsPhysical|JackPortIsInput);

    printf("ADC ports: %d\n", count_ports(adc_ports));
    printf("DAC ports: %d\n", count_ports(dac_ports));
    
    jack_client_close(client);
	    
    return 0;
}

or at least the scsynth Server and ServerOptions classes don’t have methods to access this

that is true on linux (oddly.) i guess the expectation is that linux users will manage things from the environment. there are SC_JACK_DEFAULT_INPUTS and SC_JACK_DEFAULT_OUTPUTS variables for example. we override these in Crone.sc with the hacky shell commands you found b/c we don’t want SC to connect to physical ports by default.

i guess linking to jack is an option, but in general we don’t want to set up a dependency like that. you can always use the quick and dirty method of capturing jack_lsb and searching for system:capture and system:playback patterns as you’ve already done.

but what i’d recommend as the cleanest method would be to extend crone process with an explicit hardware port count. matron and sclang and whatever can then query that via OSC.

I like the idea of using the I/O count in crone and OSC to query that info, I’m new to OSC and still adjusting my brain to see it as a networked protocol and maybe it helps with my current pursuit:

It looks maybe possible to extend device_monitor in matron to detect a usb soundcard being plugged in?

I think this gets much closer to having norns react to the card when it’s present and then being able to reboot systems that need it.

i guess that’s possible.

to be clear, i’m suggesting things that mght help you with your use case, but i don’t think any of this is going in upstream norns. it is too much functional fragmentation. norns scripts and engines are supposed to be shareable, and I think accomodating a feature like this in a general sense (“use any audio device with any script, any engine”) will have a lot of ramifications.

(for starters: let’s say you want to hotplug audio device and use the USB monitor module as you suggested. As it is, matron process actually has to reset itself and wait again on audio processea so it can do IPC handshakes. Now guess what, lots of midi adapters present themselves as aufio-class devices, libusb can’t tell the difference …)

3.0 should be more amenable to just treating the system as a launcher for whatever SC stuff.

1 Like

Sorry if I’m being dense, but does any of the above indicate that the built-in I/O can be used concurrently with external devices? SuperCollider with CV I/O makes me salivate, but I don’t want these nice converters to go to waste.

Also, running the above, I get the following error while Why? plonks along in the background, so I’m thoroughly confused. Maybe I just need to power cycle.

~ $ jack_lsp -c
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
JACK server not running

No. It’s theoretically possible with hacks that wrap alsa sessions as jack ports. This is a can of worms.

Not sure what’s up with that jack_lsp result. One stupid question is, are you sure you’re shelled into norns there? (Doesn’t look like norns prompt iirc?)

1 Like

Yikes. This sounds like that dbus thing I was reading about back when I first set up jack in Arch.

Yep, I’m shelled in alright, I just removed the IP address before the ~.