598 lines
22 KiB
C
598 lines
22 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;
|
|
*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));
|
|
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;
|
|
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;
|
|
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->clip_count == XTC_PROJECT_MAX_CLIPS || project->next_clip_id == UINT64_MAX)
|
|
return XTC_ERR_CAPACITY;
|
|
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 (!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;
|
|
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 = 0U; i < project->track_count; ++i) {
|
|
if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name))
|
|
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 ((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;
|
|
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 || !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;
|
|
}
|