#include "xtc/xtc_midi.h" #include "xtc/xtc_dsp.h" #include "../mixer/mix_internal.h" #include #include #include #define TAU 6.28318530717958647692 typedef struct { uint64_t start, end, age; unsigned int pitch, velocity, channel; size_t track; } span; typedef struct { uint64_t time; int change; } endpoint; typedef struct { unsigned int active, channel, pitch, velocity; uint64_t age; double phase; } voice; struct xtc_midi_plan { size_t fade_frames; xtc_mix_snapshot mix; span notes[XTC_PROJECT_MAX_NOTES]; size_t count; voice voices[XTC_MIDI_VOICES]; double step[128]; uint64_t cutoff[16], next_position, age; }; /** * @brief Decodes a complete note or all-notes-off MIDI message into a block event. * * @param bytes Borrowed MIDI message bytes. * @param size Number of message bytes. * @param offset Sample offset within the current block. * @param out Receives the operation result. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_midi_decode(const unsigned char *bytes, size_t size, unsigned int offset, xtc_midi_event *out) { xtc_midi_event e; unsigned int command; if (!bytes || !size || !out) return XTC_ERR_INVALID_ARGUMENT; command = bytes[0] & 240U; if (command != 128 && command != 144 && command != 176) return XTC_ERR_UNSUPPORTED; if (size != 3 || bytes[1] > 127 || bytes[2] > 127) return XTC_ERR_INVALID_ARGUMENT; if (command == 176 && bytes[1] != 120 && bytes[1] != 123) return XTC_ERR_UNSUPPORTED; e.offset = offset; e.channel = bytes[0] & 15U; e.pitch = bytes[1]; e.velocity = bytes[2]; e.type = command == 176 ? XTC_MIDI_ALL_OFF : command == 128 || !e.velocity ? XTC_MIDI_NOTE_OFF : XTC_MIDI_NOTE_ON; *out = e; return XTC_OK; } /** * @brief Orders note endpoints by position and endpoint delta for overlap counting. * * @param left Borrowed sort element to compare. * @param right Borrowed sort element to compare. * @return Negative, zero, or positive according to endpoint ordering. */ static int compare_endpoint(const void *left, const void *right) { const endpoint *a = left, *b = right; if (a->time != b->time) return a->time < b->time ? -1 : 1; return a->change - b->change; /* End before start at the same sample. */ } /** * @brief Clears live voices, scheduling continuity, and panic suppression. * * @param p Instrument plan and its single-owner runtime state. */ void xtc_midi_plan_reset(xtc_midi_plan *p) { if (!p) return; memset(p->voices, 0, sizeof(p->voices)); memset(p->cutoff, 0, sizeof(p->cutoff)); p->next_position = UINT64_MAX; p->age = 0; } /** * @brief Compiles scheduled notes and mixer state into a bounded sine-instrument plan. * * @param project Project model; borrowed for this call. * @param rate Sample rate in hertz. * @param out Receives the newly allocated object; caller owns it on success. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, xtc_midi_plan **out) { xtc_midi_plan *p; endpoint *points; size_t i; int active = 0; if (!out) return XTC_ERR_INVALID_ARGUMENT; *out = NULL; if (!project || !rate || rate > 768000) return XTC_ERR_INVALID_ARGUMENT; p = calloc(1, sizeof(*p)); points = calloc(XTC_PROJECT_MAX_NOTES * 2U, sizeof(*points)); if (!p || !points) { free(p); free(points); return XTC_ERR_OUT_OF_MEMORY; } { xtc_result r = xtc_mix_prepare(project, rate, &p->mix); if (r != XTC_OK) { free(points); free(p); return r; } } for (i = 0; i < 128; ++i) { double frequency = 440.0 * pow(2.0, ((double)i - 69.0) / 12.0); p->step[i] = frequency < rate * 0.5 ? frequency / rate : 0; } for (i = 0; i < project->note_count; ++i) { const xtc_note_info *n = project->notes + i; xtc_clip_info c; uint64_t begin, end; span *s = p->notes + p->count; xtc_result r = xtc_project_find_clip(project, n->clip_id, &c); if (r != XTC_OK) { free(points); free(p); return XTC_ERR_INVALID_STATE; } begin = n->start_tick > c.offset_tick ? n->start_tick : c.offset_tick; end = n->start_tick + n->length_tick < c.offset_tick + c.length_tick ? n->start_tick + n->length_tick : c.offset_tick + c.length_tick; if (end <= begin) continue; r = xtc_project_tick_to_sample(project, c.start_tick + begin - c.offset_tick, rate, &s->start); if (r == XTC_OK) r = xtc_project_tick_to_sample(project, c.start_tick + end - c.offset_tick, rate, &s->end); if (r == XTC_OK) r = xtc_project_tick_to_sample(project, begin - n->start_tick, rate, &s->age); if (r != XTC_OK) { free(points); free(p); return r; } if (s->end <= s->start) continue; s->track = xtc_mix_track(&p->mix, c.track_id); s->pitch = n->pitch; s->velocity = n->velocity; s->channel = n->channel; points[p->count * 2].time = s->start; points[p->count * 2].change = 1; points[p->count * 2 + 1].time = s->end; points[p->count * 2 + 1].change = -1; ++p->count; } qsort(points, p->count * 2, sizeof(*points), compare_endpoint); for (i = 0; i < p->count * 2; ++i) { active += points[i].change; if (active > (int)XTC_MIDI_VOICES) { free(points); free(p); return XTC_ERR_CAPACITY; } } free(points); xtc_midi_plan_reset(p); *out = p; return XTC_OK; } /** * @brief Releases scheduled notes and instrument state; accepts NULL. * * @param p Instrument plan and its single-owner runtime state. */ void xtc_midi_plan_destroy(xtc_midi_plan *p) { free(p); } /** * @brief Clears live voices and suppresses already-started scheduled notes on one MIDI channel. * * @param p Instrument plan and its single-owner runtime state. * @param channel MIDI channel, 0 to 15. * @param position Absolute sample position at the start of the block. */ static void channel_off(xtc_midi_plan *p, unsigned int channel, uint64_t position) { size_t i; for (i = 0; i < XTC_MIDI_VOICES; ++i) if (p->voices[i].channel == channel) p->voices[i].active = 0; p->cutoff[channel] = position == UINT64_MAX ? position : position + 1; } /** * @brief Silences live voices and suppresses scheduled notes already started at the given position. * * @param p Instrument plan and its single-owner runtime state. * @param position Absolute sample position at the start of the block. */ void xtc_midi_plan_panic(xtc_midi_plan *p, uint64_t position) { unsigned int c; if (!p) return; for (c = 0; c < 16; ++c) channel_off(p, c, position); p->next_position = position; } /** * @brief Applies a live MIDI event, stealing the oldest voice when necessary. * * @param p Instrument plan and its single-owner runtime state. * @param e MIDI event to apply. * @param position Absolute sample position at the start of the block. */ static void event(xtc_midi_plan *p, const xtc_midi_event *e, uint64_t position) { size_t i, slot = SIZE_MAX; if (e->type == XTC_MIDI_ALL_OFF) { channel_off(p, e->channel, position); return; } for (i = 0; i < XTC_MIDI_VOICES; ++i) { if (p->voices[i].active && p->voices[i].channel == e->channel && p->voices[i].pitch == e->pitch) { slot = i; break; } } if (e->type == XTC_MIDI_NOTE_OFF) { if (slot != SIZE_MAX) p->voices[slot].active = 0; return; } if (slot == SIZE_MAX) { for (i = 0; i < XTC_MIDI_VOICES; ++i) if (!p->voices[i].active) { slot = i; break; } } if (slot == SIZE_MAX) { slot = 0; for (i = 1; i < XTC_MIDI_VOICES; ++i) if (p->voices[i].age < p->voices[slot].age) slot = i; } p->voices[slot].active = 1; p->voices[slot].channel = e->channel; p->voices[slot].pitch = e->pitch; p->voices[slot].velocity = e->velocity; p->voices[slot].phase = 0; p->voices[slot].age = p->age++; } /** * @brief Accumulates scheduled and live sine voices over an event-free sample interval. * * @param p Instrument plan and its single-owner runtime state. * @param position Absolute sample position at the start of the block. * @param out Destination sample buffer with room for the requested frames. * @param right Destination right-channel buffer; NULL selects mono rendering. * @param frames Number of audio frames. */ xtc_result xtc_midi_plan_set_fade_frames(xtc_midi_plan *plan, size_t frames) { if (!plan || frames > 65536) return XTC_ERR_INVALID_ARGUMENT; plan->fade_frames = frames; return XTC_OK; } static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *right, size_t frames) { size_t i, j; for (i = 0; i < p->count; ++i) { const span *s = p->notes + i; uint64_t begin = position > s->start ? position : s->start; uint64_t end = position + frames < s->end ? position + frames : s->end; double step = p->step[s->pitch]; if (step == 0.0 || s->start < p->cutoff[s->channel] || end <= begin) continue; for (j = (size_t)(begin - position); j < (size_t)(end - position); ++j) { double phase = fmod((double)(position + j - s->start + s->age) * step, 1.0); float sample = (float)(sin(TAU * phase) * 0.1 * s->velocity / 127.0); if (p->fade_frames) { uint64_t age = position + j - s->start; uint64_t tail = s->end - 1U - (position + j); uint64_t distance = age < tail ? age : tail; if (distance < p->fade_frames) sample *= (float)distance / (float)p->fade_frames; } out[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, right ? 0U : 2U); if (right) right[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, 1); } } for (i = 0; i < XTC_MIDI_VOICES; ++i) { voice *v = p->voices + i; double step = p->step[v->pitch]; if (!v->active || step == 0.0) continue; for (j = 0; j < frames; ++j) { float sample = (float)(sin(TAU * v->phase) * 0.1 * v->velocity / 127.0); out[j] += sample; if (right) right[j] += sample; v->phase += step; if (v->phase >= 1) v->phase -= 1; } } } /** * @brief Renders scheduled and live MIDI with sample-offset events and track gain and pan. * * @param p Instrument plan and its single-owner runtime state. * @param position Absolute sample position at the start of the block. * @param events Borrowed MIDI events sorted by sample offset. * @param count Number of MIDI events. * @param out Destination sample buffer with room for the requested frames. * @param right Destination right-channel buffer; NULL selects mono rendering. * @param frames Number of audio frames. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, const xtc_midi_event *events, size_t count, float *out, float *right, size_t frames) { size_t i, cursor = 0; if (!p || !out || (right && right == out) || count > XTC_MIDI_MAX_EVENTS || (count && !events) || frames > 65536U || position > UINT64_MAX - frames) return XTC_ERR_INVALID_ARGUMENT; for (i = 0; i < count; ++i) { const xtc_midi_event *e = events + i; if (e->offset >= frames || (i && e->offset < events[i - 1].offset) || e->type < XTC_MIDI_NOTE_ON || e->type > XTC_MIDI_ALL_OFF || e->pitch > 127 || e->velocity > 127 || e->channel > 15 || (e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT; } if (!frames) return XTC_OK; if (p->next_position != position) xtc_midi_plan_reset(p); memset(out, 0, frames * sizeof(*out)); if (right) memset(right, 0, frames * sizeof(*right)); for (i = 0; i < count; ++i) { size_t next = events[i].offset; segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, next - cursor); event(p, events + i, position + next); cursor = next; } segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, frames - cursor); for (i = 0; i < frames; ++i) { out[i] = xtc_dsp_finite(out[i]); if (right) right[i] = xtc_dsp_finite(right[i]); } p->next_position = position + frames; return XTC_OK; } /** * @brief Renders scheduled and live MIDI to mono without stereo pan attenuation. * * @param p Instrument plan and its single-owner runtime state. * @param position Absolute sample position at the start of the block. * @param events Borrowed MIDI events sorted by sample offset. * @param count Number of MIDI events. * @param out Destination sample buffer with room for the requested frames. * @param frames Number of audio frames. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_midi_plan_render(xtc_midi_plan *p, uint64_t position, const xtc_midi_event *events, size_t count, float *out, size_t frames) { return xtc_midi_plan_render_stereo(p, position, events, count, out, NULL, frames); }