Midi into spreadsheet

Is there a way to get notes i play on a midi keyboard into a spreadsheet? Like if I am in a cell and play middle C, “C3” gets entered into the cell?

Maybe Bome for the MacBook or some other midi translator?

My idea is to build a table that gets processed by SC like it was “tracker” style sequencer data.

Thanks!

Does this need to be live? It would be much easier to record a midi file and use python to turn it into a CVS, or even use the excel python library

For me yes it has to be as live as possible.

I will likely try Bome Midi Translator, though of course I would prefer a free approach.

If not live, a midi monitor could dump the text file and then python etc to process it.

I’ve used midistroke before with good results:

http://www.charlie-roberts.com/midiStroke/

Could you suggest a concrete example of the spreadsheet you are thinking of, with a minimal number of rows and columns?

If you’ve got an Arduino (or something similar) kicking about this would be pretty simple to program as both the midi and keyboard libraries are quite simple.

Say 2 columns 16 rows. Column 1 would note number or null for rest, column 2 the velocity.

The idea is to load and play it in SC with code I will write later;)

So… hmm… MIDI data → SC MIDI responder. Then some magic to stuff it into a spreadsheet (right now). Then later, spreadsheet → SC.

An alternate model might be: MIDI data → SC MIDI responder → collection in SC memory. SC could save the data directly, no spreadsheet (either using SC’s archive format, or CSV). If you want to see the numbers pop up in real time, a GUI window in SC could do that. And SC can read CSV.

Is it really necessary to insert a spreadsheet? That sounds more complex, harder, and easier to break.

hjh

My main thing is to see a table of notes as I enter them, a la Renoise or the like. Displaying a couple of numbers in a GUI as SC takes the midi would be easy, but there is no SC table widget to my knowledge.

Thanks for indulging my speculation in all this…

To show what I understood, I write an example with 6 rows as follows:

60 64
60 0
64 80
67 96
67 0
64 0

Did I correctly understand you?

Yes you understood me.

like as follows?

(
s.waitForBoot{
	var notes, on, off, history, baseFolder, path, fileCount;
	MIDIClient.init;
	MIDIIn.connectAll;
	notes = Array.newClear(128);
	history = [];
	baseFolder = thisProcess.nowExecutingPath.dirname;
	fileCount = 0;
	~exportCSV = {
		var file, csv;
		path = baseFolder +/+ "test" + fileCount ++ ".csv" ;
		file = File(path, "w");
		csv = history.cs
		.replace("[ [ ", "pitch, velocity\n")
		.replace(" ], ", "\n")
		.replace("[ ", "")
		.replace("] ]", "")
		.replace("]", "");
		file.write(csv);
		(path.quote + "was saved.").postln;
		file.close;
		fileCount = fileCount + 1;
		("The number of saved files:" + fileCount).postln
	};
	~importTheLastCSV = {
		var file = File(path, "r");
		(path.quote + "was read.").postln;
		file.readAllString.postln;
		file.close };
	~openTheLastCSV = { path.openOS };
	~reset = { history = [] };
	~quit = {
		MIDIdef(\on).free;
		MIDIdef(\off).free;
		MIDIIn.disconnectAll;
		~exportCSV = nil;
		~importTheLastCSV = nil;
		~openTheLastCSV = nil;
		~reset = nil;
		~quit = nil;
	};

	MIDIdef.noteOn(\on, { |veloc, num, chan, src|
		notes[num] = Synth(\default,
			[
				\freq, num.midicps,
				\amp, veloc.linlin(0, 127, -60, -6).dbamp
			]);
		[num, veloc].postln;
		history = history.add([num, veloc])});

	MIDIdef.noteOff(\off, { |veloc, num, chan, src|
		[num, veloc].postln;
		notes[num].release;
		notes[num] = nil;
		history = history.add([num, veloc])})
}
)

~exportCSV.()
~importTheLastCSV.()
~openTheLastCSV.()
~reset.()

~quit.()

