813 lines
33 KiB
C
813 lines
33 KiB
C
#include "project_internal.h"
|
|
#include "xtc/xtc_audio.h"
|
|
#include "../graph/graph_internal.h"
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
/**
|
|
* @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);
|
|
memset(project->chains+project->track_count,0,sizeof(*project->chains));
|
|
project->chains[project->track_count].output_gain=1;
|
|
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->chains+index,project->chains+index+1,
|
|
(project->track_count-index-1)*sizeof(*project->chains));
|
|
memset(project->chains+project->track_count-1,0,sizeof(*project->chains));
|
|
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,j;for(i=0;i<project->track_count;++i)for(j=0;j<project->chains[i].count;++j)project->chains[i].slots[j].plugin.tempo=tempo;}
|
|
{size_t i;for(i=0;i<project->track_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;
|
|
if(project->marker_count>XTC_PROJECT_MAX_MARKERS)return XTC_ERR_FORMAT;
|
|
for(i=0;i<project->marker_count;++i)if(project->markers[i].tick>XTC_PROJECT_MAX_TICK ||
|
|
!xtc_project_valid_name(project->markers[i].name) || (i && project->markers[i-1].tick>=project->markers[i].tick))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(xtc_track_chain_validate(project->chains+i)!=XTC_OK ||
|
|
(project->tracks[i].is_bus && project->chains[i].count))return XTC_ERR_FORMAT;
|
|
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_track_chain chain=p->chains[i];
|
|
if(i<destination)memmove(p->chains+i,p->chains+i+1,(destination-i)*sizeof(chain));
|
|
else if(i>destination)memmove(p->chains+destination+1,p->chains+destination,(i-destination)*sizeof(chain));
|
|
p->chains[destination]=chain; }
|
|
{ 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; }
|
|
|
|
xtc_result xtc_track_chain_validate(const xtc_track_chain *c)
|
|
{
|
|
size_t i;
|
|
if(!c || c->count>XTC_TRACK_INSERTS || !isfinite(c->output_gain) || c->output_gain<0 || c->output_gain>4)return XTC_ERR_INVALID_ARGUMENT;
|
|
for(i=0;i<c->count;++i) {
|
|
const xtc_track_insert *s=c->slots+i;
|
|
if(s->enabled>1 || !isfinite(s->wet) || s->wet<0 || s->wet>1 || s->plugin.instrument ||
|
|
s->plugin.inputs!=1 || s->plugin.sample_path[0] || xtc_plugin_spec_validate(&s->plugin)!=XTC_OK)return XTC_ERR_INVALID_ARGUMENT;
|
|
}
|
|
return XTC_OK;
|
|
}
|
|
xtc_result xtc_project_get_chain(const xtc_project *p,uint64_t track,xtc_track_chain *out)
|
|
{
|
|
size_t i;if(!p || !out)return XTC_ERR_INVALID_ARGUMENT;
|
|
i=track_index(p,track);if(i==SIZE_MAX)return XTC_ERR_NOT_FOUND;
|
|
*out=p->chains[i];return XTC_OK;
|
|
}
|
|
xtc_result xtc_project_set_chain(xtc_project *p,uint64_t track,const xtc_track_chain *c)
|
|
{
|
|
size_t i;if(!p || xtc_track_chain_validate(c)!=XTC_OK)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 && c->count)return XTC_ERR_UNSUPPORTED;
|
|
p->chains[i]=*c;
|
|
{size_t j;for(j=0;j<c->count;++j)p->chains[i].slots[j].plugin.tempo=p->tempo;}
|
|
return XTC_OK;
|
|
}
|