xtc/src/midi/model.c

300 lines
11 KiB
C

#include "../project/project_internal.h"
#include "../graph/graph_internal.h"
#include <string.h>
xtc_result xtc_project_ensure_midi_route(xtc_project *c)
{
size_t i, source = SIZE_MAX, output = SIZE_MAX;
xtc_result r = XTC_OK;
for (i = 0; i < c->graph->node_count; ++i) {
xtc_node *n = c->graph->nodes + i;
if (n->type == XTC_NODE_INSTRUMENT)
source = n->id;
if (n->type == XTC_NODE_OUTPUT)
output = n->id;
}
if (source == SIZE_MAX) {
r = xtc_graph_add_instrument(c->graph, &source);
if (r == XTC_OK && output == SIZE_MAX)
r = xtc_graph_add_output(c->graph, &output);
if (r == XTC_OK)
r = xtc_graph_connect(c->graph, source, output);
}
return r;
}
/**
* @brief Adds a MIDI clip and ensures an instrument output route exists.
*
* @param p Project model; borrowed for this call.
* @param track Stable track identifier.
* @param start Start position in musical ticks.
* @param length Duration in musical ticks.
* @param out Receives the operation result.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_add_midi_clip(xtc_project *p, uint64_t track, uint64_t start, uint64_t length, uint64_t *out)
{
xtc_project *c = NULL;
xtc_result r;
uint64_t id;
if (!p || !out)
return XTC_ERR_INVALID_ARGUMENT;
r = xtc_project_clone(p, &c);
if (r == XTC_OK)
r = xtc_project_add_clip(c, track, "MIDI", start, length, &id);
if (r == XTC_OK) {
c->clips[c->clip_count - 1].midi = 1;
r = xtc_project_ensure_midi_route(c);
}
if (r == XTC_OK) {
xtc_graph *old = p->graph;
*p = *c;
c->graph = old;
*out = id;
}
xtc_project_destroy(c);
return r;
}
/**
* @brief Copies the number of notes in the project.
*
* @param p Project model; borrowed for this call.
* @param out Receives the operation result.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_note_count(const xtc_project *p, size_t *out)
{
if (!p || !out)
return XTC_ERR_INVALID_ARGUMENT;
*out = p->note_count;
return XTC_OK;
}
/**
* @brief Copies a note snapshot selected by dense list index.
*
* @param p Project model; borrowed for this call.
* @param i Zero-based index in the dense list.
* @param out Receives the operation result.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_get_note(const xtc_project *p, size_t i, xtc_note_info *out)
{
if (!p || !out)
return XTC_ERR_INVALID_ARGUMENT;
if (i >= p->note_count)
return XTC_ERR_NOT_FOUND;
*out = p->notes[i];
return XTC_OK;
}
/**
* @brief Validates a note's MIDI fields and range within a MIDI clip source.
*
* @param p Project model; borrowed for this call.
* @param clip Stable clip identifier.
* @param start Start position in musical ticks.
* @param length Duration in musical ticks.
* @param pitch MIDI note number, 0 to 127.
* @param velocity MIDI note velocity, 1 to 127.
* @param channel MIDI channel, 0 to 15.
* @return XTC_OK on success, or an error code describing the failure.
*/
static xtc_result valid_note(const xtc_project *p, uint64_t clip, uint64_t start, uint64_t length, unsigned int pitch,
unsigned int velocity, unsigned int channel)
{
xtc_clip_info c;
if (pitch > 127 || !velocity || velocity > 127 || channel > 15 || !length)
return XTC_ERR_INVALID_ARGUMENT;
if (xtc_project_find_clip(p, clip, &c) != XTC_OK)
return XTC_ERR_NOT_FOUND;
if (!c.midi)
return XTC_ERR_INVALID_STATE;
if (start >= c.source_length_tick || length > c.source_length_tick - start)
return XTC_ERR_INVALID_ARGUMENT;
return XTC_OK;
}
/**
* @brief Adds a validated MIDI note with a new stable identifier.
*
* @param p Project model; borrowed for this call.
* @param clip Stable clip identifier.
* @param start Start position in musical ticks.
* @param length Duration in musical ticks.
* @param pitch MIDI note number, 0 to 127.
* @param velocity MIDI note velocity, 1 to 127.
* @param channel MIDI channel, 0 to 15.
* @param out Receives the operation result.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_add_note(xtc_project *p, uint64_t clip, uint64_t start, uint64_t length, unsigned int pitch,
unsigned int velocity, unsigned int channel, uint64_t *out)
{
xtc_result r;
xtc_note_info n;
if (!p || !out)
return XTC_ERR_INVALID_ARGUMENT;
r = valid_note(p, clip, start, length, pitch, velocity, channel);
if (r != XTC_OK)
return r;
if (p->note_count == XTC_PROJECT_MAX_NOTES || p->next_note_id == UINT64_MAX)
return XTC_ERR_CAPACITY;
memset(&n, 0, sizeof(n));
n.id = p->next_note_id++;
n.clip_id = clip;
n.start_tick = start;
n.length_tick = length;
n.pitch = pitch;
n.velocity = velocity;
n.channel = channel;
p->notes[p->note_count++] = n;
*out = n.id;
return XTC_OK;
}
/**
* @brief Replaces a note's source range, pitch, velocity, and channel after validation.
*
* @param p Project model; borrowed for this call.
* @param id Stable identifier of the object to find or edit.
* @param start Start position in musical ticks.
* @param length Duration in musical ticks.
* @param pitch MIDI note number, 0 to 127.
* @param velocity MIDI note velocity, 1 to 127.
* @param channel MIDI channel, 0 to 15.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_edit_note(xtc_project *p, uint64_t id, uint64_t start, uint64_t length, unsigned int pitch,
unsigned int velocity, unsigned int channel)
{
size_t i;
xtc_result r;
if (!p)
return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < p->note_count; ++i)
if (p->notes[i].id == id)
break;
if (i == p->note_count)
return XTC_ERR_NOT_FOUND;
r = valid_note(p, p->notes[i].clip_id, start, length, pitch, velocity, channel);
if (r != XTC_OK)
return r;
p->notes[i].start_tick = start;
p->notes[i].length_tick = length;
p->notes[i].pitch = pitch;
p->notes[i].velocity = velocity;
p->notes[i].channel = channel;
return XTC_OK;
}
/**
* @brief Removes a note selected by stable identifier.
*
* @param p Project model; borrowed for this call.
* @param id Stable identifier of the object to find or edit.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_remove_note(xtc_project *p, uint64_t id)
{
size_t i;
if (!p)
return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < p->note_count; ++i)
if (p->notes[i].id == id)
break;
if (i == p->note_count)
return XTC_ERR_NOT_FOUND;
memmove(p->notes + i, p->notes + i + 1, (p->note_count - i - 1) * sizeof(*p->notes));
--p->note_count;
return XTC_OK;
}
/* Selection is evaluated against original properties, never transformed ones. */
static int matches(const xtc_note_info *n, const xtc_note_filter *f)
{
return n->clip_id == f->clip_id && (f->note_id == UINT64_MAX || n->id == f->note_id) &&
(f->channel == -1 || n->channel == (unsigned int)f->channel) &&
n->pitch >= f->pitch_min && n->pitch <= f->pitch_max;
}
static xtc_result transformed(const xtc_project *p, xtc_note_info *n, const xtc_note_edit *e,
unsigned int copy)
{
int pitch = (int)n->pitch + e->transpose;
if (pitch < 0 || pitch > 127) return XTC_ERR_INVALID_ARGUMENT;
n->pitch = (unsigned int)pitch;
if (e->velocity != -1) n->velocity = (unsigned int)e->velocity;
if (e->channel != -1) n->channel = (unsigned int)e->channel;
if (e->grid_tick) {
uint64_t rem = n->start_tick % e->grid_tick;
n->start_tick -= rem;
if (rem >= e->grid_tick - rem) {
if (n->start_tick > UINT64_MAX - e->grid_tick) return XTC_ERR_INVALID_ARGUMENT;
n->start_tick += e->grid_tick;
}
}
if (copy) {
if (e->interval_tick > (UINT64_MAX - n->start_tick) / copy) return XTC_ERR_INVALID_ARGUMENT;
n->start_tick += e->interval_tick * copy;
}
return valid_note(p, n->clip_id, n->start_tick, n->length_tick, n->pitch, n->velocity, n->channel);
}
xtc_result xtc_project_edit_notes(xtc_project *p, const xtc_note_filter *f,
const xtc_note_edit *e, size_t *out)
{
size_t i, count = 0, original, write = 0;
unsigned int k;
xtc_clip_info clip;
xtc_result r;
if (!p || !f || !e || !out || f->channel < -1 || f->channel > 15 ||
f->pitch_min > f->pitch_max || f->pitch_max > 127 ||
e->action < XTC_NOTES_EDIT || e->action > XTC_NOTES_DELETE)
return XTC_ERR_INVALID_ARGUMENT;
r = xtc_project_find_clip(p, f->clip_id, &clip);
if (r != XTC_OK) return r;
if (!clip.midi) return XTC_ERR_INVALID_STATE;
if (e->action != XTC_NOTES_DELETE && (e->transpose < -127 || e->transpose > 127 ||
e->velocity < -1 || e->velocity == 0 || e->velocity > 127 || e->channel < -1 || e->channel > 15 ||
(e->action == XTC_NOTES_REPEAT && (!e->copies || e->copies > XTC_PROJECT_MAX_NOTES || !e->interval_tick))))
return XTC_ERR_INVALID_ARGUMENT;
original = p->note_count;
for (i = 0; i < original; ++i) if (matches(p->notes+i, f)) ++count;
if (!count) return XTC_ERR_NOT_FOUND;
if (e->action == XTC_NOTES_REPEAT &&
(count > (XTC_PROJECT_MAX_NOTES-original) / e->copies ||
(uint64_t)count > (UINT64_MAX-p->next_note_id) / e->copies)) return XTC_ERR_CAPACITY;
/* Validate every result before changing anything. Bounded by note capacity. */
if (e->action != XTC_NOTES_DELETE)
for (i = 0; i < original; ++i) if (matches(p->notes+i, f)) {
unsigned int copies = e->action == XTC_NOTES_REPEAT ? e->copies : 0;
for (k = copies ? 1U : 0U; k <= copies; ++k) {
xtc_note_info n = p->notes[i];
r = transformed(p, &n, e, k);
if (r != XTC_OK) return r;
}
}
for (i = 0; i < original; ++i) {
xtc_note_info n = p->notes[i];
if (!matches(&n, f)) {
if (e->action == XTC_NOTES_DELETE) p->notes[write++] = n;
continue;
}
if (e->action == XTC_NOTES_DELETE) continue;
if (e->action == XTC_NOTES_EDIT) {
(void)transformed(p, &n, e, 0);
p->notes[i] = n;
} else for (k = 1; k <= e->copies; ++k) {
n = p->notes[i];
(void)transformed(p, &n, e, k);
n.id = p->next_note_id++;
p->notes[p->note_count++] = n;
}
}
if (e->action == XTC_NOTES_DELETE) p->note_count = write;
*out = count;
return XTC_OK;
}