# Recording Patterns to a MIDI file

Hello - I’m hoping someone can point me in the right direction.

I’m trying to get something like this working as well, but I think what I want is a bit more simple. I am trying to record patterns to midi files to go with audio stems in other apps. I’ve tried a number of different things, including the LFSaw example by @vmx (I have even attempted to make channel based class out of it). I have also been digging into SimpleMIDIFile, which is probably where I’d go so I can save out one file even with multiple tracks.

Again, my main goal is saving one midi file out of live changes for each pattern that I would choose - or one file with data on different midi tracks (I wasn’t aware MIDI had tracks, but tests from SimpleMIDIFile seemed to work).

My biggest struggle is how to either use the .addEvent, or .addNote. as the event is playing from a Pdef/Pbind. I thought I could use a Pfunc of some sort and manually add data to the instance of SimpleMIDIFile, but as it errors about not receiving an appropriate return type, I’m obviously doing this incorrectly.

Also, I’m currently chaining together a base Pbind to one of Type MIDI and one for a Synth node (got that help on another post) - the intent there was sending the MIDI out to a DAW or device but still make sound with SC.

I’m only including one instrument here, but would do the same for others, if there is a way (hence the 2 tracks in the SimpleMIDIFile instantiation). I’m sure I’m using Pfunc incorrectly trying to get the keys out of the current event as well.

And I just realized that this code probably won’t work for anyone because I’m loading a sample library.

