audio: add coherent speed-linked low-pass requests

Derive an optional core-rate cutoff from Q16 speed using bounded integer arithmetic. Keep unity and slow motion dry, and publish the derived cutoff with tempo at one source boundary. Filtering remains independent of WSOLA activation. The existing explicit-Hz API and default playback path are unchanged. Cover the policy, request backpressure and native wrapper transitions, and register wrapper policy checks in CI.
This commit is contained in:
Codex authored and libretroadmin committed 2026-09-14 03:48:47 +02:00
1 parent 52fda5cf45
commit a62b57ca44
9 files changed
+97 -4

No files matched your search

@@ -41,3 +41,9 @@ jobs:
ASAN_OPTIONS: detect_leaks=1:detect_stack_use_after_return=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
run: make check-layout SANITIZER='${{ matrix.sanitizer }}'
- name: Speed-linked low-pass control requests
working-directory: samples/audio/pipeline_wakeups
env:
ASAN_OPTIONS: detect_leaks=1:detect_stack_use_after_return=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
run: make check-speed-lpf SANITIZER='${{ matrix.sanitizer }}'
+9
View File
@@ -64,6 +64,7 @@
#ifdef HAVE_THREADS
#include "audio_thread_wrapper.h"
#include "audio_pipeline_stretch.h"
#include "audio_speed_lpf.h"
#endif
#ifdef HAVE_MENU
@@ -4565,6 +4566,14 @@ static INLINE void audio_driver_pipeline_render(audio_driver_state_t *audio_st,
audio_driver_state_unlock();
}
bool audio_driver_pipeline_transport_request_speed(uint32_t tempo_q16,
bool active, bool reset, bool lowpass)
{
return audio_driver_pipeline_transport_request(tempo_q16, active, reset,
lowpass ? audio_speed_lpf_cutoff(audio_driver_st.pipe_transport_rate,
tempo_q16) : 0);
}
bool audio_driver_pipeline_transport_request(uint32_t tempo_q16,
bool active, bool reset, uint32_t cutoff)
{
+7
View File
@@ -974,6 +974,13 @@ void audio_driver_pipeline_transport_release(void);
* the request. Retry before publishing affected audio. No mode activation. */
bool audio_driver_pipeline_transport_request(uint32_t tempo_q16,
bool active, bool reset, uint32_t cutoff);
/* Same transaction, with optional speed-linked low-pass coloration.
* The cutoff uses the prepared core rate and requested Q16 speed; unity
* and slow motion are dry. Filtering is independent of active (WSOLA).
* On failure retry before publishing source. */
bool audio_driver_pipeline_transport_request_speed(uint32_t tempo_q16,
bool active, bool reset, bool lowpass);
/* Consumer thread, or main thread with the wrapper parked; no worker locks
* held. Cancel owned transport/device output and reset DSP history, skipping
* exactly frames of queued native source. Zero preserves queued source.
+9
View File
@@ -8,6 +8,15 @@
#define SPEED_LPF_ONE UINT32_C(1073741824)
#define SPEED_LPF_WET UINT32_C(65536)
uint32_t audio_speed_lpf_cutoff(unsigned rate, uint32_t speed_q16)
{
uint32_t cutoff;
if (rate < 8000 || rate > 192000 || speed_q16 <= 65536) return 0;
cutoff = (uint32_t)(((uint64_t)rate * 9 * 65536)
/ ((uint64_t)speed_q16 * 20));
return cutoff < 20 ? 20 : cutoff;
}
static int64_t speed_lpf_round(int64_t value, int64_t divisor)
{
return value < 0 ? -((-value + divisor / 2) / divisor)
+5
View File
@@ -32,6 +32,11 @@ typedef struct audio_speed_lpf
bool audio_speed_lpf_init(audio_speed_lpf_t *state, unsigned rate,
unsigned channels, bool is_float);
/* Optional fast-forward coloration: floor(0.45 * rate / speed), at least
* 20 Hz. Q16 speeds at or below unity are dry (zero). Invalid rates are dry.
* This is a target policy, not an anti-aliasing replacement for the SRC. */
uint32_t audio_speed_lpf_cutoff(unsigned rate, uint32_t speed_q16);
/* Finite positive cutoff in core-rate Hz, clamped to 20..0.45*rate.
* The cascaded poles have their combined -3 dB point at this frequency.
* Coefficient and wet/dry changes take 50 ms of processed source frames.
@@ -1858,6 +1858,8 @@ static void transport_request_cases(void)
q = &st->pipe_layouts;
CHECK(!audio_driver_pipeline_transport_request(65536, true, false, 1000),
"request without owned stage");
CHECK(!audio_driver_pipeline_transport_request_speed(131072, true, false, true),
"speed request without owned stage");
CHECK(audio_driver_pipeline_transport_prepare(48000, 1), "request prepare");
memset(&input, 0, sizeof(input));
CHECK(retro_spsc_write_frames(&st->pipe_ring, &input, 1, st->pipe_frame_bytes) == 1,
@@ -1891,6 +1893,8 @@ static void transport_request_cases(void)
gen = retro_atomic_load_acquire_int(&st->pipe_data_gen);
head = retro_atomic_load_relaxed_size(&q->head);
CHECK(!audio_driver_pipeline_transport_request(131072, true, true, 2000), "full request accepted");
CHECK(!audio_driver_pipeline_transport_request_speed(131072, true, true, true),
"full speed request accepted");
CHECK(retro_atomic_load_relaxed_size(&q->head) == head && retro_atomic_load_acquire_int(&st->pipe_data_gen) == gen
&& q->published_cutoff == 1000 && q->published_control == (98304 | AUDIO_PIPELINE_STRETCH),
"full request partially published");
@@ -1901,6 +1905,16 @@ static void transport_request_cases(void)
CHECK(audio_pipeline_stretch_next(st->pipe_transport, 0, 1, &block)
&& q->current_control == 65536 && q->current_cutoff == 2000,
"inactive request did not preserve filter independence");
CHECK(audio_driver_pipeline_transport_request_speed(131072, false, false, true),
"speed request retry");
CHECK(audio_pipeline_stretch_next(st->pipe_transport, 0, 1, &block)
&& q->current_control == 65536 && q->current_cutoff == 10800,
"speed cutoff depended on WSOLA activation");
CHECK(audio_driver_pipeline_transport_request_speed(131072, true, false, false),
"speed request disable LPF");
CHECK(audio_pipeline_stretch_next(st->pipe_transport, 0, 1, &block)
&& q->current_control == (131072 | AUDIO_PIPELINE_STRETCH)
&& q->current_cutoff == 0, "disabled speed LPF was not dry");
audio_driver_deinit_internal(true);
}
printf("native transport request: 4 cases, %u failures\n", failures - before);
+5 -1
View File
@@ -118,6 +118,10 @@ check-live: $(TARGET)
check-layout: $(TARGET)
LIVE_LAYOUTS=1 LAYOUT=7.1 $(MAKE) check-live
check-speed-lpf: $(TARGET)
SPEED_LPF=1 $(MAKE) check-live
SPEED_LPF=1 $(MAKE) check-layout
check-wide: $(TARGET)
@for layout in 5.1 7.1; do \
for mode in dry lpf stretch; do \
@@ -133,4 +137,4 @@ clean:
rm -f $(OBJS)
rm -f $(TARGET)
.PHONY: clean all check check-transport check-wrapper check-tempo check-live check-wide check-layout
.PHONY: clean all check check-transport check-wrapper check-tempo check-live check-wide check-layout check-speed-lpf
@@ -124,6 +124,7 @@ static unsigned channels = 2;
static unsigned source_channels = 2;
static uint32_t source_layout = AUDIO_LAYOUT_STEREO;
static bool live_layouts;
static bool speed_lowpass;
#define LATENCY_MS 32
#define MAX_SAMPLES 65536
@@ -622,6 +623,7 @@ static void check_live_control(void *userdata)
static void wrapper_live_controls(unsigned publishes)
{
static const double tempos[] = { 0.25, 1, 32, 0.5, 1, 16, 2, 4 };
static const uint32_t cutoffs[] = { 0, 0, 675, 0, 0, 1350, 10800, 5400 };
static const uint32_t layouts[] = { AUDIO_LAYOUT_5POINT1, AUDIO_LAYOUT_7POINT1,
AUDIO_LAYOUT_STEREO, AUDIO_LAYOUT_5POINT1_SURROUND, AUDIO_LAYOUT_7POINT1,
AUDIO_LAYOUT_STEREO, AUDIO_LAYOUT_5POINT1, AUDIO_LAYOUT_7POINT1 };
@@ -638,14 +640,16 @@ static void wrapper_live_controls(unsigned publishes)
unsigned retry;
uint32_t tempo = (uint32_t)(tempos[step] * 65536.0);
check.control = active ? tempo | AUDIO_PIPELINE_STRETCH : 65536;
check.cutoff = step & 1 ? 1000 : 0;
check.cutoff = speed_lowpass ? cutoffs[step] : step & 1 ? 1000 : 0;
if (live_layouts)
{
source_layout = layouts[step];
source_channels = audio_layout_channels(source_layout);
}
check.layout = source_layout;
if (!audio_driver_pipeline_transport_request(tempo, active, false, check.cutoff))
if (!(speed_lowpass
? audio_driver_pipeline_transport_request_speed(tempo, active, false, true)
: audio_driver_pipeline_transport_request(tempo, active, false, check.cutoff)))
{
fixture_failures++;
return;
@@ -940,6 +944,7 @@ int main(int argc, char **argv)
track_conversions = use_wrapper;
live_controls = getenv("LIVE_CONTROLS") != NULL;
live_layouts = getenv("LIVE_LAYOUTS") != NULL;
speed_lowpass = getenv("SPEED_LPF") != NULL;
if (layout)
{
if (!strcmp(layout, "5.1")) source_layout = AUDIO_LAYOUT_5POINT1;
@@ -980,6 +985,11 @@ int main(int argc, char **argv)
fprintf(stderr, "LIVE_LAYOUTS requires LIVE_CONTROLS and LAYOUT=7.1\n");
return 1;
}
if (speed_lowpass && !live_controls)
{
fprintf(stderr, "SPEED_LPF requires LIVE_CONTROLS\n");
return 1;
}
lat_us = (retro_time_t*)malloc(MAX_SAMPLES * sizeof(retro_time_t));
if (!lat_us)
@@ -1032,6 +1042,7 @@ int main(int argc, char **argv)
if (use_wrapper) printf("native wrapper: 16 runs, 128 restart transactions, %u failures\n", fixture_failures);
if (live_controls) printf("live transport: 128 processing changes without metadata reset, %u failures\n", fixture_failures);
if (live_layouts) printf("live layouts: 128 source layout changes on a fixed 7.1 device, %u failures\n", fixture_failures);
if (speed_lowpass) printf("speed LPF: 128 coherent tempo/cutoff requests, %u failures\n", fixture_failures);
printf("pipeline wakeups: %u fixture failures\n", fixture_failures);
return fixture_failures ? 1 : 0;
}
+29 -1
View File
@@ -264,12 +264,40 @@ static void separate_output(void)
}
}
static void cutoff_policy(void)
{
static const unsigned rates[] = {8000, 44100, 48000, 96000, 192000};
unsigned r, step;
CHECK(audio_speed_lpf_cutoff(7999, 131072) == 0);
CHECK(audio_speed_lpf_cutoff(192001, 131072) == 0);
for (r = 0; r < sizeof(rates) / sizeof(rates[0]); r++)
{
uint32_t previous = rates[r];
CHECK(audio_speed_lpf_cutoff(rates[r], 0) == 0);
CHECK(audio_speed_lpf_cutoff(rates[r], 16384) == 0);
CHECK(audio_speed_lpf_cutoff(rates[r], 65536) == 0);
CHECK(audio_speed_lpf_cutoff(rates[r], UINT32_MAX) == 20);
for (step = 1; step <= 4096; step++)
{
uint32_t speed = 65536 + step * 496;
uint32_t got = audio_speed_lpf_cutoff(rates[r], speed);
double exact = rates[r] * 0.45 / (speed / 65536.0);
CHECK(got >= 20 && got <= previous);
CHECK(got <= exact + 1e-8 && exact - got < 1.00000001);
previous = got;
}
cases++;
}
CHECK(audio_speed_lpf_cutoff(48000, 131072) == 10800);
CHECK(audio_speed_lpf_cutoff(48000, 2097152) == 675);
}
int main(void)
{
static const unsigned rates[] = {8000, 44100, 48000, 96000, 192000};
unsigned r, c;
guarded = 1;
contracts(); response(); reset_cases(); separate_output();
contracts(); response(); reset_cases(); separate_output(); cutoff_policy();
for (r = 0; r < sizeof(rates)/sizeof(rates[0]); r++)
for (c = 1; c <= 11; c++) native_cases(rates[r], c);
guarded = 0;