Simple visual sequencer example?

Your advice is correct for an Array.

a is a LinkedList. Check the add implementation for LinkedList – it always returns the receiver. It is categorically impossible to have the Array:add problem with this class.

hjh

yay for consistency :slight_smile:

I ended up subclassing the LinkedList class and added my own insertAt() method which squeezes in the node at a given index.

I worked some more on the sequencer and was hoping to achieve something really fast and responsive, but for the current state of the project, it is not quite there yet… I think the RealtimeEventStreamPlayer class could maybe be optimised in terms of time-complexity.

In any case, I am linking my project here once again, so if anybody’s interested in it (and if not for my sequencer, maybe just for the RealtimeEventStreamPlayer class), please feel free to collab :slight_smile:

Once again thnx @jamshark70 for your contribution.

TBH I can’t think of a good reason why LinkedList doesn’t have this. It would be worth filing a feature request (or better, a pull request with the code).

I worked some more on the sequencer and was hoping to achieve something really fast and responsive, but for the current state of the project, it is not quite there yet…

I’m curious where the performance problem is. The graphics? I’d expect insertion and deletion not to be a significant drag, unless you’re inserting/deleting notes hundreds of times per second.

One could argue that Array should always have been a private class, and we should always have been using List for everything. Array’s add behavior of returning either itself (if it already has room for the new item) or a new array (if it doesn’t) is arguably an internal implementation detail that should be hidden from the user.

But [x, y, z] produces an instance of the implementation class, exposing the flaw (which I think is limited to Array and subclasses of RawArray).

Then List, by being underused, suffers from an incomplete implementation, e.g.

List.fill(512, { 0.1.rand2 }).plot;

ERROR: Message 'plot' not understood.

// or worse:
List[1, 2] + 1
-> [ 2, 3 ]  // an array again???

:man_facepalming:

Point being, if the argument should be toward consistency, then I’d suggest standardizing on those classes where add always returns the receiver, and not on Array.

hjh

One of the problems is that the sequencer is now sometimes waiting a full bar before it plays a newly inserted note.

Can’t really figure out why. I kept your ‘next bar calculation code’ based on the last note’s delta (the one which would exceed current bar + patternlength based on that note’s delta value).

I did tweak your code a little bit to incorporate rests in between the notes, this might be the reason.

What’s your server latency (really, OSC messaging latency)?

The default latency, 200 ms, is rather long. In my experience on stage is that it’s very easy to hit a command or a controller key a split-second too late, in the gap between “language already processed the event” and “sound comes out of the server.” So you think you hit it on time, when in reality, from the language’s point of view, the time you were trying to hit is already in the past.

It would theoretically be possible for the note-insertion code to detect that you’re asking for it within this latency gap, and produce an event “right now” with an adjusted latency value. That is, if you wanted to insert a note at beat 2.0, and sclang clock time is 2.05, you could do (... event stuff..., latency: s.latency - (clock.tempo * (clock.beats - insertionTime))).play (untested, I might have gotten something wrong). I’ve never tried this in my own framework, though.

If you’re running on a single machine, you can safely drop the latency to 50-70 ms: s.latency = 0.07. Latency has to account for transmission time through the network protocol (on one machine, this is negligible – I have some “ping” code in my ddwOSCSyncClocks quark, which measures one-way time as 0.29 ms) and the audio driver’s buffer size, and allow a little time for language-side processing. < 50 ms may be OK too, if you’re running the audio with a small buffer.

hjh

It was on the default so I guess 200ms. I changed it to 70ms and I have the feeling it’s running smoother already :slight_smile:

About the issue discussed above, I really have the feeling I did not have this issue before I introduced the inter-note rests…

Could you check if I am doing something obviously wrong in the updated loop?