(
SynthDef(\bplay,
{arg out = 0, buf = 0, rate = 1, amp = 0.5, pan = 0, pos = 0, rel=15;
var sig,env=1;
sig = Mix.ar(PlayBuf.ar(2,buf,BufRateScale.ir(buf) * rate,1,BufDur.kr(buf)*pos*44100,doneAction:2));
env = EnvGen.ar(Env.linen(0.0,rel,0),doneAction:0);
sig = sig * env;
sig = sig * amp;
Out.ar(out,Pan2.ar(sig.dup,pan));
)

Ndef(\hh).play;
Ndef(\hh)[1] = \xset -> Pdef(\hh);
Ndef(\hh).get(\out);

(
Ndef(\hh).play;
Pdef(\hhmidi,
Pbind(
\type, \midi,
\midiout, m,
\midicmd, \noteOn,
\chan, 1,
\out, Pfunc({ ~midiRecordnig.addNote(noteNumber: \midinote, velo: 127, startTime: TempoClock.default.seconds, dur: \dur, channel: \chan, track: \chan, sort: true)})
));

Pdef(\hhsynth,
Pbind(
\instrument, \bplay,
\out, Pfunc(Ndef(\hh).bus.index),
\group, Pfunc(Ndef(\hh).group),
\buf, d["Hats"][1],
));

Pdef(\hhseq,
Pbind(

// \dur, Pseq([0.25, 0.25, 0.5, 0.75, 0.25].scramble, inf),
// \dur,Pbjorklund2(Pseq(l, inf).asStream,12,inf)/8,
// \dur, 0.125,
\dur, Pbjorklund2(Pseq([3, 5, 7, 9, 11], inf), Pseq([12, 4, 6, 7], inf)),
));
)
(

Pdef(\hh,
Ppar([
Pdef(\hhmidi),
Pdef(\hhsynth),
])
<> PtimeClutch(Pdef(\hhseq))
);
)

(
~midiRecording = SimpleMIDIFile("~/Desktop/testinghh.mid");
~midiRecording.init1(2, TempoClock.default.tempo * 60, "4/4");
~midiRecording.timeMode = \seconds;
)
Pdef(\hh).play(quant: -1);
Pdef(\hh).stop;


Everything looks good, just one small mistake - when you use \something, it’s just a Symbol - it’s NOT referring to e.g. the value in the current environment or the current event. For this, you need ~something.

Here are two solutions:

1. Use the \callback key, which is always called after all the processing on an event is done. You can use this to hook up things like your midi recorder. This function is executed with the current Event as it’s environment, which means you can access keys from it with ~envVariable syntax. Note that ~midiRecording is defined in your current “global” environment. When your callback function is run, you’re environment is the event itself, so you don’t have access to ~midiRecording. Instead, you can just pass it in via a key. I could have called my key ~midiRecording also and no code change would have been required, but then it visually looks like you’re accessing the “outer” ~midiRecording - naming is up to you, I just did it for clarity.
	Pbind(
\type, \midi,
\midiout, m,
\midicmd, \noteOn,
\chan, 1,
\recording, ~midiRecording,
\callback, {
noteNumber: ~midinote,
velo: 127,
startTime: TempoClock.default.seconds,
dur: ~dur,
channel: ~chan,
track: ~chan,
sort: true
);
~recording.midiEvents.postln;
}
)

1. The event is passed as an arg to a Pfunc, so you can access it here. BIG caveat: your Pfunc receives the Event in the state it’s in right now. This means, it won’t have values that are specified AFTER your Pfunc. And, if you chain your Pbind into something else to do more processing or add keys, your Pfunc will not see this, since it’s happening after. Nonetheless, I still use this Pfunc technique for side-effects all the time, mostly out of habit (I think the \callback implementation is probably more correct in more cases).
Pdef(\hhmidi,
Pbind(
\type, \midi,
\midiout, m,
\midicmd, \noteOn,
\chan, 1,
\out, Pfunc({
|event|
noteNumber: event[\midinote],
velo: 127,
startTime: TempoClock.default.seconds,
dur: event[\dur],
channel: event[\chan],
track: event[\chan],
sort: true
);
~midiRecording.midiEvents.postln;
}
)
);


A few other things:

1. When I ran this, m was nil - this resulted in my pattern bailing out immediately (because Pbinds end when there’s a nil in any field). Be careful, easy mistake to make and it took a few minutes for me to figure out wtf was going on…
2. You may be better off using thisThread.clock for your time? This is the thread that your pattern is being played on (IIRC it will usually be TempoClock.default, but just in case you used a different clock). Also, beats is the musical time, reflecting tempo and things like this. I think seconds == beats for very trivial cases, but beats is more likely to get you the right musical result for more complex cases. There are probably good reasons to use seconds also, depending on your use case.
1 Like

This is all exactly what I was looking for. I knew there were bits I was unclear of (and was most sure that the keys I used in that Pfunc were not doing anything, but left it so everyone could see what I was attempting to do).

The environment explanation makes a lot of sense - I was trying to understand Events and had just read up on this last night a bunch, partly because I was trying to dissect the code that was shared above for recording which used the short syntax var dict = (); - I didn’t realize that an Event is also an IdentityDictionary and Environment (still sort of trying to wrap my head around all that).

\callback is exactly what I think I was after and must have dug through Pbinds and Pattern help for hours trying to find just that!

Oh, and m is actually in my startup file for a MIDIOut instance (I actually forgot this at one point and was overriding it by accident with the SimpleMIDIFile and things as well.

Thanks again as usual for all of the help here @scztt - hopefully one day I can contribute more on the answering side instead of only the questioning. But I can’t thank this forum and all who’ve been helping nearly enough.

No problem! All really good questions!

\callback isn’t documented, just hidden in the source code - see the play function for playerEvent: https://github.com/supercollider/supercollider/blob/develop/SCClassLibrary/Common/Collections/Event.sc#L441.

1 Like

It seems like this 98% of the way there but here is where I am with the two scenarios:

1. Using \callback. For some reason isn’t registering the .addNote. It’s like it’s not tying to the  \recording key’s function. The postln after the .addNote is tracing properly though, they just aren’t getting collected in the SimpleMIDIFile instance. I thought it might be an order of execution but I’ve tried many ways.

I was also going to try and trace this out using a temp function with a postln in it - but don’t know how to make a .addNote method on standard function.

1. With Pfunc it does work, but the noteNumber: event[\midinote] is returning a Function. I’ve tried declaring and setting variables before the .addNote function and using them with the noteNumber key but get the same thing. I’m sure this something I’m still very weak on in the language.

I’ve even tried using the \freq key, assuming that this is the root (going by the docs) and doing a .midicps on a var before using in the .addNote, but still returns a function.

I’m probably missing some tiny thing.

Pdef(\hhmidi,
Pbind(
\type, \midi,
\midiout, m,
\midicmd, \noteOn,
\chan, 1,
\out, Pfunc({ | e |
noteNumber: e[\midinote],
velo: 127,
dur: e[\dur],
channel: e[\chan],
track: e[\chan],
);
~mr.midiEvents.dopostln;
})
));


P.S. I got sick of typing ~midiRecorder and changed to ~mr

Output on my end:
[ trackNumber, absTime, type, channel, val1, val2 ] (from the SimpleMidiFile Doc under .midiEvents)

[ 1, 1256.0, noteOn, 1, a Function, 127 ]
[ 1, 1260.0, noteOff, 1, a Function, 127 ]
[ 1, 1256.0, noteOn, 1, a Function, 127 ]
[ 1, 1260.0, noteOff, 1, a Function, 127 ]
[ 1, 1260.0, noteOn, 1, a Function, 127 ]
[ 1, 1264.0, noteOff, 1, a Function, 127 ]
[ 1, 1256.0, noteOn, 1, a Function, 127 ]
[ 1, 1260.0, noteOff, 1, a Function, 127 ]
[ 1, 1260.0, noteOn, 1, a Function, 127 ]
[ 1, 1264.0, noteOff, 1, a Function, 127 ]
[ 1, 1264.0, noteOn, 1, a Function, 127 ]
[ 1, 1268.0, noteOff, 1, a Function, 127 ]
[ 1, 1256.0, noteOn, 1, a Function, 127 ]
[ 1, 1260.0, noteOff, 1, a Function, 127 ]
[ 1, 1260.0, noteOn, 1, a Function, 127 ]
[ 1, 1264.0, noteOff, 1, a Function, 127 ]
[ 1, 1264.0, noteOn, 1, a Function, 127 ]
[ 1, 1268.0, noteOff, 1, a Function, 127 ]
[ 1, 1268.0, noteOn, 1, a Function, 127 ]


Lastly, I realize the time is carried on from the clock so I’ll have to figure that out later.

This issue (at least with a Pfunc) was totally my fault.

There was no Pbind sending out notes of any kind (because it was built for samples). Adding \midinote, 60 worked in this case. I sort of assumed the default 60 would come through but apparently not.

I’ll test further with some more than just sample hits and share notes later.

Ah, I missed the question about midinote being a function.

Try noteNumber: e.use { ~midinote.value }.

hjh

1 Like

Thanks! That totally worked. Could that same thing be used to get around @scztt’s use of the undocumented \callback key which uses the event’s environment? His point was it by default works within and (by default anyway) can’t see the main environment instance of SimpleMIDIFile that this is adding the midi messages to. But it looks like it is probably more appropriate than Pfunc.

I’m also realizing now that this is working I have huge lines of redundant code for recording. Everything about the Pdef(\hhmidi) is solely for either sending MIDI out or recording and there is really nothing unique about it at this point. I’m going to look for a way to do a ‘setup’ for this purpose, so I can work a bit more ‘live’ - if anyone has thoughts… I feel like I could make a Class that extends Pdef or something in order to specify recording of MIDI. I already modified ProxyRecorder (a class I found somewhere) for Ndef’s - but all of this setup is something that seems like it could be simplified.

A simple way to make all MIDI streams record-able would be to override the existing \midi type. You could add something like this to your start-up file:

Event.addEventType(
\midi,
if (~recordTarget.notNil) {
}
}),
() // if you want some recording-related properties to have default values, pass them here
);


