Simple visual sequencer example?

Hi There,

I want to make a visual sequencer (piano roll-like), which schedules events (represented by GUI elements) on a running clock. I am used to work with Pbinds, but this time I want instant changes in the GUI to be recognised by the time-indicator. So I believe I will have to schedule the events myself in real-time.

Are there any very simple / basic examples of this? I was looking at the code of LNX Studio but I was hoping for more basic examples (as LNX is quite a big project with lot’s of code).

Thank you!

This project is a little more straightforward and lightweight that LNX:

I notices a few minor UI problems, but otherwise it seemed to work just fine. The code looks relatively clean, so I imagine it would be possible to extend this further - and, I was able to immediately get it to produce a Pattern that I was able to feed directly in to a Synth I had sitting around.

You can install by running:

Quarks.install("https://github.com/defaultxr/PianoRoll.git");
Quarks.install("https://github.com/defaultxr/Sequence");
Quarks.install("https://github.com/defaultxr/Keymap");
1 Like

Hi scztt,

Thank you for this lovely quark! will help me a lot - especially in terms of GUI coding.

Unfortunately I was hoping to find something more “real-time” as in: a dynamic event-list which is constantly being compared to the current play-position of the sequencer.

The PianRoll class you mentioned here is building a Pattern of the drawn events after every bar, so if you change things in an instant before the play-position it won’t be reflected - it will play the state drawn in the previous bar. I hope that makes sense.

Speaking from experience, an event scheduler that updates in real-time as event times change is difficult to build (both in SuperCollider, and generally). Let me sketch out what this might look like:

  1. You need a way to track all your events, in time order. You can use the Order class to do this - importantly, Order gives you the ability to do order.nextSlotFor(15) to get the next event index AFTER time=15. This quark wraps Order in some Event-specific functionality, which might be useful: https://github.com/scztt/OSequence
  2. You need a way to produce events directly from your Order. I’m doing this in OSequence:embedInStream - it works, but I only added that recently so I’m not sure if I’m covering all the edge cases correctly.
  3. You’ll need a way to fast-forward your event stream in #2 to an arbitrary point in time. This can be done easily with Order or directly on the stream using Stream:fastForward, but it’s not efficient for large numbers of notes - something to be aware of.
  4. When modify your sequence, you need to immediately start playback over again at the current point in time, but with the updated sequence data. I don’t THINK you need to restart the clock you’re using for playback… I think it would be enough to have some logic like this in your embedInStream routine:
if (sequenceHasChanged) {
     restartStreamFromTime(currentTime)
}

You can set your sequenceHasChanged property from the outside, and use this to trigger re-setup of the iteration through values.

  1. Finally, since you probably don’t want to wait for the next note in your sequence to update everything, you’ll have to manually schedule some kind of stream.next() a small amount of time after you changed the sequence (e.g. 0.1 seconds in the future). If you’ve triggered a reset as I mentioned in #4, your scheduled event will simply cause the sequence iteration to reset.

So, not that easy, but it is possible :). You’ll have efficiency problems because there’s no class in SC that allows you to efficiently jump to an arbitrary index (point in time) without iterating through everything in the list - but this really won’t be a problem unless you’re dealing with many many notes.

Good luck! Please report back if you make any progress or have any questions!

1 Like

Wow,

Didn’t think I would have to take this many things into account :slight_smile:
Thank you very much for all the info!

Will get back to you on this.

Cheers,
e

I can think of an alternative to restarting the sequence from a time point. But the details will be a bit tricky and I don’t have time just now to work them out. But it’s interesting – I’ll try to do it later.

Briefly – you need a stream to produce events with the right deltas (“inter-onset intervals”), and there’s an EventStreamPlayer to pull out these events and schedule the next event for the right time.

If you insert or delete a note, it might be a different point in the bar (in which case, nothing special needs to happen). But e.g. if you just played beat 1 in the bar, and the next note had been scheduled for beat 2, but you inserted a note at 1.5, then the stream player needs to wake up at 1.5 instead of 2.