TreeView can display tables, but unfortunately they forgot about scrolling under programmatic control. As far as I can see, there is no way to force the display to the bottom of the list. Will need to log a feature request for that. Any view whose contents can exceed the visible display (ScrollView, TextView, TreeView), you should be able to control the visible portion in code, not only by mousing over to the scroll bars. ScrollView can but the other two are IMO incomplete.

In any case, you could also make a tabular display out of base widgets.

The trouble with the spreadsheet is that you’d have to be 100% certain that any keystrokes you emit to it would be followed in the exact order, without dropping any. Otherwise you have no idea where in the worksheet you are. My gut feeling is that it may not work as well as you expect. Interprocess communication is inherently more complex than working within one process. It’s even more complicated when one of those processes is not designed to cooperate with other apps. (Though if you did get it working, there would be a bit of punk aesthetic onstage… “Wait, whaa… that’s Excel…?”)

If Office Visual Basic has an Open Sound Control module :laughing: then it might be made more reliable.

hjh

Turns out this is not correct – it’s just that the interface is really weird for it (the kind of interface you’d design if you’re rushed for time and have no plans to use the thing yourself).

myTreeView.currentItem = myTreeView.itemAt(myTreeView.numItems - 1);

I might have to propose adding some methods to simplify this…

In any case, this does jump to the bottom, so you could use TreeView for your display.

hjh

I revised the code above as follows:

(
s.waitForBoot{
	var notes, on, off, csv, baseFolder, path, write;
	MIDIClient.init;
	MIDIIn.connectAll;
	notes = Array.newClear(128);
	csv = "pitch, velocity\n";
	baseFolder = thisProcess.nowExecutingPath.dirname;
	path = baseFolder +/+ "test.csv" ;
	write = {
		var file;
		file = File(path, "w");
		file.write(csv);
		file.close
	};
	~open = { path.openOS };
	~quit = {
		MIDIdef(\on).free;
		MIDIdef(\off).free;
		MIDIIn.disconnectAll;
		~open.()
	};

	MIDIdef.noteOn(\on, { |veloc, num, chan, src|
		notes[num] = Synth(\default,
			[
				\freq, num.midicps,
				\amp, veloc.linlin(0, 127, -60, -6).dbamp
		]);
		(num.cs + veloc).postln;
		csv = csv + num ++ "," + veloc ++ "\n"});

	MIDIdef.noteOff(\off, { |veloc, num, chan, src|
		(num.cs + veloc).postln;
		notes[num].release;
		notes[num] = nil;
		csv = csv + num ++ "," + veloc ++ "\n";
		write.() })
}
)

~open.()
~quit.()

Honestly, I think you might also need each time point of pressing keys and each duration of the pressed keys as follows:

(
s.waitForBoot{
	var checkTime, started, notes, on, off, csv, baseFolder, path, write;
	MIDIClient.init;
	MIDIIn.connectAll;
	notes = Array.newClear(128);
	csv = "note-on times, pitches, velocities, durations\n";
	baseFolder = thisProcess.nowExecutingPath.dirname;
	path = baseFolder +/+ "test.csv" ;
	write = {
		var file;
		file = File(path, "w");
		file.write(csv);
		file.close
	};
	~open = { path.openOS };
	~quit = {
		MIDIdef(\on).free;
		MIDIdef(\off).free;
		MIDIIn.disconnectAll;
		~open.()
	};
	checkTime = { Main.elapsedTime };

	MIDIdef.noteOn(\on, { |vel, num, chan, src|
		var  synth, pressed;
		synth = Synth(\default, [
			\freq, num.midicps,
			\amp, vel.linlin(0, 127, -60, -6).dbamp
		]);
		pressed = checkTime.() - started;
		notes[num] = [synth, vel, pressed];
		(num.cs + vel).postln});

	MIDIdef.noteOff(\off, { |vel, num, chan, src|
		var noteOn, noteOnVel, pressed, released, dur;
		noteOn = notes[num];
		noteOnVel = noteOn[1];
		pressed = noteOn[2];
		notes[num][0].release;
		notes[num][0] = nil;
		released = checkTime.() - started;
		dur = (released - pressed).cs.padRight(16, "0");
		(num.cs + vel).postln;
		csv = csv +
		pressed.cs.padRight(15, "0") ++ "," +
		num ++ "," +
		noteOnVel ++ "," +
		dur ++ "\n";
		write.() });

	started = checkTime.();
}
)

