2026-10-27 00:53:41 +01:00
|
|
|
#include "../project/project_internal.h"
|
|
|
|
|
#include "../graph/graph_internal.h"
|
|
|
|
|
#include <string.h>
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
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;
|
|
|
|
|
size_t i, source = SIZE_MAX, output = SIZE_MAX;
|
|
|
|
|
uint64_t id;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (!p || !out)
|
|
|
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
2026-10-27 00:53:41 +01:00
|
|
|
r = xtc_project_clone(p, &c);
|
2026-10-27 01:37:21 +01:00
|
|
|
if (r == XTC_OK)
|
|
|
|
|
r = xtc_project_add_clip(c, track, "MIDI", start, length, &id);
|
2026-10-27 00:53:41 +01:00
|
|
|
if (r == XTC_OK) {
|
|
|
|
|
c->clips[c->clip_count - 1].midi = 1;
|
|
|
|
|
for (i = 0; i < c->graph->node_count; ++i) {
|
|
|
|
|
xtc_node *n = c->graph->nodes + i;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (n->type == XTC_NODE_INSTRUMENT)
|
|
|
|
|
source = n->id;
|
|
|
|
|
if (n->type == XTC_NODE_OUTPUT)
|
|
|
|
|
output = n->id;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
|
|
|
|
if (source == SIZE_MAX) {
|
|
|
|
|
r = xtc_graph_add_instrument(c->graph, &source);
|
2026-10-27 01:37:21 +01:00
|
|
|
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);
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (r == XTC_OK) {
|
2026-10-27 01:37:21 +01:00
|
|
|
xtc_graph *old = p->graph;
|
|
|
|
|
*p = *c;
|
|
|
|
|
c->graph = old;
|
|
|
|
|
*out = id;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
xtc_project_destroy(c);
|
|
|
|
|
return r;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_result xtc_project_note_count(const xtc_project *p, size_t *out)
|
|
|
|
|
{
|
2026-10-27 01:37:21 +01:00
|
|
|
if (!p || !out)
|
|
|
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
|
|
|
*out = p->note_count;
|
2026-10-27 00:53:41 +01:00
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2026-10-27 01:37:21 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2026-10-27 01:37:21 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2026-10-27 01:37:21 +01:00
|
|
|
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)
|
2026-10-27 00:53:41 +01:00
|
|
|
{
|
|
|
|
|
xtc_result r;
|
|
|
|
|
xtc_note_info n;
|
2026-10-27 01:37:21 +01:00
|
|
|
if (!p || !out)
|
|
|
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
2026-10-27 00:53:41 +01:00
|
|
|
r = valid_note(p, clip, start, length, pitch, velocity, channel);
|
2026-10-27 01:37:21 +01:00
|
|
|
if (r != XTC_OK)
|
|
|
|
|
return r;
|
|
|
|
|
if (p->note_count == XTC_PROJECT_MAX_NOTES || p->next_note_id == UINT64_MAX)
|
|
|
|
|
return XTC_ERR_CAPACITY;
|
2026-10-27 00:53:41 +01:00
|
|
|
memset(&n, 0, sizeof(n));
|
2026-10-27 01:37:21 +01:00
|
|
|
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;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2026-10-27 01:37:21 +01:00
|
|
|
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)
|
2026-10-27 00:53:41 +01:00
|
|
|
{
|
|
|
|
|
size_t i;
|
|
|
|
|
xtc_result r;
|
2026-10-27 01:37:21 +01:00
|
|
|
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;
|
2026-10-27 00:53:41 +01:00
|
|
|
r = valid_note(p, p->notes[i].clip_id, start, length, pitch, velocity, channel);
|
2026-10-27 01:37:21 +01:00
|
|
|
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;
|
2026-10-27 00:53:41 +01:00
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
2026-10-27 01:37:21 +01:00
|
|
|
|
2026-10-27 01:58:27 +01:00
|
|
|
/**
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_result xtc_project_remove_note(xtc_project *p, uint64_t id)
|
|
|
|
|
{
|
|
|
|
|
size_t i;
|
2026-10-27 01:37:21 +01:00
|
|
|
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;
|
2026-10-27 00:53:41 +01:00
|
|
|
memmove(p->notes + i, p->notes + i + 1, (p->note_count - i - 1) * sizeof(*p->notes));
|
2026-10-27 01:37:21 +01:00
|
|
|
--p->note_count;
|
|
|
|
|
return XTC_OK;
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|