xtc/src/project/project.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, &copy);
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;
}