173 lines
7.8 KiB
C
173 lines
7.8 KiB
C
|
|
#include "xtc/xtc_midi.h"
|
||
|
|
#include "../project/project_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; } 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 {
|
||
|
|
span notes[XTC_PROJECT_MAX_NOTES];
|
||
|
|
size_t count;
|
||
|
|
voice voices[XTC_MIDI_VOICES];
|
||
|
|
double step[128];
|
||
|
|
uint64_t cutoff[16], next_position, age;
|
||
|
|
};
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
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. */
|
||
|
|
}
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
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; }
|
||
|
|
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->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;
|
||
|
|
}
|
||
|
|
void xtc_midi_plan_destroy(xtc_midi_plan *p) { free(p); }
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
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++;
|
||
|
|
}
|
||
|
|
static void segment(xtc_midi_plan *p, uint64_t position, float *out, 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 || 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);
|
||
|
|
out[j] += (float)(sin(TAU * phase) * 0.1 * s->velocity / 127.0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (i = 0; i < XTC_MIDI_VOICES; ++i) {
|
||
|
|
voice *v = p->voices + i;
|
||
|
|
double step = p->step[v->pitch];
|
||
|
|
if (!v->active || !step) continue;
|
||
|
|
for (j = 0; j < frames; ++j) {
|
||
|
|
out[j] += (float)(sin(TAU * v->phase) * 0.1 * v->velocity / 127.0);
|
||
|
|
v->phase += step; if (v->phase >= 1) v->phase -= 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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)
|
||
|
|
{
|
||
|
|
size_t i, cursor = 0;
|
||
|
|
if (!p || !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));
|
||
|
|
for (i = 0; i < count; ++i) {
|
||
|
|
size_t next = events[i].offset;
|
||
|
|
segment(p, position + cursor, out + cursor, next - cursor);
|
||
|
|
event(p, events + i, position + next); cursor = next;
|
||
|
|
}
|
||
|
|
segment(p, position + cursor, out + cursor, frames - cursor);
|
||
|
|
p->next_position = position + frames;
|
||
|
|
return XTC_OK;
|
||
|
|
}
|