300 lines
11 KiB
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;
|
|
}
|