From 1029835d75a14685608bfbec0f9c90bbe0d700d2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Jan 2024 19:22:55 +0100 Subject: [PATCH] audioplayer without locks --- app/meson.build | 9 +- app/src/audio_player.c | 194 ++++++++++++++++++-------------------- app/src/audio_player.h | 15 ++- app/src/util/audiobuf.c | 109 +++++++++++++++++++++ app/src/util/audiobuf.h | 81 +++++----------- app/src/util/bytebuf.c | 104 -------------------- app/src/util/bytebuf.h | 114 ---------------------- app/tests/test_audiobuf.c | 128 +++++++++++++++++++++++++ app/tests/test_bytebuf.c | 126 ------------------------- 9 files changed, 368 insertions(+), 512 deletions(-) create mode 100644 app/src/util/audiobuf.c delete mode 100644 app/src/util/bytebuf.c delete mode 100644 app/src/util/bytebuf.h create mode 100644 app/tests/test_audiobuf.c delete mode 100644 app/tests/test_bytebuf.c diff --git a/app/meson.build b/app/meson.build index 88e2df9a..caf5ee5c 100644 --- a/app/meson.build +++ b/app/meson.build @@ -34,8 +34,8 @@ src = [ 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/util/acksync.c', + 'src/util/audiobuf.c', 'src/util/average.c', - 'src/util/bytebuf.c', 'src/util/file.c', 'src/util/intmap.c', 'src/util/intr.c', @@ -212,9 +212,10 @@ if get_option('buildtype') == 'debug' ['test_binary', [ 'tests/test_binary.c', ]], - ['test_bytebuf', [ - 'tests/test_bytebuf.c', - 'src/util/bytebuf.c', + ['test_audiobuf', [ + 'tests/test_audiobuf.c', + 'src/util/audiobuf.c', + 'src/util/memory.c', ]], ['test_cli', [ 'tests/test_cli.c', diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 8f0ad7fb..288bb214 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -66,8 +66,7 @@ static void SDLCALL sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { struct sc_audio_player *ap = userdata; - // This callback is called with the lock used by SDL_AudioDeviceLock(), so - // the audiobuf is protected + // This callback is called with the lock used by SDL_AudioDeviceLock() assert(len_int > 0); size_t len = len_int; @@ -77,8 +76,9 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif - uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); - if (!ap->played) { + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); + if (!played) { + uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); // Part of the buffering is handled by inserting initial silence. The // remaining (margin) last samples will be handled by compensation. uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms @@ -93,10 +93,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { } } - uint32_t read = MIN(buffered_samples, count); - if (read) { - sc_audiobuf_read(&ap->buf, stream, read); - } + uint32_t read = sc_audiobuf_read(&ap->buf, stream, count); if (read < count) { uint32_t silence = count - read; @@ -109,13 +106,16 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { silence); memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); - if (ap->received) { + bool received = atomic_load_explicit(&ap->received, + memory_order_relaxed); + if (received) { // Inserting additional samples immediately increases buffering - ap->underflow += silence; + atomic_fetch_add_explicit(&ap->underflow, silence, + memory_order_relaxed); } } - ap->played = true; + atomic_store_explicit(&ap->played, true, memory_order_relaxed); } static uint8_t * @@ -162,123 +162,120 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // swr_convert() returns the number of samples which would have been // written if the buffer was big enough. - uint32_t samples_written = MIN(ret, dst_nb_samples); + uint32_t samples = MIN(ret, dst_nb_samples); #ifndef SC_AUDIO_PLAYER_NDEBUG - LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written); + LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); #endif - // Since this function is the only writer, the current available space is - // at least the previous available space. In practice, it should almost - // always be possible to write without lock. - bool lockless_write = samples_written <= ap->previous_can_write; - if (lockless_write) { - sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written); + uint32_t cap = sc_audiobuf_capacity(&ap->buf); + if (samples > cap) { + // Very very unlikely: a single resampled frame should never + // exceed the audio buffer size (or something is very wrong). + // Ignore the first bytes in swr_buf to avoid memory corruption anyway. + //skipped_samples = samples - cap; + swr_buf += TO_BYTES(samples - cap); + samples = cap; } - SDL_LockAudioDevice(ap->device); - - uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); - - if (lockless_write) { - sc_audiobuf_commit_write(&ap->buf, samples_written); - } else { - uint32_t can_write = sc_audiobuf_can_write(&ap->buf); - if (samples_written > can_write) { - // Entering this branch is very unlikely, the audio buffer is - // allocated with a size sufficient to store 1 second more than the - // target buffering. If this happens, though, we have to skip old - // samples. - uint32_t cap = sc_audiobuf_capacity(&ap->buf); - if (samples_written > cap) { - // Very very unlikely: a single resampled frame should never - // exceed the audio buffer size (or something is very wrong). - // Ignore the first bytes in swr_buf - swr_buf += TO_BYTES(samples_written - cap); - // This change in samples_written will impact the - // instant_compensation below - samples_written = cap; - } - - assert(samples_written >= can_write); - if (samples_written > can_write) { - uint32_t skip_samples = samples_written - can_write; - assert(buffered_samples >= skip_samples); - sc_audiobuf_skip(&ap->buf, skip_samples); - buffered_samples -= skip_samples; - if (ap->played) { - // Dropping input samples instantly decreases buffering - ap->avg_buffering.avg -= skip_samples; - } - } - - // It should remain exactly the expected size to write the new - // samples. - assert(sc_audiobuf_can_write(&ap->buf) == samples_written); + uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples); + if (written < samples) { + uint32_t remaining = samples - written; + + // All samples that could be written without locking have been written, + // now we need to lock to drop/consume old samples + SDL_LockAudioDevice(ap->device); + + // Retry with the lock + written += sc_audiobuf_write(&ap->buf, + swr_buf + TO_BYTES(written), + remaining); + if (written < samples) { + remaining = samples - written; + // Still insufficient, drop old samples to make space + uint32_t skipped_samples = + sc_audiobuf_read(&ap->buf, NULL, remaining); + assert(skipped_samples == remaining); + + // Dropping input samples instantly decreases buffering + ap->avg_buffering.avg -= skipped_samples; + + // Now there is enough space + uint32_t w = sc_audiobuf_write(&ap->buf, + swr_buf + TO_BYTES(written), + remaining); + assert(w == remaining); } - sc_audiobuf_write(&ap->buf, swr_buf, samples_written); + SDL_UnlockAudioDevice(ap->device); } - buffered_samples += samples_written; - assert(buffered_samples == sc_audiobuf_can_read(&ap->buf)); - - // Read with lock held, to be used after unlocking - bool played = ap->played; - uint32_t underflow = ap->underflow; - + uint32_t underflow = 0; + uint32_t max_buffered_samples; + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); if (played) { - uint32_t max_buffered_samples = ap->target_buffering - + 12 * ap->output_buffer - + ap->target_buffering / 10; - if (buffered_samples > max_buffered_samples) { - uint32_t skip_samples = buffered_samples - max_buffered_samples; - sc_audiobuf_skip(&ap->buf, skip_samples); - LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 - " samples", skip_samples); - } + underflow = atomic_exchange_explicit(&ap->underflow, 0, + memory_order_relaxed); - // reset (the current value was copied to a local variable) - ap->underflow = 0; + max_buffered_samples = ap->target_buffering + + 12 * ap->output_buffer + + ap->target_buffering / 10; } else { // SDL playback not started yet, do not accumulate more than // max_initial_buffering samples, this would cause unnecessary delay // (and glitches to compensate) on start. - uint32_t max_initial_buffering = ap->target_buffering - + 2 * ap->output_buffer; - if (buffered_samples > max_initial_buffering) { - uint32_t skip_samples = buffered_samples - max_initial_buffering; - sc_audiobuf_skip(&ap->buf, skip_samples); + max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer; + } + + uint32_t can_read = sc_audiobuf_can_read(&ap->buf); + if (can_read > max_buffered_samples) { + uint32_t skip_samples = 0; + + SDL_LockAudioDevice(ap->device); + can_read = sc_audiobuf_can_read(&ap->buf); + if (can_read > max_buffered_samples) { + skip_samples = can_read - max_buffered_samples; + uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples); + assert(r == skip_samples); + LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 + " samples", skip_samples); + } + SDL_UnlockAudioDevice(ap->device); + + if (skip_samples) { + // The buffer contained too much data, ignore any reported underflow + underflow = 0; + + if (played) { + LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 + " samples", skip_samples); #ifndef SC_AUDIO_PLAYER_NDEBUG - LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", - skip_samples); + } else { + LOGD("[Audio] Playback not started, skipping %" PRIu32 + " samples", skip_samples); #endif + } } } - ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); - ap->received = true; - - SDL_UnlockAudioDevice(ap->device); + atomic_store_explicit(&ap->received, true, memory_order_relaxed); if (played) { // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = - (int32_t) samples_written - frame->nb_samples; + int32_t instant_compensation = (int32_t) written - frame->nb_samples; int32_t inserted_silence = (int32_t) underflow; // The compensation must apply instantly, it must not be smoothed ap->avg_buffering.avg += instant_compensation + inserted_silence; - // However, the buffering level must be smoothed - sc_average_push(&ap->avg_buffering, buffered_samples); + sc_average_push(&ap->avg_buffering, can_read); #ifndef SC_AUDIO_PLAYER_NDEBUG LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f", buffered_samples, sc_average_get(&ap->avg_buffering)); #endif - ap->samples_since_resync += samples_written; + ap->samples_since_resync += written; if (ap->samples_since_resync >= ap->sample_rate) { // Recompute compensation every second ap->samples_since_resync = 0; @@ -288,7 +285,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, if (abs(diff) < (int) ap->sample_rate / 1000) { // Do not compensate for less than 1ms, the error is just noise diff = 0; - } else if (diff < 0 && buffered_samples < ap->target_buffering) { + } else if (diff < 0 && can_read < ap->target_buffering) { // Do not accelerate if the instant buffering level is below // the average, this would increase underflow diff = 0; @@ -300,8 +297,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, int abs_max_diff = distance / 50; diff = CLAMP(diff, -abs_max_diff, abs_max_diff); LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d", ap->target_buffering, avg, - buffered_samples, diff); + " compensation=%d", ap->target_buffering, avg, can_read, diff); if (diff != ap->compensation) { int ret = swr_set_compensation(swr_ctx, diff, distance); @@ -413,16 +409,14 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, } ap->swr_buf_alloc_size = initial_swr_buf_size; - ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); - // Samples are produced and consumed by blocks, so the buffering must be // smoothed to get a relatively stable value. sc_average_init(&ap->avg_buffering, 32); ap->samples_since_resync = 0; ap->received = false; - ap->played = false; - ap->underflow = 0; + atomic_init(&ap->played, false); + atomic_init(&ap->underflow, 0); ap->compensation = 0; // The thread calling open() is the thread calling push(), which fills the diff --git a/app/src/audio_player.h b/app/src/audio_player.h index a03e9e35..67d47444 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -32,7 +32,7 @@ struct sc_audio_player { uint16_t output_buffer; // Audio buffer to communicate between the receiver and the SDL audio - // callback (protected by SDL_AudioDeviceLock()) + // callback struct sc_audiobuf buf; // The previous empty space in the buffer (only used by the receiver @@ -61,19 +61,16 @@ struct sc_audio_player { uint32_t samples_since_resync; // Number of silence samples inserted since the last received packet - // (protected by SDL_AudioDeviceLock()) - uint32_t underflow; + atomic_uint_least32_t underflow; // Current applied compensation value (only used by the receiver thread) int compensation; - // Set to true the first time a sample is received (protected by - // SDL_AudioDeviceLock()) - bool received; + // Set to true the first time a sample is received + atomic_bool received; - // Set to true the first time the SDL callback is called (protected by - // SDL_AudioDeviceLock()) - bool played; + // Set to true the first time the SDL callback is called + atomic_bool played; const struct sc_audio_player_callbacks *cbs; void *cbs_userdata; diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c new file mode 100644 index 00000000..68a6f588 --- /dev/null +++ b/app/src/util/audiobuf.c @@ -0,0 +1,109 @@ +#include "audiobuf.h" + +#include +#include +#include +#include + +bool +sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, + uint32_t capacity) { + assert(sample_size); + assert(capacity); + + // The actual capacity is (alloc_size - 1) so that head == tail is + // non-ambiguous + buf->alloc_size = capacity + 1; + buf->data = sc_allocarray(buf->alloc_size, sample_size); + if (!buf->data) { + LOG_OOM(); + return false; + } + + buf->sample_size = sample_size; + atomic_init(&buf->head, 0); + atomic_init(&buf->tail, 0); + + return true; +} + +void +sc_audiobuf_destroy(struct sc_audiobuf *buf) { + free(buf->data); +} + +uint32_t +sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count) { + assert(samples_count); + + // Only the reader thread can write tail without synchronization, so + // memory_order_relaxed is sufficient + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_relaxed); + + // The head cursor is updated after the data is written to the array + uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); + + uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size; + if (samples_count > can_read) { + samples_count = can_read; + } + + if (to) { + uint32_t right_limit = tail < head ? head : buf->alloc_size; + uint32_t right_count = right_limit - tail; + if (right_count > samples_count) { + right_count = samples_count; + } + memcpy(to, + buf->data + (tail * buf->sample_size), + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memcpy(to + (right_count * buf->sample_size), + buf->data, + left_count * buf->sample_size); + } + } + + uint32_t new_tail = (tail + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->tail, new_tail, memory_order_release); + + return samples_count; +} + +uint32_t +sc_audiobuf_write(struct sc_audiobuf *buf, const void *from, + uint32_t samples_count) { + // Only the writer thread can write head, so memory_order_relaxed is + // sufficient + uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); + + // The tail cursor is updated after the data is consumed by the reader + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + + uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; + if (samples_count > can_write) { + samples_count = can_write; + } + + uint32_t right_count = buf->alloc_size - head; + if (right_count > samples_count) { + right_count = samples_count; + } + memcpy(buf->data + (head * buf->sample_size), + from, + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memcpy(buf->data, + from + (right_count * buf->sample_size), + left_count * buf->sample_size); + } + + uint32_t new_head = (head + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->head, new_head, memory_order_release); + + return samples_count; +} diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index 8616d539..94f9f052 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -3,19 +3,25 @@ #include "common.h" +#include +#include #include #include -#include "util/bytebuf.h" - /** * Wrapper around bytebuf to read and write samples * * Each sample takes sample_size bytes. */ struct sc_audiobuf { - struct sc_bytebuf buf; - size_t sample_size; + void *data; + uint32_t sample_size; + uint32_t alloc_size; // in samples + + atomic_uint_least32_t head; // writer cursor, in samples + atomic_uint_least32_t tail; // reader cursor, in samples + // empty: tail == head + // full: ((tail + 1) % alloc_size) == head }; static inline uint32_t @@ -29,66 +35,31 @@ sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) { return samples * buf->sample_size; } -static inline bool +bool sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, - uint32_t capacity) { - buf->sample_size = sample_size; - return sc_bytebuf_init(&buf->buf, capacity * sample_size + 1); -} - -static inline void -sc_audiobuf_read(struct sc_audiobuf *buf, uint8_t *to, uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_read(&buf->buf, to, bytes); -} - -static inline void -sc_audiobuf_skip(struct sc_audiobuf *buf, uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_skip(&buf->buf, bytes); -} - -static inline void -sc_audiobuf_write(struct sc_audiobuf *buf, const uint8_t *from, - uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_write(&buf->buf, from, bytes); -} - -static inline void -sc_audiobuf_prepare_write(struct sc_audiobuf *buf, const uint8_t *from, - uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_prepare_write(&buf->buf, from, bytes); -} + uint32_t capacity); -static inline void -sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_commit_write(&buf->buf, bytes); -} +void +sc_audiobuf_destroy(struct sc_audiobuf *buf); -static inline uint32_t -sc_audiobuf_can_read(struct sc_audiobuf *buf) { - size_t bytes = sc_bytebuf_can_read(&buf->buf); - return sc_audiobuf_to_samples(buf, bytes); -} +uint32_t +sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count); -static inline uint32_t -sc_audiobuf_can_write(struct sc_audiobuf *buf) { - size_t bytes = sc_bytebuf_can_write(&buf->buf); - return sc_audiobuf_to_samples(buf, bytes); -} +uint32_t +sc_audiobuf_write(struct sc_audiobuf *buf, const void *from, + uint32_t samples_count); static inline uint32_t sc_audiobuf_capacity(struct sc_audiobuf *buf) { - size_t bytes = sc_bytebuf_capacity(&buf->buf); - return sc_audiobuf_to_samples(buf, bytes); + assert(buf->alloc_size); + return buf->alloc_size - 1; } -static inline void -sc_audiobuf_destroy(struct sc_audiobuf *buf) { - sc_bytebuf_destroy(&buf->buf); +static inline uint32_t +sc_audiobuf_can_read(struct sc_audiobuf *buf) { + uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + return (buf->alloc_size + head - tail) % buf->alloc_size; } #endif diff --git a/app/src/util/bytebuf.c b/app/src/util/bytebuf.c deleted file mode 100644 index 93544d72..00000000 --- a/app/src/util/bytebuf.c +++ /dev/null @@ -1,104 +0,0 @@ -#include "bytebuf.h" - -#include -#include -#include - -#include "util/log.h" - -bool -sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) { - assert(alloc_size); - buf->data = malloc(alloc_size); - if (!buf->data) { - LOG_OOM(); - return false; - } - - buf->alloc_size = alloc_size; - buf->head = 0; - buf->tail = 0; - - return true; -} - -void -sc_bytebuf_destroy(struct sc_bytebuf *buf) { - free(buf->data); -} - -void -sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) { - assert(len); - assert(len <= sc_bytebuf_can_read(buf)); - assert(buf->tail != buf->head); // the buffer could not be empty - - size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size; - size_t right_len = right_limit - buf->tail; - if (len < right_len) { - right_len = len; - } - memcpy(to, buf->data + buf->tail, right_len); - - if (len > right_len) { - memcpy(to + right_len, buf->data, len - right_len); - } - - buf->tail = (buf->tail + len) % buf->alloc_size; -} - -void -sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) { - assert(len); - assert(len <= sc_bytebuf_can_read(buf)); - assert(buf->tail != buf->head); // the buffer could not be empty - - buf->tail = (buf->tail + len) % buf->alloc_size; -} - -static inline void -sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from, - size_t len) { - size_t right_len = buf->alloc_size - buf->head; - if (len < right_len) { - right_len = len; - } - memcpy(buf->data + buf->head, from, right_len); - - if (len > right_len) { - memcpy(buf->data, from + right_len, len - right_len); - } -} - -static inline void -sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) { - buf->head = (buf->head + len) % buf->alloc_size; -} - -void -sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { - assert(len); - assert(len <= sc_bytebuf_can_write(buf)); - - sc_bytebuf_write_step0(buf, from, len); - sc_bytebuf_write_step1(buf, len); -} - -void -sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, - size_t len) { - // *This function MUST NOT access buf->tail (even in assert()).* - // The purpose of this function is to allow a reader and a writer to access - // different parts of the buffer in parallel simultaneously. It is intended - // to be called without lock (only sc_bytebuf_commit_write() is intended to - // be called with lock held). - - assert(len < buf->alloc_size - 1); - sc_bytebuf_write_step0(buf, from, len); -} - -void -sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) { - assert(len <= sc_bytebuf_can_write(buf)); - sc_bytebuf_write_step1(buf, len); -} diff --git a/app/src/util/bytebuf.h b/app/src/util/bytebuf.h deleted file mode 100644 index 1448f752..00000000 --- a/app/src/util/bytebuf.h +++ /dev/null @@ -1,114 +0,0 @@ -#ifndef SC_BYTEBUF_H -#define SC_BYTEBUF_H - -#include "common.h" - -#include -#include - -struct sc_bytebuf { - uint8_t *data; - // The actual capacity is (allocated - 1) so that head == tail is - // non-ambiguous - size_t alloc_size; - size_t head; // writter cursor - size_t tail; // reader cursor - // empty: tail == head - // full: ((tail + 1) % alloc_size) == head -}; - -bool -sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size); - -/** - * Copy from the bytebuf to a user-provided array - * - * The caller must check that len <= sc_bytebuf_read_available() (it is an - * error to attempt to read more bytes than available). - * - * This function is guaranteed not to write to buf->head. - */ -void -sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len); - -/** - * Drop len bytes from the buffer - * - * The caller must check that len <= sc_bytebuf_read_available() (it is an - * error to attempt to skip more bytes than available). - * - * This function is guaranteed not to write to buf->head. - * - * It is equivalent to call sc_bytebuf_read() to some array and discard the - * array (but this function is more efficient since there is no copy). - */ -void -sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len); - -/** - * Copy the user-provided array to the bytebuf - * - * The caller must check that len <= sc_bytebuf_write_available() (it is an - * error to write more bytes than the remaining available space). - * - * This function is guaranteed not to write to buf->tail. - */ -void -sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len); - -/** - * Copy the user-provided array to the bytebuf, but do not advance the cursor - * - * The caller must check that len <= sc_bytebuf_write_available() (it is an - * error to write more bytes than the remaining available space). - * - * After this function is called, the write must be committed with - * sc_bytebuf_commit_write(). - * - * The purpose of this mechanism is to acquire a lock only to commit the write, - * but not to perform the actual copy. - * - * This function is guaranteed not to access buf->tail. - */ -void -sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, - size_t len); - -/** - * Commit a prepared write - */ -void -sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len); - -/** - * Return the number of bytes which can be read - * - * It is an error to read more bytes than available. - */ -static inline size_t -sc_bytebuf_can_read(struct sc_bytebuf *buf) { - return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size; -} - -/** - * Return the number of bytes which can be written - * - * It is an error to write more bytes than available. - */ -static inline size_t -sc_bytebuf_can_write(struct sc_bytebuf *buf) { - return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size; -} - -/** - * Return the actual capacity of the buffer (can_read() + can_write()) - */ -static inline size_t -sc_bytebuf_capacity(struct sc_bytebuf *buf) { - return buf->alloc_size - 1; -} - -void -sc_bytebuf_destroy(struct sc_bytebuf *buf); - -#endif diff --git a/app/tests/test_audiobuf.c b/app/tests/test_audiobuf.c new file mode 100644 index 00000000..94d0f07a --- /dev/null +++ b/app/tests/test_audiobuf.c @@ -0,0 +1,128 @@ +#include "common.h" + +#include +#include + +#include "util/audiobuf.h" + +static void test_audiobuf_simple(void) { + struct sc_audiobuf buf; + uint32_t data[20]; + + bool ok = sc_audiobuf_init(&buf, 4, 20); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5}; + uint32_t w = sc_audiobuf_write(&buf, samples, 5); + assert(w == 5); + + uint32_t r = sc_audiobuf_read(&buf, data, 4); + assert(r == 4); + assert(!memcmp(data, samples, 16)); + + uint32_t samples2[] = {6, 7, 8}; + w = sc_audiobuf_write(&buf, samples2, 3); + assert(w == 3); + + uint32_t single = 9; + w = sc_audiobuf_write(&buf, &single, 1); + assert(w == 1); + + r = sc_audiobuf_read(&buf, &data[4], 8); + assert(r == 5); + + uint32_t expected[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + assert(!memcmp(data, expected, 36)); + + sc_audiobuf_destroy(&buf); +} + +static void test_audiobuf_boundaries(void) { + struct sc_audiobuf buf; + uint32_t data[20]; + + bool ok = sc_audiobuf_init(&buf, 4, 20); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5, 6}; + uint32_t w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + uint32_t r = sc_audiobuf_read(&buf, data, 9); + assert(r == 9); + + uint32_t expected[] = {1, 2, 3, 4, 5, 6, 1, 2, 3}; + assert(!memcmp(data, expected, 36)); + + uint32_t samples2[] = {7, 8, 9, 10, 11}; + w = sc_audiobuf_write(&buf, samples2, 5); + assert(w == 5); + + uint32_t single = 12; + w = sc_audiobuf_write(&buf, &single, 1); + assert(w == 1); + + w = sc_audiobuf_read(&buf, NULL, 3); + assert(w == 3); + + assert(sc_audiobuf_can_read(&buf) == 12); + + r = sc_audiobuf_read(&buf, data, 12); + assert(r == 12); + + uint32_t expected2[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + assert(!memcmp(data, expected2, 48)); + + sc_audiobuf_destroy(&buf); +} + +static void test_audiobuf_partial_read_write(void) { + struct sc_audiobuf buf; + uint32_t data[15]; + + bool ok = sc_audiobuf_init(&buf, 4, 10); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5, 6}; + uint32_t w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 4); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 0); + + uint32_t r = sc_audiobuf_read(&buf, data, 3); + assert(r == 3); + + uint32_t expected[] = {1, 2, 3}; + assert(!memcmp(data, expected, 12)); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 3); + + r = sc_audiobuf_read(&buf, data, 15); + assert(r == 10); + uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3}; + assert(!memcmp(data, expected2, 12)); + + sc_audiobuf_destroy(&buf); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_audiobuf_simple(); + test_audiobuf_boundaries(); + test_audiobuf_partial_read_write(); + + return 0; +} diff --git a/app/tests/test_bytebuf.c b/app/tests/test_bytebuf.c deleted file mode 100644 index 8e9d7c57..00000000 --- a/app/tests/test_bytebuf.c +++ /dev/null @@ -1,126 +0,0 @@ -#include "common.h" - -#include -#include - -#include "util/bytebuf.h" - -static void test_bytebuf_simple(void) { - struct sc_bytebuf buf; - uint8_t data[20]; - - bool ok = sc_bytebuf_init(&buf, 20); - assert(ok); - - sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1); - assert(sc_bytebuf_can_read(&buf) == 5); - - sc_bytebuf_read(&buf, data, 4); - assert(!strncmp((char *) data, "hell", 4)); - - sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1); - assert(sc_bytebuf_can_read(&buf) == 7); - - sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_can_read(&buf) == 8); - - sc_bytebuf_read(&buf, &data[4], 8); - assert(sc_bytebuf_can_read(&buf) == 0); - - data[12] = '\0'; - assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_can_read(&buf) == 0); - - sc_bytebuf_destroy(&buf); -} - -static void test_bytebuf_boundaries(void) { - struct sc_bytebuf buf; - uint8_t data[20]; - - bool ok = sc_bytebuf_init(&buf, 20); - assert(ok); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 6); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 12); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 18); - - sc_bytebuf_read(&buf, data, 9); - assert(!strncmp((char *) data, "hello hel", 9)); - assert(sc_bytebuf_can_read(&buf) == 9); - - sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1); - assert(sc_bytebuf_can_read(&buf) == 14); - - sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_can_read(&buf) == 15); - - sc_bytebuf_skip(&buf, 3); - assert(sc_bytebuf_can_read(&buf) == 12); - - sc_bytebuf_read(&buf, data, 12); - data[12] = '\0'; - assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_can_read(&buf) == 0); - - sc_bytebuf_destroy(&buf); -} - -static void test_bytebuf_two_steps_write(void) { - struct sc_bytebuf buf; - uint8_t data[20]; - - bool ok = sc_bytebuf_init(&buf, 20); - assert(ok); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 6); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 12); - - sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet - - sc_bytebuf_read(&buf, data, 9); - assert(!strncmp((char *) data, "hello hel", 3)); - assert(sc_bytebuf_can_read(&buf) == 3); - - sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 9); - - sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1); - assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet - - sc_bytebuf_commit_write(&buf, sizeof("world") - 1); - assert(sc_bytebuf_can_read(&buf) == 14); - - sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_can_read(&buf) == 15); - - sc_bytebuf_skip(&buf, 3); - assert(sc_bytebuf_can_read(&buf) == 12); - - sc_bytebuf_read(&buf, data, 12); - data[12] = '\0'; - assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_can_read(&buf) == 0); - - sc_bytebuf_destroy(&buf); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_bytebuf_simple(); - test_bytebuf_boundaries(); - test_bytebuf_two_steps_write(); - - return 0; -}