xtc/src/midi/instrument.c

527 lines
20 KiB
C

#include "xtc/xtc_midi.h"
#include "xtc/xtc_dsp.h"
#include "xtc/xtc_plugin.h"
#include "../mixer/mix_internal.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#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, released;
uint64_t age;
double phase;
} voice;
struct xtc_midi_plan {
size_t fade_frames, controller_frames, monitor_stride;
float *monitor;
uint64_t monitor_position;
xtc_gain_ramp volume[16], pan[16];
unsigned int sustain[16];
unsigned char control_map[16][3];
xtc_mix_snapshot mix;
span notes[XTC_PROJECT_MAX_NOTES];
xtc_plugin *plugins[XTC_PROJECT_MAX_TRACKS];
size_t live_track;
unsigned int plugin_reset;
float scratch[256];
xtc_midi_event queue[256];
size_t count;
voice voices[XTC_MIDI_VOICES];
double step[128];
uint64_t cutoff[16], next_position, age;
};
/**
* @brief Decodes a complete note, controller 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;
e.offset = offset;
e.channel = bytes[0] & 15U;
e.pitch = bytes[1];
e.velocity = bytes[2];
e.type = command == 176 ? (bytes[1] == 120 || bytes[1] == 123 ? XTC_MIDI_ALL_OFF : XTC_MIDI_CONTROL) : 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;
p->plugin_reset = 1;
{ unsigned int c; for (c = 0; c < 16; ++c) {
p->sustain[c] = 0;
(void)xtc_gain_ramp_reset(p->volume+c,1);
(void)xtc_gain_ramp_reset(p->pan+c,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);
p->live_track = SIZE_MAX;
for (i = 0; i < p->mix.count; ++i) {
xtc_plugin_spec spec;
if (xtc_project_get_instrument(project,p->mix.tracks[i].id,&spec) == XTC_OK) {
xtc_result r = xtc_plugin_create(&spec,rate,256,&p->plugins[i]);
if (r != XTC_OK) { xtc_midi_plan_destroy(p); return r; }
}
if (project->live_instrument == p->mix.tracks[i].id) p->live_track = i;
}
memcpy(p->control_map,project->control_map,sizeof(p->control_map));
p->controller_frames = rate / 200U;
if (!p->controller_frames) p->controller_frames = 1;
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)
{
size_t i;
if (!p) return;
for (i = 0; i < XTC_PROJECT_MAX_TRACKS; ++i) xtc_plugin_destroy(p->plugins[i]);
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->sustain[channel] = 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;
p->plugin_reset = 1;
}
/**
* @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;
}
if (e->type == XTC_MIDI_CONTROL) {
if (e->pitch == p->control_map[e->channel][0]) (void)xtc_gain_ramp_target(p->volume+e->channel,(float)e->velocity/127.0f,p->controller_frames);
else if (e->pitch == p->control_map[e->channel][1]) (void)xtc_gain_ramp_target(p->pan+e->channel,
e->velocity >= 64 ? (float)(e->velocity-64)/63.0f : ((float)e->velocity-64)/64.0f,p->controller_frames);
else if (e->pitch == p->control_map[e->channel][2]) {
p->sustain[e->channel] = e->velocity >= 64;
if (!p->sustain[e->channel]) for (i = 0; i < XTC_MIDI_VOICES; ++i)
if (p->voices[i].channel == e->channel && p->voices[i].released) p->voices[i].active = 0;
}
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) {
if (p->sustain[e->channel]) p->voices[slot].released = 1;
else 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].released = 0;
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 ((s->track < p->mix.count && p->plugins[s->track]) || 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;
}
if (p->monitor) xtc_mix_monitor(&p->mix, p->monitor, p->monitor_stride, s->track, position+j,
(size_t)(position-p->monitor_position)+j, sample, right != NULL);
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);
}
}
{
int live = 0; unsigned int c;
for (i = 0; i < XTC_MIDI_VOICES; ++i) if (p->voices[i].active) live = 1;
for (c = 0; c < 16; ++c) if (p->volume[c].remaining || p->pan[c].remaining) live = 1;
if (!live) return; /* Scheduled-only projects need no live-controller work. */
}
for (j = 0; j < frames; ++j) {
float levels[16], pans[16]; unsigned int c;
for (c = 0; c < 16; ++c) {
levels[c] = p->volume[c].current; pans[c] = p->pan[c].current;
if (p->volume[c].remaining) { levels[c] = 1; (void)xtc_gain_ramp_process(p->volume+c,levels+c,NULL,1); }
if (p->pan[c].remaining) { pans[c] = 1; (void)xtc_gain_ramp_process(p->pan+c,pans+c,NULL,1); }
}
for (i = 0; i < XTC_MIDI_VOICES; ++i) {
voice *v = p->voices+i;
double step = p->step[v->pitch];
float sample, pan;
if (!v->active || step == 0) continue;
sample = (float)(sin(TAU*v->phase)*.1*v->velocity/127.0) * levels[v->channel];
pan = pans[v->channel];
out[j] += sample * (right && pan > 0 ? 1-pan : 1);
if (right) right[j] += sample * (pan < 0 ? 1+pan : 1);
v->phase += step;
if (v->phase >= 1) v->phase -= 1;
}
}
}
/* Stable insertion keeps resets, scheduled offs/ons and live events ordered.
* All storage belongs to the prepared plan; callback work is strictly bounded. */
static int enqueue(xtc_midi_plan *p, size_t *count, xtc_midi_event e)
{
size_t i = *count;
if (i == 256) return 0;
while (i && p->queue[i-1].offset > e.offset) { p->queue[i] = p->queue[i-1]; --i; }
p->queue[i] = e; ++*count; return 1;
}
static xtc_result instruments(xtc_midi_plan *p, uint64_t position, const xtc_midi_event *events,
size_t count, float *out, float *right, size_t frames)
{
size_t track, offset, i, j;
for (track = 0; track < p->mix.count; ++track) {
if (!p->plugins[track]) continue;
for (offset = 0; offset < frames; offset += 256) {
size_t n = frames-offset < 256 ? frames-offset : 256, queued = 0;
uint64_t begin = position+offset, end = begin+n;
xtc_result r;
if (!offset && p->plugin_reset) {
unsigned int c;
for (c = 0; c < 16; ++c)
if (!enqueue(p,&queued,(xtc_midi_event){0,XTC_MIDI_ALL_OFF,c,0,0})) return XTC_ERR_CAPACITY;
}
/* End before start at equal offsets, including retriggered keys. */
for (i = 0; i < p->count; ++i) {
const span *s = p->notes+i;
if (s->track == track && s->end >= begin && s->end < end)
if (!enqueue(p,&queued,(xtc_midi_event){(unsigned int)(s->end-begin),XTC_MIDI_NOTE_OFF,s->channel,s->pitch,0})) return XTC_ERR_CAPACITY;
}
for (i = 0; i < p->count; ++i) {
const span *s = p->notes+i;
int chase = !offset && p->plugin_reset && s->start < begin && s->end > begin;
if (s->track == track && s->start >= p->cutoff[s->channel] &&
((s->start >= begin && s->start < end) || chase))
if (!enqueue(p,&queued,(xtc_midi_event){chase ? 0U : (unsigned int)(s->start-begin),XTC_MIDI_NOTE_ON,s->channel,s->pitch,s->velocity})) return XTC_ERR_CAPACITY;
}
if (track == p->live_track) for (i = 0; i < count; ++i) {
xtc_midi_event e = events[i];
if (e.offset >= offset && e.offset < offset+n) {
e.offset -= (unsigned int)offset;
if (!enqueue(p,&queued,e)) return XTC_ERR_CAPACITY;
}
}
r = xtc_plugin_process_midi(p->plugins[track],p->queue,queued,p->scratch,n);
if (r != XTC_OK) return r;
for (j = 0; j < n; ++j) {
float sample = p->scratch[j];
if (p->monitor) xtc_mix_monitor(&p->mix,p->monitor,p->monitor_stride,track,begin+j,offset+j,sample,right != NULL);
out[offset+j] += sample*xtc_mix_level(&p->mix,track,begin+j,right ? 0U : 2U);
if (right) right[offset+j] += sample*xtc_mix_level(&p->mix,track,begin+j,1);
}
}
}
p->plugin_reset = 0;
return XTC_OK;
}
/**
* @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_CONTROL || e->pitch > 127 || e->velocity > 127 || e->channel > 15 ||
(e->type == XTC_MIDI_NOTE_ON && !e->velocity))
return XTC_ERR_INVALID_ARGUMENT;
}
p->monitor_position = position;
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);
if (p->live_track == SIZE_MAX) event(p, events + i, position + next);
cursor = next;
}
segment(p, position + cursor, out + cursor, right ? right + cursor : NULL, frames - cursor);
{
xtc_result r = instruments(p,position,events,count,out,right,frames);
if (r != XTC_OK) {
memset(out,0,frames*sizeof(*out));
if (right) memset(right,0,frames*sizeof(*right));
xtc_midi_plan_reset(p); return r;
}
}
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);
}
void xtc_midi_plan_monitor(xtc_midi_plan *p, float *samples, size_t stride)
{ if (p) { p->monitor = samples; p->monitor_stride = stride; } }