~open.()
~quit.()

Wow! By using “append” mode of the class File, we can record (save as a CSV file) the MIDI events in real time in SuperCollider:

(
s.waitForBoot{
	var checkTime, started, notes, on, off, csv, baseFolder, path, file, write;
	MIDIClient.init;
	MIDIIn.connectAll;
	notes = Array.newClear(128);
	csv = "time, pitch, note-on velocity, note-off velocity, duration, channel\n";
	baseFolder = thisProcess.nowExecutingPath.dirname;
	path = baseFolder +/+ "test.csv" ;
	write = {
		file = File(path, "a");
		file.write(csv);
		file.close };
	~open = { path.openOS };
	~quit = {
		MIDIdef(\on).free;
		MIDIdef(\off).free;
		MIDIIn.disconnectAll;
		~open.() };
	checkTime = { Main.elapsedTime };

	MIDIdef.noteOn(\on, { |vel, num, chan, src|
		var  synth, pressed;
		synth = Synth(\default, [
			\freq, num.midicps,
			\amp, vel.linlin(0, 127, -60, -6).dbamp
		]);
		pressed = checkTime.() - started;
		notes[num] = [synth, vel, pressed];
		(num.cs + vel).postln});

	MIDIdef.noteOff(\off, { |vel, num, chan, src|
		var noteOn, noteOnVel, pressed, released, dur;
		noteOn = notes[num];
		noteOnVel = noteOn[1];
		pressed = noteOn[2];
		notes[num][0].release;
		notes[num][0] = nil;
		released = checkTime.() - started;
		dur = (released - pressed).cs.padRight(16, "0");

		(num.cs + vel).postln;

		csv = (
			pressed.cs.padRight(15, "0") ++ "," +
			num ++ "," +
			noteOnVel ++ "," +
			vel ++ "," +
			dur ++ "," +
			chan.cs ++ "\n");

		write.() });

	started = checkTime.();
	write.()
}
)

~open.()
~quit.()
(
// 'note' data structure will be an event:
// (time: absolute time, midinote: num, amp: velocity/127, sustain: dur)
// you could use an array here too
~notes = Array(256);
~rows = Array(256);

t = TreeView(nil, Rect(600, 100, 400, 600)).front;
t.columns = ["Time", "Note", "Velocity", "Duration"];
4.do { |i| t.setColumnWidth(i, 100) };

MIDIdef.noteOn(\on, { |vel, num|
	var time = TempoClock.beats;
	~notes = ~notes.add((time: time, midinote: num, amp: vel / 127));
	defer {
		var row;
		row = t.addItem([time.round(0.01).asString, num, vel, "--"]);
		~rows = ~rows.add(row);
		t.currentItem = row;  // scroll to bottom
	};
});

MIDIdef.noteOff(\off, { |vel, num|
	var time = TempoClock.beats;
	var i = ~notes.detectIndex { |note|
		note[\midinote] == num and: { note[\sustain].isNil }
	};
	if(i.isNil) {
		"Could not find note-on for note-off %".format(num).warn;
	} {
		~notes[i][\sustain] = time - ~notes[i][\time];
		defer {
			~rows[i].strings = ~rows[i].strings
			.put(3, ~notes[i][\sustain].round(0.01).asString);
		};
	};
});

t.onClose = {
	MIDIdef(\on).free;
	MIDIdef(\off).free;
};
)

Now ~notes is something you could play back directly, and save to / load from disk, etc.

hjh

1 Like