Simple visual sequencer example?


#1

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!


#2

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");

#3

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.


#4

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!


#5

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


#6

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


#7

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


#8

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


#9

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


#10

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


#11

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

-enapos