electro-music.org   Dedicated to experimental electro-acoustic
and electronic music
 
 Front Page  |  Radio  |  Forum  |  Media  |  Playlists  |  Wiki
Forum with support of Syndicator RSS
 FAQFAQ   CalendarCalendar   SearchSearch   MemberlistMemberlist   UsergroupsUsergroups   LinksLinks
 RegisterRegister   ProfileProfile   Log in to check your private messagesLog in to check your private messages   Log inLog in  Chat RoomChat Room 
 Forum index » DIY Hardware and Software » ChucK programming language
MIDI looper without sync
Post new topic   Reply to topic Moderators: Kassen
Page 1 of 1 [1 Post]
View unread posts
View new posts in the last week
Mark the topic unread :: View previous topic :: View next topic
Author Message
Antimon



Joined: Jan 18, 2005
Posts: 4145
Location: Sweden
Audio files: 373
G2 patch files: 100

PostPosted: Sun Jun 05, 2011 6:26 am    Post subject: MIDI looper without sync Reply with quote  Mark this post and the followings unread

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
Back to top
View user's profile Send private message Visit poster's website
Display posts from previous:   
Post new topic   Reply to topic Moderators: Kassen
Page 1 of 1 [1 Post]
View unread posts
View new posts in the last week
Mark the topic unread :: View previous topic :: View next topic
 Forum index » DIY Hardware and Software » ChucK programming language
Jump to:  

You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You can download files in this forum
Log in
Username:     Password:      Log me on automatically each visit    


Forum with support of Syndicator RSS
Powered by phpBB © 2001, 2005 phpBB Group
Copyright © 2003 through 2026 by electro-music.org