Then, any midi event will be recorded only when ~recordTarget is present. By using addFunc, you amend the existing midi play function. This ensures that e.g. you don’t need to use \callback, which means you won’t get into trouble when trying to specify a DIFFERENT \callback for \midi type events somewhere else.

You can pass in a default parent as the last argument, and fill it with default values for any settings you might need for recording (for example, an optional file path, which you could use to create a SimpleMIDIFile).

Of course, you can also use a different \type than midi, e.g. \recMidi so the recording behavior is optional as well.

1 Like

Brilliant @scztt - I would never have found this… knowing what I’d like to do and finding the appropriate way from browsing the help is a very wide gap for me, but getting there slowly with everyone’s help! Thank you.

Hello again,

I’ve made decent headway on this. My overarching goal is to sync up file output for audio file recording (in my case using RecNodeProxy, and the Pattern MIDI output above.

I’ve created/modified a class to try and handle all of the recording. I’ve added the last entry from @scztt (thank you) into the class init. All of this works. However, my ‘record’ method in my class starts recording audio on grid as it should when I execute it. The MIDI however is getting it’s ‘starttime’ in the .addNote() function from what was originally the TempoClock.default - this meant that when I’d record, and depending upon how much time elapsed the midi was WAY later in time.

So, I guess my question and what I don’t understand is the best way to use clocks, and probably one in my class just for recording that can still be referenced everywhere. It’d also be nice to have control over a master Tempo that changes while recording - but I don’t think I’m quite understanding clocks (even after reading through the docs and threads here numerous times).

Source files - I think I’m also unsure about scope of what the Pdef Events can see or not. I added a key reference to a clock both to each \midi Pbind as well as a var in the Class. Not totally sure about that. I feel like I know just enough to be dangerous at this point…

NOTE: I mentioned all was working fine above - that was before some of these attempts at not using the default clock. That was where I was getting the audio stems out right at ‘record’ but the MIDI notes were being written out at current logical time (I think).

Class File:

HypoRecorder {

var <>nodes;
var <>midiTracks;
var <>smf;
var <>folder;
var <>recClock;
var <>headerFormat = "aiff", <>sampleFormat = "float";

*new { |subfolder = nil |
^super.newCopyArgs().init(subfolder)
}

init { | subfolder = nil |
nodes = ();
if(subfolder != nil,
{folder = Platform.userAppSupportDir +/+ "Recordings" +/+ Document.current.title +/+ subfolder },
{folder = Platform.userAppSupportDir +/+ "Recordings" +/+ Document.current.title  }
);

File.mkdir(folder);

\midi,
if (~recordTarget.notNil) {
noteNumber: ~midinote,
velo: ~amp.range(0, 127),
startTime: ~mclock.beats,
dur: ~dur,
channel: ~chan,
track: ~chan,
);
~recordTarget.midiEvents.dopostln;
}
}),
() // if you want some recording-related properties to have default values, pass them here
);

}

