#include "project_internal.h" #include "xtc/xtc_audio.h" #include "../graph/graph_internal.h" #include #include #include /** * @brief Checks for a nonempty UTF-8 name of at most 63 bytes without ASCII controls. * * @param name NUL-terminated name. * @return Nonzero for a valid name, zero otherwise. */ int xtc_project_valid_name(const char *name) { size_t length = 0U, i = 0U; if (name == NULL) return 0; while (length < XTC_PROJECT_NAME_BYTES && name[length] != '\0') ++length; if (length == 0U || length == XTC_PROJECT_NAME_BYTES) return 0; while (i < length) { unsigned char byte = (unsigned char)name[i++]; size_t extra, j; if (byte < 32U || byte == 127U) return 0; if (byte < 128U) continue; if (byte >= 194U && byte <= 223U) extra = 1U; else if (byte >= 224U && byte <= 239U) extra = 2U; else if (byte >= 240U && byte <= 244U) extra = 3U; else return 0; if (extra > length - i) return 0; if ((byte == 224U && (unsigned char)name[i] < 160U) || (byte == 237U && (unsigned char)name[i] >= 160U) || (byte == 240U && (unsigned char)name[i] < 144U) || (byte == 244U && (unsigned char)name[i] >= 144U)) return 0; for (j = 0U; j < extra; ++j) { unsigned char tail = (unsigned char)name[i++]; if (tail < 128U || tail > 191U) return 0; } } return 1; } /** * @brief Finds a project's dense track index by stable identifier. * * @param project Project model; borrowed for this call. * @param id Stable identifier of the object to find or edit. * @return Dense track index, or SIZE_MAX if absent. */ static size_t track_index(const xtc_project *project, uint64_t id) { size_t i; for (i = 0U; i < project->track_count; ++i) if (project->tracks[i].id == id) return i; return SIZE_MAX; } /** * @brief Finds a project's dense clip index by stable identifier. * * @param project Project model; borrowed for this call. * @param id Stable identifier of the object to find or edit. * @return Dense clip index, or SIZE_MAX if absent. */ static size_t clip_index(const xtc_project *project, uint64_t id) { size_t i; for (i = 0U; i < project->clip_count; ++i) if (project->clips[i].id == id) return i; return SIZE_MAX; } /** * @brief Checks that a nonempty timeline range and its source slice fit their bounds. * * @param start Timeline start tick. * @param length Nonzero duration in ticks. * @param offset Source offset in ticks. * @param source Source duration in ticks. * @return Nonzero if both timeline and source bounds are valid, zero otherwise. */ static int valid_range(uint64_t start, uint64_t length, uint64_t offset, uint64_t source) { return start <= XTC_PROJECT_MAX_TICK && length != 0U && length <= XTC_PROJECT_MAX_TICK - start && source <= XTC_PROJECT_MAX_TICK && offset <= source && length <= source - offset; } /** * @brief Allocates an empty project with an output-only graph policy and 120 BPM tempo. * * @param out_project Receives the newly allocated project; caller owns it on success. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_create(xtc_project **out_project) { xtc_project *project; xtc_result result; if (out_project == NULL) return XTC_ERR_INVALID_ARGUMENT; *out_project = NULL; project = calloc(1U, sizeof(*project)); if (project == NULL) return XTC_ERR_OUT_OF_MEMORY; result = xtc_graph_create(64U, &project->graph); if (result != XTC_OK) { free(project); return result; } project->graph->require_output_nodes = 1; project->tempo = 120000U; project->live_instrument = UINT64_MAX; { size_t channel; for (channel = 0; channel < 16; ++channel) { project->control_map[channel][0] = 7; project->control_map[channel][1] = 10; project->control_map[channel][2] = 64; } } *out_project = project; return XTC_OK; } /** * @brief Releases a project and its graph; accepts NULL. * * @param project Project model; borrowed for this call. */ void xtc_project_destroy(xtc_project *project) { if (project != NULL) { xtc_graph_destroy(project->graph); free(project); } } /** * @brief Returns the project's borrowed graph, or NULL for a NULL project. * * @param project Project model; borrowed for this call. * @return Borrowed graph valid until project mutation or destruction, or NULL for NULL. */ const xtc_graph *xtc_project_graph(const xtc_project *project) { return project == NULL ? NULL : project->graph; } /** * @brief Copies and validates a graph before replacing the project's graph. * * @param project Project model; borrowed for this call. * @param source Graph model; borrowed for this call. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_set_graph(xtc_project *project, const xtc_graph *source) { xtc_graph *copy; xtc_result result; if (project == NULL || source == NULL) return XTC_ERR_INVALID_ARGUMENT; if (source->node_count > 64U || source->edge_count > 4096U) return XTC_ERR_CAPACITY; result = xtc_graph_validate(source); if (result != XTC_OK) return result; result = xtc_graph_create(64U, ©); if (result != XTC_OK) return result; copy->node_count = source->node_count; copy->edge_count = source->edge_count; copy->next_id = source->next_id; copy->require_output_nodes = source->require_output_nodes; memcpy(copy->nodes, source->nodes, source->node_count * sizeof(*copy->nodes)); memcpy(copy->edges, source->edges, source->edge_count * sizeof(*copy->edges)); { size_t i; for (i = 0; i < copy->node_count; ++i) if (copy->nodes[i].type == XTC_NODE_PLUGIN) copy->nodes[i].plugin.tempo = project->tempo; } xtc_graph_destroy(project->graph); project->graph = copy; return XTC_OK; } /** * @brief Allocates an independent copy of project metadata and its graph. * * @param source Project model; borrowed for this call. * @param out_project Receives the newly allocated project; caller owns it on success. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_clone(const xtc_project *source, xtc_project **out_project) { xtc_project *copy; xtc_result result; if (out_project == NULL) return XTC_ERR_INVALID_ARGUMENT; *out_project = NULL; if (source == NULL) return XTC_ERR_INVALID_ARGUMENT; copy = malloc(sizeof(*copy)); if (copy == NULL) return XTC_ERR_OUT_OF_MEMORY; *copy = *source; copy->graph = NULL; result = xtc_project_set_graph(copy, source->graph); if (result != XTC_OK) { free(copy); return result; } *out_project = copy; return XTC_OK; } /** * @brief Copies the current track and clip counts. * * @param project Project model; borrowed for this call. * @param tracks Receives the number of tracks. * @param clips Receives the number of clips. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_get_counts(const xtc_project *project, size_t *tracks, size_t *clips) { if (project == NULL || tracks == NULL || clips == NULL) return XTC_ERR_INVALID_ARGUMENT; *tracks = project->track_count; *clips = project->clip_count; return XTC_OK; } /** * @brief Copies a track snapshot selected by dense list index. * * @param project Project model; borrowed for this call. * @param index Zero-based index in the dense list. * @param track Receives a copy of the track metadata. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_get_track(const xtc_project *project, size_t index, xtc_track_info *track) { if (project == NULL || track == NULL) return XTC_ERR_INVALID_ARGUMENT; if (index >= project->track_count) return XTC_ERR_NOT_FOUND; *track = project->tracks[index]; return XTC_OK; } /** * @brief Copies a clip snapshot selected by dense list index. * * @param project Project model; borrowed for this call. * @param index Zero-based index in the dense list. * @param clip Receives a copy of the clip metadata. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_get_clip(const xtc_project *project, size_t index, xtc_clip_info *clip) { if (project == NULL || clip == NULL) return XTC_ERR_INVALID_ARGUMENT; if (index >= project->clip_count) return XTC_ERR_NOT_FOUND; *clip = project->clips[index]; return XTC_OK; } /** * @brief Copies a clip snapshot selected by stable identifier. * * @param project Project model; borrowed for this call. * @param id Stable identifier of the object to find or edit. * @param clip Receives a copy of the clip metadata. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_find_clip(const xtc_project *project, uint64_t id, xtc_clip_info *clip) { if (project == NULL) return XTC_ERR_INVALID_ARGUMENT; return xtc_project_get_clip(project, clip_index(project, id), clip); } /** * @brief Appends a track with unity gain and a new stable identifier. * * @param project Project model; borrowed for this call. * @param name NUL-terminated name. * @param id Receives the new stable identifier. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_t *id) { xtc_track_info track = {0}; if (project == NULL || id == NULL || !xtc_project_valid_name(name)) return XTC_ERR_INVALID_ARGUMENT; if (project->track_count == XTC_PROJECT_MAX_TRACKS || project->next_track_id == UINT64_MAX) return XTC_ERR_CAPACITY; track.id = project->next_track_id++; track.gain = 1.0f; track.bus_id = UINT64_MAX; memcpy(track.name, name, strlen(name) + 1U); project->tracks[project->track_count++] = track; *id = track.id; return XTC_OK; } /** * @brief Removes a track and its clips, notes, and automation. * * @param project 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_track(xtc_project *project, uint64_t id) { size_t index, i, count = 0U; if (project == NULL) return XTC_ERR_INVALID_ARGUMENT; index = track_index(project, id); if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND; for (i = 0U; i < project->clip_count; ++i) if (project->clips[i].track_id != id) project->clips[count++] = project->clips[i]; project->clip_count = count; for (i = 0; i < project->track_count; ++i) if (project->tracks[i].bus_id == id) project->tracks[i].bus_id = UINT64_MAX; if (project->live_instrument == id) project->live_instrument = UINT64_MAX; memmove(project->instruments+index,project->instruments+index+1, (project->track_count-index-1)*sizeof(*project->instruments)); memset(project->instruments+project->track_count-1,0,sizeof(*project->instruments)); memmove(project->tracks + index, project->tracks + index + 1U, (project->track_count - index - 1U) * sizeof(*project->tracks)); --project->track_count; count = 0; for (i = 0; i < project->note_count; ++i) if (clip_index(project, project->notes[i].clip_id) != SIZE_MAX) project->notes[count++] = project->notes[i]; project->note_count = count; count = 0; for (i = 0; i < project->automation_count; ++i) if (project->automation[i].track_id != id) project->automation[count++] = project->automation[i]; project->automation_count = count; return XTC_OK; } /** * @brief Appends a silent region on an existing track with a new stable identifier. * * @param project Project model; borrowed for this call. * @param track Stable track identifier. * @param name NUL-terminated name. * @param start Start position in musical ticks. * @param length Duration in musical ticks. * @param id Receives the new stable identifier. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_add_clip(xtc_project *project, uint64_t track, const char *name, uint64_t start, uint64_t length, uint64_t *id) { xtc_clip_info clip = {0}; if (project == NULL || id == NULL || !xtc_project_valid_name(name) || !valid_range(start, length, 0U, length)) return XTC_ERR_INVALID_ARGUMENT; if (track_index(project, track) == SIZE_MAX) return XTC_ERR_NOT_FOUND; if (project->tracks[track_index(project, track)].is_bus) return XTC_ERR_INVALID_STATE; if (project->clip_count == XTC_PROJECT_MAX_CLIPS || project->next_clip_id == UINT64_MAX) return XTC_ERR_CAPACITY; clip.gain = 1.0f; clip.id = project->next_clip_id++; clip.track_id = track; clip.start_tick = start; clip.length_tick = clip.source_length_tick = length; memcpy(clip.name, name, strlen(name) + 1U); project->clips[project->clip_count++] = clip; *id = clip.id; return XTC_OK; } /** * @brief Removes a clip and its notes. * * @param project 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_clip(xtc_project *project, uint64_t id) { size_t index; if (project == NULL) return XTC_ERR_INVALID_ARGUMENT; index = clip_index(project, id); if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND; memmove(project->clips + index, project->clips + index + 1U, (project->clip_count - index - 1U) * sizeof(*project->clips)); --project->clip_count; { size_t i, count = 0; for (i = 0; i < project->note_count; ++i) if (project->notes[i].clip_id != id) project->notes[count++] = project->notes[i]; project->note_count = count; } return XTC_OK; } /** * @brief Atomically moves or trims a clip within its existing source bounds. * * @param project Project model; borrowed for this call. * @param id Stable identifier of the object to find or edit. * @param track Stable track identifier. * @param start Start position in musical ticks. * @param length Duration in musical ticks. * @param offset Sample offset within the current block. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_edit_clip(xtc_project *project, uint64_t id, uint64_t track, uint64_t start, uint64_t length, uint64_t offset) { size_t index; if (project == NULL) return XTC_ERR_INVALID_ARGUMENT; index = clip_index(project, id); if (index == SIZE_MAX || track_index(project, track) == SIZE_MAX) return XTC_ERR_NOT_FOUND; if (project->tracks[track_index(project, track)].is_bus) return XTC_ERR_INVALID_STATE; if (!valid_range(start, length, offset, project->clips[index].source_length_tick)) return XTC_ERR_INVALID_ARGUMENT; project->clips[index].track_id = track; project->clips[index].start_tick = start; project->clips[index].length_tick = length; project->clips[index].offset_tick = offset; return XTC_OK; } /** * @brief Returns the constant tempo in milli-BPM, or zero for a NULL project. * * @param project Project model; borrowed for this call. * @return Tempo in milli-BPM, or zero for NULL. */ unsigned int xtc_project_tempo(const xtc_project *project) { return project == NULL ? 0U : project->tempo; } /** * @brief Sets the constant tempo within the range 20000 to 300000 milli-BPM. * * @param project Project model; borrowed for this call. * @param tempo Tempo in milli-BPM. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_set_tempo(xtc_project *project, unsigned int tempo) { if (project == NULL || tempo < 20000U || tempo > 300000U) return XTC_ERR_INVALID_ARGUMENT; project->tempo = tempo; {size_t i;for(i=0;itrack_count;++i)if(project->instruments[i].instrument)project->instruments[i].tempo=tempo;} { size_t i; for (i = 0; i < project->graph->node_count; ++i) if (project->graph->nodes[i].type == XTC_NODE_PLUGIN) project->graph->nodes[i].plugin.tempo = tempo; } return XTC_OK; } /** * @brief Converts a musical tick to an absolute sample position, rounding down. * * @param project Project model; borrowed for this call. * @param tick Musical position in ticks. * @param rate Sample rate in hertz. * @param sample Receives the sample position. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_tick_to_sample(const xtc_project *project, uint64_t tick, unsigned int rate, uint64_t *sample) { if (project == NULL || sample == NULL || rate == 0U || rate > 768000U) return XTC_ERR_INVALID_ARGUMENT; if (tick > XTC_PROJECT_MAX_TICK) return XTC_ERR_CAPACITY; *sample = tick * rate * UINT64_C(60000) / ((uint64_t)project->tempo * XTC_TICKS_PER_QUARTER); return XTC_OK; } /** * @brief Converts an absolute sample position to a musical tick, rounding down. * * @param project Project model; borrowed for this call. * @param sample Absolute sample position. * @param rate Sample rate in hertz. * @param tick Receives the musical tick. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sample, unsigned int rate, uint64_t *tick) { uint64_t scale, value, divisor, whole; if (project == NULL || tick == NULL || rate == 0U || rate > 768000U) return XTC_ERR_INVALID_ARGUMENT; scale = (uint64_t)project->tempo * XTC_TICKS_PER_QUARTER; divisor = (uint64_t)rate * UINT64_C(60000); whole = sample / divisor; if (whole > XTC_PROJECT_MAX_TICK / scale) return XTC_ERR_CAPACITY; value = whole * scale + (sample % divisor) * scale / divisor; if (value > XTC_PROJECT_MAX_TICK) return XTC_ERR_CAPACITY; *tick = value; return XTC_OK; } /** * @brief Validates project IDs, timeline ranges, names, graph, notes, and automation. * * @param project Project model; borrowed for this call. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_validate(const xtc_project *project) { size_t i, j; if (project->tempo < 20000U || project->tempo > 300000U || project->track_count > XTC_PROJECT_MAX_TRACKS || project->clip_count > XTC_PROJECT_MAX_CLIPS || project->note_count > XTC_PROJECT_MAX_NOTES) return XTC_ERR_FORMAT; for (i = 0; i < 16; ++i) for (j = 0; j < 3; ++j) { unsigned int cc = project->control_map[i][j]; size_t k; if (cc > 128 || cc == 120 || cc == 123) return XTC_ERR_FORMAT; for (k = 0; k < j; ++k) if (cc != 128 && cc == project->control_map[i][k]) return XTC_ERR_FORMAT; } if (project->live_instrument != UINT64_MAX) { size_t target = track_index(project,project->live_instrument); if (target == SIZE_MAX || !project->instruments[target].instrument) return XTC_ERR_FORMAT; } for (i = 0U; i < project->track_count; ++i) { if (project->instruments[i].instrument && (project->tracks[i].is_bus || xtc_plugin_spec_validate(project->instruments+i) != XTC_OK)) return XTC_ERR_FORMAT; if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name)) return XTC_ERR_FORMAT; { const xtc_track_info *t = project->tracks+i; size_t bus = track_index(project, t->bus_id); if ((t->is_bus != 0 && t->is_bus != 1) || (t->bus_id != UINT64_MAX && (t->is_bus || bus == SIZE_MAX || !project->tracks[bus].is_bus))) return XTC_ERR_FORMAT; } for (j = 0U; j < i; ++j) if (project->tracks[j].id == project->tracks[i].id) return XTC_ERR_FORMAT; } for (i = 0U; i < project->clip_count; ++i) { const xtc_clip_info *clip = project->clips + i; size_t path_length = 0U; while (path_length < XTC_ASSET_PATH_BYTES && clip->asset_path[path_length]) { unsigned char byte = (unsigned char)clip->asset_path[path_length++]; if (byte < 32U || byte == 127U) return XTC_ERR_FORMAT; } if (path_length == XTC_ASSET_PATH_BYTES || (path_length && clip->asset_path[0] != '/')) return XTC_ERR_FORMAT; if (!isfinite(clip->gain) || clip->gain < 0 || clip->gain > 4 || clip->fade_in_tick > clip->source_length_tick || clip->fade_out_tick > clip->source_length_tick || (clip->midi && (clip->gain != 1 || clip->fade_in_tick || clip->fade_out_tick))) return XTC_ERR_FORMAT; if ((clip->midi != 0 && clip->midi != 1) || (clip->midi && path_length)) return XTC_ERR_FORMAT; if (clip->id >= project->next_clip_id || track_index(project, clip->track_id) == SIZE_MAX || !xtc_project_valid_name(clip->name) || !valid_range(clip->start_tick, clip->length_tick, clip->offset_tick, clip->source_length_tick)) return XTC_ERR_FORMAT; if (project->tracks[track_index(project, clip->track_id)].is_bus) return XTC_ERR_FORMAT; for (j = 0U; j < i; ++j) if (project->clips[j].id == clip->id) return XTC_ERR_FORMAT; } for (i = 0; i < project->note_count; ++i) { const xtc_note_info *n = project->notes + i; size_t c = clip_index(project, n->clip_id); if (c == SIZE_MAX || !project->clips[c].midi || n->id >= project->next_note_id || n->pitch > 127 || !n->velocity || n->velocity > 127 || n->channel > 15 || !n->length_tick || n->start_tick >= project->clips[c].source_length_tick || n->length_tick > project->clips[c].source_length_tick - n->start_tick) return XTC_ERR_FORMAT; for (j = 0; j < i; ++j) if (project->notes[j].id == n->id) return XTC_ERR_FORMAT; } if (project->automation_count > XTC_MAX_AUTOMATION_POINTS) return XTC_ERR_FORMAT; for (i = 0; i < project->track_count; ++i) { const xtc_track_info *t = project->tracks + i; if (!isfinite(t->gain) || t->gain < 0 || t->gain > 4 || !isfinite(t->pan) || t->pan < -1 || t->pan > 1 || (t->mute != 0 && t->mute != 1) || (t->solo != 0 && t->solo != 1)) return XTC_ERR_FORMAT; } for (i = 0; i < project->automation_count; ++i) { const xtc_automation_point *a = project->automation + i; if (track_index(project, a->track_id) == SIZE_MAX || a->tick > XTC_PROJECT_MAX_TICK || a->step > 1 || !isfinite(a->value) || (a->parameter != XTC_AUTOMATION_GAIN && a->parameter != XTC_AUTOMATION_PAN) || a->value < (a->parameter == XTC_AUTOMATION_GAIN ? 0 : -1) || a->value > (a->parameter == XTC_AUTOMATION_GAIN ? 4 : 1)) return XTC_ERR_FORMAT; for (j = 0; j < i; ++j) if (project->automation[j].track_id == a->track_id && project->automation[j].parameter == a->parameter && project->automation[j].tick == a->tick) return XTC_ERR_FORMAT; } return XTC_OK; } /** * @brief Sets an absolute audio asset reference, or clears it to make a region silent. * * @param project Project model; borrowed for this call. * @param id Stable identifier of the object to find or edit. * @param path NUL-terminated file or asset path. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_set_clip_asset(xtc_project *project, uint64_t id, const char *path) { size_t index, length = 0U, i; if (!project || !path) return XTC_ERR_INVALID_ARGUMENT; while (length < XTC_ASSET_PATH_BYTES && path[length]) ++length; if (length == XTC_ASSET_PATH_BYTES || (length && path[0] != '/')) return XTC_ERR_INVALID_ARGUMENT; for (i = 0; i < length; ++i) if ((unsigned char)path[i] < 32U || path[i] == 127) return XTC_ERR_INVALID_ARGUMENT; index = clip_index(project, id); if (index == SIZE_MAX) return XTC_ERR_NOT_FOUND; if (project->clips[index].midi) return XTC_ERR_INVALID_STATE; memmove(project->clips[index].asset_path, path, length + 1U); return XTC_OK; } xtc_result xtc_project_duplicate_clip(xtc_project *p, uint64_t id, uint64_t track, uint64_t start, uint64_t *out) { size_t index, i, count = 0, original_notes; xtc_clip_info c; if (!p || !out) return XTC_ERR_INVALID_ARGUMENT; index = clip_index(p,id); if (index == SIZE_MAX || track_index(p,track) == SIZE_MAX) return XTC_ERR_NOT_FOUND; if (p->tracks[track_index(p,track)].is_bus) return XTC_ERR_INVALID_STATE; c = p->clips[index]; if (!valid_range(start,c.length_tick,c.offset_tick,c.source_length_tick)) return XTC_ERR_INVALID_ARGUMENT; for (i = 0; i < p->note_count; ++i) if (p->notes[i].clip_id == id) ++count; if (p->clip_count == XTC_PROJECT_MAX_CLIPS || p->next_clip_id == UINT64_MAX || count > XTC_PROJECT_MAX_NOTES-p->note_count || p->next_note_id > UINT64_MAX-count) return XTC_ERR_CAPACITY; c.id = p->next_clip_id++; c.track_id = track; c.start_tick = start; p->clips[p->clip_count++] = c; original_notes = p->note_count; for (i = 0; i < original_notes; ++i) if (p->notes[i].clip_id == id) { xtc_note_info n = p->notes[i]; n.id = p->next_note_id++; n.clip_id = c.id; p->notes[p->note_count++] = n; } *out = c.id; return XTC_OK; } xtc_result xtc_project_rename_track(xtc_project *p, uint64_t id, const char *name) { size_t i; if (!p || !xtc_project_valid_name(name)) return XTC_ERR_INVALID_ARGUMENT; i = track_index(p,id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND; memmove(p->tracks[i].name,name,strlen(name)+1); return XTC_OK; } xtc_result xtc_project_rename_clip(xtc_project *p, uint64_t id, const char *name) { size_t i; if (!p || !xtc_project_valid_name(name)) return XTC_ERR_INVALID_ARGUMENT; i = clip_index(p,id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND; memmove(p->clips[i].name,name,strlen(name)+1); return XTC_OK; } xtc_result xtc_project_move_track(xtc_project *p, uint64_t id, size_t destination) { size_t i; xtc_track_info track; if (!p || destination >= p->track_count) return XTC_ERR_INVALID_ARGUMENT; i = track_index(p,id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND; { xtc_plugin_spec instrument = p->instruments[i]; if (i < destination) memmove(p->instruments+i,p->instruments+i+1,(destination-i)*sizeof(instrument)); else if (i > destination) memmove(p->instruments+destination+1,p->instruments+destination,(i-destination)*sizeof(instrument)); p->instruments[destination] = instrument; } track = p->tracks[i]; if (i < destination) memmove(p->tracks+i,p->tracks+i+1,(destination-i)*sizeof(track)); else if (i > destination) memmove(p->tracks+destination+1,p->tracks+destination,(i-destination)*sizeof(track)); p->tracks[destination] = track; return XTC_OK; } xtc_result xtc_project_set_clip_audio(xtc_project *p, uint64_t id, float gain, uint64_t in, uint64_t out) { size_t i; if (!p || !isfinite(gain) || gain < 0 || gain > 4) return XTC_ERR_INVALID_ARGUMENT; i = clip_index(p, id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND; if (p->clips[i].midi) return XTC_ERR_INVALID_STATE; if (in > p->clips[i].source_length_tick || out > p->clips[i].source_length_tick) return XTC_ERR_INVALID_ARGUMENT; p->clips[i].gain = gain; p->clips[i].fade_in_tick = in; p->clips[i].fade_out_tick = out; return XTC_OK; } xtc_result xtc_project_split_audio(xtc_project *p, uint64_t id, uint64_t tick, uint64_t *out) { size_t i; xtc_clip_info right; uint64_t left_length; if (!p || !out) return XTC_ERR_INVALID_ARGUMENT; i = clip_index(p, id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND; right = p->clips[i]; if (right.midi) return XTC_ERR_INVALID_STATE; if (tick <= right.start_tick || tick >= right.start_tick + right.length_tick) return XTC_ERR_INVALID_ARGUMENT; if (p->clip_count == XTC_PROJECT_MAX_CLIPS || p->next_clip_id == UINT64_MAX) return XTC_ERR_CAPACITY; left_length = tick - right.start_tick; right.id = p->next_clip_id++; right.start_tick = tick; right.offset_tick += left_length; right.length_tick -= left_length; right.fade_in_tick = 0; p->clips[i].length_tick = left_length; p->clips[i].fade_out_tick = 0; p->clips[p->clip_count++] = right; *out = right.id; return XTC_OK; } xtc_result xtc_project_set_instrument(xtc_project *p, uint64_t track, const xtc_plugin_spec *spec) { size_t i; xtc_project *candidate = NULL; xtc_result r; if (!p) return XTC_ERR_INVALID_ARGUMENT; i = track_index(p,track); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND; if (p->tracks[i].is_bus) return XTC_ERR_INVALID_STATE; if (spec && (!spec->instrument || xtc_plugin_spec_validate(spec) != XTC_OK)) return XTC_ERR_UNSUPPORTED; r = xtc_project_clone(p,&candidate); if (r == XTC_OK && spec) r = xtc_project_ensure_midi_route(candidate); if (r == XTC_OK) { if (spec) { candidate->instruments[i] = *spec; candidate->instruments[i].tempo=p->tempo; candidate->live_instrument = track; } else { memset(candidate->instruments+i,0,sizeof(*candidate->instruments)); if (candidate->live_instrument == track) candidate->live_instrument = UINT64_MAX; } { xtc_graph *old = p->graph; *p = *candidate; candidate->graph = old; } } xtc_project_destroy(candidate); return r; } xtc_result xtc_project_get_instrument(const xtc_project *p, uint64_t track, xtc_plugin_spec *out) { size_t i; if (!p || !out) return XTC_ERR_INVALID_ARGUMENT; i = track_index(p,track); if (i == SIZE_MAX || !p->instruments[i].instrument) return XTC_ERR_NOT_FOUND; *out = p->instruments[i]; return XTC_OK; } xtc_result xtc_project_set_live_instrument(xtc_project *p, uint64_t track) { xtc_plugin_spec s; if (!p) return XTC_ERR_INVALID_ARGUMENT; if (track != UINT64_MAX && xtc_project_get_instrument(p,track,&s) != XTC_OK) return XTC_ERR_NOT_FOUND; p->live_instrument = track; return XTC_OK; } xtc_result xtc_project_get_live_instrument(const xtc_project *p, uint64_t *track) { if (!p || !track) return XTC_ERR_INVALID_ARGUMENT; *track = p->live_instrument; return XTC_OK; }