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