Antimon

Joined: Jan 18, 2005 Posts: 4145 Location: Sweden
Audio files: 373
G2 patch files: 100
|
Posted: Sun Jun 05, 2011 6:26 am Post subject:
MIDI looper without sync |
 |
|
I'm working on this to be used at the Midsummer event here, and I was asked to post it as an example on how you might work with MIDI and timing and stuff.
It's written to be used by my monome 256, but if you edit out the OSC bits and add your own calls to the click method close to the bottom, you might make use of it with other control surfaces or the computer keyboard.
I often get complaints that I don't write enough comments (I try to use long variable and method names instead), so if you're wondering about some aspect on this feel free to ask questions in this thread.
Also, it's a bit of a work in progress, so there may be bugs even though it seems to be working ok at the moment.
/Stefan
| Code: |
//================ Constants ================
// These select MIDI devices used for note input and output
4 => int DEST_DEVICE;
2 => int SOURCE_DEVICE;
//===========> Roll the dice =========>
// This is just some experimental calls that may or may not have any useful effect
// please ignore if it confuses you
Math.rand2(0,3);
Math.rand2(0,3);
Math.rand2(0,3);
Math.rand2(0,3);
Math.rand2(0,3);
//============ OSC / monome communication ============
OscSend xmit;
xmit.setHost("localhost", 8080);
OscRecv orec;
8000 => orec.port;
orec.listen();
orec.event("/midilooper/press,i,i,i") @=> OscEvent pressEvent;
// This is used when using several OSC apps with the same monome
1 => int interfaceIsMine;
fun void lightKey(int x, int y, int state) {
if (interfaceIsMine) {
// <<< "lightkey: ", x, y, state >>>;
xmit.startMsg("/midilooper/led", "i i i");
x => xmit.addInt;
y => xmit.addInt;
state => xmit.addInt;
}
}
fun void clear(int state) {
if (interfaceIsMine) {
if (state) {
for (4 => int x; x < 12; x++) {
lightKey(x, 4, 1);
lightKey(x, 12, 1);
}
} else {
xmit.startMsg("/midilooper/clear", "i");
0 => xmit.addInt;
}
}
}
//=================> MIDI =============>
MidiOut midiOut;
midiOut.open(DEST_DEVICE);
<<< "Opened midi out: ", midiOut.name() >>>;
fun void midiNoteMsg(int channel, int note, int velocity) {
MidiMsg midiMsg;
0x90 + channel => midiMsg.data1;
note => midiMsg.data2;
velocity => midiMsg.data3;
midiOut.send(midiMsg);
}
// This will send a note on followed by a note off, and hang this shred
// for some time (duration) in between.
fun void playNote(int channel, int note, int velocity, dur duration) {
// <<< "Play note ", channel, note, velocity, duration/second >>>;
midiNoteMsg(channel, note, velocity);
duration => now;
midiNoteMsg(channel, note, 0);
}
//================ Note Routing ================
// Used to run note events in a separate mother shred, so notes don't
// get cut off when the players stop, causing hanging notes.
// This is due to the fact that when a shred finishes (by exiting gracefully
// or getting killed) it ungracefully kills all shreds it has spawned.
class NotePlayerEvent extends Event {
int note;
int channel;
int velocity;
dur duration;
fun void trigger(int inChannel, int inNote, int inVelocity, dur inDuration) {
inChannel => channel;
inNote => note;
inVelocity => velocity;
inDuration => duration;
signal();
}
fun void playMyNote() {
spork ~ playNote(channel, note, velocity, duration);
}
}
NotePlayerEvent notePlayerEvent;
fun void noteRouter() {
while (true) {
notePlayerEvent => now;
notePlayerEvent.playMyNote();
}
}
spork ~ noteRouter();
Event auditionEvent;
int auditionNoteOff;
fun void auditionNote(int channel, int note, int velocity) {
// <<< "Audition ", channel, note, velocity >>>;
if (velocity > 0) {
// Note on
midiNoteMsg(channel, note, velocity);
// Wait until we get signalled and the note off matches my note
// This assumes that we can never get two note offs at the exact same moment
do {
auditionEvent => now;
} while (auditionNoteOff != note);
// Note off
midiNoteMsg(channel, note, 0);
} else {
note => auditionNoteOff;
auditionEvent.broadcast();
}
}
//===========> The Loopers ============
// Represents a note on + off event pair, and the times they occur,
// relative to some origin on the time axis
class NoteEvent {
dur timeOn;
0::ms => dur timeOff;
int channel;
int note;
int velocity;
}
// The main class for dealing with MIDI notes in this application.
// Contains an array of note events, a state describing wether this looper
// is currently recording or playing or neither
class NoteArrayLooper {
0::ms => dur duration;
NoteEvent notes[0];
time recStart;
int recording;
1 => int resetLoopLengthOnRecord;
//=======> recorder =========>
fun int findSucceedingEvent(dur targetTime) {
for (0 => int i; i < notes.size(); i++) {
if (notes[i].timeOn >= targetTime) {
return i;
}
}
return notes.size();
}
fun void insertNewEvent(dur timeOn, int channel, int note, int velocity) {
findSucceedingEvent(timeOn) => int noteIndex;
// Move notes out of the way
notes.size(notes.size()+1);
for (notes.size()-1 => int i; i > noteIndex; i--) {
notes[i-1] @=> notes[i];
}
new NoteEvent @=> notes[noteIndex];
timeOn => notes[noteIndex].timeOn;
channel => notes[noteIndex].channel;
note => notes[noteIndex].note;
velocity => notes[noteIndex].velocity;
}
fun void setNoteOffTime(dur timeOff, int note) {
for (0 => int i; i < notes.size(); i++) {
if (notes[i].note == note && notes[i].timeOff == 0::ms) {
timeOff => notes[i].timeOff;
}
}
}
fun void noteEvent(int channel, int note, int velocity) {
// <<< "Note event: ", channel, note, velocity >>>;
if (recording) {
now - recStart => dur offset;
// Confine the time offset to the duration, unless resetLoopLengthOnRecord is set or the duration hasn't been set yet
if (!resetLoopLengthOnRecord && duration > 0::ms) {
while (offset > duration) {
duration -=> offset;
}
}
if (velocity > 0) {
// Note on
insertNewEvent(offset, channel, note, velocity);
} else {
// Note off
setNoteOffTime(offset, note);
}
}
}
fun void startRecording() {
if (!recording) {
1 => recording;
now => recStart;
stopPlaying();
if (resetLoopLengthOnRecord) {
0::ms => duration;
}
notes.size(0);
}
}
fun void stopRecording() {
if (recording) {
0 => recording;
if (resetLoopLengthOnRecord || duration <= 0::ms) {
now - recStart => duration;
}
}
}
//============ player ============
0 => int playing;
0 => int channelOverride;
0 => int playNoteIndex;
Event playEvent;
fun void nextEvent(dur timeToWait) {
timeToWait => now;
playEvent.signal();
}
// gets the channel. If channelOverride is set to something other than
// zero, a channel will be randomized for one of the channels found by
// looking at channelOverride's bits (bit 0 => channel 0, bit 1 => channel 1 etc)
fun int getChannel(int noteChannel) {
if (channelOverride == 0) {
return noteChannel;
} else {
0 => int overrideCount;
for (0 => int i; i < 4; i++) {
(channelOverride & (1 << i)) => int channelCheck;
if (channelCheck) {
overrideCount++;
}
}
Math.rand2(0, overrideCount-1) => int thisOverride;
0 => int overrideIndex;
for (0 => int i; i < 4; i++) {
(channelOverride & (1 << i)) => int channelCheck;
if (channelCheck) {
if (overrideIndex++ == thisOverride) {
return i;
}
}
}
}
// Shouldn't happen, but if we end up here, return noteChannel
<<< "unexpected in getChannel", channelOverride, noteChannel >>>;
return noteChannel;
}
// Extract data from the note event and send it to the note playing router
fun void playNoteEvent(NoteEvent @ noteEvent) {
dur noteDuration;
if (noteEvent.timeOff < noteEvent.timeOn) {
noteEvent.timeOff - noteEvent.timeOn + duration => noteDuration;
} else {
noteEvent.timeOff - noteEvent.timeOn => noteDuration;
}
getChannel(noteEvent.channel) => int thisNotesChannel;
notePlayerEvent.trigger(thisNotesChannel, noteEvent.note, noteEvent.velocity, noteDuration);
}
// This is run as a shred, reading from the note array and sending note events
// in a loop until playing is set to false. The waiting is done using the
// playEvent rather than using the actual time directly => now, so that we can
// end the playLoop quickly and gracefully (I have this thing about killing
// shreds... probably irrational, I know).
fun void playLoop() {
<<< "Play loop starting" >>>;
while (playing) {
notes[playNoteIndex].timeOn => dur nextTime;
dur previousTime;
if (playNoteIndex > 0) {
notes[playNoteIndex-1].timeOn => previousTime;
} else {
// If at start of note array, we assume that the current time
// matches the starting point
0::ms => previousTime;
}
nextTime - previousTime => dur timeToWait;
if (timeToWait > 0::ms) {
spork ~ nextEvent(timeToWait);
playEvent => now;
} else {
<<< "Unexpected: timeToWait <= zero: ", (timeToWait/1::second) >>>;
}
// Do a second check on the playing switch, since it may have been set
// to false while we were waiting
if (playing) {
// play note
playNoteEvent(notes[playNoteIndex]);
// advance index, possibly waiting at the end of the array
playNoteIndex++;
if (playNoteIndex >= notes.size()) {
duration - notes[notes.size()-1].timeOn => dur timeToEnd;
if (timeToEnd >= 0::ms) {
spork ~ nextEvent(timeToWait);
playEvent => now;
} else {
<<< "Unexpected: timeToEnd <= zero: ", (timeToWait/1::second) >>>;
}
0 => playNoteIndex;
}
}
}
}
// Set status and start up playLoop shred
fun void startPlaying() {
if (!playing && notes.size() > 0 && duration > 0::ms) {
1 => playing;
0 => playNoteIndex;
spork ~ playLoop();
}
}
// Set status and signal playLoop shred, so it may exit gracefully
fun void stopPlaying() {
if (playing) {
0 => playing;
playEvent.signal();
}
}
// Set the lights on the monome control surface for this looper
fun void repaint(int dx, int dy) {
lightKey(dx + 0, dy + 0, playing);
for (0 => int x; x < 4; x++) {
1 << x => int expo;
channelOverride & expo => int onOrOff;
if (onOrOff > 0) {
lightKey(dx + x, dy + 1, 1);
} else {
lightKey(dx + x, dy + 1, 0);
}
}
lightKey(dx + 0, dy + 2, recording);
lightKey(dx + 1, dy + 2, resetLoopLengthOnRecord);
}
// Called when a button has been pressed on the monome control surface
// for this looper.
// Returns 1 when the click caused a change in the state of this thing,
// which may prompt a call to repaint() later on.
fun int click(int x, int y) {
// <<< "looper click ", x, y>>>;
0 => int dirty;
if (y == 0) {
if (x == 0) {
if (playing) {
stopPlaying();
} else {
startPlaying();
}
1 => dirty;
}
} else if (y == 1) {
(channelOverride ^ (1 << x)) => channelOverride;
1 => dirty;
} else if (y == 2) {
if (x == 0) {
if (recording) {
stopRecording();
startPlaying();
} else {
stopPlaying();
startRecording();
}
1 => dirty;
} else if (x == 1) {
1 - resetLoopLengthOnRecord => resetLoopLengthOnRecord;
1 => dirty;
}
} else if (y == 3) {
}
return dirty;
}
}
//========== Array storage ============
NoteArrayLooper noteLoopers[4*3];
//========== Note input / Audition ============
// Here we handle input from a MIDI keyboard. This may be routed directly
// to a MIDI destination, if audition is set to true.
1 => int audition;
[1,1,1,1] @=> int auditionDests[]; // bitwise random mux to four destinations
// dx, dy points at a 4x4 square on the monome
fun void repaintAudition(int dx, int dy) {
lightKey(dx + 0, dy + 2, audition);
for (0 => int x; x < 4; x++) {
lightKey(dx + x, dy + 3, auditionDests[x]);
}
}
// x,y points inside a 4x4 square on the monome
fun int clickAudition(int x, int y) {
if (y == 3) {
1 - auditionDests[x] => auditionDests[x];
return 1;
} else if (y == 2) {
if (x == 0) {
1 - audition => audition;
return 1;
}
}
return 0;
}
// Calculate a MIDI channel to send data to. This will be a random channel
// selected from the currently active found in the auditionDests array.
fun int getDestinationChannel() {
0 => int auditionDestCount;
for (0 => int i; i < auditionDests.size(); i++) {
if (auditionDests[i]) {
auditionDestCount++;
}
}
if (auditionDestCount > 0) {
Math.rand2(0,auditionDestCount-1) => int dest;
0 => int destI;
for (0 => int i; i < auditionDests.size(); i++) {
if (auditionDests[i]) {
if (destI++ == dest) {
return i;
}
}
}
}
return -1;
}
// A MIDI note message has been received from the MIDI source
fun void noteInMsg(int note, int velocity) {
// <<< "NoteInMsg ", note, velocity >>>;
getDestinationChannel() => int channel;
if (channel > -1) {
for (0 => int i; i < noteLoopers.size(); i++) {
noteLoopers[i].noteEvent(channel, note, velocity);
}
if (audition) {
spork ~ auditionNote(channel, note, velocity);
// This delay is required for the auditioning to work
// Otherwise note offs will collide.
0.001::ms => now;
}
}
}
//========== MIDI input ============>
MidiIn midiIn;
midiIn.open(SOURCE_DEVICE);
<<< "Opened midi in: ", midiIn.name() >>>;
// We're listening to all channels on the opened MIDI device.
// If you want to check the MIDI channel you get it by doing
// (midiMsg.data1 & 0x0f), after you have read the message.
fun void midiListener() {
MidiMsg midiMsg;
while (true) {
midiIn => now;
while (midiIn.recv(midiMsg)) {
midiMsg.data1 & 0xf0 => int msg;
if (msg == 0x90) {
// Note On, possibly off with zero velocity
noteInMsg(midiMsg.data2, midiMsg.data3);
} else if (msg == 0x80) {
// Note off
noteInMsg(midiMsg.data2, 0);
}
}
}
}
spork ~ midiListener();
//========= High level interface stuff ==========
// Refresh the lights on all keys on the monome control surface
fun void refresh() {
for (0 => int x; x < 4; x++) {
for (0 => int y; y < 3; y++) {
noteLoopers[y*4 + x].repaint(x*4, y*4);
}
}
repaintAudition(12,12);
}
// Someone has clicked a key on the monome control surface
fun int click(int x, int y, int state) {
if (state) {
if (y < 12) {
y/4 => int yIndex;
x/4 => int xIndex;
noteLoopers[yIndex*4 + xIndex] @=> NoteArrayLooper @ looper;
if (looper.click(x % 4, y % 4)) {
looper.repaint(xIndex*4, yIndex*4);
}
}
if (x >= 12 && y >= 12) {
if (clickAudition(x % 4, y % 4)) {
repaintAudition(12, 12);
}
}
}
return 0;
}
// Do a startup animation
fun void resetInterface() {
clear(1);
500::ms => now;
clear(0);
refresh();
}
//========= OSC takeover ===========
// This is used when using multiple OSC apps with the same monome
// ignore if you're not interested in this stuff, it isn't necessary
fun void readTakeover() {
OscRecv comOscRecv;
8001 => comOscRecv.port;
comOscRecv.listen();
comOscRecv.event("/midilooper/takeover,i") @=> OscEvent takeoverEvent;
while (true) {
takeoverEvent => now;
while (takeoverEvent.nextMsg() != 0) {
takeoverEvent.getInt() => interfaceIsMine;
<<< "Midilooper got takeover message: " + interfaceIsMine >>>;
resetInterface();
}
}
}
spork ~ readTakeover();
//==== Main loop ====
resetInterface();
// Listen for OSC input from the monome control surface
fun void oscListener() {
while (true) {
pressEvent => now;
while (pressEvent.nextMsg() != 0) {
pressEvent.getInt() => int x;
pressEvent.getInt() => int y;
pressEvent.getInt() => int keyState;
click(x, y, keyState) => int newState;
}
}
}
spork ~ oscListener();
//=============> Main loop ==============
<<< "Entering main loop" >>>;
while (true) {
1::second => now;
}
|
_________________ Antimon's Window
@soundcloud @Flattr home - you can't explain music |
|