free {
nodes.do(_.clear);
nodes = nil;
}

add { |proxies, midis = 2, clock |
midiTracks = midis;
if(clock.isNil, { recClock = TempoClock.default }, { recClock = clock });
this.prepareNodes(proxies);
{ this.open(proxies) }.defer(0.5);
}

prepareNodes { |proxies|
proxies.do{ |proxy, i|
var n = Ndef(proxy);
n.play;
n.postln;
i -> RecNodeProxy.newFrom(n, 2)
);
}
}

open { |proxies |
var dateTime  = Date.getDate.format("%Y%m%d-%Hh%m");
proxies.do{ |proxy, i|
var fileName  = ("%/%-%.%").format(
);

};
smf = SimpleMIDIFile(folder +/+ dateTime ++ "midi.mid");
}

record { |paused=false |
smf.init1(midiTracks, recClock.tempo * 60, "4/4");
smf.timeMode = \seconds;
nodes.do(_.record(paused, recClock, -1));
}

stop {
this.close
}

close {
nodes.do(_.close);
smf.midiEvents.dopostln;
smf.metaEvents.dopostln;
smf.write;
}

pause {
nodes.do(_.pause)
}

unpause {
nodes.do(_.unpause)
}

closeOne { |node|

}
}



Usage File:

(
SynthDef(\bplay,
{arg out = 0, buf = 0, rate = 1, amp = 0.5, pan = 0, pos = 0, rel=15;
var sig,env=1;
sig = Mix.ar(PlayBuf.ar(2,buf,BufRateScale.ir(buf) * rate,1,BufDur.kr(buf)*pos*44100,doneAction:2));
env = EnvGen.ar(Env.linen(0.0,rel,0),doneAction:0);
sig = sig * env;
sig = sig * amp;
Out.ar(out,Pan2.ar(sig.dup,pan));
)

~rec = HypoRecorder('test');
~recClock = TempoClock.new(TempoClock.default.tempo);

~rec.record();
~rec.stop;

(
Ndef(\lead, { | out, freq = 48, relTime = 2 |
var sig = 0, temp, env, curv;
out.postln;
// curv = [\step, \sin, \wel].scramble;
env = EnvGen.kr(Env.perc(0.5, releaseTime:relTime, curve: \step), doneAction: 2);
8.do{ | i |
temp = LFPulse.ar(freq + Rand(0, i), LFPulse.kr(Rand(0, i).round(rrand(0.125, i))).midicps)!2 / 8;
sig = sig + temp * env * 0.9;
Out.ar(out, sig * 0.05);
}
})
)

(
\type, \midi,
\midiout, m,
\midicmd, \noteOn,
\chan, 3,
\mclock, ~recClock,
\recordTarget, ~rec.smf,
));

Pbind(
\dur, Pseq([0.125, 0.5, 1, 2, 0.25, 0.125, 0.125, 0.5, Rest(4), Rest(2), Rest(1)].scramble, inf),
\degree, Pseq(Scale.hijaz.degrees.mirror.scramble -5, inf),
\octave, Pwhite(2, 4, inf).round(1),
\relTime, Pseq([1, 2, 3, 0.5], inf),
))
)

