Converting a Pbind with 'phrase' events into a NRT Score

Hi, I’m trying to convert a Pbind that uses the ‘phrase’ type into a NRT Score, using the .asScore method, but I can’t get it to work. Here’s a simple example that produces an error:

(
Pdef(\phraze, { arg sustain = 1;
    Pbind(
        \degree, Pseq(~degree),
        \dur, sustain.value / ~degree.size.max(1),
    )
});

x = Pfindur(2, Pbind(
    \type, \phrase,
    \instrument, \phraze,
    \degree, [0, 2, 4, 6],
    \legato, 1
)).asScore(2, timeOffset: 0.001);
)

Is there a way to do this with the .asScore method? Or perhaps there is another way? I’m still fairly new to NRT, so maybe I’m missing something obvious…

To be honest, I don’t see an obvious solution right away.

.asScore assumes that it will be able to retrieve all of the events from the event stream by .next.

The \phrase event type spawns a new thread for every subphrase. This thread’s data are not accessible to the ScoreStreamPlayer that is collecting the events.

The only thing I can think of is to convert the phrases into .seq or .par invocations in Pspawner (which unfortunately requires changes to your code – I can’t think of a way to do it transparently).

hjh

Ah, I was afraid that might be the case. Thanks for responding, James! Pspawner is a good suggestion. I might also try to make an event type that is like ‘phrase’ but embeds the subpatterns instead of spawning new threads :thinking:

I just remembered, there’s a ScoreClock quark that might handle your case. But I didn’t try it for myself.

hjh

ScoreClock seems to be broken, but I was able to get it working by making a few minor changes (change every instance of server.serverRunning_ to server.statusWatcher.serverRunning_). This is a known issue, but it was filed 5 years ago with no response, so I think the repository might be abandoned. I’ll file a bug fix anyway and hopefully get it merged. It’s really a brilliant and elegant quark, and maybe could even be the canonical way of generating a Score because it can also be used on Routines or anywhere else a clock is used to schedule OSC messages.

I also came up with another solution that works specifically for ‘phrase’ Pbinds. This overwrites ScoreStreamPlayer.makeScore and inserts a special case for the ‘phrase’ type, basically copying the code from the ‘phrase’ eventType function, but instead of playing the subphrases in a new thread, it converts them to Scores and merges them with the outer Score:

+ ScoreStreamPlayer {
    makeScore { | stream, duration = 1, event, timeOffset = 0 |
		var ev, startTime, proto;

		proto = (
			server: this,

			schedBundle: { | lag, offset, server ... bundle |
				this.add(offset * tempo + lag + beats, bundle)
			},
			schedBundleArray: { | lag, offset, server, bundle |
				this.add(offset * tempo + lag + beats, bundle)
			}
		);

		event = event ?? { Event.default };
		event = event.copy.putAll(proto);
		beats = timeOffset;
		tempo = 1;
		bundleList = [];
		maxTime = timeOffset + duration;

		// call from a routine so that the stream has this as clock
		Routine {
			thisThread.clock = this;
			while {
                thisThread.beats = beats;
				ev = stream.next(event.copy);
                (maxTime >= beats) and: { ev.notNil }
			} {
				ev.putAll(proto);

                // added this to get scores from \phrase patterns:

                if(ev[\type] === \phrase) {
                    var pat, event, outerEvent, recursionLevel, instrument, embeddingLevel, freq, rest;
                    var phraseScore;

                    ev.use {
                        embeddingLevel = ~embeddingLevel ? 0; // infinite recursion catch
                        freq = ~freq.value;
                        rest = freq.isKindOf(Symbol); // check for outer rests
                        if(rest) { ~freq = freq };
                        pat = (~repository ? Pdef.all).at(~instrument);
                        if(pat.notNil and: { embeddingLevel < 8 })
                        {
                            pat = pat.pattern; // optimization. outer pattern takes care for replacement
                            // preserve information from outer pattern, but not delta.

                            recursionLevel = ~recursionLevel;
                            if(~transparency.isNil or:
                                { ~transparency > (recursionLevel ? 0) }
                            ) {
                                outerEvent = ev.copy;
                            } {
                                outerEvent = Event.default;
                                outerEvent.use {
                                    ~type = \phrase;
                                    ~recursionLevel = recursionLevel;
                                };
                            };

                            if(recursionLevel.notNil) {
                                if(recursionLevel > 0) {
                                    // in recursion, some inner values have to be overridden
                                    instrument = ~instrument;
                                    pat = pat.collect { |inval|
                                        inval.use {
                                            ~instrument = instrument;
                                            ~parent = outerEvent;
                                            ~recursionLevel = recursionLevel - 1;
                                        };
                                        inval
                                    };
                                } {
                                    // play pattern in the ordinary way
                                    ~type = \note;
                                };
                            } {	// avoid recursion, if instrument not set.
                                outerEvent.put(\embeddingLevel, embeddingLevel + 1);
                                outerEvent.parent_(Event.parentEvents.default);
                            };
                            // maybe add a Pprotect here.
                            // pat.asProtected
                            pat = Pfindur(~sustain.value, pat);
                            outerEvent.put(\delta, nil); // block delta modification by Ppar
                            outerEvent.put(\instrument, ~synthDef ? \default);

                            phraseScore = pat.asScore(~sustain.value, beats, outerEvent);
                            bundleList = bundleList ++ phraseScore.score.drop(1).drop(-1);
                        } {
                            ~type = \note;
                            ~play.value;
                        }
                    }
                } {
                    ev.play;
                };

                // original version is just this:

                // ev.play;

				beats = ev.delta * tempo + beats
			}
		}.next;

		bundleList = bundleList.sort { | a, b | b[0] >= a[0] };

		if((startTime = bundleList[0][0]) < 0) {
			timeOffset = timeOffset - startTime;
		};

//		bundleList.do { | b | b[0] = b[0] + timeOffset }

		^Score(bundleList.add([duration + timeOffset, [\c_set, 0, 0]]))
	}
}

