#include "../project/project_internal.h" #include "../graph/graph_internal.h" #include 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; }