Files
RetroArch/.github/workflows/Linux-samples-audio.yml
T
LibretroAdmin 5b3ec8d48a Emscripten: its JavaScript runtime moves to os/emscripten
The platform, audio and camera libraries, the pre-js and the closure
externs the web build links in live under os/emscripten.
2026-10-29 20:33:27 +01:00

1330 lines
67 KiB
YAML

name: CI Linux samples/audio
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
env:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
samples-audio:
name: Build and run samples/audio
runs-on: ubuntu-latest
env:
APT_PKGS: build-essential libasound2-dev libsdl2-dev libopenal-dev libpipewire-0.3-dev libpulse-dev pulseaudio ffmpeg gcc-mingw-w64-x86-64 wine64 ccache
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Apt cache key
id: apt
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
- name: Restore apt cache
uses: actions/cache/restore@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
restore-keys: ${{ steps.apt.outputs.prefix }}
- name: Install dependencies
timeout-minutes: 20
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
- name: Save apt cache
if: env.APT_CACHE_SAVE == '1'
uses: actions/cache/save@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Restore ccache
uses: actions/cache/restore@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
restore-keys: |
ccache-${{ github.workflow }}-${{ github.job }}-
- name: Enable ccache
run: |
set -eu
export CCACHE_DIR="$HOME/.ccache"
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
ccache -M 250M
ccache -z
- name: Build and run mixer_lock_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/mixer_lock
run: |
set -eu
# Includes audio/audio_driver.c and drives the mixer's public
# entry points with a real state lock in place. The lock is a
# plain mutex, so any path that reaches a locking entry point
# while already holding it parks the thread against itself;
# loading the first menu sound did exactly that
# (libretro/RetroArch#19485). Every case runs on a watchdog:
# a reintroduced recursive acquisition aborts with a stack
# instead of hanging the job. Also covers the lock being
# released on out-of-range slots and a NULL stream name
# surviving removal.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x mixer_lock_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./mixer_lock_test
echo "[pass] mixer_lock_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./mixer_lock_test
echo "[pass] mixer_lock_test (TSan)"
- name: Build and run thread_handshake_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/thread_handshake
run: |
set -eu
# Links audio/audio_thread_wrapper.c against a scripted
# driver. The wrapped thread parks in one of two waits and
# the main thread's block() needs stopped_ack from either; a
# stop() landing between start()'s signal and the thread's
# re-check used to leave it in the one wait that never
# acknowledged. Two thousand start/stop round-trips hit that
# window on the previous wrapper on every run. The scripted
# device can also stop returning from a call: a stop must
# then still complete once the call does, and must be
# reported exactly once meanwhile - prompt handshakes are
# asserted not to be reported.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x thread_handshake_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 \
./thread_handshake_test
echo "[pass] thread_handshake_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 600 ./thread_handshake_test
echo "[pass] thread_handshake_test (TSan)"
- name: Build and run mmdevice_active_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/mmdevice_active
run: |
set -eu
# The open-endpoint table the WASAPI notifications consult,
# set from two threads and matched from a third, none of
# which has touched it before.
make clean && make -j$(nproc) all SANITIZER=address,undefined
timeout 300 ./mmdevice_active_test
echo "[pass] mmdevice_active_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./mmdevice_active_test
echo "[pass] mmdevice_active_test (TSan)"
- name: Build and run graph_clock_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/graph_clock
run: |
set -eu
# The two conversions the JACK and PipeWire clock fits add.
# Neither a JACK server nor a PipeWire daemon can be started
# here, so unlike alsa_clock this mirrors the arithmetic
# rather than driving the drivers - a weaker check, said
# plainly, and aimed at the parts where being wrong is
# silent.
#
# JACK's current_frames is 32 bits and wraps in about a day
# at 48 kHz, sooner if the server was up before RetroArch
# started, since the counter is the server's. The difference
# is taken in 32 bits where the wrap cancels; widening first
# turns one wrap into a four-billion-frame jump. A run is
# started two seconds short of the wrap so it goes round
# mid-measurement.
#
# PipeWire's ticks are in units of pw_time.rate, a fraction
# that is 1/samplerate. The same physical drift has to read
# the same ppm at 44.1k and 48k, which is what the fraction
# is there for.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x graph_clock_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./graph_clock_test
echo "[pass] graph_clock_test (ASan)"
- name: Build and run alsa_clock_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/alsa_clock
run: |
set -eu
# The ALSA device-clock estimate, driving the shipping
# audio/drivers/alsa.c on a real PCM opened on the null
# device. The four libasound calls the estimate is built from
# are scripted via --wrap, so a device can be made to run at a
# chosen offset and the driver's own answer read back out of
# what it logs - what is asserted is the line a user sees.
#
# Two clocks are covered. The audio timestamp, where a driver
# counts one, is the DMA or link clock compared straight
# against the system clock and needs neither a rate nor a
# delay. Where there is none - the null PCM reports none, and
# nor do plenty of real devices - the position is fitted
# against the timestamp it was taken with. That position is
# inferred from the write count less the delay, so one case
# puts an absurd but constant delay in: it shifts the line
# and leaves its slope alone, which is why this is a fit and
# not a difference of two samples.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x alsa_clock_test
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./alsa_clock_test
echo "[pass] alsa_clock_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
timeout 60 ./alsa_clock_test
echo "[pass] alsa_clock_test (TSan)"
- name: Build and run alsa_wait_writable_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/alsa_wait_writable
run: |
set -eu
# Links the shipping audio/drivers/alsa.c and interposes the
# five libasound calls alsa_wait_writable() makes via
# -Wl,--wrap, so the devices no stock PCM can produce on
# demand are scripted: a running stream whose poll never
# signals, a prepared stream reporting no space, a start that
# lands in an underrun storm, and wakes that never deliver
# space. Each must return 0 within a bounded lap and wall-time
# budget rather than parking the audio thread; the previous
# wait was an unbounded snd_pcm_wait(-1). The conforming path
# runs end-to-end on the null PCM, and the harness skips
# cleanly if no ALSA configuration is available at all.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x alsa_wait_writable_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./alsa_wait_writable_test
echo "[pass] alsa_wait_writable_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./alsa_wait_writable_test
echo "[pass] alsa_wait_writable_test (TSan)"
- name: Build and run alsa_lifecycle_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/alsa_lifecycle
run: |
set -eu
# Links the shipping audio/drivers/alsa.c against the null PCM
# and scripts the libasound calls that decide stop/start and
# the blocking write - can_pause, pause, drop, prepare, writei,
# wait, recover - via -Wl,--wrap. alive() must follow stop()
# and start() on a device that can pause, one that cannot, and
# one that says it can and refuses; the blocking write must
# write first, wait only on EAGAIN, and never without a bound.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x alsa_lifecycle_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./alsa_lifecycle_test
echo "[pass] alsa_lifecycle_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./alsa_lifecycle_test
echo "[pass] alsa_lifecycle_test (TSan)"
- name: Build and run alsa_chmap_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/alsa_chmap
run: |
set -eu
# The shipping ALSA driver against the null PCM, with the
# channel map, the channel counts the card takes and writei
# scripted via -Wl,--wrap: a card is handed the frontend's
# channels in its own order - its map's, or ALSA's FL FR RL
# RR FC LFE SL SR without one - by write() and through a lent
# mmap span, reports the layout it has, and steps 7.1 down to
# 5.1 or quad before stereo. A short writei must not lose the
# frame stride.
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 120 ./alsa_chmap_test
echo "[pass] alsa_chmap_test (ASan)"
- name: Build and run pipeline_stall_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/pipeline_stall
run: |
set -eu
# Includes audio/audio_driver.c and runs the threaded pipeline's
# shipping producer and consumer against a scripted driver whose
# write_raw() path reaches the device without the resampler or
# the arenas. The device is then made to stop taking audio. The
# consumer must take nothing from the ring it cannot deliver and
# report no progress it did not make; the producer must record
# the stall after one bounded wait and drop at once thereafter;
# and both must recover when the device returns. The previous
# consumer took the chunk first, threw it away on finding no
# room, and cleared the stall - the frontend paced at one frame
# per failed pass for as long as the device was stuck.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x pipeline_stall_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 180 \
./pipeline_stall_test
echo "[pass] pipeline_stall_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 400 ./pipeline_stall_test
echo "[pass] pipeline_stall_test (TSan)"
- name: Build and run upmix_test (plain, ASan + UBSan)
shell: bash
working-directory: samples/audio/upmix
run: |
set -eu
# The stereo-to-wider stage at the last step before a device
# opened with a wider speaker layout. A layout is a mask of
# positions in ascending bit order, never a count: 5.1 with
# the rear pair at the back and 5.1 with it at the sides are
# two layouts whose slots differ. The fronts carry the stereo
# unchanged, the rear pair -3 dB in its own slots, the centre
# the -6 dB sum, the LFE the bass through a 120 Hz low-pass and
# none of the treble; stereo is a copy; a mask the stage
# cannot fill is refused.
make clean && make -j$(nproc) all
./upmix_test
echo "[pass] upmix_test"
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./upmix_test
echo "[pass] upmix_test (ASan)"
- name: Build and run discrete_multichannel_test (plain, ASan + UBSan)
shell: bash
working-directory: samples/audio/discrete_multichannel
run: |
set -eu
# A core's 5.1 through the multi-channel batch entry to a 5.1
# device without the fold and the upmix in between: each
# device channel carries its core channel's tone and none of
# the others', in all four format pairings, through the
# resampler off unity; over the threaded ring; recorded; and
# to a stereo device, the fold at the BS.775 gains with the
# LFE dropped. The last case runs the shipping WASAPI driver
# over the fake device from wasapi_exclusive_pacing, so a
# 5.1 core reaches a Dolby Digital decoder as AC-3 bursts
# that decode back to its own channels. Includes the
# shipping audio_driver.c.
make clean && make -j$(nproc) all
./discrete_multichannel_test
echo "[pass] discrete_multichannel_test"
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./discrete_multichannel_test
echo "[pass] discrete_multichannel_test (ASan)"
- name: Build and run embedded_alsa_test (the embedded ALSA driver, mock kernel device)
shell: bash
working-directory: samples/audio/embedded_alsa
run: |
set -eu
# The "tinyalsa" driver in audio/drivers/alsa.c talks the
# kernel PCM ioctls directly. abi_check compares its copy of
# that ABI - extracted from the driver itself, so it cannot
# go stale - against <sound/asound.h>: every struct size and
# offset, constant and ioctl number. The test then runs the
# driver against a mock device that refines parameters the
# way the kernel does.
make clean && make -j$(nproc) check
echo "[pass] embedded_alsa_test"
make clean && make -j$(nproc) check SANITIZER=address,undefined
echo "[pass] embedded_alsa_test (ASan)"
- name: Build and run ealsa_test (the embedded ALSA driver against a mock PCM device)
shell: bash
working-directory: samples/audio/ealsa
run: |
set -eu
# The embedded driver in audio/drivers/alsa.c talks to the
# kernel's PCM ABI through one seam - open, ioctl, close,
# poll - so a scripted device that answers the way a card
# does runs it with no card present, and can refuse a rate, a
# format, a channel count or a period grid, which is where a
# negotiation goes wrong. Built with HAVE_TINYALSA alone, so
# the build never sees an alsa-lib header either. It also
# carries what tinyalsa_write_test used to check of the
# driver this one replaced: a full device refusing in both
# modes, a device that never signals, a poll cut short by a
# signal, and the first write reporting what it took.
make clean && make -j$(nproc) all
./ealsa_test
echo "[pass] ealsa_test"
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./ealsa_test
echo "[pass] ealsa_test (ASan)"
- name: Build and run bitstream_test (plain, ASan + UBSan)
shell: bash
working-directory: samples/audio/bitstream
run: |
set -eu
# audio/audio_bitstream.c: a coded stream on its way to a
# receiver, which the PCM pipeline has no place for. Real
# DTS and AC-3 from ffmpeg, handed over frame by frame:
# bursts leave whole and never partial, the queue paces the
# source rather than the source running ahead, a gap
# produces the pause burst the standard has for it rather
# than silence (which makes a receiver drop its lock), and
# the PCM frames reported are the device time the bytes
# occupy.
make clean && make -j$(nproc) all
./bitstream_test
echo "[pass] bitstream_test"
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./bitstream_test
echo "[pass] bitstream_test (ASan)"
- name: Build and run opensl_test (the Android driver against a mock OpenSL player)
shell: bash
working-directory: samples/audio/opensl
run: |
set -eu
# audio/drivers/opensl.c is Android-only and has no build
# here, so it is built against stand-in <SLES/*.h> over a
# scripted player whose queue a thread drains: the block
# arithmetic at the latencies of issue #6405 and below, the
# queue accounting the hang there came from, float and
# 16-bit, a full queue, and a device that stops consuming.
make clean && make -j$(nproc) all
./opensl_test
echo "[pass] opensl_test"
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./opensl_test
echo "[pass] opensl_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./opensl_test
echo "[pass] opensl_test (TSan)"
- name: Build and run qsa_test (the QNX driver against a mock QSA device)
shell: bash
working-directory: samples/audio/qsa
run: |
set -eu
# audio/drivers/alsa_qsa.c is QNX-only and has no build here,
# so it is built against a stand-in <sys/asoundlib.h> over a
# scripted device: init and its failure path, the rate it
# reports back, blocking and non-blocking writes, stop and
# start, underrun recovery, and a device that stops draining.
# Not a substitute for a QNX build; it is the coverage the
# driver had none of.
make clean && make -j$(nproc) all
./qsa_test
echo "[pass] qsa_test"
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./qsa_test
echo "[pass] qsa_test (ASan)"
- name: Build and run ctr_audio_test (the 3DS CSND and NDSP drivers against a scripted device)
shell: bash
working-directory: samples/audio/ctr_audio
run: |
set -eu
# audio/drivers/ctr_csnd_audio.c and ctr_dsp_audio.c against a
# stand-in <3ds.h> whose system tick and channel position are
# the device: buffer_size() and write_avail() in bytes of what
# write() takes, and a write longer than the ring taking half
# of it - ASan is what sees a copy run past the ring.
make clean && make -j$(nproc) all
./ctr_audio_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./ctr_audio_test
echo "[pass] ctr_audio_test (ASan)"
- name: Build and run switch_audio_test (the Switch audout driver against a scripted service)
shell: bash
working-directory: samples/audio/switch_audio
run: |
set -eu
# audio/drivers/switch_audio.c's libnx build against a
# stand-in <switch.h> whose audout service queues buffers and
# plays them out on the test's word: write_avail() must be
# what a write takes, across the released buffers, and
# buffer_size() every buffer carrying its share of the
# latency setting.
make clean && make -j$(nproc) all
./switch_audio_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./switch_audio_test
echo "[pass] switch_audio_test (ASan)"
- name: Build and run switch_audren_test (the Switch audren driver's renderer ownership, ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/switch_audren
run: |
set -eu
# audio/drivers/switch_libnx_audren_audio.c's libnx build
# against a stand-in <switch.h> whose renderer thread plays a
# wave buffer a frame. libnx's audrv is not thread-safe and
# the driver takes no lock: every renderer call must come from
# the writer's thread while another reads write_avail(), a
# write_avail() on the writer's own thread must see what the
# renderer played, non-blocking writes must keep taking audio,
# and stop and start must pause and resume the voice. TSan
# reports two threads in the renderer as a race on the
# stand-in's wave buffer list.
make clean && make -j$(nproc) all
./switch_audren_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./switch_audren_test
echo "[pass] switch_audren_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./switch_audren_test
echo "[pass] switch_audren_test (TSan)"
- name: Build and run wiiu_ax_test (the Wii U AX driver's callback against its writer)
shell: bash
working-directory: samples/audio/wiiu_ax
run: |
set -eu
# audio/drivers/wiiu_audio.c against the tree's own Wii U
# headers, with stand-in AX and OS functions behind them: the
# frame callback on one thread and the writer on another keep
# the count of buffered frames in one atomic word, and every
# frame accepted must be buffered or counted played. TSan is
# what sees a field the two share without ordering.
make clean && make -j$(nproc) all
./wiiu_ax_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./wiiu_ax_test
echo "[pass] wiiu_ax_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./wiiu_ax_test
echo "[pass] wiiu_ax_test (TSan)"
make clean
- name: Build and run xenon360_test (the libxenon driver against a scripted sound queue)
shell: bash
working-directory: samples/audio/xenon360
run: |
set -eu
# audio/drivers/xenon360_audio.c against stand-in libxenon
# headers: buffer_size() is the latency setting, write_avail()
# what a write takes, the queue never past it, and a write
# longer than the staging buffer goes in passes - ASan is what
# sees a pass overrun it.
make clean && make -j$(nproc) all
./xenon360_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./xenon360_test
echo "[pass] xenon360_test (ASan)"
- name: Build and run audioworklet_test (the web driver's write against its own ring)
shell: bash
working-directory: samples/audio/audioworklet
run: |
set -eu
# audio/drivers/audioworklet.c, #included by the test and
# built against the tree's emscripten stand-ins: write()
# returns the bytes it took, the ring holds exactly that, and
# frames a write gives up on are counted, not logged.
make clean && make -j$(nproc) all
./audioworklet_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./audioworklet_test
echo "[pass] audioworklet_test (ASan)"
- name: Build and run rwebaudio_test (the web driver's channels, C and JavaScript halves)
shell: bash
working-directory: samples/audio/rwebaudio
run: |
set -eu
# audio/drivers/rwebaudio.c against a stand-in for its
# JavaScript half: frames of the layout the page took go out
# one plane per channel, and every byte count is in frames of
# that layout. Then os/emscripten/library_rwebaudio.js itself
# under Node with a mock AudioContext: the destination is
# widened only past stereo and only as far as its
# maxChannelCount allows, and a stereo one is left as it was.
make clean && make -j$(nproc) check
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./rwebaudio_test
echo "[pass] rwebaudio_test (ASan)"
- name: Build and run pipewire_write_test (the PipeWire driver's write against its own ring)
shell: bash
working-directory: samples/audio/pipewire_write
run: |
set -eu
# audio/drivers/pipewire.c, #included by the test and built
# against the system's PipeWire headers, with the graph as a
# loop wait that plays a quantum: a write longer than the
# ring's mark takes what fits, or blocking goes in parts, in
# whole frames, and the write path logs nothing.
make clean && make -j$(nproc) all
./pipewire_write_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./pipewire_write_test
echo "[pass] pipewire_write_test (ASan)"
- name: Build and run wasapi_mic_test (the WASAPI microphone's fetch against a scripted capture client)
shell: bash
working-directory: samples/audio/wasapi_mic
run: |
set -eu
# audio/drivers/wasapi.c's text against the shared fake WASAPI
# with a capture client added: a shared-mode FIFO set shorter
# than one engine packet still takes every packet, and a
# packet with no room waits for the FIFO to be read.
make clean && make -j$(nproc) all
./wasapi_mic_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./wasapi_mic_test
echo "[pass] wasapi_mic_test (ASan)"
- name: Build and run oss_report_test (the OSS driver's reports on a device that cannot answer)
shell: bash
working-directory: samples/audio/oss_report
run: |
set -eu
# audio/drivers/oss.c, #included by the test, over Linux's
# soundcard.h: a device that has gone away reports no room and
# no size, and a second of frames asking logs nothing.
make clean && make -j$(nproc) all
./oss_report_test
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./oss_report_test
echo "[pass] oss_report_test (ASan)"
- name: Build and run live_drivers_test (a live PulseAudio server, a null ALSA PCM)
shell: bash
working-directory: samples/audio/live_drivers
run: |
set -eu
# A server with a null sink, and an ALSA config naming a null
# PCM: the real drivers against the real libraries, no
# hardware. Pulse's telemetry is cached from the server
# thread's callbacks and its writer takes no mainloop lock;
# ALSA is run once as the device reports
# itself and once told it cannot pause, the fallback from
# snd_pcm_pause to snd_pcm_drop. With the threaded pipeline a
# plug PCM opens for mmap and lends its buffer; a plug over a
# file plugin reads the lent bytes back.
export XDG_RUNTIME_DIR=/tmp/xdg
mkdir -p "$XDG_RUNTIME_DIR/pulse"
printf '%s\n' 'pcm.nullpcm { type null }' \
'pcm.plugnull { type plug slave.pcm "nullpcm" }' \
'pcm.filenull { type file slave.pcm "nullpcm" file "/tmp/ra_live_drivers_lend.raw" format raw }' \
'pcm.plugfile { type plug slave.pcm "filenull" }' > "$HOME/.asoundrc"
pulseaudio --daemonize=no --exit-idle-time=-1 --disallow-exit -n \
-L "module-null-sink sink_name=nullsink rate=48000 channels=2" \
-L "module-native-protocol-unix auth-anonymous=1 socket=$XDG_RUNTIME_DIR/pulse/native" \
--log-target=file:/tmp/pa.log &
PA_PID=$!
sleep 3
make clean && make -j$(nproc) all
./live_drivers_test
echo "[pass] live_drivers_test"
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./live_drivers_test
echo "[pass] live_drivers_test (ASan)"
# Pulse's writer hands audio to the server's thread through a
# ring and an eventcount, which is what TSan is for.
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1:suppressions=$PWD/tsan.supp ./live_drivers_test
echo "[pass] live_drivers_test (TSan)"
kill $PA_PID || true
- name: Build and run binaural_test (plain, ASan + UBSan)
shell: bash
working-directory: samples/audio/binaural
run: |
set -eu
# A layout rendered to two ears for headphones: a left speaker
# arrives at the left ear first, louder and brighter; the rear
# pair duller than the front; centre and LFE alike to both
# ears; a centred source keeps its level and a full-scale one
# leaves at full scale; the two 5.1s render their rear pair
# from different directions; stereo is a mild crossfeed.
make clean && make -j$(nproc) all
./binaural_test
echo "[pass] binaural_test"
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./binaural_test
echo "[pass] binaural_test (ASan)"
- name: Build and run pipeline_rate_control_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/pipeline_rate_control
run: |
set -eu
# Includes audio/audio_driver.c and runs the threaded pipeline's
# shipping producer, consumer and rate controller against a
# device whose 8 ms buffer drains at exactly 48000 frames a
# second of wall-clock time, fed one 800-frame video frame every
# 1/60 s on an absolute schedule, audio sync off. The clocks
# match, so the right ratio is 1.0: after a four-second settle
# the mean rate adjustment over three seconds must be within
# 500 ppm of it, and the device must have taken 99.5% of what
# was produced. Rate control on the pipeline samples pipe and
# device together, at the producer; reading the device alone,
# which the consumer holds between half and full, pins the
# ratio at a bound and the pipe overflows behind it. Real time,
# so a loaded runner can shift the mean; the tolerance is ten
# times the settled residual.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x pipeline_rate_control_test
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./pipeline_rate_control_test
echo "[pass] pipeline_rate_control_test (ASan)"
# A core late by 60 ms every 2 s, audio sync off: the silence is
# the stalls' own and no more, the late audio is not kept, and
# rate control is not pinned against a backlog. And with sync on:
# the writer waits, nothing is dropped.
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 12 4 jitter
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 12 4 sync-jitter
echo "[pass] pipeline_rate_control_test (late core, ASan)"
# The 64 ms default buffer, sync off, a steady core and a late one:
# the consumer's chunks are whole publishes and the pipe's fill
# swings by one with the phase; nothing is dropped from a steady
# core at any phase.
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 16 4 big
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 16 4 big-jitter
echo "[pass] pipeline_rate_control_test (64 ms device, ASan)"
# The producer held 4 ms every five seconds, as a loaded runner
# holds it: a publish pushed across a sink window boundary. The
# estimate must settle and apply about zero, not the shift.
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 16 4 big-stall
echo "[pass] pipeline_rate_control_test (stalled producer, ASan)"
# A float core on a float ring into a float device: a signal a
# tenth of an int16 LSB high arrives as published, not as zero.
ASAN_OPTIONS=detect_leaks=0 timeout 120 ./pipeline_rate_control_test 16 4 float
echo "[pass] pipeline_rate_control_test (float core, ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 240 ./pipeline_rate_control_test
echo "[pass] pipeline_rate_control_test (TSan)"
- name: Build and run resampler_reset_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/resampler_reset
run: |
set -eu
# Every resampler backend's reset(): after it the stream
# continues as a freshly initialised backend would, to the
# sample, at the ratios the frontend meets - and the reset
# makes no allocation, counted through wrapped malloc, calloc
# and memalign_alloc.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x resampler_reset_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 ./resampler_reset_test
echo "[pass] resampler_reset_test (ASan)"
- name: Build and run ff_speedup_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/ff_speedup
run: |
set -eu
# Includes audio/audio_driver.c with the clock replaced by a
# counter. The fast-forward speedup multiplier must follow the
# core's cadence on both pipelines: measured at the flush inline,
# and by the producer at its publish cadence on the threaded
# pipeline, where the consumer's own flush interval is the device
# draining the previous chunk and would only confirm the previous
# multiplier. Re-entry after an idle spell must start from unity
# rather than read the idle time as one flush interval.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x ff_speedup_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 ./ff_speedup_test
echo "[pass] ff_speedup_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./ff_speedup_test
echo "[pass] ff_speedup_test (TSan)"
- name: Build and run device_list_driver_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/device_list_driver
run: |
set -eu
# Includes audio/audio_driver.c with the alsa and null drivers in
# the table. Picking a driver in the menu writes the setting but
# does not reinitialise audio, and the Device list - rebuilt on
# each open - was asked of the running driver, so it showed the
# old driver's devices under the new driver's name until a
# restart. The list now comes from the configured driver, and
# is freed by the driver that built it. Cases: configured and
# running agree; configured differs from running; back again;
# nothing running; and release by the builder, once.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x device_list_driver_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./device_list_driver_test
echo "[pass] device_list_driver_test (ASan)"
- name: Build and run device_label_path_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/device_label_path
run: |
set -eu
# A device list entry with the hardware's name after the path,
# "/dev/dsp0 (HDA Intel)", is stored by the menu as shown and
# comes back to init as the device string; oss opened it as a
# path and failed, so a device picked from the list could not
# be used. Compiles oss and audioio whole with open() redirected
# to a recorder, and checks each reaches it with the path.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x device_label_path_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./device_label_path_test
echo "[pass] device_label_path_test (ASan)"
- name: Build and run rate_control_bounds_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/rate_control_bounds
run: |
set -eu
# Includes audio/audio_driver.c and runs the rate control's
# shipping computation against a scripted driver. At the
# setpoint, empty and full it must give 1.0 and the band's two
# edges; with write_avail() reporting five times buffer_size()
# it must give the upper edge and no more. Before the clamp an
# over-reporting driver drove it nine times past the band.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x rate_control_bounds_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./rate_control_bounds_test
echo "[pass] rate_control_bounds_test (ASan)"
- name: Build and run latency_contract_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/audio/latency_contract
run: |
set -eu
# Real drivers on backends that need no hardware - the ALSA null
# PCM, SDL's dummy audio, OpenAL Soft's null device - opened at
# 64 and 32 ms and checked against audio_driver.h's contract:
# buffer_size() is the setting in bytes of the output format,
# write_avail() never exceeds it, a buffer's worth offered takes
# room, and the room comes back as the device plays. The sdl
# driver before fdda5f7 reports 42.7 ms against 64 here.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x latency_contract_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./latency_contract_test
echo "[pass] latency_contract_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1:suppressions=$PWD/tsan.supp \
timeout 300 ./latency_contract_test
echo "[pass] latency_contract_test (TSan)"
- name: Build and run asio_convert_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/asio_convert
run: |
set -eu
# audio/drivers/asio_convert.h is the ASIO driver's sample
# conversion, pure C, so it runs here without Windows. Every
# ASIOSampleType the specification defines is converted for full
# scale, zero, half and one 16-bit step in both signs and checked
# byte for byte against the layout the specification gives - the
# Int32LSB16..24 family as low-aligned, sign-extended words, the
# MSB types big-endian. Saturation and the silence fill past the
# supplied frames are checked too. The converter this replaced
# wrote five types and memset the rest to silence.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x asio_convert_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./asio_convert_test
echo "[pass] asio_convert_test (ASan)"
- name: Build and run coreaudio_clock_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/coreaudio_clock
run: |
set -eu
# The device clock fitted from the timestamp the render
# callback is handed, which the driver used to discard.
#
# The fit is the same shape as the ASIO one and is checked
# there. What is different is the time axis: mHostTime is in
# mach absolute units, not nanoseconds, and the ratio is one
# to one on Intel and 125 to 3 on Apple silicon. Ignoring it
# reads a 48 kHz device as 1152 Hz - invisible on Intel and
# wrong on every Mac since 2020 - so the same clock is driven
# through both timebases here.
#
# Also mRateScalar, which is the HAL's own figure for the
# same thing, read only where it is flagged valid.
#
# Arithmetic only, so it runs here without a Mac.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x coreaudio_clock_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./coreaudio_clock_test
echo "[pass] coreaudio_clock_test (ASan)"
- name: Build and run coreaudio_ring_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/coreaudio_ring
run: |
set -eu
# The CoreAudio ring keeps a frame's channels together. Its
# capacity is the sample count rounded up to a power of two,
# which for six channels is not a whole number of frames, so
# the cursors wrap mid-frame - and that does not matter,
# because a FIFO returns samples in the order they went in
# wherever they sit. This runs the traffic for every layout,
# then forces every transfer across the wrap, to keep that
# answer honest rather than assumed.
#
# What does rotate the channels is a transfer whose count is
# not a whole number of frames, which is why the callback
# trims short reads. The last case does that on purpose and
# fails if it goes unnoticed, so the trim has a guard.
#
# Arithmetic only, so it runs here without a Mac.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x coreaudio_ring_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./coreaudio_ring_test
echo "[pass] coreaudio_ring_test (ASan)"
- name: Build and run sdl_lost_wakeup_test (the SDL2 driver against SDL's dummy device)
shell: bash
working-directory: samples/audio/sdl_lost_wakeup
run: |
set -eu
# The producer's waits and the capture parks are measured; the
# lend pair is asserted: whole-frame spans, published to a
# device that pulls them, refused while stopped.
make clean && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
SDL_AUDIODRIVER=dummy timeout 300 ./sdl_lost_wakeup_test 1
echo "[pass] sdl_lost_wakeup_test (ASan)"
- name: Build and run coreaudio_write_test (the shipping writer, lend included, on stubs)
shell: bash
working-directory: samples/audio/coreaudio_write
run: |
set -eu
# run.py lifts the writer, the render callback and the lend
# pair out of audio/drivers/coreaudio.c and drives them on a
# simulated device clock; no Apple framework is involved.
python3 run.py
echo "[pass] coreaudio_write_test"
- name: Build and run asio_clock_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/asio_clock
run: |
set -eu
# The device clock read from the driver's own time
# information, rather than from when its callbacks arrived.
#
# Two things here do not need an interface. The ASIOTime
# layout is an ABI: the sample position and system time are
# 64-bit values the SDK splits into 32-bit halves on Windows,
# and getting that wrong does not fail to build, it reads a
# driver's numbers as garbage in a real-time callback. The
# offsets are asserted against the SDK's, and asio.c carries
# the same assertions for the compiler that sees the real
# Windows ABI.
#
# And the estimate itself: a clock at a known offset is fed
# in with scheduling jitter on the timestamps, and the answer
# has to be the clock. A two-point estimate fails that - the
# anchor's own noise divided by the window reads as drift,
# 50 ppm for half a millisecond at ten seconds - which is why
# it is a least-squares fit over every callback instead.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x asio_clock_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./asio_clock_test
echo "[pass] asio_clock_test (ASan)"
- name: Build and run asio_ring_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/asio_ring
run: |
set -eu
# Also the write's frame alignment: the callback reads the
# ring a frame at a time, so a write must end on one, and
# the rule is checked for every layout the driver can open.
#
# audio/drivers/asio_ring.h sizes the ring in front of the ASIO
# device: the latency setting less the device's reported stage,
# floored at four periods where that is under 6 ms, 6 ms where it
# is over, two periods at the least. Arithmetic only, so it runs
# here without Windows. The expected frames are worked by hand
# from those rules for the devices that set them - ASIO4ALL at 64
# frames, a wrapper locked at 528, a USB DAC at 8 and 128 - and
# the two points where the rules meet.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x asio_ring_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./asio_ring_test
echo "[pass] asio_ring_test (ASan)"
- name: Build and run dsound_pump_test (MinGW, under wine)
shell: bash
working-directory: samples/audio/dsound_pump
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# audio/drivers/dsound.c end to end on wine's DirectSound, fed
# by an ALSA null PCM, which plays at real-time pace on a
# runner with no sound card. Pins that the pump - which now
# waits for the play cursor to move the distance it needs,
# worked out from the rate, instead of retrying every
# millisecond - still moves audio through blocking writes,
# fills silence when the writer stops, and lets free() return
# at once while it waits.
printf 'pcm.!default { type null }\nctl.!default { type hw card 0 }\n' > ~/.asoundrc
make clean && make -j$(nproc) all
WINEDEBUG=-all timeout 300 make check
echo "[pass] dsound_pump_test (wine)"
- name: Build and run wdmks_loop_test (MinGW, under wine)
shell: bash
working-directory: samples/audio/wdmks_loop
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# audio/drivers/wdmks.c compiled whole, its WaveRT loop driven
# by hand: the test owns the driver's state, points the
# position register at a word of its own and moves the
# hardware itself, so nothing reaches the kernel and wine
# needs no sound card. Pins that every byte the cursor passes
# is cleared, so the loop plays silence when the frontend
# stops writing - a pause, a menu, a stall - instead of the
# last loop of audio round and round; that a write after
# that lands a margin ahead of the hardware rather than a
# loop behind it; that a full loop is not taken for an
# overrun; and that a start puts the model back at the top
# of the loop with the hardware; and that the resync margin is
# the FIFO the pin reports plus half a millisecond, two
# milliseconds where it reports none; and that gaps in the
# audio are counted once each, neither a start nor every
# sample of a pause.
make clean && make -j$(nproc) all
WINEDEBUG=-all timeout 120 make check
echo "[pass] wdmks_loop_test (wine)"
- name: Check MMDevice notification targets on both thread backends
shell: bash
working-directory: samples/audio/mmdevice_notification
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
for threads in 0 1; do
make clean check THREADS=$threads SANITIZER=address,undefined
make clean THREADS=$threads CC=x86_64-w64-mingw32-gcc LDFLAGS=-static && make -j$(nproc) all THREADS=$threads CC=x86_64-w64-mingw32-gcc LDFLAGS=-static
WINEDEBUG=-all wine ./mmdevice_notification_test.exe
done
make clean
- name: Build and run wasapi_exclusive_pacing_test (ASan + UBSan, then TSan)
shell: bash
working-directory: samples/audio/wasapi_exclusive_pacing
run: |
set -eu
# audio/drivers/wasapi.c, unmodified but for its Windows include
# lines, against a fake device on a real-time clock: exclusive
# and event-driven, one period per endpoint buffer, every period
# taken or counted unanswered. The writer is a frontend with audio
# sync off: one frame of core audio per non-blocking write, no rate
# control. Passes when no period goes unanswered in either mode and
# nothing is dropped once the buffer holds a frame. Before the pump
# thread and the drain-first shared write this run gave 3-10% of
# periods unanswered; the first smaller-period attempt without the
# pump gave 75%.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x wasapi_exclusive_pacing_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./wasapi_exclusive_pacing_test 3
echo "[pass] wasapi_exclusive_pacing_test (ASan)"
make clean && make -j$(nproc) all SANITIZER=thread
timeout 180 ./wasapi_exclusive_pacing_test 3
echo "[pass] wasapi_exclusive_pacing_test (TSan)"
- name: Build and run sink_rate_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/sink_rate
run: |
set -eu
# The frontend's sink rate estimator against a scripted driver
# on a synthetic clock: a device 120 ppm fast and 80 ppm slow are
# measured and the resampling bias converges onto them; 5000 ppm
# stops at the clamp; with rate control on the bias absorbs rate
# control's mean adjustment; a paused window, a count that went
# backwards, and a stalled window move nothing; disabled, nothing
# moves at all.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x sink_rate_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 ./sink_rate_test
echo "[pass] sink_rate_test (ASan)"
- name: Build and run pacing_test (ASan + UBSan)
shell: bash
working-directory: samples/runloop/pacing
run: |
set -eu
# The three pacing decisions the runloop makes every
# iteration, run from runloop.h so this is the shipping code
# rather than a copy of it. The gap limiter over all 128
# combinations of the pace bits by nonblocking by fastmotion -
# exactly one may engage, because engaging next to another
# source double-paces and engaging under fast-forward
# throttles it to 1x, and both look like performance bugs
# rather than pacing bugs. The frame period over the rates a
# core can report including zero and a million, bounded and
# monotonic. And the filter on the measured loop rate, which
# has to reject a stall: one 0.9 s sample used to pull a 60
# fps reading to 8 and take several frames to recover.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x pacing_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 60 ./pacing_test
echo "[pass] pacing_test (ASan)"
- name: Build the ALSA objects through configure (--enable-alsa)
shell: bash
run: |
set -eu
# The harnesses above compile audio/drivers/alsa.c with
# -DHAVE_ALSA on the command line. A distribution build takes
# HAVE_ALSA from config.h instead, and the file gates its
# alsa-lib includes on it, so build the objects that way too:
# configure with ALSA on, then compile them. That covers the
# object a distribution ships as well as the ones the
# harnesses build (libretro/RetroArch#19525). The embedded
# driver takes its define the same way and shares the file,
# so the arrangements below cover it too.
./configure --enable-alsa --disable-qt --disable-ffmpeg \
--disable-x11 --disable-wayland
grep -q '^HAVE_ALSA = 1' config.mk
make obj-unix/release/audio/drivers/alsa.o \
obj-unix/release/audio/common/alsa.o
echo "[pass] alsa-lib alone, through configure"
# The embedded driver is in the same file behind its own
# define, and configure puts HAVE_TINYALSA in config.h as
# well as on the command line - so it has the same question
# to answer as HAVE_ALSA did, and one object serves both
# drivers. The two arrangements a distribution can ask for:
# the embedded driver with no alsa-lib anywhere, and both in
# the one object.
rm -f obj-unix/release/audio/drivers/alsa.o
./configure --enable-tinyalsa --disable-alsa --disable-qt \
--disable-ffmpeg --disable-x11 --disable-wayland
grep -q '^HAVE_TINYALSA = 1' config.mk
grep -q '^HAVE_ALSA = 0' config.mk
make obj-unix/release/audio/drivers/alsa.o
echo "[pass] the embedded driver alone, through configure"
rm -f obj-unix/release/audio/drivers/alsa.o \
obj-unix/release/audio/common/alsa.o
./configure --enable-alsa --enable-tinyalsa --disable-qt \
--disable-ffmpeg --disable-x11 --disable-wayland
grep -q '^HAVE_TINYALSA = 1' config.mk
grep -q '^HAVE_ALSA = 1' config.mk
make obj-unix/release/audio/drivers/alsa.o \
obj-unix/release/audio/common/alsa.o
echo "[pass] both drivers in the one object, through configure"
- name: Build and run stretch and native SRC chain (C89, ASan + UBSan)
working-directory: samples/audio/stretch
run: |
make -j$(nproc) check SANITIZER=address,undefined
- name: Build and run WSOLA kernel tests (C89, ASan + UBSan)
working-directory: samples/audio/wsola_search
run: |
make -j$(nproc) check SANITIZER=address,undefined
- name: Build and run native speed LPF tests (C89, ASan + UBSan)
working-directory: samples/audio/speed_lpf
run: make check SANITIZER=address,undefined
- name: Build and run experimental sinc HQ tests (C89, ASan + UBSan)
working-directory: samples/audio/sinc_hq
run: |
make -j$(nproc) check SANITIZER=address,undefined
- name: Build and run sinc AVX tests (C89, ASan + UBSan)
working-directory: samples/audio/sinc_hq
run: |
if grep -qw avx /proc/cpuinfo; then
make clean
make -j$(nproc) check CC="gcc -mavx" SANITIZER=address,undefined
else
echo "AVX unavailable on this runner; scalar/SSE tests ran above."
fi
- name: Build and run sinc kernel dispatch test (C89, ASan + UBSan)
working-directory: samples/audio/sinc_dispatch
run: |
# A baseline build carries the AVX and AVX-512 kernels through
# per-function targets and runs each one the runner supports
# against the scalar kernel; the -mavx build must agree.
make -j$(nproc) check SANITIZER=address,undefined
if grep -qw avx /proc/cpuinfo; then
make clean
make -j$(nproc) check CC="gcc -mavx" SANITIZER=address,undefined
fi
- name: Build and run sinc fast-forward test (C89, ASan + UBSan, MinGW)
working-directory: samples/audio/sinc_fastforward
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# Live ratios under the table's design are decimated first:
# rejection as a table built for that ratio, the nominal path
# untouched, the output bound, crossfaded stage changes.
make -j$(nproc) check SANITIZER=address,undefined
if grep -qw avx /proc/cpuinfo; then
make clean
make -j$(nproc) check CC="gcc -mavx" SANITIZER=address,undefined
fi
make clean
WINEDEBUG=-all timeout 300 make check CC=x86_64-w64-mingw32-gcc \
RUN="wine ./sinc_fastforward_test.exe"
- name: Build and run sinc sibling test (C89, ASan + UBSan, MinGW)
working-directory: samples/audio/sinc_sibling
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# Siblings share their parent's tables: a fresh stream either
# way, own state only, either free order.
make -j$(nproc) check SANITIZER=address,undefined
make clean
WINEDEBUG=-all timeout 300 make check CC=x86_64-w64-mingw32-gcc \
RUN="wine ./sinc_sibling_test.exe"
- name: Build and run mic_handshake_test (ASan + UBSan)
working-directory: samples/audio/mic_handshake
run: |
# The shipping capture worker against a clocked fake device, and
# only the resampler the microphone flush uses being built.
make clean
make -j$(nproc) SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 300 ./mic_handshake_test 1
- name: Build and run pipeline_format_test (ASan + UBSan)
working-directory: samples/audio/pipeline_format
run: |
# Real init and one batch through each format and transport;
# the integer front pair exists only behind its hint.
make -j$(nproc) check SANITIZER=address,undefined
- name: Build and run pipeline_rate_change_test (ASan + UBSan, mingw)
working-directory: samples/audio/pipeline_rate_change
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# A core rate change taken by the running driver: one device
# open, the tone at its pitch on both sides, the ring grown
# where the new rate needs it, time stretch refused.
make -j$(nproc) check SANITIZER=address,undefined
make clean
WINEDEBUG=-all timeout 300 make check CC=x86_64-w64-mingw32-gcc \
LDFLAGS="-lm -static" RUN="wine ./pipeline_rate_change_test.exe"
- name: Build and run unity_identity_test (plain, ASan + UBSan)
working-directory: samples/audio/unity_identity
run: |
# The unity passthrough end to end: an s16 batch at a pinned
# ratio of 1.0 reaches an s16 device with zero conversions,
# counted through linker wraps; the fallbacks still convert.
make -j$(nproc) check
make clean
make -j$(nproc) check SANITIZER=address,undefined
- name: Build and run lend_write_test (plain, ASan + UBSan)
working-directory: samples/audio/lend_write
run: |
# The driver lend pair: the pipeline produces its output in
# the driver's own buffer, bit-identical to the written
# stream, with write() never called while a lend is live.
make -j$(nproc) check
make clean
make -j$(nproc) check SANITIZER=address,undefined
- name: Build and run extra-channel capacity test (ASan + UBSan)
working-directory: samples/audio/extra_capacity
run: |
make -j$(nproc) check SANITIZER=address,undefined
make clean
make -j$(nproc) check SIMD_MASK=RESAMPLER_SIMD_SSE SANITIZER=address,undefined
- name: Build and run the pipeline fill cap lane (plain)
shell: bash
working-directory: samples/audio/pipeline_clocked
run: |
set -eu
# The threaded pipeline against a device that drains on a
# clock, with Audio Sync the only thing pacing the core: the
# ring sits full and every publish waits behind it. With
# audio_pipeline_fill_cap the queue has to come down to the
# device's buffer and no more than a publish past it, where a
# blocking inline write leaves it, without the device running
# dry. With Audio Sync off it has to take the queue down too,
# with a core up to a frame late not reaching the device as
# silence. And display-paced with Audio Sync on, rate control
# has to hold the queue where the frame-synchronous path
# holds it. With audio the only clock, rate control has to
# leave the ratio alone, inline and threaded, and still hold
# the fill when a present blocks behind the runloop's back.
# Plain, not sanitized: it is the clock that is measured,
# and a sanitizer's slowdown is a host that starves the
# consumer whatever the cap does. Underruns with the cap are
# retried, since a host that takes the consumer off its core
# shows the same with the cap off; only a cap that never runs
# clean fails.
make clean && make -j$(nproc)
test -x pipeline_clocked_test
PACED_ONLY=1 timeout 600 ./pipeline_clocked_test 3
echo "[pass] pipeline_clocked_test (fill cap)"
- name: Build and run resampler option tests (C89, ASan + UBSan)
working-directory: samples/audio/resampler_options
run: |
make -j$(nproc) check SANITIZER=address,undefined
- name: Build and run CoreMIDI input queue test (ASan + UBSan, TSan)
working-directory: samples/midi/coremidi_queue
run: |
# midi/drivers/coremidi_queue.h is the macOS driver's lock-free
# input queue; it builds and runs anywhere.
make check SANITIZER=address,undefined
make clean
make check SANITIZER=thread
- name: ccache statistics
if: ${{ !cancelled() }}
continue-on-error: true
run: ccache -sv
- name: Save ccache
# Only master's pushes keep a cache: pull requests restore
# master's, so one push does not evict the next one's.
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}