That can actually be done according to this sketch:

~stream = (the_pattern).asStream;
~player = EventStreamPlayer(~stream);

// when you need to reschedule:
// 1. drop the old player
~player.stop;
// 2. make a new player
~player = EventStreamPlayer(~stream);
// 3. schedule it directly for the right time
clock.schedAbs(the_right_next_time, ~player);

Technically, the old player is still on the clock, but the link to ~stream is broken and it will be discarded (at the old wake-up time). The new player will pick up exactly where ~stream left off.

The tricky bits are the data format, and figuring out what is the “right next time.” Maybe I can play with that later today, but I can’t just now.

hjh

Hi James,

Very interesting!

I’m trying to understand the part “the new player will pick up where the ~stream left of”. Do you mean the .next() value of the stream will be persistent when assigning it to a new player? But we also need to change the pattern itself right, and thus the stream?

Anyway, I’m very very curious to see the code :slight_smile:

Cheers,
J

I have to apologize – it turned out to be more difficult than I expected, and, because of work demands, I don’t have time in the near future to finish a working example. I did try, but the details are too tricky for my available time.

What I mentioned about replacing the stream player, for rescheduling, is easy. The hard part is managing and streaming out the data.

hjh

My primary interest is in real-time sonification, which has related issues. I’m also less concerned with sample accuracy, or metric tempo did that makes things easier for me.

My solution had been to use SC for the synthesis, and python for the control signals. Running a mini framework (derived from Eli Fieldsteel’s example), with python sending OSC messages to init synths, start pbinds, etc. I find it a LOT easier to do things like threading and queue management, (not to mention communication with external APIs is, data analysis, etc) in python

A-ha… I got something.

I was struggling with navigating through an array of notes and changing the array at the same time, without losing place:

  • If, for instance, you’re at index 5 now, and you insert something at an earlier index (say, 2), then the current note has moved to index 6 and the next note will be index 7.
  • Similarly, if you’re at index 5 now, and you delete something at index 2, then the next note will be index 5.

So it seemed like there would have to be some interaction between the insertion/deletion functions and the internal state of the stream. That’s messy and dangerous.

So then I was thinking about data structures where you can step forward and backward without worrying about indices – a linked list. This makes it much easier. The event stream that’s playing simply steps forward through the list. Inserting or deleting notes simply changes the links of the preceding and following notes (which can be done without any knowledge of the player’s state).

This code is a demo only. It is not properly encapsulated – so you have to do some work to be able to run several of them at the same time, safely. But the principle is sound.

