~hecanjog/pippi

8870a489507d395e947d1870d815bbf1e660ea1e — Erik Schoster a month ago b94ecd2
astrid midi output & basic message quant support

grid quantization:
- replaces the unused `completed` field in `lpmsg_t` with interval &
  offset values used for per-message quantization to arbitrary grids.
- first pass simplistic phasor-like approach to supporting the interval
  and offset values to add a variable delay when scheduling render
  playback.

midi output:
- adds new midi output thread with better support for composing
  arbitrary midi messages (like program changes, etc) WIP
M astrid/src/astrid.c => astrid/src/astrid.c +148 -76
@@ 318,7 318,7 @@ int lpipc_getid(char * path) {
        return -1;
    }

    if(fstat(fd, &st) == -1) {
if(fstat(fd, &st) == -1) {
        syslog(LOG_ERR, "lpipc_getid fstat: %s. Error: %s\n", path, strerror(errno));
        close(fd);
        return -1;


@@ 1206,10 1206,11 @@ int lpmidi_trigger_notemap(int device_id, int note) {

        msg.initiated = now;

        syslog(LOG_INFO, "Sending message from lpmidi trigger notemap\ninitiated %f\nscheduled %f\ncompleted %f\nmax_processing_time %f\nonset_delay %ld\nvoice_id %ld\ncount %ld\ntype %d\nmsg %s\nname %s\n\n", 
        syslog(LOG_INFO, "Sending message from lpmidi trigger notemap\ninitiated %f\nscheduled %f\ninterval %f\noffset %f\nmax_processing_time %f\nonset_delay %ld\nvoice_id %ld\ncount %ld\ntype %d\nmsg %s\nname %s\n\n", 
            msg.initiated,
            msg.scheduled,
            msg.completed,
            msg.interval,
            msg.offset,
            msg.max_processing_time,
            msg.onset_delay,
            msg.voice_id, 


@@ 1256,6 1257,25 @@ int lpmidi_relay_to_instrument(char * instrument_name, unsigned char mtype, unsi
    return 0;
}

int lpmidi_encode_eventbytes(unsigned char buf[3], int channel, unsigned char message_type, int param, int value) {
    unsigned char status=0;
    memset(buf, 0, sizeof(unsigned char) * 3);

    status = message_type | (channel & 0x0F);

    buf[0] = status;
    buf[1] = param;
    buf[2] = value;

    return 0;
}

int lpmidi_encode_msg(lpmsg_t * msg, int channel, unsigned char message_type, int param, int value) {
    memset(msg->msg, 0, LPMAXMSG);
    lpmidi_encode_eventbytes((unsigned char *)msg->msg, channel, message_type, param, value);
    return 0;
}

int lpmidi_get_device_id_by_name(const char * device_name) {
    snd_seq_t * seq_handle;
    snd_seq_client_info_t * cinfo;


@@ 2171,40 2191,6 @@ int parse_message_from_cmdline(char * cmdline, size_t cmdlength, lpmsg_t * msg) 
    return 0;
}

int midi_triggerq_open() {
    int qfd;

    umask(0);
    if(mkfifo(ASTRID_MIDI_TRIGGERQ_PATH, S_IRUSR | S_IWUSR | S_IWGRP) == -1 && errno != EEXIST) {
        syslog(LOG_ERR, "midi_triggerq_open mkfifo: Error creating msg queue FIFO. Error: %s\n", strerror(errno));
        return -1;
    }

    if((qfd = open(ASTRID_MIDI_TRIGGERQ_PATH, O_RDWR)) < 0) {
        syslog(LOG_ERR, "midi_triggerq_open open: Error opening msg queue FIFO. Error: %s\n", strerror(errno));
        return -1;
    };

    return qfd;
}

int midi_triggerq_schedule(int qfd, lpmidievent_t t) {
    if(write(qfd, &t, sizeof(lpmidievent_t)) != sizeof(lpmidievent_t)) {
        syslog(LOG_ERR, "midi_triggerq_schedule write: Could not write to q. Error: %s\n", strerror(errno));
        return -1;
    }

    return 0;
}

int midi_triggerq_close(int qfd) {
    if(close(qfd) == -1) {
        syslog(LOG_ERR, "midi_triggerq_close close: Error closing msg queue FIFO. Error: %s\n", strerror(errno));
        return -1; 
    }

    return 0;
}


/* SCHEDULING


@@ 2583,19 2569,6 @@ void lpscheduler_tick(lpscheduler_t * s) {
void scheduler_schedule_event(lpscheduler_t * s, lpbuffer_t * buf, size_t onset_delay) {
    lpevent_t * e;

    /*
    if(s->nursery_head != NULL) {
        e = s->nursery_head;
        s->nursery_head = (void *)e->next;
        e->next = NULL; 
    } else {
        e = (lpevent_t *)LPMemoryPool.alloc(1, sizeof(lpevent_t));
        s->event_count += 1;
        e->id = s->event_count;
        e->onset = 0;
        e->callback_onset = 0;
    }
    */
    e = (lpevent_t *)LPMemoryPool.alloc(1, sizeof(lpevent_t));
    s->event_count += 1;
    e->id = s->event_count;


@@ 2607,8 2580,7 @@ void scheduler_schedule_event(lpscheduler_t * s, lpbuffer_t * buf, size_t onset_
    e->onset = s->ticks + onset_delay;

    syslog(LOG_INFO, "scheduling event ID %ld\n", e->id);
    syslog(LOG_INFO, "scheduler got buffer with onset %ld\n", e->onset);
    syslog(LOG_INFO, "scheduler got buffer value 10 %f\n", buf->data[10]);
    syslog(LOG_INFO, "   with onset %ld\n", e->onset);

    start_waiting(s, e);
}


@@ 2936,6 2908,8 @@ void * instrument_message_thread(void * arg) {
    //double processing_time_so_far, onset_delay_in_seconds, now=0;
    lpinstrument_t * instrument = (lpinstrument_t *)arg;
    int is_scheduled = 0;
    double now = 0.f;
    size_t onset_delay, ticks, grid_interval=0;

    instrument->is_waiting = 1;
    while(instrument->is_running) {


@@ 2946,12 2920,14 @@ void * instrument_message_thread(void * arg) {
        }

        is_scheduled = ((instrument->msg.flags & LPFLAG_IS_SCHEDULED) == LPFLAG_IS_SCHEDULED);
        syslog(LOG_DEBUG, "C MSG: name=%s\n", instrument->msg.instrument_name);
        syslog(LOG_DEBUG, "C MSG: scheduled=%f\n", instrument->msg.scheduled);
        syslog(LOG_DEBUG, "C MSG: voice_id=%d\n", (int)instrument->msg.voice_id);
        syslog(LOG_DEBUG, "C MSG: type=%d\n", (int)instrument->msg.type);
        syslog(LOG_DEBUG, "C MSG: flags=%d\n", (int)instrument->msg.flags);
        syslog(LOG_DEBUG, "C MSG: is_scheduled=%d\n", is_scheduled);
        syslog(LOG_INFO, "C MSG: name=%s\n", instrument->msg.instrument_name);
        syslog(LOG_INFO, "C MSG: scheduled=%f\n", instrument->msg.scheduled);
        syslog(LOG_INFO, "C MSG: interval=%f\n", instrument->msg.interval);
        syslog(LOG_INFO, "C MSG: offset=%f\n", instrument->msg.offset);
        syslog(LOG_INFO, "C MSG: voice_id=%d\n", (int)instrument->msg.voice_id);
        syslog(LOG_INFO, "C MSG: type=%d\n", (int)instrument->msg.type);
        syslog(LOG_INFO, "C MSG: flags=%d\n", (int)instrument->msg.flags);
        syslog(LOG_INFO, "C MSG: is_scheduled=%d\n", is_scheduled);

        // Handle shutdown early
        if(instrument->msg.type == LPMSG_SHUTDOWN) {


@@ 2995,32 2971,26 @@ void * instrument_message_thread(void * arg) {
        // Now the fun stuff
        switch(instrument->msg.type) {
            case LPMSG_RENDER_COMPLETE:
                /* FIXME do this in another thread? */
                // Renders from the internal callback AND/OR external renderers (AKA python)
                if((buf = deserialize_buffer(instrument->msg.msg, &bufmsg)) == NULL) {
                    syslog(LOG_ERR, "DAC could not deserialize buffer. Error: (%d) %s\n", errno, strerror(errno));
                    continue;
                }

                // tracking the render time is still useful... this might not be right place tho
                /*
                if(lpscheduler_get_now_seconds(&now) < 0) {
                    syslog(LOG_ERR, "Could not get now seconds for loop retriggering\n");
                    now = 0;
                /* Schedule the buffer for playback */
                lpscheduler_get_now_seconds(&now);
                onset_delay = 0;
                if(bufmsg.interval > 0) {
                    ticks = (size_t)(now * instrument->samplerate);
                    grid_interval = (size_t)(bufmsg.interval * instrument->samplerate);
                    onset_delay = grid_interval - (ticks % grid_interval);
                }

                processing_time_so_far = now - instrument->msg.initiated;
                onset_delay_in_seconds = instrument->msg.scheduled - processing_time_so_far;
                if(onset_delay_in_seconds < 0) onset_delay_in_seconds = 0.f;

                instrument->msg.onset_delay = (size_t)(onset_delay_in_seconds * ASTRID_SAMPLERATE);

                syslog(LOG_INFO, "msg.onset_delay %ld\n", instrument->msg.onset_delay);
                */
                if(bufmsg.offset > 0) {
                    onset_delay += (size_t)(bufmsg.offset * instrument->samplerate);
                }

                /* Schedule the buffer for playback */
                syslog(LOG_INFO, "RENDER COMPLETE: scheduling buffer with value 10 %f\n", buf->data[10]);
                scheduler_schedule_event(instrument->async_mixer, buf, 0);
                scheduler_schedule_event(instrument->async_mixer, buf, onset_delay);
                //scheduler_debug(instrument->async_mixer);
                break;



@@ 3077,6 3047,10 @@ void * instrument_message_thread(void * arg) {

            case LPMSG_MIDI_TO_DEVICE:
                syslog(LOG_DEBUG, "C MSG: midi to device\n");
                // put onto the internal midiout q
                if(send_message(instrument->midiout_message_q_name, instrument->msg) < 0) {
                    syslog(LOG_ERR, "Could not send midi message... (%d) %s\n", errno, strerror(errno));
                }
                break;

            default:


@@ 3439,6 3413,90 @@ void * instrument_midi_listener_thread(void * arg) {
    return 0;
}

void * instrument_midi_output_thread(void * arg) {
    lpmsg_t msg = {0};
    snd_seq_t * seq_handle;
    snd_midi_event_t * midi_event;
    snd_seq_event_t event;
    int ret, port;

    lpinstrument_t * instrument = (lpinstrument_t *)arg;

    syslog(LOG_ERR, "%s MIDI output thread starting for device ID %d\n", instrument->name, instrument->midi_device_id);

    if((instrument->midioutmsgq = astrid_msgq_open(instrument->midiout_message_q_name)) == (mqd_t) -1) {
        syslog(LOG_CRIT, "Could not open midiout msgq for instrument %s. Error: %s\n", instrument->name, strerror(errno));
        return NULL;
    }
    syslog(LOG_ERR, "Opened midiout message queue for %s with fd %d\n", instrument->name, instrument->midioutmsgq);

    if((ret = snd_seq_open(&seq_handle, "default", SND_SEQ_OPEN_OUTPUT, 0)) < 0) {
        syslog(LOG_ERR, "%s midi output: Could not open ALSA seq. Error: (%d) %s\n", instrument->name, ret, snd_strerror(ret));
        return 0;
    }

    snd_seq_set_client_name(seq_handle, &instrument->midiout_message_q_name[1]);

    if((port = snd_seq_create_simple_port(seq_handle, "output", 
                    SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, 
                    SND_SEQ_PORT_TYPE_APPLICATION)) < 0) {
        syslog(LOG_ERR, "%s midi output: Could not create ALSA port. Error: (%d) %s\n", instrument->name, port, snd_strerror(port));
        snd_seq_close(seq_handle);
        return 0;
    }

    if((ret = snd_seq_connect_to(seq_handle, port, instrument->midi_device_id, 0)) < 0) {
        syslog(LOG_ERR, "%s midi output: Could not connect to ALSA port. Error: (%d) %s\n", instrument->name, ret, snd_strerror(ret));
        snd_seq_close(seq_handle);
        return 0;
    }

    if(snd_midi_event_new(256, &midi_event) < 0) {
        syslog(LOG_ERR, "%s midi output: Could not create MIDI event. Error: (%d) %s\n", instrument->name, ret, snd_strerror(ret));
        snd_seq_close(seq_handle);
        return 0;
    }

    snd_midi_event_init(midi_event);

    while(instrument->is_running) {
        snd_seq_ev_clear(&event);

        if(astrid_msgq_read(instrument->midioutmsgq, &msg) == (mqd_t) -1) {
            syslog(LOG_ERR, "%s midi output: Could not read message from midi output q. Error: (%d) %s\n", instrument->name, errno, strerror(errno));
            usleep((useconds_t)10000);
            continue;
        }
        syslog(LOG_ERR, "%s MIDI output thread got msg for device ID %d\n", instrument->name, instrument->midi_device_id);

        if(snd_midi_event_encode(midi_event, (unsigned char *)msg.msg, sizeof(unsigned char) * 3, &event) < 0) {
            syslog(LOG_ERR, "%s midi output: Could not decode MIDI message from lpmsg_t body. Error: (%d) %s\n", instrument->name, errno, strerror(errno));
            usleep((useconds_t)10000);
            continue;
        }

        //syslog(LOG_ERR, "mtype=%d\n", midi_event->type);

        snd_seq_ev_set_source(&event, port);
        snd_seq_ev_set_subs(&event);
        snd_seq_ev_set_direct(&event);

        if((ret = snd_seq_event_output_direct(seq_handle, &event)) < 0) {
            syslog(LOG_ERR, "%s midi output: Could not send midi event. Error: (%d) %s\n", instrument->name, errno, strerror(errno));
            usleep((useconds_t)10000);
            continue;
        }
    }

    snd_seq_close(seq_handle);

    syslog(LOG_DEBUG, "Closing midiout message queue...\n");
    if(instrument->midioutmsgq != (mqd_t) -1) astrid_msgq_close(instrument->midioutmsgq);

    return 0;
}


lpinstrument_t * astrid_instrument_start(
    char * name, 
    int channels, 


@@ 3508,6 3566,7 @@ lpinstrument_t * astrid_instrument_start(
    // Set the message q names
    snprintf(instrument->qname, NAME_MAX, "/%s-msgq", instrument->name);
    snprintf(instrument->serial_message_q_name, NAME_MAX, "/%s-serial-msgq", instrument->name);
    snprintf(instrument->midiout_message_q_name, NAME_MAX, "/%s-midiout", instrument->name);

    // Set the tty path
    if(tty != NULL) {


@@ 3632,7 3691,7 @@ lpinstrument_t * astrid_instrument_start(
    instrument->is_running = 1;
    syslog(LOG_INFO, "%s is running...\n", name);

    /* Open the message queue */
    /* Open the message queues */
    if((instrument->msgq = astrid_msgq_open(instrument->qname)) == (mqd_t) -1) {
        syslog(LOG_CRIT, "Could not open msgq for instrument %s. Error: %s\n", instrument->name, strerror(errno));
        return NULL;


@@ 3677,6 3736,11 @@ lpinstrument_t * astrid_instrument_start(
            syslog(LOG_ERR, "Could not initialize instrument midi listener thread. Error: %s\n", strerror(errno));
            return NULL;
        }

        if(pthread_create(&instrument->midi_output_thread, NULL, instrument_midi_output_thread, (void*)instrument) != 0) {
            syslog(LOG_ERR, "Could not initialize instrument midi output thread. Error: %s\n", strerror(errno));
            return NULL;
        }
    }

    /* start the cleanup thread */


@@ 3748,6 3812,14 @@ int astrid_instrument_stop(lpinstrument_t * instrument) {
            if(ret == ESRCH) syslog(LOG_ERR, "ESRCH\n");
            syslog(LOG_ERR, "Error while attempting to join with midi listener thread. Ret: %d Errno: %d (%s)\n", ret, errno, strerror(ret));
        }

        syslog(LOG_DEBUG, "Joining with midi output thread...\n");
        if((ret = pthread_join(instrument->midi_output_thread, NULL)) != 0) {
            if(ret == EINVAL) syslog(LOG_ERR, "EINVAL\n");
            if(ret == EDEADLK) syslog(LOG_ERR, "DEADLOCK\n");
            if(ret == ESRCH) syslog(LOG_ERR, "ESRCH\n");
            syslog(LOG_ERR, "Error while attempting to join with midi output thread. Ret: %d Errno: %d (%s)\n", ret, errno, strerror(ret));
        }
    }

    syslog(LOG_DEBUG, "Joining with serial listener thread...\n");

M astrid/src/astrid.h => astrid/src/astrid.h +41 -20
@@ 64,15 64,19 @@
#define ASTRID_MAX_PARAMS 4096

#ifndef NOTE_ON
#define NOTE_ON 144
#define NOTE_ON 0x90
#endif

#ifndef NOTE_OFF
#define NOTE_OFF 128
#define NOTE_OFF 0x80
#endif

#ifndef CONTROL_CHANGE
#define CONTROL_CHANGE 176
#define CONTROL_CHANGE 0xB0
#endif

#ifndef PROGRAM_CHANGE
#define PROGRAM_CHANGE 0xC0
#endif

#define SPACE ' '


@@ 96,13 100,13 @@ typedef struct lpmidievent_t {
    double onset;
    double now;
    double length;
    char type;
    char note;
    char velocity;
    char program;
    char bank_msb;
    char bank_lsb;
    char channel;
    int type;
    int note;
    int velocity;
    int program;
    int bank_msb;
    int bank_lsb;
    int channel;
} lpmidievent_t;

/* These events are what is stored in the 


@@ 172,10 176,12 @@ typedef struct lpinstrument_t {
    char qname[NAME_MAX]; 
    char external_relay_name[NAME_MAX]; // just python, really 
    char serial_message_q_name[NAME_MAX]; 
    char midiout_message_q_name[NAME_MAX]; 
    int ext_relay_enabled;
    mqd_t msgq;
    mqd_t exmsgq;
    mqd_t serialmsgq;
    mqd_t midioutmsgq;
    lpmsg_t msg;
    lpmsg_t cmd;



@@ 194,6 200,7 @@ typedef struct lpinstrument_t {
    pthread_t message_feed_thread;
    pthread_t serial_listener_thread;
    pthread_t midi_listener_thread;
    pthread_t midi_output_thread;
    pthread_t message_scheduler_pq_thread;
    lpscheduler_t * async_mixer;
    lpbuffer_t * lastbuf;


@@ 299,28 306,42 @@ mqd_t astrid_msgq_open(char * qname);
int astrid_msgq_close(mqd_t mqd);
int astrid_msgq_read(mqd_t mqd, lpmsg_t * msg);


/* TODO add POSIX message queues for these too */
int midi_triggerq_open();
int midi_triggerq_schedule(int qfd, lpmidievent_t t);
int midi_triggerq_close(int qfd);

/* TODO evaluate if we really need this. The old use-case was to 
 * store all MIDI values in shared memory as they arrive, so that 
 * they are available to be read in the async instrument callbacks.
 *
 * That's still useful (necessary really, for some things) but ought 
 * to be placed into a forthcoming global LMDB session along with all 
 * the other session-scope params.
 */
int lpmidi_setcc(int device_id, int cc, int value);
int lpmidi_getcc(int device_id, int cc);
int lpmidi_setnote(int device_id, int note, int velocity);
int lpmidi_getnote(int device_id, int note);

/* TODO notemaps should also probably be moved to LMDB. We want them 
 * to also be global / session-scope so that any event astrid knows 
 * about can be mapped to any valid astrid message.
 *
 * In other words, these really aren't MIDI event maps, they should 
 * just be general event-to-message(s) mappings.
 */
int lpmidi_add_msg_to_notemap(int device_id, int note, lpmsg_t msg);
int lpmidi_remove_msg_from_notemap(int device_id, int note, int index);
int lpmidi_print_notemap(int device_id, int note);
int lpmidi_trigger_notemap(int device_id, int note);
int lpmidi_relay_to_instrument(char * instrument_name, unsigned char mtype, unsigned char mid, unsigned char mval);

int lpmidi_get_device_id_by_name(const char * device_name);

/* TODO same goes for serial values: store them in the LMDB session */
int lpserial_setctl(int device_id, int param_id, size_t value);
int lpserial_getctl(int device_id, int ctl, lpfloat_t * value);

/* in-progress MIDI APIs */
int lpmidi_relay_to_instrument(char * instrument_name, unsigned char mtype, unsigned char mid, unsigned char mval);
int lpmidi_encode_eventbytes(unsigned char buf[3], int channel, unsigned char message_type, int param, int value);
int lpmidi_encode_msg(lpmsg_t * msg, int channel, unsigned char message_type, int param, int value);
int lpmidi_get_device_id_by_name(const char * device_name);

/* FIXME there are only JACK ports, now... */
int astrid_get_playback_device_id();
int astrid_get_capture_device_id();



@@ 334,9 355,9 @@ lpbuffer_t * lpsampler_create(char * name, double length_in_seconds, int channel
int lpsampler_destroy(char * name);
int lpsampler_destroy_and_unmap(char * name, lpbuffer_t * buf);

/* FIXME move get/setid and get/setvalue IPC usages to LMDB */
int lpipc_setid(char * path, int id); 
int lpipc_getid(char * path); 

int lpipc_createvalue(char * path, size_t size);
int lpipc_setvalue(char * path, void * value, size_t size);
int lpipc_getvalue(char * path, void ** value);

M libpippi/src/pippiconstants.h => libpippi/src/pippiconstants.h +1 -1
@@ 68,7 68,7 @@
#define FNV1_32_MAGIC_NUMBER ((u_int32_t)0x811c9dc5)

#define LPMAXNAME 16
#define LPMAXMSG (PIPE_BUF - (sizeof(double) * 4) - (sizeof(size_t) * 3) - (sizeof(uint16_t) * 2) - LPMAXNAME)
#define LPMAXMSG (PIPE_BUF - (sizeof(double) * 5) - (sizeof(size_t) * 3) - (sizeof(uint16_t) * 2) - LPMAXNAME)
#define LPMAXPAT 512 - sizeof(size_t)

enum Wavetables {

M libpippi/src/pippitypes.h => libpippi/src/pippitypes.h +9 -10
@@ 40,19 40,18 @@ typedef struct lpmsg_t {
     * */
    double scheduled;     

    /* Timestamp of render completion or trigger away.
    /* Optional grid quantization values for renders.
     *
     * This is assigned either at the point that the message 
     * is deserialized along with the buffer and placed onto 
     * the mixer queue, or just after a MIDI event or other 
     * trigger has been sent away.
     * `interval` in seconds specifies the grid quantization 
     * as an interval while `offset` sets the phase offset.
     *
     * Donno if it is needed, since the message will be 
     * destroyed just after it is set, ideally? Or will it be?
     *
     * Possibly better just to log this at the debug level.
     * A 1 second interval will add a delay to the onset time 
     * for renders so that the event arrives only every N ticks 
     * where N=samplerate * interval, offset by X ticks where 
     * X=samplerate * offset.
     * */
    double completed;     
    double interval;
    double offset;     

    /* The longest a message in this sequence has taken 
     * so far to be processed and reach completion.

M pippi/renderer.pxd => pippi/renderer.pxd +15 -14
@@ 85,7 85,8 @@ cdef extern from "astrid.h":
    ctypedef struct lpmsg_t:
        double initiated
        double scheduled 
        double completed
        double interval
        double offset
        double max_processing_time
        size_t onset_delay
        size_t voice_id


@@ 99,13 100,13 @@ cdef extern from "astrid.h":
        double onset
        double now
        double length
        char type
        char note
        char velocity
        char program
        char bank_msb
        char bank_lsb
        char channel
        int type
        int note
        int velocity
        int program
        int bank_msb
        int bank_lsb
        int channel

    ctypedef struct lpinstrument_t:
        const char * name


@@ 144,10 145,6 @@ cdef extern from "astrid.h":

    int send_message(char * qname, lpmsg_t msg)

    int midi_triggerq_open()
    int midi_triggerq_schedule(int qfd, lpmidievent_t t)
    int midi_triggerq_close(int qfd)

    int astrid_msgq_open(char * qname)
    int astrid_msgq_read(int mqd, lpmsg_t * msg)
    int astrid_msgq_close(int mqd)


@@ 157,6 154,7 @@ cdef extern from "astrid.h":
    int lpmidi_setnote(int device_id, int note, int velocity)
    int lpmidi_getnote(int device_id, int note)
    int lpmidi_relay_to_instrument(char * instrument_name, unsigned char mtype, unsigned char mid, unsigned char mval)
    int lpmidi_encode_msg(lpmsg_t * msg, int channel, unsigned char message_type, int param, int value)

    int lpserial_getctl(int device_id, int ctl, lpfloat_t * value)



@@ 207,7 205,7 @@ cdef class MessageEvent:
    cpdef int schedule(MessageEvent self, double now=*)

cdef class MidiEvent:
    cdef lpmidievent_t * event
    cdef lpmsg_t * msg
    cpdef int schedule(MidiEvent self, double now)

cdef class ParamBucket:


@@ 215,7 213,8 @@ cdef class ParamBucket:
    cdef str _play_params

cdef class EventTriggerFactory:
    cpdef midi(self, double onset, double length, double freq=*, double amp=*, int note=*, int program=*, int bank_msb=*, int bank_lsb=*, int channel=*, int device=*)
    cpdef midinote(self, double onset, str instrument_name, double length, double freq=*, double amp=*, int note=*, int velocity=*, int channel=*, int device=*)
    cpdef midi(self, double onset, str instrument_name, int b1, int b2, int b3, int device=*)

cdef class MidiEventListenerProxy:
    cdef int default_device_id


@@ 237,6 236,8 @@ cdef class Instrument:
    cdef public dict cache
    cdef public lpmsg_t msg # a copy of the last message received
    cdef public size_t last_reload
    cdef public double grid_interval
    cdef public double grid_offset
    cdef public double max_processing_time
    cdef public int channels
    cdef public double samplerate

M pippi/renderer.pyx => pippi/renderer.pyx +72 -79
@@ 1,14 1,12 @@
#cython: language_level=3

import array
from cpython cimport array
from libc.stdlib cimport calloc, free
from libc.string cimport strcpy, memcpy, strncpy
import logging
from logging.handlers import SysLogHandler
import importlib
import importlib.util
from multiprocessing import Process, SimpleQueue
import numbers
import os
from pathlib import Path
import platform


@@ 18,6 16,9 @@ import sys
import time
import warnings

from libc.stdlib cimport calloc, free
from libc.string cimport strcpy, memcpy, strncpy
from cpython cimport array
cimport cython
import numpy as np
cimport numpy as np


@@ 25,7 26,7 @@ cimport numpy as np
from pippi import dsp, midi, ugens
from pippi.soundbuffer cimport SoundBuffer

NUM_COMRADES = 32
NUM_COMRADES = 8

class InstrumentError(Exception):
    pass


@@ 42,7 43,7 @@ if not logger.handlers:
    warnings.simplefilter('always')


cdef bytes serialize_buffer(SoundBuffer buf, int is_looping, lpmsg_t msg):
cdef bytes serialize_buffer(SoundBuffer buf, int is_looping, lpmsg_t msg, double grid_interval, double grid_offset):
    cdef bytearray strbuf
    cdef size_t audiosize, length, msgsize
    cdef int channels, samplerate


@@ 54,6 55,12 @@ cdef bytes serialize_buffer(SoundBuffer buf, int is_looping, lpmsg_t msg):
    samplerate = <int>buf.samplerate
    length = <size_t>len(buf)

    if grid_interval > 0:
        msg.interval = grid_interval

    if grid_offset > 0:
        msg.offset = grid_offset

    # size of audio data 
    audiosize = length * channels * sizeof(lpfloat_t)



@@ 96,7 103,7 @@ cdef class MessageEvent:
        self.msg = <lpmsg_t *>calloc(1, sizeof(lpmsg_t))
        self.msg.initiated = 0
        self.msg.scheduled = onset
        self.msg.completed = 0
        #self.msg.completed = 0
        self.msg.max_processing_time = max_processing_time
        self.msg.onset_delay = 0
        #self.msg.voice_id = astrid_get_voice_id()


@@ 117,50 124,38 @@ cdef class MessageEvent:
            free(self.msg)

cdef class MidiEvent:
    # FIXME just use the MessageEvent with an LPMSG_MIDI_TO_DEVICE type
    def __cinit__(self,
            double onset,
            str instrument_name,
            double length,
            char type,
            char note,
            char velocity,
            char program,
            char bank_msb,
            char bank_lsb,
            char channel
            int message_type,
            int note,
            int velocity,
            int channel
        ):
        instrument_name_byte_string = instrument_name.encode('utf-8')
        cdef char * byte_instrument_name = instrument_name_byte_string

        self.event = <lpmidievent_t *>calloc(1, sizeof(lpmidievent_t))
        self.event.onset = onset
        self.event.now = 0
        self.event.length = length
        self.event.type = type
        self.event.note = note
        self.event.velocity = velocity
        self.event.program = program
        self.event.bank_msb = bank_msb
        self.event.bank_lsb = bank_lsb
        self.event.channel = channel

    cpdef int schedule(MidiEvent self, double now):
        self.event.now = now
        cdef int qfd = midi_triggerq_open()
        if qfd < 0:
            logger.exception('Error opening MIDI fifo q')
            return -1

        if midi_triggerq_schedule(qfd, self.event[0]) < 0:
            logger.exception('Error scheduling MidiEvent')
            return -1
        self.msg = <lpmsg_t *>calloc(1, sizeof(lpmsg_t))
        self.msg.initiated = 0
        self.msg.scheduled = onset
        self.msg.onset_delay = 0
        self.msg.voice_id = 0
        self.msg.count = 0
        self.msg.type = LPMSG_MIDI_TO_DEVICE
        self.msg.flags = LPFLAG_IS_SCHEDULED if onset > 0 else LPFLAG_NONE

        if midi_triggerq_close(qfd) < 0:
            logger.exception('Error closing MIDI fifo q')
            return -1
        strcpy(self.msg.instrument_name, byte_instrument_name)
        lpmidi_encode_msg(self.msg, channel, message_type, note, velocity)

        return 0 
    cpdef int schedule(MidiEvent self, double now):
        self.msg.initiated = now
        return send_play_message(self.msg[0])

    def __dealloc__(self):
        if self.event is not NULL:
            free(self.event)
        if self.msg is not NULL:
            free(self.msg)

cdef class MidiEventListenerProxy:
    def __cinit__(self, default_device_id=1):


@@ 205,8 200,8 @@ cdef class EventTriggerFactory:

        return params

    cpdef midi(self, double onset, double length, double freq=0, double amp=1, int note=60, int program=1, int bank_msb=0, int bank_lsb=0, int channel=1, int device=-1):
        cdef char _note, velocity
    cpdef midinote(self, double onset, str instrument_name, double length, double freq=0, double amp=-1, int note=60, int velocity=127, int channel=1, int device=-1):
        cdef char _note, _velocity
        cdef MidiEvent noteon
        cdef MidiEvent noteoff
        


@@ 215,13 210,30 @@ cdef class EventTriggerFactory:
        else:
            _note = <char>max(0, min(127, note))

        velocity = <char>max(0, min(127, (amp * 127)))
        if amp >= 0:
            _velocity = <char>max(0, min(127, (amp * 127)))
        else:
            _velocity = <char>max(0, min(127, velocity))

        logger.error('midinote()')

        noteon = MidiEvent(onset, length, <char>NOTE_ON, note, velocity, program, bank_msb, bank_lsb, <char>channel)
        noteoff = MidiEvent(onset + length, 0, <char>NOTE_OFF, note, velocity, program, bank_msb, bank_lsb, <char>channel)
        noteon = MidiEvent(onset, instrument_name, length, NOTE_ON, _note, _velocity, channel)
        logger.error(f'{noteon=}')
        noteoff = MidiEvent(onset + length, instrument_name, 0, NOTE_OFF, _note, _velocity, channel)
        logger.error(f'{noteoff=}')

        return [noteon, noteoff]

    cpdef midi(self, double onset, str instrument_name, int b1, int b2, int b3, int device=-1):
        cdef int event_type = NOTE_ON
        cdef int channel = 0

        return MidiEvent(
            onset, 
            instrument_name, 
            0, event_type, b2, b3, channel
        )

    def play(self, double onset, str instrument_name, *args, **kwargs):
        params = self._parse_params(*args, **kwargs)
        return MessageEvent(onset, instrument_name, LPMSG_PLAY, params, 0)


@@ 646,29 658,7 @@ cdef class Instrument:

    def reload(self):
        logger.debug('Reloading instrument %s from %s' % (self.name, self.path))
        spec = importlib.util.spec_from_file_location(self.name, self.path)
        if spec is not None:
            renderer = importlib.util.module_from_spec(spec)
            try:
                spec.loader.exec_module(renderer)
            except Exception as e:
                logger.exception('Error reloading instrument module: %s' % str(e))

            if not hasattr(renderer, '_'):
                renderer._ = None

            self.renderer = renderer
        else:
            logger.error('Error reloading instrument. Null spec at path:\n  %s' % self.path)

        # fill the cache again if there is something to fill:
        if hasattr(self.renderer, 'cache'):
            self.cache = self.renderer.cache()

        if hasattr(self.renderer, 'stream'):
            ctx = self.get_event_context()
            self.graph = self.renderer.stream(ctx)

        self.load_renderer(self.name, self.path)

    def get_session_int_param(self, u_int32_t hash_key, int default):
        logger.debug('Getting INT param from key %d (with default %d)' % (hash_key, default))


@@ 769,10 759,10 @@ cdef int render_event(Instrument instrument, str msgstr):
    cdef bint loop
    cdef bytes bufstr
    cdef bytes render_params = instrument.msg.msg
    cdef int dacid = 0
    cdef EventContext ctx 
    instrument_byte_string = instrument.name.encode('UTF-8')
    cdef char * _instrument_ascii_name = instrument_byte_string
    cdef int grid_interval=0, grid_offset=0

    ctx = instrument.get_event_context(msgstr)



@@ 781,6 771,14 @@ cdef int render_event(Instrument instrument, str msgstr):
    if hasattr(instrument.renderer, 'before'):
        instrument.renderer.before(ctx)

    if hasattr(instrument.renderer, 'grid'):
        if isinstance(instrument.renderer.grid, tuple):
            grid_interval, grid_offset = instrument.renderer.grid
        elif isinstance(instrument.renderer.grid, numbers.Real):
            grid_interval = instrument.renderer.GRID
        elif callable(instrument.renderer.grid):
            grid_interval, grid_offset = instrument.renderer.grid(ctx)

    players, loop = collect_players(instrument)

    for player in players:


@@ 794,12 792,7 @@ cdef int render_event(Instrument instrument, str msgstr):
                    if snd is None:
                        continue

                    if isinstance(snd, tuple):
                        dacid, snd = snd
                    else:
                        dacid = 0

                    bufstr = serialize_buffer(snd, loop, instrument.msg)
                    bufstr = serialize_buffer(snd, loop, instrument.msg, grid_interval, grid_offset)
                    astrid_instrument_publish_bufstr(_instrument_ascii_name, bufstr, len(bufstr))

            except Exception as e:


@@ 851,7 844,7 @@ cdef int trigger_events(Instrument instrument):

    ctx = instrument.get_event_context()

    #logger.debug('trigger generation event %s w/params %s' % (str(instrument), trigger_params))
    logger.error('trigger generation event %s w/params %s' % (str(instrument), trigger_params))

    if hasattr(instrument.renderer, 'trigger_before'):
        instrument.renderer.trigger_before(ctx)


@@ 872,7 865,7 @@ cdef int trigger_events(Instrument instrument):
            return 1

    # Schedule the trigger events
    #logger.debug('Scheduling %d trigger events: %s' % (len(trigger_events), trigger_events))
    logger.error('Scheduling %d trigger events: %s' % (len(trigger_events), trigger_events))

    if lpscheduler_get_now_seconds(&now) < 0:
        logger.exception('Error getting now seconds during %s trigger scheduling' % ctx.instrument_name)


@@ 884,7 877,7 @@ cdef int trigger_events(Instrument instrument):
            continue
        if t.schedule(now) < 0:
            logger.exception('Error trying to schedule event from %s trigger generation' % ctx.instrument_name)
        #logger.debug('Scheduled event %s' % t)
        logger.error('Scheduled event %s' % t)

    if hasattr(instrument.renderer, 'trigger_done'):
        instrument.renderer.trigger_done(ctx)

M setup.py => setup.py +4 -2
@@ 4,7 4,7 @@ from setuptools.extension import Extension
from Cython.Build import cythonize
import numpy as np

dev = False
dev = True

INCLUDES = ['libpippi/vendor', 'libpippi/src', '/usr/local/include', np.get_include()]
MACROS = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")]


@@ 33,7 33,9 @@ ext_modules = cythonize([
                'libpippi/vendor/lmdb/libraries/liblmdb',
                'astrid/src'
            ],           
            define_macros=MACROS
            define_macros=MACROS,
            extra_compile_args=['-g'],
            extra_link_args=['-g'],
        ),

        Extension('pippi.microcontrollers', ['pippi/microcontrollers.pyx'],