xtc/src/audio/recorder.c

230 lines
7.5 KiB
C
Raw Normal View History

#define _POSIX_C_SOURCE 200809L
#include "wave_internal.h"
#include <stdlib.h>
#include <float.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
2026-10-27 01:37:21 +01:00
struct xtc_recorder {
float *ring;
size_t capacity;
uint64_t write_position, read_position, written;
unsigned int error, rate;
FILE *file;
char *path, *temporary;
int finished, replace, committed, preserve;
};
2026-10-27 01:37:21 +01:00
/**
* @brief Allocates a bounded recording ring and opens a temporary mono float32 WAV file.
*
* @param path NUL-terminated file or asset path.
* @param rate Sample rate in hertz.
* @param capacity Recording ring capacity in frames, from 1 to 2097152.
* @param out Receives the newly allocated object; caller owns it on success.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_recorder_create_replace(const char *path, unsigned int rate, size_t capacity, int replace, xtc_recorder **out)
{
xtc_recorder *r;
size_t length = 0;
int fd;
2026-10-27 01:37:21 +01:00
if (!out)
return XTC_ERR_INVALID_ARGUMENT;
*out = NULL;
if ((replace != 0 && replace != 1) || !path || !rate || rate > 768000 || !capacity || capacity > 2097152U)
2026-10-27 01:37:21 +01:00
return XTC_ERR_INVALID_ARGUMENT;
while (length < 4096 && path[length])
++length;
if (!length || length == 4096)
return XTC_ERR_INVALID_ARGUMENT;
if (sizeof(float) != 4 || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128)
return XTC_ERR_UNSUPPORTED;
if (!__atomic_always_lock_free(sizeof(uint64_t), 0) || !__atomic_always_lock_free(sizeof(unsigned int), 0))
return XTC_ERR_UNSUPPORTED;
if (!replace) {
struct stat info;
if (lstat(path, &info) == 0 || errno != ENOENT) return XTC_ERR_IO;
}
r = calloc(1, sizeof(*r));
2026-10-27 01:37:21 +01:00
if (!r)
return XTC_ERR_OUT_OF_MEMORY;
r->replace = replace;
2026-10-27 01:37:21 +01:00
r->capacity = capacity;
r->rate = rate;
r->ring = malloc(capacity * sizeof(float));
2026-10-27 01:37:21 +01:00
r->path = malloc(length + 1);
r->temporary = calloc(length + 12, 1);
if (!r->ring || !r->path || !r->temporary) {
xtc_recorder_destroy(r);
return XTC_ERR_OUT_OF_MEMORY;
}
memcpy(r->path, path, length + 1);
2026-10-27 01:37:21 +01:00
memcpy(r->temporary, path, length);
memcpy(r->temporary + length, ".tmp.XXXXXX", 12);
fd = mkstemp(r->temporary);
2026-10-27 01:37:21 +01:00
if (fd < 0) {
xtc_recorder_destroy(r);
return XTC_ERR_IO;
}
r->file = fdopen(fd, "w+b");
2026-10-27 01:37:21 +01:00
if (!r->file) {
close(fd);
xtc_recorder_destroy(r);
return XTC_ERR_IO;
}
if (!xtc_wave_header(r->file, rate, 0)) {
xtc_recorder_destroy(r);
return XTC_ERR_IO;
}
*out = r;
return XTC_OK;
}
2026-10-27 01:37:21 +01:00
/**
* @brief Enqueues an entire audio block without blocking; overflow sets a sticky error.
*
* @param r Recorder handle.
* @param input Borrowed input sample buffer.
* @param frames Number of audio frames.
*/
void xtc_recorder_push(xtc_recorder *r, const float *input, size_t frames)
{
uint64_t write, read;
size_t i;
2026-10-27 01:37:21 +01:00
if (!r || !frames || __atomic_load_n(&r->error, __ATOMIC_ACQUIRE))
return;
write = __atomic_load_n(&r->write_position, __ATOMIC_RELAXED);
read = __atomic_load_n(&r->read_position, __ATOMIC_ACQUIRE);
if (!input || frames > r->capacity || frames > r->capacity - (size_t)(write - read) ||
write > UINT64_C(1073741814) - frames) {
__atomic_store_n(&r->error, (unsigned int)(input ? XTC_ERR_CAPACITY : XTC_ERR_IO), __ATOMIC_RELEASE);
return;
}
2026-10-27 01:37:21 +01:00
for (i = 0; i < frames; ++i)
r->ring[(size_t)((write + i) % r->capacity)] = input[i];
__atomic_store_n(&r->write_position, write + frames, __ATOMIC_RELEASE);
}
2026-10-27 01:37:21 +01:00
/**
* @brief Writes queued samples to the temporary WAV file on the control thread.
*
* @param r Recorder handle.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_recorder_drain(xtc_recorder *r)
{
uint64_t read, write;
2026-10-27 01:37:21 +01:00
if (!r || !r->file || r->finished)
return XTC_ERR_INVALID_STATE;
read = __atomic_load_n(&r->read_position, __ATOMIC_RELAXED);
write = __atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE);
while (read < write) {
size_t offset = (size_t)(read % r->capacity);
size_t count = r->capacity - offset;
2026-10-27 01:37:21 +01:00
if (count > write - read)
count = (size_t)(write - read);
if (!xtc_wave_write(r->file, r->ring + offset, count)) {
__atomic_store_n(&r->error, (unsigned int)XTC_ERR_IO, __ATOMIC_RELEASE);
return XTC_ERR_IO;
}
2026-10-27 01:37:21 +01:00
read += count;
r->written += count;
__atomic_store_n(&r->read_position, read, __ATOMIC_RELEASE);
}
return (xtc_result)__atomic_load_n(&r->error, __ATOMIC_ACQUIRE);
}
2026-10-27 01:37:21 +01:00
/**
* @brief Copies the number of accepted frames and returns the sticky recording status.
*
* @param r Recorder handle.
* @param frames Receives the accepted frame count.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_recorder_status(const xtc_recorder *r, uint64_t *frames)
{
2026-10-27 01:37:21 +01:00
if (!r || !frames)
return XTC_ERR_INVALID_ARGUMENT;
*frames = __atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE);
return (xtc_result)__atomic_load_n(&r->error, __ATOMIC_ACQUIRE);
}
2026-10-27 01:37:21 +01:00
/**
* @brief Drains and finalizes the WAV file, then renames it after producer quiescence.
*
* @param r Recorder handle.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_recorder_finish(xtc_recorder *r)
{
xtc_result result;
int failed = 0;
2026-10-27 01:37:21 +01:00
if (!r || !r->file || r->finished)
return XTC_ERR_INVALID_STATE;
result = xtc_recorder_drain(r);
2026-10-27 01:37:21 +01:00
if (result == XTC_ERR_IO)
failed = 1;
if (fseek(r->file, 0, SEEK_SET) || !xtc_wave_header(r->file, r->rate, (uint32_t)r->written) || fflush(r->file) ||
fsync(fileno(r->file)))
failed = 1;
if (fclose(r->file))
failed = 1;
r->file = NULL;
if (!failed) {
if (r->replace ? rename(r->temporary, r->path) : link(r->temporary, r->path))
failed = 1;
else {
r->committed = 1;
if (!r->replace) (void)unlink(r->temporary);
}
}
r->preserve = failed && (__atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE) != 0 || r->written != 0);
r->finished = 1;
return failed ? XTC_ERR_IO : result;
}
2026-10-27 01:37:21 +01:00
/**
* @brief Releases recorder storage and removes any unfinished temporary file; accepts NULL.
*
* @param r Recorder handle.
*/
void xtc_recorder_destroy(xtc_recorder *r)
{
2026-10-27 01:37:21 +01:00
if (!r)
return;
if (r->file) {
if (__atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE))
(void)xtc_recorder_finish(r); /* Producer must already be quiescent. */
else { fclose(r->file); r->file = NULL; }
}
if (r->temporary && !r->preserve)
2026-10-27 01:37:21 +01:00
unlink(r->temporary);
free(r->temporary);
free(r->path);
free(r->ring);
free(r);
}
xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capacity, xtc_recorder **out)
{
return xtc_recorder_create_replace(path, rate, capacity, 0, out);
}
const char *xtc_recorder_recovery_path(const xtc_recorder *r)
{
if (!r) return "";
return r->committed ? r->path : r->temporary ? r->temporary : "";
}
/* Internal cancellation for a failed recovery transaction; never removes the
* destination, which may have been created by another process. */
void xtc_recorder_abort(xtc_recorder *r)
{
if (!r) return;
if (r->file) fclose(r->file);
r->file = NULL;
r->finished = 1;
r->preserve = 0;
}