Transcribing classical piano music to supercollider

Hello Everyone. This is my first post.

I’m trying to transcribe classical piano music to SuperCollider.

//////////////// Chopin Prelude in Em OP.28,No.4 /////////////////

(
 SynthDef(\mdapiano, { |out=0, freq=440, gate=1,
	                        vel= 100, decay= 0.8, release= 0.8, hard= 0.8,
		                    velhard= 0.8, muffle= 0.8, velmuff= 0.8, velcurve= 0.8, stereo= 0.5,
		                    tune= 0.5, random= 0.1, stret= 0.1, sus= 0|

   var son = MdaPiano.ar(freq, gate, vel,  decay, release, hard,
		                        velhard, muffle, velmuff, velcurve, stereo,
		                        tune, random, stret, sus);
	FreeSelf(1-gate);
    Out.ar(out, son );
}).add;
)



(  //                 1      2      3     4      5      6      7         8
var tNotes = [ 59,71, 71,72, 71,72, 71,72, 71,70, 69,71, 69,71, 69,71,69, 69,68,
  //          9                     10     11        12
	          67,69,71,74,72,64,69, 66,69, 66,71,69, 67,66,60,59,63,66,74,72,71,
 //           13     14     15     16                             17
	          71,72, 71,72, 71,72, 71,70,70,71,70,69,70,79,78,76, 76,75,84,75,75,76,79,71,
 //	          18                    19        20     21     22     23
	          74,72,76,64,69,66,69, 66,71,69, 66,64, 64,66, 64,66, 64,\r,
 //           24                           25
	          [52,54,59,64],[51,54,59,63], [52,55,59,64],
             ];
 //                     1    2    3    4    5    6    7          8
var tDur   = [ 3/4,1/4, 3,1, 3,1, 3,1, 3,1, 3,1, 3,1, 3,3/4,1/4, 3,1,
 //            9                          10   11         12
               1,1/2,1/2,1/2,1/2,1/2,1/2, 3,1, 3,1/2,1/2, 1/2,1/2,1/2,1/2,1/2,1/2,1/3,1/3,1/3,
 //            13   14   15   16                                     17
	           3,1, 3,1, 3,1, 3/4,1/4,1/2,1/8,1/8,1/8,1/8,1,3/4,1/4,  1/2,1/2,1/2,1/2,1/2,1/2,1/2,1/2,
 //            18                           19         20        21   22   23
               1/2,1/2,1/3,1/3,1/3,3/2,1/2, 3,1/2,1/2, 15/4,1/4, 3,1, 3,1, 2,2,
//             24   25
	           2,2, 4,
	         ];

//                 1                     2
var bNotes = [ \r, Pseq([[55,59,64]],8), Pseq([[54,57,64]],4),Pseq([[54,57,63]],4),
//            3
	          Pseq([[53,57,63]],4),Pseq([[53,57,62]],2),Pseq([[53,56,62]],2),
//            4
	          Pseq([[52,56,62]],4),Pseq([[52,55,62]],2),Pseq([[52,55,61]],2),
//            5                                          6
	          Pseq([[52,55,60]],4),Pseq([[52,54,60]],4), Pseq([[52,54,60]],4),Pseq([[51,54,60]],4),
//            7                     8
	          Pseq([[50,54,60]],8), Pseq([[50,53,60]],4),Pseq([[50,53,59]],4), // bar 7,8
//            9                                          10
	          Pseq([[48,52,59]],2),Pseq([[48,52,57]],6), Pseq([[47,52,57]],2),Pseq([[47,51,57]],2),Pseq([[48,52,57]],4),
//            11                                         12
	          Pseq([[47,51,57]],4),Pseq([[48,52,57]],4), Pseq([[47,51,57]],2),Pseq( [\r], 6),
//            13                    14
	          Pseq([[55,59,64]],8), Pseq([[54,57,64]],4),Pseq([[53,57,63]],4),  // bar 13, 14
//            15
	          Pseq([[53,56,63]],2),Pseq([[53,56,62]],2),Pseq([[52,56,62]],4),
//            16
	          Pseq([[52,55,62]],2),Pseq([[52,55,61]],2),Pseq([[49,52,58]],2),Pseq([[48,52,57]],2),
//            17
		      [23,47],Pseq([[57,60,66,69]],3),[55,59,63,66],Pseq([[55,59,64]],3),
//            18
	          Pseq([[57,60,64]],2),45,[52,54,60],Pseq([[47,52,59]],2),Pseq([[48,52,57]],2),
//            19
	          Pseq([[47,52,59]],4),Pseq([[48,52,57]],4),
//            20
	          Pseq([[47,52,59]],4),Pseq([[47,51,59]],2),Pseq([[47,51,57]],2),
//            21
      	      Pseq([[48,55]],4), Pseq([[48,58]],2),Pseq([[48, 52, 57]],2),
//            22
              Pseq([[47,52,57]],2), Pseq([[47,52,56]],2),Pseq([[47,52,55]],4),
//            23             24                  25
	          [46,48,55],\r, [35,47],[35,42,47], [30,42]
             ];

//               1-22               23   24   25
var bDur   = [1, Pseq([1/2], 22*8), 2,2, 2,2, 4];

var instr = \mdapiano;

var bA = Pbind(\instrument, instr ,\midinote, Pseq(bNotes), \dur, Pseq(bDur),
	 \legato, 0.8, \vel, 60, \random, 0.0, \stret, 0.0);

var tA = Pbind(\instrument, instr ,\midinote, Pseq(tNotes), \dur, Pseq(tDur),
	\legato, 0.8, \vel, 90, \random, 0.0, \stret, 0.0);

~score = Ppar([bA,tA]);
~score.play(TempoClock(50/60));
)