Now the typical way of making a Score from Pbind.asScore works on phrases (I think… needs more testing though):

(
Pdef(\phraze, { arg sustain = 1;
    Pbind(
        \degree, Pseq(~degree),
        \dur, sustain.value / ~degree.size.max(1),
    )
});

x = Pfindur(2, Pbind(
    \type, \phrase,
    \instrument, \phraze,
    \degree, [0, 2, 4, 6],
    \legato, 1
)).asScore(2, timeOffset: 0.001);

x.score.printAll;
)

Prints the correct score:

[ 0.0, [ /g_new, 1, 0, 0 ] ]
[ 0.001, [ 9, default, 1000, 0, 1, out, 0, freq, 261.6255653006, amp, 0.1, pan, 0.0 ] ]
[ 0.251, [ 15, 1000, gate, 0 ] ]
[ 0.251, [ 9, default, 1001, 0, 1, out, 0, freq, 329.62755691287, amp, 0.1, pan, 0.0 ] ]
[ 0.501, [ 15, 1001, gate, 0 ] ]
[ 0.501, [ 9, default, 1002, 0, 1, out, 0, freq, 391.99543598175, amp, 0.1, pan, 0.0 ] ]
[ 0.751, [ 15, 1002, gate, 0 ] ]
[ 0.751, [ 9, default, 1003, 0, 1, out, 0, freq, 493.88330125612, amp, 0.1, pan, 0.0 ] ]
[ 1.001, [ 15, 1003, gate, 0 ] ]
[ 1.001, [ 9, default, 1000, 0, 1, out, 0, freq, 261.6255653006, amp, 0.1, pan, 0.0 ] ]
[ 1.251, [ 15, 1000, gate, 0 ] ]
[ 1.251, [ 9, default, 1001, 0, 1, out, 0, freq, 329.62755691287, amp, 0.1, pan, 0.0 ] ]
[ 1.501, [ 15, 1001, gate, 0 ] ]
[ 1.501, [ 9, default, 1002, 0, 1, out, 0, freq, 391.99543598175, amp, 0.1, pan, 0.0 ] ]
[ 1.751, [ 15, 1002, gate, 0 ] ]
[ 1.751, [ 9, default, 1003, 0, 1, out, 0, freq, 493.88330125612, amp, 0.1, pan, 0.0 ] ]
[ 2.001, [ 15, 1003, gate, 0 ] ]
[ 2.001, [ c_set, 0, 0 ] ]

Good catch for ScoreClock :+1:

Also an interesting idea for makeScore – it raises a design question, how much should other parts of the codebase reimplement things that belong to Event? (Another solution along the same lines would be to factor out the part of \phrase that spawns the sub-player, to allow Score to override it without breaking the rest of the logic. There are a number of things in SC that are factored as “one giant function/method” – I had difficulty, for instance, overriding small pieces of the code to render a NodeProxy as a compile-able string, due to some lengthy methods that, in principle, could be broken up. Maybe an opportunity here to refactor \phrase and make the behavior easier to extend.)

hjh

For sure, it’s not a great design choice, but I think overriding event keys to collect OSC messages into a Score was probably not an ideal design choice to begin with because it makes too many assumptions about how event keys are used in different event types, not to mention in user-defined types / play functions.

It makes more sense to repurpose clock scheduling methods directly, as ScoreClock does, since they already have access to all the OSC messages that are being scheduled by the clock.