// can't just Pseq because we need to account for insertions
		this.pattern = Prout { |inval|

			//Starting variables
			var item, node,
			clock = TempoClock,
			barline = clock.nextBar,
			nextTime, delta, restTime, endTime;
			baseBarBeat = barline;

			//start with the first node in the sequence
			node = sequence.nodeAt(0);
			while {
				//While we have items in the sequence do this loop (infinite)
				item = node.tryPerform(\obj);
				item.notNil
			} {
				//Our next event's start-time
				nextTime = barline + item[\time];

				//if this is bigger then current beats this means we are in the start of our loop ->
				//so we yield silence till the first event starts
				if(clock.beats < nextTime) {
					inval = Event.silent(nextTime - clock.beats).yield;
				};

				// now we arrived at the event
				//calculate the end time of the event
				endTime = item[\time] + item[\sustain];

				// calculate the "rest" time after this node.
				// if it's last in sequence we substract it with the 'next-bar' time a.k.a. sequence length
				if(node.next.notNil) {
					restTime = node.next.obj[\time] - endTime;
					delta =  node.next.obj[\time] - item[\time]; //used for next-barline calculation
				} {
					restTime = patternDur - endTime;
					delta =  patternDur - item[\time]; //used for next-barline calculation
				};

				//we need to update the 'next-bar' variable once we will reach the end of the current bar-sequence
				if(clock.beats + delta - barline >= patternDur) {
					barline = barline + patternDur;
					baseBarBeat = barline;
				};

				//we prepare our Event
				if( midiOut.notNil, {
					inval = item.copy.put(\type, \midi).put(\midiout, midiOut).put(\channel, 1).put(\midinote, 0);
				},{
					inval = item.copy.put(\type, \note).put(\instrument, synthDef).put(\note, 0);
				});

				//whenever there is a crossover with notes (multi-voiced) we need to calculate the duration and legato
				if( node.next.notNil and: {  node.next.obj[\time] <  endTime } , {

					var dur, legato;
					dur = node.next.obj[\time] - item[\time];
					legato = item[\sustain] / dur;

					//simply play the note until start of the next note - the legato will take care of the crossover part
					inval = inval.copy.put(\dur, dur).put(\legato, legato).yield;

				}, {
					//smoothly lined up with rests so we simply take the note's duration as \dur argument
					inval = inval.copy.put(\dur, item[\sustain]).yield;

					//we also need to play the silent event after the note is finished until reaching to the next one
					inval =  Event.silent( restTime ).yield;
				});

				//continue the loop
				node = node.next;
				if(node.isNil) { node = sequence.nodeAt(0) };
			}
		};

		this.stream = pattern.asStream;

Thnx a million!

You shouldn’t need to have any logic for “rests between notes.”

If you need a quarter note on beat 1, quarter rest on beat 2, and quarter note on beat 3, then I would store in the list:

(time: 0, dur: 1, note: …)
(time: 2, dur: 1, note: …)

So the delta between the two notes would be a half note, but both notes would play for only a quarter note, and implicitly you get a rest in between them. The key is “implicitly” – you don’t actually have to do anything for a rest! (As is often the case in computer science, overcomplicating a problem introduces bugs.)

The one exception is at the beginning of a bar. But in that case, I would do nothing special with the data structure except have a Rest() object for the note.

hjh

Ah of course!

Why am I always complicating things… :frowning:

Simply doing

inval = inval.copy.put(\dur, delta, \sustain, item[\sustain]).yield;

takes care of every situation.


I figured out the issue with why “sometimes” a new note has to wait a full bar to play. This scenario only happens when:

  • the current timer (clock.beats) has past the start-time of the current last note in the sequence
  • one inserts a new note after the current last note in the sequence

this is an edge-case as the next-bar is already incremented in the pattern-loop, since we surpassed the current (last) note’s delta over the pattern-length.

And the rescheduling function reschedules the next player to start at the newly inserted note (based on the ‘already’ incremented next-bar variable).