(
var patternDur = 4,
baseBarBeat;

a = LinkedList.new;

[
	(degree: 0, time: 0),
	(degree: 1, time: 1),
	(degree: 2, time: 2),
	(degree: 3, time: 3)
].do { |item| a.add(item) };

// can't just Pseq because we need to account for insertions
~pattern = Prout { |inval|
	var item, node,
	clock = thisThread.clock,
	barline = clock.nextBar,
	nextTime, delta;
	// the rescheduling function needs to be aware
	// of the most recent phrase boundary for this thread
	baseBarBeat = barline;
	// nodeAt is "private" but we need access to the chain
	node = a.nodeAt(0);
	while {
		item = node.tryPerform(\obj);
		item.notNil
	} {
		// wait until next time
		nextTime = barline + item[\time];
		if(clock.beats < nextTime) {
			inval = Event.silent(nextTime - clock.beats).debug("rest").yield;
		};
		[clock.beats, item].debug("got item");
		// now update counters and do this one
		if(node.next.notNil) {
			delta = node.next.obj[\time] - item[\time];
		} {
			delta = patternDur - item[\time];
		};
		if(clock.beats + delta - barline >= patternDur) {
			barline = barline + patternDur;
			baseBarBeat = barline;
		};
		inval = item.copy.put(\dur, delta).yield;
		node = node.next;
		if(node.isNil) { node = a.nodeAt(0) };  // loop
	}
};

~stream = ~pattern.asStream;

// functions to change sequence
~reschedule = { |time|
	var phaseNow, reschedTime, nextToPlay, clock;
	phaseNow = (p.clock.beats - baseBarBeat) % patternDur;
	phaseNow.debug("phaseNow");
	nextToPlay = a.detect { |item| item[\time] >= phaseNow };
	nextToPlay.debug("nextToPlay");
	if(nextToPlay.notNil) {
		reschedTime = baseBarBeat + nextToPlay[\time];
	} {
		// next "pattern barline":
		reschedTime = (p.clock.beats - baseBarBeat).roundUp(patternDur);
	};
	reschedTime.debug("reschedTime");

	// swap out stream players
	clock = p.clock;
	p.stop;
	p = EventStreamPlayer(~stream);
	clock.schedAbs(reschedTime, p.refresh);
};

~insertNote = { |time, degree|
	var node, new;
	// search for place to insert
	node = a.nodeAt(0);
	while {
		node.notNil and: { node.obj[\time] < time }
	} {
		node = node.next;
	};
	new = LinkedListNode((degree: degree, time: time));
	if(node.notNil) {
		// change A --> C into A --> B --> C; B = new; C = node
		new.prev = node.prev;  // B <-- A
		node.prev.next = new;  // A --> B
		new.next = node;  // B --> C
		node.prev = new;  // C <-- B
	} {
		new.prev = a.last;  // add at the end
		a.last.next = new;
	};

	~reschedule.(time);
};

~deleteNote = { |time, degree|
	var node, next;
	// search for node to delete
	node = a.nodeAt(0);
	while {
		node.notNil and: {
			node.obj[\time] != time and: { node.obj[\degree] != degree }
		}
	} {
		node = node.next;
	};
	if(node.notNil) {
		next = node.next;
		next.prev = node.prev;
		node.prev.next = next;
	};
	// not really necessary to reschedule
	// the Prout will add rests automatically
	// if the next note to play was deleted
};
)

p = EventStreamPlayer(~stream).play(doReset: true);

// hit this during beat 2
~insertNote.(1.75, 8);

got item: [ 1420.0, ( 'degree': 0, 'time': 0 ) ]
got item: [ 1421.0, ( 'degree': 1, 'time': 1 ) ]
phaseNow: 1.292918184
nextToPlay: ( 'degree': 8, 'time': 1.75 )
reschedTime: 1421.75
-> a TempoClock
	// YES, plays at the right time
got item: [ 1421.75, ( 'degree': 8, 'time': 1.75 ) ]
got item: [ 1422.0, ( 'degree': 2, 'time': 2 ) ]

// hit this during beat 2
~deleteNote.(1.75, 8);

got item: [ 1428.0, ( 'degree': 0, 'time': 0 ) ]
got item: [ 1429.0, ( 'degree': 1, 'time': 1 ) ]
-> a LinkedListNode
	// thread wakes up at 1429.75,
	// and skips over the deleted note
rest: ( 'dur': Rest(0.25), 'delta': 0.25 )
got item: [ 1430.0, ( 'degree': 2, 'time': 2 ) ]

p.stop;

hjh

James, this is quite amazing!
Can’t wait to use this in my project.
Thnx a million. <3

-enapos

Hi James,

Thank you so much for the effort in researching my ‘real-time’ sequencer question.
I’m still a bit puzzled by the implementation still - I’m not so familiar with Clock and its internal mechanics, so it’s a bit hard for me to figure out what’s going on exactly.

I made a class out of your code and connected it to my visual sequencer and it is working half of the time. Sometimes when I enter a note it gives me the error:

WARNING: ‘next’ exists a method name, so you can’t use it as pseudo-method.

Any idea why this would be happening?
If you ever have the time to test out my code - it would help me a lot.

Thnx a million!

I see only a license and a readme in your repository. After committing more files, you have to git push again.

TBH I probably don’t have time to play around with a big chunk of code.