//////////////////////////

Here is my problem. In piano music,
a chord is held down simultaneously. But then
some voices can move while other notes are still held down.
i.e the chord notes have different durations? Does anyone
know an elegant way to deal with this?

thanks, Stewart

~voice1=[[1,2],[[0,2],2],[[-1,1s,3],2]];
~voice2=[[\r,2],[4,1],[3,1],[\r,2]];
 
Ppar(
[Pbind(#[\degree,\dur],Pseq(~voice1)),
Pbind(#[\degree,\dur],Pseq(~voice2))
]).play;

I would mimic any score softwares. To achieve what you want to do in regular Score notation softs (e.g. MuseScore, …) you would keep the notes that must be held on one voice and the other notes on a 2nd voice.

This is what I have done here. ~voice1 holds the notes of the 1st voice. ~voice2 holds the notes of the 2nd voice. And I play these in parallel with a Ppar.

Note also how ~voice1/2 are structured : there are an array of 2 values

  • 1 note or an array of notes (i.e. a chord) or \r for a rest
  • a duration

This examples plays this:
image

This is probably what’s throwing you off. Duration is not actually the key to this problem. Sustain is.

SC expresses rhythm in terms of Inter-Onset Intervals (IOIs), or deltas. At any point in the sequence, there can be only one “next time point” – that is, only one time delta.

SC uses the \dur key for time delta. So, as you probably already found, a Pbind sequence cannot have an array for \dur, ever.

But, notes can sustain for as long or as short as needed, even overlapping into the next event.

Taking a famous just-intonation “comma pump” example, the time delta here is always 1 beat – it doesn’t matter that the bottom voice is in half notes or that the top voice is syncopated – there is something on every beat, and nothing in between the beats.

comma-pump

But we can use the \sustain key to hold the notes for different lengths. (In an event, \sustain can be an array.)

(
p = Pbind(
	\midinote, Pseq([
		[55, 62, 67],
		69,
		[60, 64],
		67
	], inf),
	\dur, 1,  // IOIs -- all qtrs
	\sustain, Pseq([
		[2, 2, 1],
		2,
		2,
		1
	], inf) * 0.9
).play;
)

lgvr is also correct that you can use Ppar to split voices. The approach I’m outlining is more like a MIDI file. Both will play back the notes just fine, but they have different possibilities and different challenges for manipulating the music data.

hjh

1 Like

Thanks to both of you. Understanding the difference of \dur and \sustain is exactly
the solution.

The Ppar also is interesting. I found that Ppar explicitly add \delta to all events.

Cheers, Stewart

Maybe useful: whenever I model “traditional” music in supercollider, I make use of my Panola quark.

It allows using a kind of text-based music notation, which to me at least is much easier to use than midi note numbers. If desired, Panola allows you to easily transform the text based notation into midi note numbers to perform further calculations/transformations.

There’s a short Panola tutorial here: Panola - pattern notation language tutorial

Here’s a 4-part video tutorial where I use Panola and apply transformations on the midi note numbers extracted from it:

2 Likes

I just went through the Panola Tutorial. Great Idea!

Can it do JamShark70’s example exactly?

According to the tutorial

// You can make chords using angular brackets. Only note properties of the first
// note in the chord (other than octave number and note modifier (see later)) are
// taken into account.

In JamShark70’s example, \sustain can act individual on each note in a chord.


// JamShark70

(
t = TempoClock(1);
p = Pbind(
\midinote, Pseq([
[55, 62, 67],
69,
[60, 64],
67
], inf),
\dur, 1, // IOIs – all qtrs
\sustain, Pseq([
[2, 2, 1],
2,
2,
1
], inf) * 0.9
).trace.play(t);
)
// trace

( ‘dur’: 1, ‘midinote’: [ 55, 62, 67 ], ‘sustain’: [ 1.8, 1.8, 0.9 ] )
( ‘dur’: 1, ‘midinote’: 69, ‘sustain’: 1.8 )
( ‘dur’: 1, ‘midinote’: [ 60, 64 ], ‘sustain’: 1.8 )
( ‘dur’: 1, ‘midinote’: 67, ‘sustain’: 0.9 )


// Panola

(
t = TempoClock(2);
~ex = Panola.new(" a4 g4");
~player = ~ex.asPbind.trace.play(t);
)

// trace

( ‘instrument’: default, ‘legato’: 0.9, ‘dur’: 0.25, ‘lag’: 0.0,
‘amp’: 0.5, ‘midinote’: [ 67, 59, 67 ], ‘tempo’: 0.33333333333333 )
( ‘instrument’: default, ‘legato’: 0.9, ‘dur’: 0.25, ‘lag’: 0.0,
‘amp’: 0.5, ‘midinote’: 69, ‘tempo’: 0.33333333333333 )
( ‘instrument’: default, ‘legato’: 0.9, ‘dur’: 0.25, ‘lag’: 0.0,
‘amp’: 0.5, ‘midinote’: [ 60, 64 ], ‘tempo’: 0.33333333333333 )
( ‘instrument’: default, ‘legato’: 0.9, ‘dur’: 0.25, ‘lag’: 0.0,
‘amp’: 0.5, ‘midinote’: 67, ‘tempo’: 0.33333333333333 )

Comments:
1. Why ‘tempo’: 0.33333333333333 ?
2. I thought ‘dur’ is in beats so a quarter note should be 1 beat?
3. I understand that “pdur” is just \legato. I belive that
\sustain = \dur * \legato * \stretch if not given explicitly.

BTW, I’m new to SC and Computer Music. Beautiful but Overwhelming.

I learned a lot from both your posts.

regards, Stewart

My mistake. Panola code above should be

(
t = TempoClock(2);
~ex = Panola.new(" a4 g4");
~player = ~ex.asPbind.trace.play(t);
)

My mistake again. This is embarrassing. Cutting and pasting code as text
mangles it! Hope you get the idea.

Please see this post:

hjh

Hi Stewart,

If you have polyphony with different durations for different voices, you need to use several lines of Panola and combine them with Ppar. There’s no separate sustain/duration (well there is something to make notes legato/staccato, and something to modulate the “lag” but those are different things).

The tempo thing is part of a bad design decision I made. You can get rid of it by specifying argument include_tempo=false in the “asPbind” family of functions.

For your score example, I’d use 2 voices: upper voice is one Panola string, chords in the lower voice is a second Panola string, and both get combined in a Ppar, e.g.

(
s.waitForBoot {
	var upper, lower, pattern;
	upper = Panola("g4_4 a_2 g_4");
	lower = Panola("<g3_2 d4>  <c4_2 e>");
	pattern = Ppar([upper.asPbind, lower.asPbind]);
	~player = pattern.play;
}
)

Also, in case you’ve not seen it, the wslib library has a SimpleMIDIFile class that will load, for example, the midi file at http://kern.humdrum.org/search?s=t&keyword=Chopin.

I.e.

m = SimpleMIDIFile.read("~/prelude28-04.mid".asAbsolutePath);
m.timeMode = 'seconds';
m.inspect

It’s true that Panola has some weirdness related to durations and tempo. It’s a known problem but so far it doesn’t lead to problems getting the audio output you want. Fixing it has not exactly been high on my priority list but at the same time it’s kind of stupid that it’s wrong, so maybe I should bump that priority a bit…

For what it’s worth, the internal duration/tempo weirdness in Panola should be fixed in the latest github version.

Unfortunately this might break your scripts if you rely on calls to “totalDuration” (which now returns a value in beats instead of “whole notes”), or process raw values related to “tempo” (but the majority of users probably doesn’t do any of this).

I should have fixed this years ago but this thread finally made it happen (and I hope I didn’t mess up again :slight_smile: )

1 Like

Trying to understand a little more. I do a trace on the above and
turn on Server Dump OSC.

\delta is the Inter-Onset Interval, which is = \dur * \stretch unless
explicitly given. \stretch is 0 by default.
\sustain = \dur * \stretch * \legato unless explicitly given.
\legato I think is 0.8 by default.
\sustain is the time from when the synth is created until \gate = 0
is sent to the synth.

I got rid of the multiplication by 0.9 in your example.

( 'dur': 1, 'midinote': [ 55, 62, 67 ], 'sustain': [ 2, 2, 1 ] )
[ "#bundle", 16557380629189213132, 
  [ 9, "default", 62553, 0, 1, "out", 0, "freq", 195.998, "amp", 0.1, "pan", 0 ]
]
[ "#bundle", 16557380629189213132, 
  [ 9, "default", 62554, 0, 1, "out", 0, "freq", 293.665, "amp", 0.1, "pan", 0 ]
]
[ "#bundle", 16557380629189213132, 
  [ 9, "default", 62555, 0, 1, "out", 0, "freq", 391.995, "amp", 0.1, "pan", 0 ]
]
( 'dur': 1, 'midinote': 69, 'sustain': 2 )
[ "#bundle", 16557380631336696780, 
  [ 9, "default", 62556, 0, 1, "out", 0, "freq", 440, "amp", 0.1, "pan", 0 ]
]
[ "#bundle", 16557380631336696780, 
  [ 15, 62555, "gate", 0 ]
]
( 'dur': 1, 'midinote': [ 60, 64 ], 'sustain': 2 )
[ "#bundle", 16557380633484180428, 
  [ 9, "default", 62557, 0, 1, "out", 0, "freq", 261.626, "amp", 0.1, "pan", 0 ],
  [ 9, "default", 62558, 0, 1, "out", 0, "freq", 329.628, "amp", 0.1, "pan", 0 ]
]
[ "#bundle", 16557380633484180428, 
  [ 15, 62553, "gate", 0 ]
]
[ "#bundle", 16557380633484180428, 
  [ 15, 62554, "gate", 0 ]
]
( 'dur': 1, 'midinote': 67, 'sustain': 1 )
[ "#bundle", 16557380635631664076, 
  [ 15, 62556, "gate", 0 ]
]
[ "#bundle", 16557380635631664076, 
  [ 9, "default", 62559, 0, 1, "out", 0, "freq", 391.995, "amp", 0.1, "pan", 0 ]
]
[ "#bundle", 16557380637779147724, 
  [ 15, 62557, "gate", 0 ],
  [ 15, 62558, "gate", 0 ]
]
[ "#bundle", 16557380637779147724, 
  [ 15, 62559, "gate", 0 ]
]
```````````````````````````````````````````
Looking at trace and OSC together is very informative!

Here is my question.  The OSC bundle time stamp is in
seconds since 1900. This number is too large for SC to handle.

What is the integer and fractional part? I can subtract the time
 \gate = 0 sent time from synth creation time (same node id in OSC)  to get a smaller number. Can I decode this time difference (hopefully in SC) to get
the time in seconds as a float value? This should be close to
the \sustain value in the trace with a little latency.

thanks, Stewart

(BTW – that part of your text is folded into the code block – the end of the code block should be exactly three backticks, not 43 of them.)

Yeah, a 64-bit integer printed in decimal isn’t going to be usable. (It might be a good idea to file a feature request to change this to hex – then at least you could separate the high and low ints by copying the corresponding characters – SC_OscUtils.hpp line 148.)

So, basically, ignore the timestamp.

When I’ve needed to debug OSC message timing, I use the class pasted below. I think I stole most of it from someone, years ago, making just a few changes. Save this file as DebugNetAddr.sc into your Extensions directory (sc-ide File menu → Save as Extension).

DebugNetAddr : NetAddr {
	var <doc, <>active=true;
	var <>list;
	var funcs, <>dumpAll = true;

	sendRaw { arg rawArray;
		if(active) { this.dump(nil, rawArray) };
		super.sendRaw(rawArray);
	}
	sendMsg { arg ... args;
		if(active) { this.dump(nil, [args]) };
		super.sendMsg(*args);
	}
	sendBundle { arg time ... args;
		if(active) { this.dump(time, args) };
		super.sendBundle(time, *args);
	}
	dump { arg time, args;
		var str, docStr, beats, beatsThisThread;
		if(dumpAll) {
			if(#['/status', '/quit'].includes(args.tryPerform(\at, 0).tryPerform(\at, 0).asSymbol))
			{ ^this };
			if(doc.isNil) { this.makeDocument };

			// should get the beats outside the { }.defer block
			beats = SystemClock.beats;
			beatsThisThread = thisThread.clock.tryPerform(\beats);
			if(list.notNil) {
				list.add([beats, beatsThisThread, args])
			} {
				defer {
					str = "latency %\tSysClock logical time %\tthisThread's logical time %\n"
					.format(time, beats, beatsThisThread);
					args.do { arg msg;
						str = str ++ Char.tab;
						msg = msg.collect { arg el;
							if(el.isKindOf(RawArray) and: { el.size > 32 })
							{ "data[" + el.size + "]" } { el };
						};
						str = str ++ msg.asCompileString ++ Char.nl;
					};
					str.postln;

					doc !? { doc.selectedString_(str ++ Char.nl) }
				};
			};
		};
		args.do { |msg| funcs.value(msg, time) };
	}
	makeDocument {
		if(thisProcess.platform.ideName == "scapp") {
			try {
				doc = Document(this.asCompileString)
				.onClose_({ doc = nil; active = false })
				.background_(Color.grey(0.8));
			} {
				doc = nil; // active = false
			}
		} {
			doc = nil
		};
	}

	addFunc { |func|
		funcs = funcs.addFunc(func);
	}
	removeFunc { |func|
		funcs.removeFunc(func);
	}
	== { |that|
		^addr == that.addr and: { port == that.port }
	}
}

Recompile, and then you can do:

s.addr = DebugNetAddr("127.0.0.1", 57110);
s.boot;

s.addr.active = false;  // turn off tracing
s.addr.active = true;  // turn tracing on again

Outgoing messages to the server will be printed with time information:

latency nil	SysClock logical time 685.127977358	thisThread's logical time 685.127977358

SysClock is in seconds, and should be consistent across all TempoClocks. thisThread time is in the clock’s beats. It’s much easier just to read the clock times directly, instead of trying to reverse-engineer them from a gigantic integer.

hjh

1!

Just to nitpick – cheers