Recording MIDI Sequences?

Sorry for all the questions!

Are there any good ugens for recording midi information and then playing them back ?
I tend to like to play my keyboars and twiddle knobs when writing, and being able to record patterns would be super-useful in getting my ideas out quickly.

Any pointers or suggestion on directions I should investigate if it doesn’t exist ?

thanks,
John

Taking the question at face value, the answer has to be no – because in SC, UGens are never responsible for MIDI input or output.

MIDI I/O is strictly the responsibility of the language client. UGens are for server-side signal processing. There is no connection between the two.

But of course the language is able to receive MIDI data, and store it in a format of your choosing. But there’s no built-in “MIDI sequence” object.

The most basic approach would be to keep a list of incoming MIDI messages. But IMO this format is not convenient for playback. MIDI represents the note is a pair of messages, separated in time. If you record the messages, then it’s your responsibility to pair up note on/off.

But, as a basic demo:

// simple MIDI recorder
(
l = List.new;

MIDIdef.noteOn(\recOn, { |vel, num|
	l.add([\on, SystemClock.seconds, num, vel]);
});

MIDIdef.noteOff(\recOff, { |vel, num|
	l.add([\off, SystemClock.seconds, num, vel]);
});
)

// stop recording
MIDIdef(\recOn).free; MIDIdef(\recOff).free;

// convert raw times to deltas
(
var deltas = l.flop.at(1).differentiate.put(0, 0);
l.do { |row, i| row[1] = deltas[i] };
)

// playback is inconvenient because note-off doesn't match up with note-on
(
var synths = Array.newClear(128);

r = Routine {
	l.do { |row|
		var type, delta, num, vel;
		#type, delta, num, vel = row;
		if(type == \on) {
			s.makeBundle(s.latency, {
				if(synths[num].notNil) {
					synths[num].release;
				};
				synths[num] = Synth(\default, [freq: num.midicps, amp: vel/127]);
			});
		} {
			s.makeBundle(s.latency, {
				synths[num].release;
				synths[num] = nil;
			});
		};
		delta.wait;
	};
}.play;
)

But this way it would be easy to interleave CC data.

SimpleMIDIFile in the wslib quark might be easier for this.

For my own purposes, I wanted notes to be represented as a single object rather than pairs of messages. So, using the ddwMIDI quark (these are not base classes – you would have to install the extension; see help files about Quarks for details), you can do this:

MIDIPort.init;

MIDIPort.sources;  // find index of your keyboard: mine is 4

c = MIDIChannel([4, \omni]);
m = MIDIRecSocket([4, \omni]);

// recording is live now, play stuff

// when finished:
b = m.stopRecord;

// that returned a MIDIRecBuf with your notes

b.dumpSeq  // already in deltas; also each note knows its 'length'

// much easier to sequence
(
p = Pbind(
	\instrument, \default,
	\seqnote, Pseq(b.notes),
	// this translates SequenceNote data
	// into entries that the default Event understands
	#[midinote, dur, sustain, amp], Pfunc { |ev|
		var n = ev[\seqnote];
		[n.freq, n.dur, n.length, n.args]
	}
).play;
)

But I didn’t include CC recording in MIDIRecSocket/MIDIRecBuf.

hjh

1 Like

This is beyond helpful. Thank you, and thank you for correcting my understanding of Ugens. I had a feeling I was going to need to wrap my own or use a quark. I’ll take a look at this and loop back to this thread with any questions.

thanks again.

Here is an example from LFSaw to record MIDI data: https://sccode.org/1-5bv
It uses the SimpleMIDIFile from the wslib quark.

I used the example to record midi and play it back like this:

(
~startMIDIRec = {
	var dict = ();
	dict.data = [];
	dict.startTime = nil;
	dict.responders = [\noteOn, \noteOff, \polytouch, \cc, \program, \touch, \bend].collect{|msgType|
		MIDIFunc({|...args|
			var time = Date.getDate.rawSeconds;
			var val, ctlNum, chan, src;
			[msgType, time].postln;
			// handle arguments for different msgTypes
			[\noteOn, \noteOff, \control, \polytouch ].includes(msgType).if({
				# val, ctlNum, chan, src = args;
				args.postln;
			},{
				# val, chan, src = args;
			});

			dict.startTime.isNil.if({
				dict.startTime = time;
			});

			dict.data = dict.data.add(
				// [ time, type, channel, val1, val2 ]
				ctlNum.notNil.if({
					[ time - dict.startTime, msgType, chan, ctlNum, val ]
				}, {
					[ time - dict.startTime, msgType, chan, val]
				})
			);
		}, msgType: (msgType == \cc).if({\control}, {msgType});
		)
	};
	dict.writeData = {
		var filePath = PathName.tmp ++ "MIDI-%.mid".format(Date.getDate.stamp);
		var mFile = SimpleMIDIFile( filePath ); 

		dict.responders.do(_.free);
		mFile.init1( 1, 120, "4/4" );	
		mFile.timeMode = \seconds;  
		mFile.addAllMIDIEvents(
			dict.data.collect{|row| [0] ++ row }, true
		);
		mFile.adjustEndOfTrack;
		mFile.write(filePath);
		dict.midiFile = mFile;
		mFile
	};
	dict
};
)

// connect your midi devices
MIDIIn.connectAll;

// start recording midi data
~midiRecordings = ~startMIDIRec.();

// stop recording and get the SimpleMIDIFile
~midiFile = ~midiRecordings.writeData;

// use the midifile to do what you want (see SimpleMIDIFile helpfile)
// for example convert to pattern and play
~midiFile.p.play;

// or extract controller events (didn't test)
~midiFile.controllerEvents;

1 Like

Something else that may be useful with this is having a score-type entry, where note-offs aren’t recorded, and the length of the note is dictated by another value currently set. (i.e. the next note is an 1/8th note, a half note, this is part of a triplet, etc.). I quite like that style of editing, so I wanted to put that as a placeholder here so I don’t forget.

Thanks again.