The warning: if you’re using an Event as an object, and you do e.next = { ... } then you might think you have defined an alternate next on the event. You haven’t. next is actually defined for all objects already, and there is no way to override that at runtime. So later, when you call e.next, it will call the class library method instead of your function.

The only real “internal” about clocks that you need to understand is that there is no way to remove something from a clock once you schedule it. So, how do you stop a routine – what does r.stop really do? After r.stop, the routine is still in the clock’s queue. The difference is, r.stop sets the routine’s status so that, the next time it wakes up, it will just do nothing and yield nil. The effect from the user’s point of view is: you stop the routine, and it doesn’t do anything after that – but the reality is that it does do one more thing – it wakes up and checks the status, and then decides not to do anything.

The upshot is that if r is already scheduled and you schedule it again, then it’s scheduled twice. That’s very bad. So you can’t reuse the same scheduled object.

	// swap out stream players
	clock = p.clock;
	p.stop;
	p = EventStreamPlayer(~stream);
	clock.schedAbs(reschedTime, p.refresh);

In this block of code, p is the currently active EventStreamPlayer. p.stop sets its status to have nothing to do next time – it will still wake up, but at that time, it will find that it’s done. Then p = EventStreamPlayer(~stream); makes an all-new stream player, based on the old stream (to pick up seamlessly where the other stream player left off) – and then this is scheduled for the new time. So, for that brief moment, there are two stream players on the clock:

  • old time: old player (which is already dead, but doesn’t know it yet)
  • new time: new player

And what the user sees is: nothing happens at the old time, and the right thing happens at the new time.

hjh

Ok I understand it better already :slight_smile:
I’ll dive deeper into it.

By the way: the code was on the /develop branch, not on /master, I should’ve mentioned this. I merged /master into /develop so you should see everything now on /master as well.

It’s not that much code, but I understand that time is precious :slight_smile:

Thnx,
-enapos

Hi James,

I know exactly when the errors occurs now. It happens when you insert a note at the end of the list.

The following error:

WARNING: ‘next’ exists a method name, so you can’t use it as pseudo-method.

happens when you insert the note at the end in your original example…
You can test this out by changing the line in your code

to

Any idea why? I have tried to analyse and solve this but couldn’t figure it out.
Help much appreciated as always.

-&apos

Ok I fixed the warning by replacing

sequence.last

with

sequence.findNodeOfObj(sequence.last)

in the insertNote() method.

last() is getting the LinkedListNode’s object instead of the node itself.

1 Like

Yeah, I had just spotted that. Sorry I missed it earlier.

findNodeOfObj for the last one will be very slow (scan the whole list). I’d suggest sequence.slotAt(\tail) – ugly hack but goes directly to the right place.

			node = sequence.slotAt(\tail);
			new.prev = node;  // add at the end
			node.next = new;

hjh

Thx James,

Although that did fix the error, there is another problem:

Adding notes by linking new LinkedListNodes into the sequence by means of pointing the new node’s prev and next pointers to the corresponding prev and next nodes etc. does not add that new LinkedListNode to the LinkedList.
Whenever I call .last() it is only looking at the last node in the list added with the provided setter methods (which happens to be the very first node, inserted at initialisation).

The LinkedList class does not really provide an insert method other than .put() but this will overwrite the object at the index argument instead of squeezing it in at this index.

So do you know if there is a way to link new nodes together and also add them to the list? Or do you think I should simply write my own LinkedList class :stuck_out_tongue:.

Or maybe I’m missing something obvious…

Thnx!

Yeah, unfortunately, LinkedList’s interface is lacking in this regard.

I think the easiest fix for that is to use the normal add method. I should have thought of that, but didn’t.

// Old way, remove this:
	} {
		new.prev = a.last;  // add at the end
		a.last.next = new;
	};

// New way, try this:
	} {
		a.add(new.obj);
	};

hjh

If I’m not mistaken, it’s safer to assign the result of add to a again.
At least with lists, not assigning the result of an add operation can yield very hard to debug problems where the list sometimes is and sometimes isn’t updated.

a = a.add(new.obj);