diff --git a/MuMIDI.h b/MuMIDI.h new file mode 100644 index 0000000..b51982f --- /dev/null +++ b/MuMIDI.h @@ -0,0 +1,99 @@ +//********************************************* +//***************** NCM-UnB ******************* +//******** (c) Carlos Eduardo Mello *********** +//********************************************* +// This softwre may be freely reproduced, +// copied, modified, and reused, as long as +// it retains, in all forms, the above credits. +//********************************************* + +/** @file MuMIDI.h + * + * @brief MIDI definitions + * + * @author Carlos Eduardo Mello + * @date 3/3/2019 + * + * @details + * + * This file describes the MIDI related data structures and definitions + * which are use throughout the MuM Library for input and output within the + * new Realtime playback and input functionality. Some of the definitions + * here were previously found in other class header files. They were + * transfered to this place to facilitate project organization and #include + * directives. + * + **/ + +#ifndef MuMIDI_H +#define MuMIDI_H + +/** + * @brief MIDI event structure + * + * @details + * + * This structure is used to describe a typical MIDI event + * associated with a time stamp. MIDI events are ued by + * MuM to output note-on and note-off info, for playback and + * sequencing. See MuNote::MIDIOn() and MuNote::MIDIOff() + * for more details. This structure is also used by MuPlayer + * in output queues and by MuRecorder in input ring buffers + **/ +struct MuMIDIMessage +{ + //! @brief MIDI status byte: [1XXXCCCC] + unsigned char status; + //! @brief MIDI data byte: pitch number (0-127) [0VVVVVVV] + unsigned char data1; + //! @brief MIDI data byte: key velocity (0-127) [0VVVVVVV] + unsigned char data2; + //! @brief time stamp in seconds + float time; +}; +typedef struct MuMIDIMessage MuMIDIMessage; + +/** + * @brief MIDI Buffer structure + * + * @details + * + * MuMIDIBuffer is a structure containing a buffer of + * MuMIDIMessages. The also structure contains two + * variables to keep track of the number of messages + * in the buffer. + * + * The 'data' field in this structure is a pointer to + * receive a dynamically allocated array of type MuMIDIMessage. + * Normally, code passing this struture is responsible for + * allocating this memory, while code in the receiving end + * should free the buffer when it is no longer needed. The + * other two fields, 'max' and 'count' control buffer size. + * 'max' should contain the maximum number of elements in + * the array. This value should be modified only once, when + * memory is allocated. 'count' stores the number of elements + * currently in use. For obvious reasons, 'count' should + * allways be less or equal to 'max'. This simple + * vector of MIDI messages is used to copy and pass + * blocks of MIDI data between various parts of MuM, + * particularly in the input class (see MuRecorder). + * + **/ +struct MuMIDIBuffer +{ + //! @brief pointer to the first message in the buffer + MuMIDIMessage * data; + //! @brief maximum number of messages allowed in the buffer + long max; + //! @brief number of used/valid messages in the buffer + long count; +}; +typedef struct MuMIDIMessage MuMIDIMessage; + +//! @brief default size for MIDI message buffers +const long DEFAULT_BUFFER_SIZE = 1024; + + + + +#endif /* MuMIDI_H */ diff --git a/MuMaterial.cpp b/MuMaterial.cpp index b9d29d8..b7a06e1 100755 --- a/MuMaterial.cpp +++ b/MuMaterial.cpp @@ -1032,7 +1032,7 @@ MuMaterial MuMaterial::GetNotes( int voiceNumber, long from, long through ) // if requested range is valid... if( ( (from >= 0) && (from < n) ) && ( (through >=0) && (through < n) ) && - ( (through > from) ) + ( (through >= from) ) ) { for( i = from; i <= through; i++ ) @@ -1131,6 +1131,57 @@ MuMaterial MuMaterial::GetNotesSoundingAt(int voiceNumber, float time) return outMaterial; } +MuMaterial MuMaterial::GetFrase(int voiceNumber, long from) +{ + MuMaterial mat; + MuNote note; + long i, through; + bool foundRest = false; + + if(voiceNumber >= NumberOfVoices()) + { + lastError.Set(MuERROR_INVALID_VOICE_NUMBER); + return mat; + } + + long n = NumberOfNotes(voiceNumber); + + if(from >= n) + { + lastError.Set(MuERROR_INVALID_PARAMETER); + return mat; + } + + // Starting at the requested note... + for(i = from; i < n; i++) + { + // check every note... + note = GetNote(i); + + // if we find a rest... + if(note.Amp() == 0.0 || note.Pitch() == 0) + { + // we point to the last note before the rest... + through = (i - 1); + // and extract the frase up to that point... + mat = GetNotes(0, from, through); + // then flag that we found the rest... + foundRest = true; + // and get out of the loop... + break; + } + } + + // if no rest was found... + if(!foundRest) + { + // return the notes through the end of the voice... + mat = GetNotes(0, from, (n-1)); + } + + return mat; +} + bool MuMaterial::Contains( int voiceNumber, int pitch ) { lastError.Set(MuERROR_NONE); @@ -1870,6 +1921,10 @@ void MuMaterial::CycleRhythm(int voiceNumber, int times) Sort(0); } +void MuMaterial::AddRestToNote(int voiceNumber, long noteNumber, float ratio) +{ + +} // Segmentation @@ -2807,6 +2862,125 @@ void MuMaterial::LoadScore(string fileName, short mode) // [PUBLIC] } } +// populates the receiving material with data from a MIDI buffer... +void MuMaterial::LoadMIDIBuffer(MuMIDIBuffer inBuffer, short mode) +{ + lastError.Set(MuERROR_NONE); + long i,j,n; + MuNote note; + MuMIDIMessage firstEvent,secondEvent; + bool foundNote = false; + + // Go through every noteOn event, comparing it to the remaining + // events, until we find a suitable note termination... + n = inBuffer.count; + for(i = 0; i < n; i++) + { + firstEvent = inBuffer.data[i]; + foundNote = false; + + // if this is a noteOn event and key velocity is not zero... + if( ( (firstEvent.status & 0xF0) == 0x90) && (firstEvent.data2 != 0) ) + { + // we look ahead for its corresponding noteOff event... + for(j = i+1; j < n; j++) + { + secondEvent = inBuffer.data[j]; + + // if next event is for the same channel and same pitch... + if(((firstEvent.status & 0x0F) == (secondEvent.status & 0x0F)) && + (firstEvent.data1 == secondEvent.data1) ) + { + // if this is a noteOff for the same pitch... + if( ((secondEvent.status & 0xF0) == 0x80) || + // or if it is a noteOn and key velocity is 0... + (((secondEvent.status & 0xF0) == 0x90) && (secondEvent.data2 == 0)) + ) + { + // we store the complete note... + note.SetFromMIDI(firstEvent, secondEvent); + AddNote(note); + note.Clear(); + // if we found a complete note, we skip the inner loop + // and move on to the next noteOn... + foundNote = true; + break; + } + } + } + + // after comparing to all remaining events, if we couldn't find + // a note termination, we store the event without duration... + if (!foundNote) + { + note.SetStart(firstEvent.time); + note.SetPitch(firstEvent.data1); + note.SetAmp(firstEvent.data2/128.0); + note.SetInstr((firstEvent.status & 0x0F) + 1); + note.SetDur(0); + AddNote(note); + note.Clear(); + + } + } + } + + // After checking the entire buffer for notes, + // decide what to do with the notes that have duration 0. + n = NumberOfNotes(); + for(i = 0; i < n; i++) + { + note = GetNote(i); + if(note.Dur() == 0) + { + switch(mode) + { + // in purge mode, remove all incomplete notes... + case MIDI_BUFFER_MODE_PURGE: + { + RemoveNote(i); + i--; + n--; + break; + } + + // in extend mode, incomplete notes last until the end + // of material. if note starts beyond the duration of + // all other notes, it gets purged. + case MIDI_BUFFER_MODE_EXTEND: + { + float dur = Dur()-note.Start(); + if (dur != 0) + { + note.SetDur(dur); + SetNote(i, note); + } + else + { + RemoveNote(i); + } + break; + } + // in melodic mode, incomlete notes last until + // the begining of next note. if note is the last + case MIDI_BUFFER_MODE_MELODIC: + { + if(i == n-1) + { + RemoveNote(i); + } + else + { + MuNote nextNote = GetNote(i+1); + note.SetDur(nextNote.Start() - note.Start()); + SetNote(i, note); + } + break; + } + } + } + } +} // Generates Orchestra Definitions... string MuMaterial::Orchestra(void) // [PUBLIC] diff --git a/MuMaterial.h b/MuMaterial.h index f9da9d3..710e020 100755 --- a/MuMaterial.h +++ b/MuMaterial.h @@ -99,6 +99,10 @@ const short EIGHTH_DEGREE = 7; const short LOAD_MODE_TIME = 0; const short LOAD_MODE_DIRECT = 1; +const short MIDI_BUFFER_MODE_PURGE = 0; +const short MIDI_BUFFER_MODE_EXTEND = 1; +const short MIDI_BUFFER_MODE_MELODIC = 2; + /** * @class MuMaterial * @@ -925,6 +929,36 @@ class MuMaterial * **/ MuMaterial GetNotesSoundingAt(int voiceNumber, float time); + + /** + * @brief Returns all the notes before the next rest + * + * @details + * GetFrase() scans the requested voice looking for a rest, starting at + * the note indicated by index 'from'. When it finds a rest, GetFrase()returns + * a material object containing (in voice 0) every note, within voice 'voiceNumber' + * of this material, between note 'from' and the last note before the rest. + * + * GetFrase() is meant to be used with voices that contain mostly melodic + * data. It also assumes that frases are separated by rests. When used with + * complex, chord-like textures, containing multiple rests, the result is + * unpredictable. If GetFrase() finds no rest in the requeste voice, + * the entire voice content is returned in the output material. + * + * (See also: in order to force frase limits without modifying the + * rhythmic flow of the music, it is possible to use AddRestToNote() to + * insert a small rest between frases. Check documentation for more info) + * + * @param + * voiceNumber (int) - voice index + * @param + * from (long) - index to starting note + * + * @return + * MuMaterial - material containing requested frase + * + **/ + MuMaterial GetFrase(int voiceNumber, long from); /** * @brief returns true if material contains input note (pitch) at voice 'voiceNumber' @@ -1388,6 +1422,34 @@ class MuMaterial **/ void CycleRhythm(int voiceNumber, int times); + /** + * @brief Turns part of the note into a rest + * + * @details + * AddRestToNote() transforms part of a note's length into silence, + * by cropping some of the note's duration and appending a rest of the + * corresponding size right after it. The size of the rest will always + * be exactly the same as that of the time taken from the end of the note, + * so that the rhythmic flow stays untouched. surrounding notes remain untouched. + * + * The third argument is optional and informs how much of the note + * should be cropped. A value of 1.0 turns the entire note into a rest; + * a value of 0.5 removes half of the note's length; and so forth. + * If no value is provided, a default value of 0.25 (1/4th rest) is used. + * + * (See also: this method can be used in conjunction with GetFrase(), to + * mark frase limits, when extracting sections of a melody from a given voice) + * + * @param + * voiceNumber (int) - voice index + * @param + * noteNumber (long ) - index of the note to be shortened + * @param + * ratio (float) - rest length as a percentage of the note (1.0 = 100%) + * + **/ + void AddRestToNote(int voiceNumber, long noteNumber, float ratio = 0.25); + // Segmentation /** @@ -2153,6 +2215,49 @@ class MuMaterial * **/ void LoadScore(string fileName, short mode = LOAD_MODE_TIME); + + + /** + * @brief + * Loads an MuMIDIBuffer into material + * + * @details + * Loads a MIDI buffer (as defined in MuMIDI.h), into the receiving material. The buffer + * is a structure that contains a pointer to an array of MIDI messages and counter variables + * that keep track of the number of messages in the array. 'inBuffer.data' contains the MID + * events to be loaded. Each event is a structure of type MuMIDIMessage. 'inBuffer.max' + * contains the size of the array, as it was allocated. 'inBuffer.count' contains the number + * of valid messages in the array. 'count' may, occasionaly be less than 'max', but it + * shoould never exceed it. + * + * MIDI buffers are mostly used for MIDI input by MuRecorder, but may be employed + * independently if there is any need to deal with music in MIDI format for some specific + * application. All related definitions are found in MuMIDI.h. + * + * LoadMIDIBuffer can be used in one of two modes. If MIDI_BUFFER_MODE_PURGE (the default) + * is selected and the method finds noteOn events with no corresponding noteOffs by the end + * of the buffer, these starting events are disarded to avoid MIDI panic situations. if + * instead the buffer was loaded in EXTEND mode (MIDI_BUFFER_MODE_EXTEND), the unpaired + * note starts will all be terminated with the last noteOff found in the buffer. In other + * words, the notes will be sustained or extended. + * + * @note + * If inBufer contains a valid MIDI buffer, LoadMIDIBuffer will release the associated memory + * after extracting its contents as notes. + * + * @param + * inBuffer (MuMIDIBuffer) - a structure containing a buffer of MIDI messages + * + * @param + * mode (short) - defines how incomplete notes are treated. MIDI_BUFFER_MODE_PURGE means + * unterminated notes are discarded; MIDI_BUFFER_MODE_EXTEND means all unterminated + * notes will be endend with the last noteOff. + * + * @return + * void - LoadMIDIBuffer returns void. If an error is found it is stored as the last error + * in the receiving material + **/ + void LoadMIDIBuffer(MuMIDIBuffer inBuffer, short mode = MIDI_BUFFER_MODE_PURGE); /** * @brief diff --git a/MuNote.cpp b/MuNote.cpp index 55124e4..5244c70 100755 --- a/MuNote.cpp +++ b/MuNote.cpp @@ -326,3 +326,15 @@ MuMIDIMessage MuNote::MIDIOff(void) noteOff.time = ( start + dur ); return noteOff; } + +void MuNote::SetFromMIDI(MuMIDIMessage noteOn, MuMIDIMessage noteOff) +{ + // This was a quick and dirty implementation!! + // FIX: perform sanity checks on input data. + // FIX: verify ranges, data types and numeric conversions + SetStart(noteOn.time); + SetPitch(noteOn.data1); + SetAmp(noteOn.data2/128.0); + SetInstr((noteOn.status & 0x0F) + 1); + SetDur(noteOff.time - noteOn.time); +} diff --git a/MuNote.h b/MuNote.h index d95488b..aab8634 100755 --- a/MuNote.h +++ b/MuNote.h @@ -27,6 +27,7 @@ #define _MU_NOTE_H_ #include "MuParamBlock.h" +#include "MuMIDI.h" // Constants @@ -71,28 +72,6 @@ struct cs_pitch }; typedef struct cs_pitch cs_pitch; -/** - * @brief Note information as MIDI events - * - * @details - * - * This structure is used to describe a note as MIDI data. - * An MuNote object can output note-on and note-off info, - * using this struct. See MuNote::MIDIOn() and MuNote::MIDIOff() - * for more details. - **/ -struct MuMIDIMessage -{ - //! @brief MIDI status byte: [1XXXCCCC] - unsigned char status; - //! @brief MIDI data byte: pitch number (0-127) [0VVVVVVV] - unsigned char data1; - //! @brief MIDI data byte: key velocity (0-127) [0VVVVVVV] - unsigned char data2; - //! @brief time stamp in seconds - float time; -}; -typedef struct MuMIDIMessage MuMIDIMessage; /** * @class MuNote @@ -598,5 +577,23 @@ class MuNote * **/ MuMIDIMessage MIDIOff(void); + + /** + * @brief Returns a deactivation event for the note as an MuMIDIMessage struct + * + * @details + * + * MIDIOff() converts the note's data to MIDI format and returns the + * note-off event for the note. Note data is assigned as follows: + * + * @return MuMIDIMessage structure + * + **/ + void SetFromMIDI(MuMIDIMessage noteOn, MuMIDIMessage noteOff); }; #endif diff --git a/MuPlayer.cpp b/MuPlayer.cpp new file mode 100644 index 0000000..3a1788d --- /dev/null +++ b/MuPlayer.cpp @@ -0,0 +1,475 @@ +// +// MuPlayer.cpp +// MuMRT +// +// Created by Carlos Eduardo Mello on 2/17/19. +// Copyright © 2019 Carlos Eduardo Mello. All rights reserved. +// + +#include "MuPlayer.h" + +bool MuPlayer::pause = false; +bool MuPlayer::stop = false; +pthread_mutex_t MuPlayer::sendMIDIlock; + +MuPlayer::MuPlayer(void) +{ + // initialize all fields of playback pool + // to default values... + for(int i = 0; i < MAX_QUEUES; i++) + { + eqPool[i].buffer = NULL; + eqPool[i].n = 0; + eqPool[i].active = false; + eqPool[i].loading = false; + eqPool[i].paused = false; + eqPool[i].next = 0; + eqPool[i].material.Clear(); + eqPool[i].queueThread = 0; + eqPool[i].loadingTime = 0; + } + // initialize MIDI objects... + midiClient = 0; + midiOutPort = 0; + midiDest = 0; + + // clear scheduler thread variable... + schedulerThread = 0; +} + +MuPlayer::~MuPlayer(void) +{ + +} + +void MuPlayer::CleanPlaybackPool(void) +{ + // Clean Playback Pool + for(int i = 0; i < MAX_QUEUES; i++) + { + if(eqPool[i].buffer) + delete [] eqPool[i].buffer; + eqPool[i].buffer = NULL; + eqPool[i].n = 0; + eqPool[i].active = false; + eqPool[i].loading = false; + eqPool[i].paused = false; + eqPool[i].next = 0; + eqPool[i].material.Clear(); + if(eqPool[i].queueThread != 0) + { + pthread_cancel(eqPool[i].queueThread); + eqPool[i].queueThread = 0; + } + eqPool[i].loadingTime = 0; + } +} + +bool MuPlayer::Init(void) +{ + long n,i; + OSStatus err = noErr; + + // create Client... + if(midiClient == 0) + { + err = MIDIClientCreate(CFSTR("MuM Playback"), NULL, NULL, &midiClient); + if(err == noErr) + { + // Create Output Port... + err = MIDIOutputPortCreate(midiClient, CFSTR("MuM Output"), &midiOutPort); + if(err == noErr) + { + // List Possible Destinations... + n = MIDIGetNumberOfDestinations(); + if(n != 0) + { + CFStringRef name; + char cname[64]; + MIDIEndpointRef dest; + + // List Possible MIDI Destinations... + for(i = 0; i < n; i++) + { + dest = MIDIGetDestination(i); + if (dest != 0) + { + MIDIObjectGetStringProperty(dest, kMIDIPropertyName, &name); + CFStringGetCString(name, cname, sizeof(cname), 0); + CFRelease(name); + cout << "[Destination " << i << "]: " << cname << endl << endl; + } + } + + // Choose a MIDI destination for playback... + midiDest = MIDIGetDestination(0); + + if(StartScheduler()) + return true; + } + else + { + cout << "No MIDI destinations present!\n" << endl; + } + } + else + { + cout << "Failed to open output port!\n" << endl; + } + } + else + { + cout << "Failed to create MIDI client!\n" << endl; + } + } + else + { + cout << "Client already initialized! (call reset MIDI)\n" << endl; + } + + return false; + +} + +bool MuPlayer::SelectMIDIDestination(int destNumber) +{ + if(destNumber > 0) + { + midiDest = MIDIGetDestination(destNumber); + if (midiDest != 0) + { + return true; + } + } + + return false; +} + +string MuPlayer::ListDestinations(void) +{ + string list; + long n,i; + + // Get number of destinations... + if(midiClient != 0) + { + n = MIDIGetNumberOfDestinations(); + if(n != 0) + { + CFStringRef name; + char cname[64]; + MIDIEndpointRef dest; + + // List Possible MIDI Destinations... + for(i = 0; i < n; i++) + { + dest = MIDIGetDestination(i); + if (dest != 0) + { + MIDIObjectGetStringProperty(dest, kMIDIPropertyName, &name); + CFStringGetCString(name, cname, sizeof(cname), 0); + CFRelease(name); + list += cname; + list += "\n"; + } + } + } + } + + return list; +} + +void MuPlayer::Reset(void) +{ + // Stop scheduler thread + if(schedulerThread != 0) + { + pthread_cancel(schedulerThread); + schedulerThread = 0; + } + + // Release all queue buffers and threads + CleanPlaybackPool(); + + // Release MIDI components... + if(midiOutPort != 0) + { + CFRelease(&midiOutPort); + midiOutPort = 0; + } + + if(midiClient != 0) + { + CFRelease(&midiClient); + midiClient = 0; + } + + midiDest = 0; +} + +bool MuPlayer::Play(MuMaterial & inMat, int mode) +{ + int i; + int selectedQueue = -1; + MuNote note; + + // First find a usable event queue... + if(mode == PLAYBACK_MODE_NORMAL) + { + // at the end of this loop, if at + // least one queue is available, + // selectedQueue contains its index.. + for (i = 0; i < MAX_QUEUES; i++) + { + // if the current queue is not being played or filled,... + if(eqPool[i].active == false && eqPool[i].loading == false) + { + // it can be selected for a new material... + selectedQueue = i; + // mark queue as under construction... + eqPool[i].loading = true; + break; + } + } + // if unused queue is found... + if(selectedQueue >= 0) + { + // start the queue's working thread... + StartQueueThread(inMat,selectedQueue); + } + else + { + // otherwise report failure... + return false; + } + } + + return true; +} + +bool MuPlayer::StartQueueThread(MuMaterial & inMat, int queueIdx) +{ + int res; + + // make a copy of the input material so the thread can + // work on it safely, as it will be working assynchronously + eqPool[queueIdx].material = inMat; + + // Start the thread... + res = pthread_create(&(eqPool[queueIdx].queueThread), NULL, MuPlayer::EnqueueMaterial, (void*)(&eqPool[queueIdx])); + if(res) + { + // if we fail, terminate process... + cout << "THREAD ERROR! - Terminating..." << endl; + exit(EXIT_FAILURE); + } + + // if successful... + return true; +} + +void * MuPlayer::EnqueueMaterial(void* arg) +{ + int numVoices, i; + MuNote note; + long numNotes, nextEvent, j, k; + EventQueue * queue = (EventQueue *)arg; + + // get the total number of notes in input material... + numNotes = queue->material.NumberOfNotes(); + + // each note needs two MIDI events (on/off) + numNotes *= 2; + + // allocate memory for the note events... + if(numNotes > 0) + { + // Attention! this buffer needs to be released when + // the scheduler is done sending its events... + queue->buffer = new MuMIDIMessage[numNotes]; + if(queue->buffer) + queue->n = numNotes; + } + + // extract MIDI events from notes... + if(queue->buffer) + { + nextEvent = 0; + numVoices = queue->material.NumberOfVoices(); + + for(i = 0; i < numVoices; i++) + { + numNotes = queue->material.NumberOfNotes(i); + for (j = 0; j < numNotes; j++) + { + note = queue->material.GetNote(i, j); + queue->buffer[nextEvent] = note.MIDIOn(); + nextEvent++; + queue->buffer[nextEvent] = note.MIDIOff(); + nextEvent++; + } + } + + // sort events by timestamp... + long n = queue->n; + for(j = n; j >= 1; j-- ) + { + for( k = 0; k < j-1; k++ ) + { + if( queue->buffer[k].time > queue->buffer[k+1].time ) + { + // swap messages... + MuMIDIMessage temp; + temp = queue->buffer[k]; + queue->buffer[k] = queue->buffer[k+1]; + queue->buffer[k+1] = temp; + } + } + } + queue->material.Clear(); + queue->next = 0; + queue->paused = false; + + // IMPORTANT: LOADING TIME + // The following timestamp is registering this moment, after + // the event buffer has been successfully allocated and filled, + // to be the initial time for playback of this queue. All events + // in the queue will be referenced from this point. The amount + // of microseconds retrieved hear will be added to the timestamp + // of every event so the scheduler can compare stamps and decide + // when to send the messages. + queue->loadingTime = ClockStamp(); + //cout << "[Loading Time]: " << queue->loadingTime << endl; + + // after the queue is set to 'active' the scheduler may + // use it at any moment (even at interrupt time). That's + // why this MUST BE THE LAST ACTION! + queue->active = true; + + // after the queue is active we turn off the loading flag... + queue->loading = false; + } + + // after the work is done we terminate this thread... + pthread_exit(NULL); +} + +bool MuPlayer::StartScheduler(void) +{ + int res; + res = pthread_create(&schedulerThread, NULL, MuPlayer::ScheduleEvents, (void*)eqPool); + if(res) + { + cout << "THREAD ERROR! - Terminating..." << endl; + exit(EXIT_FAILURE); + } + + return true; +} + +// FIX FIX FIX: FINISH IMPLEMENTING THIS CAREFULLY!! +// 1) REMEMBER TO RESET EMPTY QUEUES SO THEY CAN BE REUSED +// 2) REMEMBER TO IMPLEMENT GLOBAL PAUSE AND STOP CORRECTLY +// 3) Individual queue pause and stop must but planned better +// for later +void * MuPlayer::ScheduleEvents(void * pl) +{ + int i; + + MuPlayer * player = (MuPlayer *)pl; + EventQueue * pool = player->eqPool; + + // this thread will terminate + // when the Player's stop flag is set... + while (!MuPlayer::stop) + { + // only do work if the player is not paused... + if(!MuPlayer::pause) + { + for(i = 0; i < MAX_QUEUES; i++) + { + // if curent queue is active,... + if (pool[i].active == true) + { + // look for its next event... + MuMIDIMessage msg = pool[i].buffer[pool[i].next]; + long msgTime = (long)(msg.time * ONE_SECOND) + (pool[i].loadingTime); + // get current time from the system + long currTime = ClockStamp(); + + // if the timestamp on the message is expired... + if( currTime >= msgTime) + { + // schedule it to be sent to destination... + SendMIDIMessage(msg,player->midiOutPort, player->midiDest); + // advance event counter... + pool[i].next += 1; + // if this is the last event in the buffer, + // this queue needs to be reset... + if(pool[i].next >= pool[i].n) + { + // reset queue + delete [] pool[i].buffer; + pool[i].buffer = 0; + pool[i].n = 0; + pool[i].paused = false; + pool[i].next = 0; + pool[i].queueThread = 0; + pool[i].loadingTime = 0; + // lastly deactivate queue + pool[i].active = false; + pool[i].loading = false; + } + } + } + } // end MAX_QUEUES loop + usleep(10); // idle for a moment... + } // end if(!pause) + else + { + usleep(100); // idle for a moment... + } + } // end infinite loop + + + pthread_exit(NULL); +} + +void MuPlayer::SendMIDIMessage(MuMIDIMessage msg, MIDIPortRef outPort, MIDIEndpointRef dest) +{ + //pthread_mutex_lock(&sendMIDIlock); + + if((outPort != 0) && (dest != 0)) + { + Byte msgBuff[MESSAGE_LENGTH]; + msgBuff[0] = msg.status; + msgBuff[1] = msg.data1; + msgBuff[2] = msg.data2; + + MIDITimeStamp timestamp = 0.0; + Byte buffer[1024]; // storage space for MIDI Packets + MIDIPacketList * packetlist = (MIDIPacketList*)buffer; + MIDIPacket * packet = MIDIPacketListInit(packetlist); + packet = MIDIPacketListAdd( packetlist, sizeof(buffer), + packet, timestamp, + MESSAGE_LENGTH, msgBuff ); + MIDISend(outPort, dest, packetlist); + } + + //pthread_mutex_unlock(&sendMIDIlock); +} + + +void MuPlayer::Pause(bool T_F) +{ + pause = T_F; +} + +void MuPlayer::Stop(void) +{ + stop = true; +} + + + + diff --git a/MuPlayer.h b/MuPlayer.h new file mode 100644 index 0000000..cd39de1 --- /dev/null +++ b/MuPlayer.h @@ -0,0 +1,596 @@ +// +// MuPlayer.hpp +// MuMRT +// +// Created by Carlos Eduardo Mello on 2/17/19. +// Copyright © 2019 Carlos Eduardo Mello. All rights reserved. +// + +/** @file MuPlayer.h + * + * @brief MuPlayer Class Interface + * + * @author Carlos Eduardo Mello + * @date 2/17/2019 + * + * @details + * + * MuPlayer orquestrates the realtime playback facilities + * in the MuM library. It handles everything from scheduling + * materials for playback to managing working threads + * and playback controls. Normally only a single object + * of this class should be instantiated for a MuM based + * application. + * + * For more details about how to use an MuPlayer object to + * play Musical Materials dynamically during progam execution + * see the MuPlayer Class Documentation. + * + **/ + + +#ifndef MU_PLAYER_H +#define MU_PLAYER_H + +#include +#include +#include +#include +#include "MuMaterial.h" +using namespace std; + + +//!@brief Maximum number of queues objects in the playback pool +const int MAX_QUEUES = 10; + +//!@brief Normal Playback Mode: imediate playback of scheduled materials +const int PLAYBACK_MODE_NORMAL = 1; + +//!@brief Game Playback Mode: materials requested through callback +const int PLAYBACK_MODE_GAME = 2; + +//!@brief size of midi message +const int MESSAGE_LENGTH = 3; + +/** + * @brief Event Queue - MIDI events to be played + * + * @details + * + * This structure contais a pointer to an array of MIDI events + * and the number of events in this array. It is used to pass + * a sequence of events to a player event thread so it can quickly + * access the necessary data for playback. The structure is only + * a convenient wrapper for a data buffer and its size. It is assumed + * by the receiving end that the buffer pointer actually points to + * a valid vector. + * + **/ +struct EventQueue +{ + //! @brief address of an MuMIDIMessage array + MuMIDIMessage * buffer; + //! @brief number of valid indexes in the message array + long n; + //! @brief index of next message to be sent + long next; + //! @brief activation flag: true == active, false == inactive; + // a queue should not be picked for playback when it is active. + bool active; + //! @brief loading flag: set while the working thread is filling up the queue; + // a queue should not be picked for playback when it is loading. + bool loading; + //! @brief pause flag: true == paused, false == running + bool paused; + //! @brief queue thread: fills event queue with events from input MuMaterial + pthread_t queueThread; + //! @brief reference to input material to be associated with this queue + MuMaterial material; + //! @brief time in microseconds when the event queue is loaded and ready to be played + long loadingTime; +}; +typedef struct EventQueue EventQueue; + +/** + * @class MuPlayer + * + * @brief MuPlayer Class + * + * @details + * + * INTRO: + * + * MuPlayer is the only class in the MuM Library realtime + * playback module. Playback is currently done with MIDI and + * can be directed to any enabled MIDI destinations in the system. + * Normally only a single Player object is needed to play various + * materials within the lifetime of a MuM based application. + * MuPlayer assigns MuMaterial objects to playback queues and + * schedules them for playback. A queue can be scheduled + * using one of two ways: normal mode or game mode. In normal mode + * queues are scheduled for immediate playback. In game mode, client + * code registers a callback function which the Player will call + * when it needs a new material to keep playback going. + * + * Alternatively, any MuMaterial can be played from code with + * Csound by calling PlaybackWithCsound() on the object to be + * heard. However this call is synchronous and it starts + * a system level process which will play the entire length + * of the material before returning control to the Library. + * + * @note + * currently only normal playback mode is implemented (check the + * MuM Library page on GitHub frequently for new functionality). + * + * INITIALIZATION: + * + * Before being used, an MuPlayer needs to be + * initialized. This initialization creates the necessary + * infrastructure for the player to interact with the MIDI + * system in the current platform. Initialization is done + * with a call to Init(). When using CoreMIDI, Init() + * creates a MIDI client and an output port associated with + * it. It also verifies the available MIDI destinations at the + * moment of the call and displays a list of destinations in + * standard output (std::cout). By default, Init() selects the + * first available destination, but this choice can be overruled + * with a subsequent call to SelectMIDIDestination(). Init() also + * starts the scheduler thread which is responsible for actually + * sending the MIDI events. ResetMIDI() releases all MIDI + * resources created by Init(). After this call, the Player + * needs to be initialized again in order be usable. + * + * USING PLAYERS: + * + * Once initialized, the player can receive requests + * to play musical materials with calls to Play(). This method + * takes an MuMaterial as argument and passes the material to + * a working queue. It also takes a choice of playback + * mode, which determines when/how the materials will be played. + * Assuming the player has been initialized beforehand, + * Play() can be called at any time during program execution. + * + * PLAYBACK CONTROLS: + * + * MuPlayer can be used to pause, resume or stop + * playback. It is possible to pause/resume/stop an individual + * (playback queue) or the entire playback system. See Pause() + * and Stop() for more details on how to use the playback + * controls of MuPlayer. + * + * UNDER THE HOOD + * + * In order to allow many materials to playback simultaneously, + * MuPlayer employs Posix Threads. The class contains a scheduler thread + * and several working queue threads. The queue threads are initiated + * when a request for playback is made. They strip the input materials + * extracting MIDI events, put them in chronological order inside a + * MIDI event queue and flag the queue as active. The scheduler, on + * the other hand, starts when the player is initialized and keeps + * looking for active queues in the pool. For every active queue it finds + * the next pending event and checks its timestamp. If it is expired the + * scheduler sends it. Then it moves to the next active queue and so on, + * until the applications terminates or the player is paused, stopped or + * reset. When the scheduler reaches the last event in a queue, it + * resets the queue and marks it as inactive, so it can be used again + * by the player. + * + * The player comunicates to its threads through one-way flags. + * For example, only the queue thread can set the active flag and it only + * does that once, when the event queue is filled up. After that the + * working queue thread will terminate. Only the scheduler thread + * will read this flag and turn it off when the event queue is completely + * empty. Only the player will look for inactive queue so it can + * play another material. Similarly, the playback controls are + * implemented using this same type of mechanism. Each queue has a + * pause flag which can only be set by the player and read by the + * scheduler thread. When the scheduler detects an acitve queue, + * it checks to see if it is paused. If it is, and there is a pending event + * to be played, it discards it, unless it is a noteOff event. + * if the entire player is paused, the scheduler ignores all queues + * and just idles for a few microseconds before checking again. + * + * SAMPLE: + * + * Normal workflow for playback with MuM can be summarized by the following + * piece of code: + * + * @code {.cpp} + * + * MuPlayer player; + * player.Init(); + * //... + * MuMaterial mat; + * mat.MajorScale(0.5); + * player.Play(mat, PLAYBACK_MODE_NORMAL); + * // ... + * mat.Transpose(-5); + * player.Play(mat,PLAYBACK_MODE_NORMAL); + * //... + * player.Pause(true); // pause playback + * //... + * player.Pause(false); // resume playback + * //... + * @endcode + * + * @note + * Currently, only PLAYBACK_MODE_NORMAL is implemented. + * + * @warning + * + * MuPlayer's playback queue pool is allocated at compile time. + * Its size is limited by the MAX_QUEUES constant. Depending + * on expected density of materials in the application, it may + * be necessary to increase this value before compiling. Otherwise, + * once all queues in the pool are in use, requests to play new + * materials may not be honored. + * + **/ + + +class MuPlayer +{ + private: + + EventQueue eqPool[MAX_QUEUES]; // Playback Pool + MIDIClientRef midiClient; // MIDI Client (CoreMIDI) + MIDIPortRef midiOutPort; // OUTPUT Port (CoreMIDI) + MIDIEndpointRef midiDest; // Destination Endpoint (CoreMIDI) + + pthread_t schedulerThread; + static pthread_mutex_t sendMIDIlock; + + static bool pause; // flag to communicate pause command to scheduler + static bool stop; // flag to communicate stop command to scheduler + + public: + + // Constructor/Destructor + + /** + * @brief Default Constructor + * + * @details + * This constructor sets internal player data fields to reasonable default values + * + **/ + + MuPlayer(void); + + /** + * @brief Destructor + * + * @details + * currently, the MuPlayer Destructor does not handle any specific tasks. + **/ + ~MuPlayer(void); + + /** + * @brief clears all pool data (buffers, materials) and zeroes fields + * + * @details + * This method clears all data from playback pool. It goes through each + * queue releasing memory buffers, zeroeing structure fields and emptying + * materials. It is called once by the Player's constructor and is reused + * when necessary, by other methods. + * + **/ + void CleanPlaybackPool(void); + + /** + * @brief Initializes the MuPlayer MIDI configurations and starts event scheduler thread + * + * @details + * + * Init() is responsible for initializing the MIDI environment + * for the Player. The CoreMIDI implementation of this method + * starts out by creating a MIDI client and an associated MIDI output + * port, so the Library can send MIDI events to the system. After that + * it requests the list of current destinations to CoreMIDI and displays + * the list to standard output (std::cout). Init() always selects the + * first available destination for playback, but this choice can be + * changed by a subsequent call to SelectMIDIDestination(), using one + * of the destination numbers displayed by Init(). + * + * Normally there shouldn't be any problems with initialization, but it + * is always safer to check the return value for this method. If Init() + * for any reason returns 'false', it means one or more of the CoreMIDI + * calls failed, in which case the MuPlayer object should not be used. + * Init() may also return false if for some reason it cannot start the + * scheduler thread. + * + * @return + * bool - true for success, false for error in initializing the MIDI + * environment or starting the scheduler thread. + * + **/ + bool Init(void); + + /** + * @brief Selects a MIDI destination for playback + * + * @details + * + * SelectMIDIDestination() takes a destination number and stores it + * for use by the player, replacing any prior selections. + * Valid destination numbers are supplied by CoreMIDI and can be + * verified with a call to DisplayDestinations() or by checking + * Init()'s console output. + * + * @param + * destNumber (int) - number of the desired MIDI destination + * + * @return + * void + * + **/ + bool SelectMIDIDestination(int destNumber); + + /** + * @brief Lists MIDI destinations available for playback in the system + * + * @details + * + * ListDestinations() show the avialable MIDI destinations at the time + * of the call. The numbers in the list can be used by + * SelectMIDIDestination() to choose a target for playback. It should + * be noted that this information is inherently dynamic. MIDI devices + * or applications may be started or finished right after a call + * to ListDestinations(). Therefore it is important to allways check + * the return value from SelectMIDIDestination(). + * + * @return + * + * string: a standard C++ string containing a list of possible MIDI + * destinations. This string needs to be parsed to extract each + * destination. Data in the string is organized according to the + * following scheme: + * + * line 0: Description for Destination 0 + * line 1: Description for Destination 1 + * line 2: Description for Destination 2 + * line n: Description for Destination n + * + * Destinations are separated by a carriage return ("\n"). + * Destination numbers are implied by the line position in + * the string. The lines are numbered starting from 0, so the + * very first line describes destination 0, the second line + * describes destination 1, and so forth. This string may + * simply be sent to standart output (std::cout) for visual + * verification at the console or parsed into discrete units + * for further manipulation. + **/ + string ListDestinations(void); + + /** + * @brief cancels MIDI setup, stops scheduler and releases all resources for MuPlayer + * + * @details + * + * Reset() releases all resources created by Init() + * and zeroes all the internal variables associated with them. + * It also stops the scheduler thread and throws away any active + * queues. Reset() effectively puts MuPlayer back at its original + * uninitialized state. After a call to ResetMIDI(), an MuPlayer + * object cannot be used until it is initialized again. + * + * @return + * void + * + **/ + void Reset(void); + + /** + * @brief initiates a playback queue for a requested material and mode + * + * @details + * + * Play() takes the input MuMaterial object contained in the 'inMat' + * argument and assigns an event queue from the Player's playback pool + * to handle that input material. + * + * Play() goes through the playback pool only once, + * looking for inactive queues to use. Once it finds one, it calls + * StartQueueThread() with a reference to the MuMaterial to be played. + * That method stores a copy of the material inside the queue structure + * and starts the working thread, which, in turn, extracts data from the + * material and activates the queue. If Play() cannot find an inactive + * queue to use, it returns false, in which case the playback request + * will not be honored. + * + * The playback pool is just an array of EventQueue structures, which + * can be used and reused during the course of the application. + * The pool has a fixed size which is determined at compile time. + * Since not all queues are in use all the time, + * recycling them allows more efficient use of resources. + * If for any reason the pool size turns out to be too small, it can be + * increased by changing the value of MAX_QUEUES at the begining of + * MuPlayer's header file. + * + * @code {.cpp} + * + * const int MAX_QUEUES = 100; + * + * @endcode + * + * @param + * inMat (MuMaterial&) - material to be played + * + * @param + * mode (int) - playback mode to be used + * (currently, only PLAYBACK_MODE_NORMAL is implemented) + * + * @return + * + * bool - Play() returns false if (a) it couldn't find an idle event + * queue in the pool or (b) the call to StartQueueThread() fails + * (see StartQueueThread() for details), otherwise it returns true. + * + **/ + bool Play(MuMaterial & inMat, int mode); + + /** + * @brief starts an event queue working thread + * + * @details + * + * Each queue has a working thread associated with it. It is + * used to fill up the queue with MIDI events extracted from + * the material being played. StartQueueThread() initiates this + * thread. The thread terminates automatically when all events + * from the input material are enqueued for playback. + * + * @param + * inMat (MuMaterial &): reference to the material to be enqueued + * + * @param + * queueIdx (int): index of the selected queue + * + * @return + * bool: StartQueueThread() returns false if it cannot start + * the working thread and true otherwise + * + **/ + bool StartQueueThread(MuMaterial & inMat, int queueIdx); + + /** + * @brief extracts MIDI events from input material and puts them + * in the corresponding playback event queue. + * + * @details + * + * EnqueueMaterial() is the thread function for a queue's working + * thread. It is initiated by StartQueueThread() and is responsible + * for getting each note from the input material converted to MIDI + * events and placed in the queue in chronological order, so they + * can be scheduled for playback by the scheduler thread. When this + * method concludes its work, it sets the queue's 'active' flag to + * true, so its events can be accessd by the scheduler. + * + * @return + * void *: EnqueueMaterial() terminates when the tread exits + * + **/ + static void * EnqueueMaterial(void*); + + /** + * @brief starts the event scheduling thread + * + * @details + * + * StartScheduler() initiates the MIDI event scheduling thread + * within an MuPlayer. Once successfully started, the scheduler + * will keep looking for pending events on every active queue + * untill it is stopped or paused, or the Player is destroyed. + * + * @return + * bool: StartScheduler() returns false if it cannot start + * the scheduler thread and true otherwise + * + **/ + bool StartScheduler(void); + + /** + * @brief + * scheduler thread function: gets data from queues and sends to MIDI + * system at the appropriate time + * + * @details + * + * ScheduleEvents() is the scheduler thread function. It is started + * from StartScheduler() and runs continuously until the Player is + * stopped. Before starting its main loop, ScheduleEvents() checks + * if the pause flag is set by the Player, in which case it will + * just idle until the next turn. If the player is not paused, + * it goes through each queue in the pool, checking if they are active. + * For any active queues, ScheduleEvents() will look for the next event + * in the queue, check its timestamp, and send it to the MIDI system + * if the stamp is expired. + * + * @param + * pool (void *): pointer to the plyback pool; as the scheduler thread + * function is static, it needs to be passed the playback pool to allow + * access to the event queues. This void pointer needs to be cast to + * (EventQueue *). + * + * @return + * void *: ScheduleEvents() terminates when the tread exits + * + **/ + static void * ScheduleEvents(void * pool); + + /** + * @brief sends MIDI messages to the MIDI System + * + * @details + * + * SendMIDIMessage() is called by ScheduleEvents()to deliver + * a single MIDI message at a time to its destination. + * The time stamp within 'msg' is always ignored. + * SendMIDIMessage() always delivers every message immediately. + * Keeping track of time between events is done by calling code. + * + * @note + * + * This method should NOT be called directly by client code. It is + * meant to be called by MuPlayer internal code running on another + * thread. + * + * + * @param + * msg (MuMIDIMessage) - MIDI event to be delivered + * + * @param + * outPort (MIDIPortRef) - MIDI Output Port associated with the + * Playback Manager's MIDI Client (CoreMIDI) + * + * @param + * dest (MIDIEndpointRef) - Destination Endpoint selected by the + * Playback Manager from the lst of available destinations in the + * MIDI system (CoreMIDI) + * + * @return + * void + * + **/ + static void SendMIDIMessage(MuMIDIMessage msg, MIDIPortRef outPort, MIDIEndpointRef dest); + + /** + * @brief pauses playback for all active queues in the playback pool + * + * @details + * + * Pause() can be used to pause and resume playback for all event queues + * controlled by the Player. Its single argument defines which + * action will take place after the call. If 'T_F' contains 'true', the + * Player pauses all queues. If it contains 'false', playback is resumed + * in all queues. + * + * @param + * T_F (bool) - true == pause, false == resume + * + * @return + * void + * + **/ + void Pause(bool T_F); + + /** + * @brief stops all playback and cancels all event queues + * + * @details + * + * Stop() can be used to stop playback. When Stop() is called, + * all event queues are deactivated. It is not possible to resume + * previously scheduled playback once the Player issues a + * stop command. it is possible, however to make new requests, as + * long as the player is not Reset(). + * + * @return + * void + * + **/ + void Stop(void); +}; + +#endif /* MU_PLAYER_H */ diff --git a/MuRecoder.cpp b/MuRecoder.cpp new file mode 100644 index 0000000..6f5c7a0 --- /dev/null +++ b/MuRecoder.cpp @@ -0,0 +1,290 @@ +// +// MuRecoder.cpp +// MuMRT +// +// Created by Carlos Eduardo Mello on 3/4/19. +// Copyright © 2019 Carlos Eduardo Mello. All rights reserved. +// + +#include "MuRecorder.h" + +MuRecorder::MuRecorder(void) +{ + midiClient = 0; + midiInPort = 0; + midiSource = 0; + + buff1.data = NULL; + buff1.max = 0; + buff1.count = 0; + + buff2.data = NULL; + buff2.max = 0; + buff2.count = 0; + + currentBuffer = NULL; + + initialStamp = 0; +} + +MuRecorder::~MuRecorder(void) +{ + if(midiInPort != 0 && midiSource != 0) + MIDIPortDisconnectSource(midiInPort,midiSource); + + if(buff1.data != NULL) + delete [] buff1.data; + + if(buff2.data != NULL) + delete [] buff2.data; +} + +bool MuRecorder::Init(long buffSize = DEFAULT_BUFFER_SIZE) +{ + // remember when the Recorder started to run... + initialStamp = ClockStamp(); + + // ALLOCATE MIDI BUFFERS... + if(buff1.data == NULL) + { + buff1.data = new MuMIDIMessage[buffSize]; + if(buff1.data != NULL) + { + buff1.max = buffSize; + buff1.count = 0; + } + } + + if(buff2.data == NULL) + { + buff2.data = new MuMIDIMessage[buffSize]; + if(buff2.data != NULL) + { + buff2.max = buffSize; + buff2.count = 0; + } + } + + currentBuffer = &buff1; + + // INITIALIZE MIDI PORTS... + long n,i; + OSStatus err = noErr; + + // create Client... + if(midiClient == 0) + { + err = MIDIClientCreate(CFSTR("MuM Recorder"), NULL, NULL, &midiClient); + if(err == noErr) + { + // Create Input Port... + err = MIDIInputPortCreate(midiClient, CFSTR("MuM Input"), MIDIInputCallback, this, &midiInPort); if(err == noErr) + { + // Count Available MIDI Sources... + n = MIDIGetNumberOfSources(); + if(n != 0) + { + CFStringRef name; + char cname[64]; + MIDIEndpointRef source; + + // List Possible Sources... + for(i = 0; i < n; i++) + { + source = MIDIGetSource(i); + if (source != 0) + { + MIDIObjectGetStringProperty(source, kMIDIPropertyName, &name); + CFStringGetCString(name, cname, sizeof(cname), 0); + CFRelease(name); + cout << "[Source " << i << "]: " << cname << endl << endl; + } + } + + // Choose the first MIDI source to get input from... + midiSource = MIDIGetSource(0); + OSStatus result; + result = MIDIPortConnectSource(midiInPort, midiSource, NULL); + if(result == noErr) + return true; + } + else + { + cout << "No MIDI destinations present!\n" << endl; + } + } + else + { + cout << "Failed to open output port!\n" << endl; + } + } + else + { + cout << "Failed to create MIDI client!\n" << endl; + } + } + else + { + cout << "Client already initialized! (call reset MIDI)\n" << endl; + } + return false; +} + +bool MuRecorder::SelectMIDISource(int sourceNumber) +{ + OSStatus result; + + if(midiSource != 0) + { + result = MIDIPortDisconnectSource(midiInPort,midiSource); + if(result != noErr) + cout << "Couldn't disconnect from previously selected source..." << endl; + } + + if(sourceNumber > 0) + { + midiSource = MIDIGetSource(sourceNumber); + if (midiSource != 0) + { + result = MIDIPortConnectSource(midiInPort, midiSource, NULL); + if(result == noErr) + return true; + else + cout << endl << "MIDI Source Connection Failed!" << endl; + } + } + + return false; +} + +void MuRecorder::ToggleCurrentBuffer(void) +{ + if (currentBuffer == &buff1) + currentBuffer = &buff2; + else + currentBuffer = &buff1; +} + +MuMIDIBuffer MuRecorder::GetData(void) +{ + MuMIDIBuffer outBuffer; + outBuffer.data = NULL; + outBuffer.max = 0; + outBuffer.count = 0; + MuMIDIBuffer * previous; + + // Keep the address of where the data is... + previous = currentBuffer; + + // redirect input to the other buffer... + ToggleCurrentBuffer(); + + // allocate memory to copy data... + long i; + long n = previous->count; + if(n > 0) + { + outBuffer.data = new MuMIDIMessage[n]; + if(outBuffer.data != NULL) + { + for(i = 0; i < n; i++) + outBuffer.data[i] = previous->data[i]; + outBuffer.max = n; + outBuffer.count = n; + + // flush previous buffer, so we don't run out of space + previous->count = 0; + } + } + return outBuffer; +} + +void MuRecorder::MIDIInputCallback (const MIDIPacketList *list, void *procRef,void *srcRef) +{ + //cout << "MIDIInputCallback was called" << endl; + + unsigned int i; + MuRecorder * recorder = (MuRecorder *)procRef; + const MIDIPacket *packet = &(list->packet[0]); + UInt16 nBytes,j; + MuMIDIMessage msg; + msg.time = ((ClockStamp() - recorder->initialStamp)/ (float)ONE_SECOND); + + for ( i = 0; i < list->numPackets; i++) + { + nBytes = packet->length; + + j = 0; + while(j < nBytes) + { + Byte next = packet->data[j]; + // if this is a note event (on or off)... + if( ((next & 0xF0) == 0x90) || ((next & 0xF0) == 0x80)) + { + // extract and store it... + msg.status = next; + msg.data1 = packet->data[j+1]; + msg.data2 = packet->data[j+2]; + recorder->AddMessageToBuffer(msg); + j += 3; + } + else // otherwise just ignore it and move to the next byte... + { + j++; + } + } + + // when done with this packet, move to the next... + packet = MIDIPacketNext(packet); + } +} + +void MuRecorder::AddMessageToBuffer(MuMIDIMessage msg) +{ + if(currentBuffer->count < (currentBuffer->max - 1)) + { + currentBuffer->data[currentBuffer->count] = msg; + currentBuffer->count++; + } +} + +// Obs.: +// 1) upon return, if .count == 0 or .data == NULL, join operation failed +// 2) calling code is responsible for releasing buffer memory. +MuMIDIBuffer MuRecorder::JoinMIDIBuffers(MuMIDIBuffer buff1, MuMIDIBuffer buff2) +{ + MuMIDIBuffer res; + res.data = NULL; + res.max = 0; + res.count = 0; + long i; + long n = buff1.count + buff2.count; + + if(n > 0) + { + // allocate memory enough to put data from both buffers... + MuMIDIMessage * temp = new MuMIDIMessage[n]; + if(temp != NULL) + { + // copy data from buff1... + if((buff1.data != NULL) && (buff1.count > 0)) + { + for (i = 0; i < buff1.count; i++) + temp[i] = buff1.data[i]; + } + + // copy data from buff2... + if((buff2.data != NULL) && (buff2.count > 0)) + { + for(i = 0; i < buff2.count; i++) + temp[i+buff1.count] = buff2.data[i]; + } + + res.data = temp; + res.max = n; + res.count = n; + } + } + + return res; +} \ No newline at end of file diff --git a/MuRecorder.h b/MuRecorder.h new file mode 100644 index 0000000..a4168de --- /dev/null +++ b/MuRecorder.h @@ -0,0 +1,430 @@ +//********************************************* +//***************** NCM-UnB ******************* +//******** (c) Carlos Eduardo Mello *********** +//********************************************* +// This softwre may be freely reproduced, +// copied, modified, and reused, as long as +// it retains, in all forms, the above credits. +//********************************************* + +/** @file MuRecorder.h + * + * @brief MuRecorder Class Interface + * + * @author Carlos Eduardo Mello + * @date 3/3/2019 + * + * @details + * + * MuRecorder introduces MIDI input to the MuM library. + * The class starts an independent thread that constantly + * looks for incomming MIDI data an adds it to a pair + * of input buffers that the rest of the class can access. + * When user code wants to check for available data, it + * calls GetData(), which copies available MIDI messages + * from one of the buffers, leaving the other one free + * to keep receiving other messages. Normally only a + * single object of this class needs to be instantiated + * within a MuM based application. + * + * For more details about how to use an MuRecorder object + * see the MuRecorder Class Documentation. + * + **/ + +#ifndef MuRecoder_H +#define MuRecoder_H + +#include +#include +#include "MuUtil.h" +#include "MuMIDI.h" + +/** + * @class MuRecorder + * + * @brief MuRecorder Class + * + * @details + * + * INTRO: + * + * MuRecorder is the class responsible for MIDI input in the + * MuM Library. The class listens to system MIDI connections + * from devices and applications and stores received MIDI events + * in a pair of input buffers. From there, these events can be + * retrieved by calling code using MuRecorder's methods. + * + * INITIALIZATION: + * + * Before being used, an MuRecorder needs to be + * initialized. This initialization creates the necessary + * infrastructure for the player to interact with the MIDI + * system in the current platform. Initialization is done + * with a call to Init(). When using CoreMIDI, Init() + * creates a MIDI client and an input port associated with + * it. It also verifies the available MIDI sources at the + * moment of the call and displays a list of sources in + * standard output (std::cout). By default, Init() selects the + * first available source, but this choice can be overruled + * with a subsequent call to SelectMIDISource(). ResetMIDI() + * releases all MIDI resources created by Init(). After this call, + * the Recorder needs to be initialized again in order be usable. + * + * Once MIDI connections are in place, Init() starts a listener thread + * which is responsible for actually receiving MIDI events. The listener + * pols system resources frequently looking for MIDI messages. When + * a message is received, it gets copied to the current input buffer + * and stamped with current system time with ClockStamp(). + * + * USING RECORDERS: + * + * Once initialized, the recorder object will immediately start + * listening to incomming events. Each event received from + * the system is timestamped and stored in the currently + * available buffer. + * + * Whenever client code needs to get MIDI data it + * calls GetData(). GetData() toggles current buffer + * redirecting input to the other buffer, while copying data. + * This way the listener thread doesn't have to wait to + * store new messages. GetData() makes a copy of the + * available data, puts it into a MIDI buffer structure + * (MuMIDIBuffer) and returnes it to the caller. + * It is important to note that these buffer + * structures depend on dynamic memory + * allocation and that CALLING CODE IS RESPONSIBLE FOR + * DEALLOCATING this memory when no longer needed. + * + * Internally the MIDI buffers returned by GetData() are very simple + * structures. They contain three fields: 'data' (which is a pointer + * a dynamic array of MIDI message structures, 'max' which is + * the number of elements allocated for this array and 'count' + * which contains the number of valid elements (elements that are + * in use). For more details about the buffer structure see MuMIDIBuffer. + * The buffers returned by GetData() can be added into larger buffers + * with MuRecorder::JoinMIDIBuffers() so user code can access + * all incomming that is continuously stored by the recorder. + * This data can also be converted to a music material for use in MuM + * with a call to MuMaterial::LoadMIDIBuffer(); + * + * UNDER THE HOOD + * + * In order to keep working continuously and without generating wrong timestamps, + * the listener works in a separate thread. Also it stores the data collected + * from MIDI system in two separate, alternating buffers. Whenever GetData() + * gets called, the listener stops writing in the current buffer and moves + * to the other one. Meanwhile, GetData(), operating in the main thread, + * copies data from the first buffer and returns it to calling code. The + * two input buffers are pre-allocated when the recorder is initialized, + * so that when getting new data, the listener never has to allocate memory, + * or move data around. It just copies a MIDI message structure into a new + * address in one of the arrays. Switching arrays is also a very cheap + * operation (changing a pointer address) which is done by GetData(), so + * that, as far as the listener is concerned it is just storing another + * message to a given address. + * + * SAMPLE: + * + * There are many ways to use an MuRecorder in a MuM Application. + * It all depends on how we need to collect input and what we want + * to do with the it. For example, the recorder could be called + * continuosly in a tight loop to get small blocks of data and + * interpret them as they come. Or the app could listen for a + * certain amount of time or until a certain event arrives, before + * getting the data out. Whatever the strategy, using MuRecorder + * usually involves: + * + * @code {.cpp} + * + * // instantiating a recorder object + * MuPlayer rec; + * + * // initializing it... + * rec.Init(); + * + * // giving it some time to record events... + * usleep(ONE_SECOND); + * + * // getting some data from the recorder... + * MuMIDIBuffer buffer = GetData(); + * + * // puting it inside a music material so it can be manipulated... + * MuMaterial mat.LoadMIDIBuffer(buffer, MIDI_BUFFER_MODE_PURGE); + * + * // releasing buffer memory... + * if(buffer.data) + * delete [] buffer.data; + * + * @endcode + * + * @note + * Currently, only PLAYBACK_MODE_NORMAL is implemented. + * + * @warning + * + * MIDI BUFFERS MUST BE DEALLOCATED BY CALLING CODE to avoid + * memory leaks. + * + **/ + +class MuRecorder +{ + private: + + // DATA... + MuMIDIBuffer buff1; + MuMIDIBuffer buff2; + MuMIDIBuffer * currentBuffer; // points to one of the input buffers + + // MIDI CONNECTIONS... + MIDIClientRef midiClient; // MIDI Client (CoreMIDI) + MIDIPortRef midiInPort; // Input Port (CoreMIDI) + MIDIEndpointRef midiSource; // Source Endpoint (CoreMIDI) + + long initialStamp; + + /** + * @brief toggles the current input buffer to be used by MIDI input + * callback function + * + * @details + * + * MuRecorder uses two alternating input buffers to store incomming MIDI + * events. When client code requests data, the MIDI callback has to be + * redirected to a different buffer in order to keep running smoothly + * and avoid conflicts with the data reading routine. So whenever GetData() + * is called, it runs ToggleCurrentBuffer() to point the listener to + * the next available buffer. + * + * + * @return + * void + * + **/ + void ToggleCurrentBuffer(void); + +public: + // Constructor/Destructor + + /** + * @brief Default Constructor + * + * @details + * This constructor sets internal player data fields to reasonable default values + * + **/ + MuRecorder(void); + + /** + * @brief Destructor + * + * @details + * MuRecorder destructor releases memory from the input buffers and + * disconnects the input port MIDI sources. + **/ + ~MuRecorder(void); + + /** + * @brief Initializes the MuRecorder MIDI configuration and installs + * MIDI callback function + * + * @details + * + * Init() is responsible for initializing the MIDI environment + * for the Recorder. The CoreMIDI implementation of this method + * starts out by creating a MIDI client and an associated MIDI input + * port, so the Library can receive MIDI events from the system. + * When creating the input port, Init() installs a MIDI input callback + * which is later called by the system whenever MIDI data is available + * for retrieval. After that, the method requests the list of current + * sources to CoreMIDI and displays that list to standard output + * (std::cout). Init() always selects the first available source for + * input, but this choice can be changed by a subsequent call to + * SelectMIDISource(), using one of the source numbers displayed by Init(). + * + * Normally there shouldn't be any problems with initialization, but it + * is always safer to check the return value for this method. If Init() + * for any reason returns 'false', it means one or more of the CoreMIDI + * calls failed, in which case the MuRecorder object should not be used. + * Init() will also fail if it cannot allocate memory for the input buffers. + * + * @param + * buffSize (long) - size of input buffers; each buffer will be + * 'buffSize' events long. + * + * @return + * bool - true for success, false for error in initializing the MIDI + * environment or allocating the input buffers. + * + **/ + bool Init(long buffSize); + + /** + * @brief Selects a source for MIDI input + * + * @details + * + * SelectMIDISurce() takes a source number and stores it + * for use by the player, replacing any prior selections. + * Valid source numbers are supplied by CoreMIDI and can be + * verified with a call to DisplaySources() or by checking + * Init()'s console output. + * + * @param + * destNumber (int) - number of the desired MIDI source + * + * @return + * void + * + **/ + bool SelectMIDISource(int sourceNumber); + + /** + * @brief returns a buffer structure containing the latest input MIDI events + * + * @details + * + * GetData() returns recent input data collected by the MIDI input callback + * in one of the input buffers. Immediately after starting and before copying + * any data, GetData() redirects the current buffer pointer to a different + * input buffer, so that the MIDI callback thread can keep doing its job + * while data is being copied without any conflicts. Once all data is copied, + * the method resets the input buffer count to zero so the callback can use + * the buffer from the begining next time around. + * + *@attention + * + * Subsequent calls to GetData() will keep toggling from one buffer to + * the other, so that it will never be reading from the same place the + * Recorder is storing data. This means that once a block of MIDI data + * is copied from the input buffer by GetData() it will no longer be + * available, as the data will be overwritten by the callback after + * the next call to GetData(). Hence client code should store that + * data if it intends to reuse it. + * + * @return + * + * MuMIDIBuffer - GetData() returns a MIDI buffer structure containing a + * pointer to the data copied from the input buffers. The structure also + * has a 'max' field with data array size and a 'count' field reporting + * the number of elements actually used in the array. + * Obs.: the two length fields ('max' and 'count') can be used to populate + * only part of a buffer, when necessary. When returned by GetData(), however, + * these two fields will always contain the same value. The buffers returned + * by GetData() can be appended to a larger buffer with a call to + * JoinMIDIBuffers(). + * + **/ + MuMIDIBuffer GetData(void); + + /** + * @brief gets called by MIDI system when there is MIDI data available + * + * @details + * + * MIDIInputCallback() is a readProc function that needs to be + * provided by client code when ceating a MIDI input port with CoreMIDI. + * It gets called directly by the system in a special high priority + * thread, providing MuRecorder with the latest received MIDI data. + * Data is received in the form of a MIDI packet list, which + * needs to be parsed in a specific manner in order to retrieve the + * MIDI packets and ultimately the MIDI events inside it. + * This entire process is implemented by MuRecorder in this function + * (for details on how to parse a MIDIPacketList, please see CoreMIDI + * documentation) + * + * @note + * + * MIDIInputCallback is a static method of the MuRecorder class but should + * not be called by client code. + * + * @param + * + * list (MIDIPacketList) - a CoreMIDI struture containing packets of MIDI data. + * THis list is parsed by the function to extract MIDI events which are in turn + * stored into MuRecorder's input buffers. + * + * @param + * + * procRef (void *) - This is a context pointer. It contains a pointer to + * the MuRecorder object where the callback was registered. This is needed + * to access the input buffers, since the function is static and has no direct + * connection to the object. + * + * @param + * + * srcRef (void *) - This other pointer points to the MIDI source CoreMIDI + * object to which the recorder's input port was conected. It may be used + * to retrieve information from that source, if necessary. + * + **/ + static void MIDIInputCallback(const MIDIPacketList *list, void *procRef,void *srcRef); + + /** + * @brief stores a single MIDI message in the current input buffer + * + * @details + * + * AddMessageToBuffer() stores the requested MuMIDIMessage in the next + * available position of the current input buffer. If the buffer is + * full, AddMessageToBuffer() fails silently. AddMessageToBuffer() is + * called by the MIDIInputCallback when it needs to add a new MIDI + * event to one of the input buffers. + * + * @warning + * + * MIDIInputCallback is a static method of the MuRecorder class but should + * NEVER be called directly by client code, as this would completely disrupt + * data count and possibly mess up array boundary controls in the input buffers. + * + * @param + * + * msg (MuMIDIMessage) - the message being stored by the function. This + * structure is provided by the MIDI callback function. + * + * + * @return + * + * void + * + **/ + void AddMessageToBuffer(MuMIDIMessage msg); + + /** + * @brief adds two MIDI buffers and returns a larger one with the joined data + * + * @details + * + * JoinMIDIBuffers() takes two MuMIDIBuffers as input and returns a larger + * buffer containing data from both. The method creates a new buffer and + * copies the contents of 'buff1' and then 'buff2' to it. + * + * @note + * + * This new larger buffer must be released by calling code when it is no + * longer needed. + * + * @param + * + * buff1 (MuMIDIBuffer) - first buffer to be added; data from this buffer + * goes at the begining of the resulting buffer. + * + * @param + * + * buff1 (MuMIDIBuffer) - second buffer to be added; data from this buffer + * goes at the end of the resulting buffer; + * + * @return + * + * MuMIDIBuffer - the return value is an MuMIDIBuffer structure containing + * the joined MIDI data. The memory allocated for the 'data' field in the + * returning buffer structure must be released to avoid memory leaks. If + * calling code needs a copy of this buffer data, the 'data' field should + * be deep copied. + * + **/ + static MuMIDIBuffer JoinMIDIBuffers(MuMIDIBuffer buff1, MuMIDIBuffer buff2); +}; + +#endif /* MuRecoder_H */ diff --git a/MuUtil.cpp b/MuUtil.cpp index e94253d..136fd4e 100644 --- a/MuUtil.cpp +++ b/MuUtil.cpp @@ -179,3 +179,17 @@ extern void ShowInts( int * array, int n ) cout << array[i] << " "; cout << endl; } + +// UTILITIES ================================== + +extern long ClockStamp(void) +{ + timeval tv; + gettimeofday(&tv, NULL); + return ((tv.tv_sec * ONE_SECOND) + (tv.tv_usec)); +} + +extern long TimeToStamp(float secs) +{ + return (long)(secs * ONE_SECOND); +} diff --git a/MuUtil.h b/MuUtil.h index 300aa07..dc17991 100644 --- a/MuUtil.h +++ b/MuUtil.h @@ -23,6 +23,7 @@ #ifndef _MU_UTIL_H_ #define _MU_UTIL_H_ +#include #include "MuError.h" // CONSTANTS @@ -54,6 +55,9 @@ const short ACC_FAVOR_FLATS = 1; //!@brief acidentals to use for altered notes: sharps const short ACC_FAVOR_SHARPS = 2; +//!@brief One second duration in microseconds +const long ONE_SECOND = 1000000; + // PROTOTYPES @@ -161,5 +165,44 @@ extern void SortFloats( float * array, int size); **/ extern void ShowInts( int * array, int size ); +// Utility... +/** + * @brief looks up the current system time and returns it as a + * microsecond value + * + * @details + * + * ClockStamp() reads the current system time using gettimeofDay() + * It then converts the 'timeval' structure returned by the system call + * to the corresponding value in microseconds. This method is extern and + * can be used at any time by calling code for calculating time offsets + * and other usefull utilities. + * + * @return + * unsigned long: the current system time in microseconds (for + * information about reference time values type 'man gettimeofday' + * at a unix terminal) + * + **/ +extern long ClockStamp(void); + +/** + * @brief + * + * converts input time from seconds to microseconds + * + * @details + * + * TimeToStamp() simply returns the time provided in 'secs' to its + * corresponding value in microseconds. In otherwords, it multiplies + * 'secs' by 1000000. This method is extern and can be used + * at any time by calling code for calculating time offsets + * and other usefull utilities + * + * @return + * unsigned long: requested time in microseconds + * + **/ +extern long TimeToStamp(float secs); #endif