230 lines
7.5 KiB
C
230 lines
7.5 KiB
C
#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>
|
|
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* @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;
|
|
if (!out)
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
*out = NULL;
|
|
if ((replace != 0 && replace != 1) || !path || !rate || rate > 768000 || !capacity || capacity > 2097152U)
|
|
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));
|
|
if (!r)
|
|
return XTC_ERR_OUT_OF_MEMORY;
|
|
r->replace = replace;
|
|
r->capacity = capacity;
|
|
r->rate = rate;
|
|
r->ring = malloc(capacity * sizeof(float));
|
|
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);
|
|
memcpy(r->temporary, path, length);
|
|
memcpy(r->temporary + length, ".tmp.XXXXXX", 12);
|
|
fd = mkstemp(r->temporary);
|
|
if (fd < 0) {
|
|
xtc_recorder_destroy(r);
|
|
return XTC_ERR_IO;
|
|
}
|
|
r->file = fdopen(fd, "w+b");
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
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;
|
|
}
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
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;
|
|
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;
|
|
}
|
|
read += count;
|
|
r->written += count;
|
|
__atomic_store_n(&r->read_position, read, __ATOMIC_RELEASE);
|
|
}
|
|
return (xtc_result)__atomic_load_n(&r->error, __ATOMIC_ACQUIRE);
|
|
}
|
|
|
|
/**
|
|
* @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)
|
|
{
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
if (!r || !r->file || r->finished)
|
|
return XTC_ERR_INVALID_STATE;
|
|
result = xtc_recorder_drain(r);
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Releases recorder storage and removes any unfinished temporary file; accepts NULL.
|
|
*
|
|
* @param r Recorder handle.
|
|
*/
|
|
void xtc_recorder_destroy(xtc_recorder *r)
|
|
{
|
|
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)
|
|
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;
|
|
}
|