(
)
)

(
Ndef(\hh).play;
Pdef(\hhmidi,
Pbind(
\type, \midi,
\midiout, m,
\midicmd, \noteOn,
\chan, 0,
\mclock, ~recClock,
\recordTarget, ~rec.smf,
));

Pdef(\hhsynth,
Pbind(
\instrument, \bplay,
\out, Pfunc(Ndef(\hh).bus.index),
\group, Pfunc(Ndef(\hh).group),
\buf, d["Hats"][1],
));

Pdef(\hhseq,
Pbind(
\dur, Pbjorklund2(Pseq([3, 5, 7, 9, 11], inf), Pseq([12, 4, 6, 7], inf)),
));
)
(

Pdef(\hh,
Ppar([
Pdef(\hhmidi),
Pdef(\hhsynth),
])
<> PtimeClutch(Pdef(\hhseq))
);
)

(
Ndef(\b).play;
Pdef(\bmidi,
Pbind(
\type, \midi,
\midiout, m,
\midicmd, \noteOn,
\chan, 1,
\mclock, ~recClock,
\recordTarget, ~rec.smf,
));

Pdef(\bsynth,
Pbind(
\out, Pfunc(Ndef(\b).bus.index),
\group, Pfunc(Ndef(\b).group),
\instrument, \bplay,
\buf, d["Bass Drums"][5],
));

Pdef(\bseq,
Pbind(
// \dur, 0.125,
\dur, 1,
\amp, 0.4
));
)
(

Pdef(\b,
Ppar([
Pdef(\bmidi),
Pdef(\bsynth),
])
<> PtimeClutch(Pdef(\bseq))
)
)

Pdef(\hh).play(quant: -1);
Pdef(\hh).stop;

Ndef(\hh)[10] = \filter -> { | in | DelayC.ar(in, 3, 5) };
Ndef(\hh).set(\wet10, 0.5);

Pdef(\b).play(quant: -1);
Pdef(\b).stop;

Ndef(\b)[10] = \filter -> { | in | DelayC.ar(in, 6, 5) };
Ndef(\b).set(\wet10, 0.5);


And I have to apologize a bit for the messy “Usage” code here.

Firstly, I am using samples in a start-up file not readily available. Secondly, I’ve converted some ProxySpace code to Ndef equivalents and a lot of code may not be totally working.

The idea is though - using samples for beats ensures I’m sort of on the grid and easily spots problems (plus something I will likely take advantage of). That’s using a SynthDef. Otherwise, I imagine I’d be creating sound using Ndefs/SynthDefs. So I included what I’m working on currently as examples.

It might be better if I used a simpler example of execution, and I will post a working example when I feel I’ve gotten to that point.

I think what you want is to create a new clock when you start playback: ~clock = TempoClock(1, 0)
The first arg is the tempo, second is the beats to start at. The third arg is for seconds, but I believe it really means something like “count beats as if they started from this absolute time” - I’m not sure if this is an unclear API, or just a bug - but regardless, this argument isn’t so useful.

The \tempo key in events is basically turned into thisThread.clock.tempo = ~tempo. Setting the tempo NOT from an Event - e.g. from “outside” the clock does not provide reliable timing, so it should be avoided. You can also use e.g. ~clock.sched and related methods to do a tempo change at a specific time.