@jamshark70 currently fixed this issue by inserting the following line in the rescheduling method:

	//we search for the next node to play in the sequence given the current phase
	nextNodeToPlay = sequence.detect { |item| item[\time] >= phaseNow };


	//if nextNodeToPlay is last in sequence and we already past the previous last note...
	//we need to reset the barline to the current one
	if( nextNodeToPlay == sequence.last and: { phaseNow > sequence.findNodeOfObj(sequence.last).prev.obj[\time]  }, {
		barline = barline - patternDur;
	});


	//if we found one we take its time-value as a starting-point to schedule our next player
	if(nextNodeToPlay.notNil) {
		reschedTime = barline + nextNodeToPlay[\time];
	} {
	//if not we just take the next bar
		reschedTime = (player.clock.beats - barline).roundUp(patternDur);
	}; 

I don’t know if this is good - it feels kind of backwards resetting the counter back like this, but for now I can’t find another way :slight_smile: , In any case this seems to fix the problem.

Or we could simply calculate the barline at the beginning of the Prout-loop like this:

barline = (clock.beats - (clock.beats % patternDur));

And never do the increment manually… :face_with_monocle:

I’m afraid I haven’t had much bandwidth lately for this thread. I think if it’s working now, probably should just leave it at that.

It was fun to write up these techniques, though :wink:

hjh

Hehe sounds good.

In any case it’s working quite well now.

I’ll work further on the project and let you know what I end up with :slight_smile: Thnx for the help and feedback!

-e

Hi @jamshark70 ,

I’ve been working on the project some more and everything is going great. There is one issue though, that I would love to get resolved:

I want to be able to stop/start the player (and make it ‘reset’ to time zero, after it is stopped). Currently the scheduling algorithm is working on the TempoClock default instance (or currentThread.clock), and as you know the player is based on the clock.beats to schedule the notes. I tried to replace the clock in your original code with a TempoClock instance, but whenever insert more than one note, everything get’s shifted in time, so the reassigning of player-part is not functioning correctly I believe:

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

Do you have any idea how to get this working with a TempoClock instance?
You can test it out by running code below (your original code but with a TempoClock instance, and then run the latest two lines consecutively)

(
var patternDur = 4,
baseBarBeat;

~myClock = TempoClock();

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 = ~myClock,
	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(~myClock, doReset: true);
~insertNote.(1.75, 8);
~insertNote.(1.33, 8);
  1. If you’re going to create TempoClocks frequently, make sure to .stop them. Each TempoClock makes a new C++ thread in the backend. If you never stop them, then you keep accumulating threads.

  2. TempoClock.default is a TempoClock instance! So there is really no functional difference, except that you’re creating a new clock during initialization instead of using a shared instance.

  3. I tried your code example and I don’t see any incorrect behavior.

hjh

Thnx James for the tips,
I’m still figuring out what went wrong beforehand, but it seems to be working now. Sometimes I was getting a

ERROR: clock is not running.
ERROR: Primitive '_TempoClock_Beats' failed.

or

ERROR: clock is not running.
ERROR: Primitive '_TempoClock_SchedAbs' failed.

error inside of the rescheduling method. I fixed this by removing the 'clock = player.clock" line, and always using the instance variable ‘clock’ of my RealTimeEventStreamPlayer class instance instead of the locally assigned player.clock one. I don’t know why that made a difference as a clock does not stop when a player that uses that clock has been stopped, right?

In any case it’s working now - I’ll upload a video about the project soon :slight_smile:
thnx!

I fixed this by removing the 'clock = player.clock" line, and always using the instance variable ‘clock’ of my RealTimeEventStreamPlayer class instance instead of the locally assigned player.clock one.

clock (instance variable) and player.clock should always be the same, correct? If it makes a difference to remove the reference to player.clock, then it means that the sequencer’s clock and the player’s clock got out of sync. So some variable is being set incorrectly, somewhere.

That might also explain the strange time offsets.

hjh