If I understand your scenario right, you have two options w/r/t tempo:

1. You can record notes based on clock.seconds. SuperCollider would then be handling all tempo changes, and your DAW project would consume the “rendered” times and could keep a fixed tempo. Because seconds is always started from some point in the far past, you’d probably need to track what your “start seconds” is (get it from TempoClock:seconds when you start playback), and then add notes at e.g. TempoClock.seconds - startSeconds).
2. You can record notes baed on clock.beats AND record tempo changes as MIDI events. Then, you should end up with a MIDI file that may have tempo changes, and your DAW would be responsible for interpreting these and changing tempo. You should be able to do this in your recording code by simply checking whether thisThread.clock.tempo changed since the last event, and generating a MIDI tempo change event accordingly. You might need to be cautious about whether this tempo change goes before or after the note…

You’re probably unsure because it’s super confusing . It’s probably good to separate Event patterns into two parts:

1. Event generation
A Pbind steps through each key, requests the next value for the associated Stream, and adds it to the Event it’s building. This is generally done in a context of whatever currentEnvironment you had when you ran .play.
The current in-progress Event is passed as the input value to the Stream. This is why e.g. Pfunc({ }) gets the current Event as it’s first argument. Usually this doesn’t matter, because mostly Patterns just produce new values rather than modify values passed in to them (but some do!).
2. Event playback
Once a Pbind or other event-construction thing is finished generating a new event, Event:play is called. This triggers the call to ~play, calls the ~eventType[~type] function, etc etc. All of this occurs with the current Event as the environment, so ~envVariables point to Event keys. Usually if an Event value is a function it is evaluated, which means that function can also access Event keys with ~envVars.

There is nothing special about Pdef related to any of the above scope stuff - in 99% of cases, a Pbind wrapped in a Pdef should behave the same as the raw Pbind itself.

I think, at least for now, this is really what I’m after. I’m not expecting the DAW to necessarily track all changes to tempo (but would be a huge benefit if it could). Mainly I’m just trying to keep the midi events in sync with the audio output. This would give great flexibility towards using that MIDI data for other instruments and editing. I had thought long and hard about exactly what you say here about TempoClock.seconds - startSeconds - because that was obviously what was wrong with what I have. That probably is easier than what I was making out of it - especially with my default TempoClock issues. I’m trying to start small and make it more robust as I go.

One question about the code above with 3 Pdefs to play midi and synth with one pattern driving both.

This all works, especially with the SynthDef example.

However, when instead try and do the same with a NodeProxy, using NodeProxy Roles, The MIDI is not being triggered:

m = MIDIOut(0, MIDIClient.destinations[0].uid); \\ this is actually in my linux startup.scd
(
Ndef(\a1, { | out, freq |
var env = EnvGen.kr(Env.perc(0.01, releaseTime: Rand(0.0, 2.0), doneAction: 2));
// freq = freq.midicps;
Out.ar(out, LFSaw.ar(freq!2, 0, SinOsc.kr(LFPulse.kr(550, 0.5, 1)), 0, 0.3)) * env }
);
)

(
Pdef(\a1midi, Pbind(\type, \midi, \midiout, m, \midicmd, \noteOn, \chan, 0,));
Pdef(\a1synth, Pbind(\out, Pfunc(Ndef(\a1).bus.index), \group, Pfunc(Ndef(\a1).group)));
Pdef(\a1src,
Pbind(
\degree, Pseq([1,4,5,7], inf),
\octave, Pshuf([2,3,4],inf),
\dur, Pseq([Pbjorklund2(2, 5, 3), Rest(2)], inf),
\amp, 0.3
));
)

(
Pdef(\a1, 	Ppar([
Pdef(\a1midi),
Pdef(\a1synth),
])
<> PtimeClutch(Pdef(\a1src))
);
)

Ndef(\a1)[1] = \xset -> Pdef(\a1);


So, when I do that \xset on slot 1 of the Ndef it does play as expected, but no MIDI events are being triggered.

If I exec,  Pdef(\a1).play it does what it should but doesn’t obviously use the Ndef for the synth - and of course running them both is nowhere near in sync or the same.