diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16b43b1..1de9290 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,7 +50,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libwayland-dev libwayland-egl1-mesa libxkbcommon-dev libegl-dev libgl-dev + sudo apt-get install -y libwayland-dev libwayland-egl1-mesa libxkbcommon-dev libegl-dev libgl-dev libasound2-dev - name: Build run: make @@ -78,7 +78,7 @@ jobs: - name: Install X11 dependencies run: | sudo apt-get update - sudo apt-get install -y libx11-dev libegl-dev libgl-dev + sudo apt-get install -y libx11-dev libegl-dev libgl-dev libasound2-dev - name: Build run: make diff --git a/Makefile b/Makefile index 293dbca..1905b01 100644 --- a/Makefile +++ b/Makefile @@ -35,13 +35,14 @@ TEST_MSG_BIN = /tmp/test_messages TEST_TIMER_BIN = /tmp/test_timer TEST_NET_BIN = /tmp/test_net TEST_FS_BIN = /tmp/test_filesystem +TEST_AUDIO_BIN = /tmp/test_audio ifdef EMSCRIPTEN CC = emcc CFLAGS = -Wall -Wextra -fPIC -I. LDFLAGS = -sSIDE_MODULE=1 -sUSE_WEBGL2=1 -sMIN_WEBGL_VERSION=1 -sMAX_WEBGL_VERSION=2 LIB_EXT = wasm - FIND_SOURCES = find webgl -name "*.c" + FIND_SOURCES = ( find webgl -name "*.c"; find audio -name "*.c"; ) LANG = c else ifeq ($(PLATFORM_OS),ios) IOS_MIN ?= 16.0 @@ -50,15 +51,15 @@ else ifeq ($(PLATFORM_OS),ios) MIN_FLAG = $(if $(filter iphoneos,$(SDK)),-miphoneos-version-min,-mios-simulator-version-min)=$(IOS_MIN) CFLAGS = -Wall -Wextra -I. -arch $(ARCH) -isysroot "$(SDK_PATH)" $(MIN_FLAG) -fobjc-arc -Wno-deprecated-declarations LIB_EXT = a - FIND_SOURCES = ( find ios -name '*.m'; find unix -name '*.c'; ) + FIND_SOURCES = ( find ios -name '*.m'; find unix -name '*.c'; find audio -name '*.c'; ) LANG = objective-c HAS_WINDOWING := 1 else ifeq ($(PLATFORM_OS),darwin) CC = clang CFLAGS = -Wall -Wextra -fPIC -I. -DGL_SILENCE_DEPRECATION -Wno-deprecated-declarations - LDFLAGS = -dynamiclib -framework AppKit -framework Cocoa -framework OpenGL -framework IOSurface -framework Security -framework CoreFoundation -install_name @rpath/$(LIBNAME) + LDFLAGS = -dynamiclib -framework AppKit -framework Cocoa -framework OpenGL -framework IOSurface -framework Security -framework CoreFoundation -framework AudioToolbox -install_name @rpath/$(LIBNAME) LIB_EXT = dylib - FIND_SOURCES = ( find macos -name "*.m"; find unix -name "*.c"; ) + FIND_SOURCES = ( find macos -name "*.m"; find unix -name "*.c"; find audio -name "*.c"; ) LANG = objective-c TEST_LDFLAGS = -L$(abspath $(OUTDIR)) -lplatform -rpath $(abspath $(OUTDIR)) HAS_WINDOWING := 1 @@ -72,7 +73,7 @@ else ifeq ($(PLATFORM_OS),linux) ifneq ($(WAYLAND_LIBS),) CFLAGS += $(shell pkg-config --cflags wayland-client wayland-egl xkbcommon egl gl 2>/dev/null) LDFLAGS += $(WAYLAND_LIBS) - FIND_SOURCES = ( find wayland -name "*.c"; find unix -name "*.c"; ) + FIND_SOURCES = ( find wayland -name "*.c"; find unix -name "*.c"; find audio -name "*.c"; ) HAS_WINDOWING := 1 else # Try to detect X11 libraries as fallback @@ -80,14 +81,20 @@ else ifeq ($(PLATFORM_OS),linux) ifneq ($(X11_LIBS),) CFLAGS += $(shell pkg-config --cflags x11 egl gl 2>/dev/null) LDFLAGS += $(X11_LIBS) - FIND_SOURCES = ( find x11 -name "*.c"; find unix -name "*.c"; ) + FIND_SOURCES = ( find x11 -name "*.c"; find unix -name "*.c"; find audio -name "*.c"; ) HAS_WINDOWING := 1 else # Fallback to unix-only (no windowing support) - FIND_SOURCES = find unix -name "*.c" + FIND_SOURCES = ( find unix -name "*.c"; find audio -name "*.c"; ) HAS_WINDOWING := 0 endif endif + # Optional ALSA playback. Without it, WAV load/mix still build and the device open fails. + ALSA_LIBS := $(shell pkg-config --libs alsa 2>/dev/null) + ifneq ($(ALSA_LIBS),) + CFLAGS += $(shell pkg-config --cflags alsa 2>/dev/null) -DHAVE_ALSA + LDFLAGS += $(ALSA_LIBS) + endif # Optional OpenSSL support for TLS on Linux OPENSSL_LIBS := $(shell pkg-config --libs openssl 2>/dev/null) ifneq ($(OPENSSL_LIBS),) @@ -102,9 +109,9 @@ else ifeq ($(PLATFORM_OS),windows) LDFLAGS = -shared \ -Wl,--out-implib,$(OUTDIR)/libplatform.dll.a \ -lopengl32 -lgdi32 -luser32 -lcomdlg32 \ - -lole32 -lshell32 -ladvapi32 -lws2_32 -lxinput1_4 -lsecur32 + -lole32 -lshell32 -ladvapi32 -lws2_32 -lxinput1_4 -lsecur32 -lwinmm LIB_EXT = dll - FIND_SOURCES = find windows -name "*.c" + FIND_SOURCES = ( find windows -name "*.c"; find audio -name "*.c"; ) LANG = c # Put the test binary next to the DLL so Windows finds it at runtime TEST_SRC = $(OUTDIR)/test_platform_api_tmp.c @@ -114,6 +121,7 @@ else ifeq ($(PLATFORM_OS),windows) TEST_TIMER_BIN = $(OUTDIR)/test_timer.exe TEST_NET_BIN = $(OUTDIR)/test_net.exe TEST_FS_BIN = $(OUTDIR)/test_filesystem.exe + TEST_AUDIO_BIN = $(OUTDIR)/test_audio.exe HAS_WINDOWING := 1 else $(error Unsupported OS: $(PLATFORM_UNAME_S)) @@ -188,6 +196,10 @@ endif @$(TEST_FS_BIN) @rm -f $(TEST_FS_BIN) @echo "Filesystem tests passed." + @$(CC) -I. tests/test_audio.c $(TEST_LDFLAGS) -o $(TEST_AUDIO_BIN) + @$(TEST_AUDIO_BIN) + @rm -f $(TEST_AUDIO_BIN) + @echo "Audio tests passed." endif clean: diff --git a/README.md b/README.md index c0496fc..5e5e3db 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,23 @@ A lightweight, cross-platform C library providing a unified API for window manag - **Event System**: Unified event handling for mouse, keyboard, and window events - **Input Processing**: Comprehensive keyboard and mouse input with modifier support - **Networking**: Portable TCP/UDP sockets, DNS resolution, TLS (Schannel/Secure Transport/OpenSSL), and non-blocking I/O +- **Audio**: SDL-style WAV playback (PCM, float, A-law, mu-law) with a queued or callback device and a software mixer + +Load a WAV and queue it to the default device. Devices start paused, as in SDL. ADPCM is not supported. + +```c +AXaudiospec spec; +uint8_t *samples = NULL; +uint32_t nbytes = 0; +if (axLoadWAV("jump.wav", &spec, &samples, &nbytes)) { + int dev = axAudioOpen(&spec, NULL); + if (dev) { + axAudioQueue(dev, samples, nbytes); + axAudioPause(dev, FALSE); + } + axFreeWAV(samples); +} +``` - **File Dialogs**: Native file open/save dialogs - **System Integration**: Theme detection, directory paths, timing utilities - **Minimal Dependencies**: Simple C API with no bloat diff --git a/audio/audio_backend.c b/audio/audio_backend.c new file mode 100644 index 0000000..79fdf11 --- /dev/null +++ b/audio/audio_backend.c @@ -0,0 +1,301 @@ +/** + * @file audio_backend.c + * @brief Platform playback for the shared S16 device. + * + * macOS and iOS use AudioQueue, Windows uses waveOut, Linux uses ALSA when + * the headers were available at build time. Other targets compile a stub + * that reports no device so WAV loading and mixing still work. + */ +#include "audio_backend.h" + +#include + +#if defined(__APPLE__) +#include +#include + +#define AX_HW_BUFFERS 3 + +static AudioQueueRef g_queue = NULL; +static AudioQueueBufferRef g_bufs[AX_HW_BUFFERS]; +static int g_hw_paused = 1; + +static void apple_fill(void *userdata, AudioQueueRef queue, AudioQueueBufferRef buf) +{ + (void)userdata; + ax_audio_device_fill(buf->mAudioData, (int)buf->mAudioDataByteSize); + AudioQueueEnqueueBuffer(queue, buf, 0, NULL); +} + +bool_t +ax_audio_hw_open(int freq, int channels, int frames) +{ + AudioStreamBasicDescription fmt; + int i; + UInt32 bytes; + if (g_queue) return TRUE; + if (frames < 256) frames = 256; + memset(&fmt, 0, sizeof(fmt)); + fmt.mSampleRate = freq; + fmt.mFormatID = kAudioFormatLinearPCM; + fmt.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked; + fmt.mChannelsPerFrame = (UInt32)channels; + fmt.mBitsPerChannel = 16; + fmt.mFramesPerPacket = 1; + fmt.mBytesPerFrame = (UInt32)(channels * 2); + fmt.mBytesPerPacket = fmt.mBytesPerFrame; + if (AudioQueueNewOutput(&fmt, apple_fill, NULL, NULL, NULL, 0, &g_queue) != noErr) + return FALSE; + bytes = (UInt32)(frames * channels * 2); + for (i = 0; i < AX_HW_BUFFERS; i++) { + if (AudioQueueAllocateBuffer(g_queue, bytes, &g_bufs[i]) != noErr) { + ax_audio_hw_close(); + return FALSE; + } + g_bufs[i]->mAudioDataByteSize = bytes; + memset(g_bufs[i]->mAudioData, 0, bytes); + AudioQueueEnqueueBuffer(g_queue, g_bufs[i], 0, NULL); + } + g_hw_paused = 1; + return TRUE; +} + +void +ax_audio_hw_close(void) +{ + int i; + if (!g_queue) return; + AudioQueueStop(g_queue, true); + for (i = 0; i < AX_HW_BUFFERS; i++) g_bufs[i] = NULL; + AudioQueueDispose(g_queue, true); + g_queue = NULL; + g_hw_paused = 1; +} + +void +ax_audio_hw_pause(bool_t pause) +{ + if (!g_queue) return; + if (pause) { + AudioQueuePause(g_queue); + g_hw_paused = 1; + } else if (g_hw_paused) { + AudioQueueStart(g_queue, NULL); + g_hw_paused = 0; + } +} + +#elif defined(_WIN32) || defined(__MINGW32__) +#define WIN32_LEAN_AND_MEAN +#include +#include + +#define AX_HW_BUFFERS 3 + +static HWAVEOUT g_wave = NULL; +static WAVEHDR g_hdrs[AX_HW_BUFFERS]; +static int g_hw_paused = 1; + +static void CALLBACK wave_done(HWAVEOUT wave, UINT msg, DWORD_PTR user, DWORD_PTR p1, DWORD_PTR p2) +{ + WAVEHDR *hdr = (WAVEHDR *)p1; + (void)user; + (void)p2; + if (msg != WOM_DONE || !hdr) return; + ax_audio_device_fill(hdr->lpData, (int)hdr->dwBufferLength); + waveOutWrite(wave, hdr, sizeof(*hdr)); +} + +bool_t +ax_audio_hw_open(int freq, int channels, int frames) +{ + WAVEFORMATEX wfx; + int i; + MMRESULT rc; + if (g_wave) return TRUE; + if (frames < 256) frames = 256; + memset(&wfx, 0, sizeof(wfx)); + wfx.wFormatTag = WAVE_FORMAT_PCM; + wfx.nChannels = (WORD)channels; + wfx.nSamplesPerSec = (DWORD)freq; + wfx.wBitsPerSample = 16; + wfx.nBlockAlign = (WORD)(channels * 2); + wfx.nAvgBytesPerSec = (DWORD)(freq * wfx.nBlockAlign); + rc = waveOutOpen(&g_wave, WAVE_MAPPER, &wfx, (DWORD_PTR)wave_done, 0, CALLBACK_FUNCTION); + if (rc != MMSYSERR_NOERROR) { + g_wave = NULL; + return FALSE; + } + for (i = 0; i < AX_HW_BUFFERS; i++) { + memset(&g_hdrs[i], 0, sizeof(g_hdrs[i])); + g_hdrs[i].dwBufferLength = (DWORD)(frames * channels * 2); + g_hdrs[i].lpData = (LPSTR)malloc(g_hdrs[i].dwBufferLength); + if (!g_hdrs[i].lpData) { + ax_audio_hw_close(); + return FALSE; + } + memset(g_hdrs[i].lpData, 0, g_hdrs[i].dwBufferLength); + waveOutPrepareHeader(g_wave, &g_hdrs[i], sizeof(g_hdrs[i])); + waveOutWrite(g_wave, &g_hdrs[i], sizeof(g_hdrs[i])); + } + waveOutPause(g_wave); + g_hw_paused = 1; + return TRUE; +} + +void +ax_audio_hw_close(void) +{ + int i; + if (g_wave) { + waveOutReset(g_wave); + for (i = 0; i < AX_HW_BUFFERS; i++) { + if (g_hdrs[i].lpData) { + waveOutUnprepareHeader(g_wave, &g_hdrs[i], sizeof(g_hdrs[i])); + free(g_hdrs[i].lpData); + g_hdrs[i].lpData = NULL; + } + } + waveOutClose(g_wave); + g_wave = NULL; + } else { + for (i = 0; i < AX_HW_BUFFERS; i++) { + free(g_hdrs[i].lpData); + g_hdrs[i].lpData = NULL; + } + } + g_hw_paused = 1; +} + +void +ax_audio_hw_pause(bool_t pause) +{ + if (!g_wave) return; + if (pause) { + waveOutPause(g_wave); + g_hw_paused = 1; + } else { + waveOutRestart(g_wave); + g_hw_paused = 0; + } +} + +#elif defined(__linux__) && defined(HAVE_ALSA) +#include +#include +#include + +static snd_pcm_t *g_pcm = NULL; +static pthread_t g_thread; +static int g_thread_started = 0; +static volatile int g_run = 0; +static int g_period_bytes = 0; +static int g_period_frames = 0; +static int16_t *g_buf = NULL; + +static void *alsa_thread(void *arg) +{ + (void)arg; + while (g_run) { + snd_pcm_sframes_t wrote; + ax_audio_device_fill(g_buf, g_period_bytes); + wrote = snd_pcm_writei(g_pcm, g_buf, (snd_pcm_uframes_t)g_period_frames); + if (wrote < 0) + snd_pcm_recover(g_pcm, (int)wrote, 1); + } + return NULL; +} + +bool_t +ax_audio_hw_open(int freq, int channels, int frames) +{ + snd_pcm_hw_params_t *params = NULL; + int err, dir = 0; + unsigned int rate = (unsigned int)freq; + snd_pcm_uframes_t period = (snd_pcm_uframes_t)frames; + if (g_pcm) return TRUE; + if (frames < 256) frames = 256; + err = snd_pcm_open(&g_pcm, "default", SND_PCM_STREAM_PLAYBACK, 0); + if (err < 0) return FALSE; + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_hw_params_any(g_pcm, params); + snd_pcm_hw_params_set_access(g_pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_hw_params_set_format(g_pcm, params, SND_PCM_FORMAT_S16); + snd_pcm_hw_params_set_channels(g_pcm, params, (unsigned int)channels); + snd_pcm_hw_params_set_rate_near(g_pcm, params, &rate, &dir); + snd_pcm_hw_params_set_period_size_near(g_pcm, params, &period, &dir); + if (snd_pcm_hw_params(g_pcm, params) < 0) { + snd_pcm_close(g_pcm); + g_pcm = NULL; + return FALSE; + } + g_period_frames = (int)period; + g_period_bytes = (int)(period * (snd_pcm_uframes_t)channels * 2); + g_buf = (int16_t *)calloc(1, (size_t)g_period_bytes); + if (!g_buf) { + snd_pcm_close(g_pcm); + g_pcm = NULL; + return FALSE; + } + g_run = 1; + if (pthread_create(&g_thread, NULL, alsa_thread, NULL) != 0) { + free(g_buf); + g_buf = NULL; + snd_pcm_close(g_pcm); + g_pcm = NULL; + g_run = 0; + return FALSE; + } + g_thread_started = 1; + return TRUE; +} + +void +ax_audio_hw_close(void) +{ + g_run = 0; + if (g_pcm) + snd_pcm_drop(g_pcm); /* unblock a thread stuck in snd_pcm_writei */ + if (g_thread_started) { + pthread_join(g_thread, NULL); + g_thread_started = 0; + } + free(g_buf); + g_buf = NULL; + if (g_pcm) { + snd_pcm_close(g_pcm); + g_pcm = NULL; + } +} + +void +ax_audio_hw_pause(bool_t pause) +{ + /* Paused devices are fed silence by ax_audio_device_fill. snd_pcm_pause + * is not implemented by every ALSA plugin, so it is not required here. */ + (void)pause; +} + +#else + +bool_t +ax_audio_hw_open(int freq, int channels, int frames) +{ + (void)freq; + (void)channels; + (void)frames; + return FALSE; +} + +void +ax_audio_hw_close(void) +{ +} + +void +ax_audio_hw_pause(bool_t pause) +{ + (void)pause; +} + +#endif diff --git a/audio/audio_backend.h b/audio/audio_backend.h new file mode 100644 index 0000000..4e27f9a --- /dev/null +++ b/audio/audio_backend.h @@ -0,0 +1,21 @@ +/** + * @file audio_backend.h + * @brief Hardware playback hooks used by the shared WAV device. + * + * The shared device always feeds the backend signed 16-bit interleaved + * frames in host byte order. Backends open the default output device and + * call ax_audio_device_fill() whenever the hardware needs more samples. + */ +#ifndef AUDIO_BACKEND_H +#define AUDIO_BACKEND_H + +#include +#include "../platform.h" + +void ax_audio_device_fill(void *dst, int nbytes); + +bool_t ax_audio_hw_open(int freq, int channels, int frames); +void ax_audio_hw_close(void); +void ax_audio_hw_pause(bool_t pause); + +#endif diff --git a/audio/audio_wav.c b/audio/audio_wav.c new file mode 100644 index 0000000..a247611 --- /dev/null +++ b/audio/audio_wav.c @@ -0,0 +1,627 @@ +/** + * @file audio_wav.c + * @brief WAV loader and SDL-style playback device. + * + * Loads RIFF WAVE PCM (8/16/24/32-bit), IEEE float 32, A-law, and mu-law. + * Playback is queued or callback-driven, then converted to S16 for the + * platform backend. Microsoft and IMA ADPCM are intentionally unsupported. + */ +#include "audio_backend.h" + +#include +#include +#include + +#define AX_AUDIO_QUEUE_MAX (4u * 1024u * 1024u) +#define AX_AUDIO_DEV_ID 1 + +static char g_audio_error[256]; +static int g_audio_ready = 0; + +typedef struct { + int open; + int paused; + int hw_open; + AXaudiospec spec; + axmutex_t lock; + uint8_t *queue; + uint32_t cap; + uint32_t len; + uint32_t rd; +} ax_audio_dev; + +static ax_audio_dev g_dev; + +static void audio_set_error(char const *msg) +{ + if (!msg) msg = "unknown audio error"; + snprintf(g_audio_error, sizeof(g_audio_error), "%s", msg); +} + +static uint16_t rd_le16(uint8_t const *p) +{ + return (uint16_t)(p[0] | (p[1] << 8)); +} + +static uint32_t rd_le32(uint8_t const *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static int format_bytes(AXaudioformat format) +{ + switch (format) { + case AX_AUDIO_U8: return 1; + case AX_AUDIO_S16: return 2; + case AX_AUDIO_S32: return 4; + case AX_AUDIO_F32: return 4; + default: return 0; + } +} + +static int valid_spec(AXaudiospec const *spec) +{ + if (!spec) return 0; + if (spec->freq < 8000 || spec->freq > 192000) return 0; + if (spec->channels < 1 || spec->channels > 2) return 0; + return format_bytes(spec->format) != 0; +} + +/* ITU-T G.711. Tables keep the decoder free of float. */ +static int16_t mulaw_decode(uint8_t sample) +{ + int sign, exponent, mantissa, value; + sample = (uint8_t)~sample; + sign = sample & 0x80; + exponent = (sample >> 4) & 0x07; + mantissa = sample & 0x0f; + value = ((mantissa << 3) + 0x84) << exponent; + value -= 0x84; + return (int16_t)(sign ? -value : value); +} + +static int16_t alaw_decode(uint8_t sample) +{ + int sign, exponent, mantissa, value; + sample ^= 0x55; + sign = sample & 0x80; + exponent = (sample >> 4) & 0x07; + mantissa = sample & 0x0f; + if (exponent == 0) + value = (mantissa << 4) + 8; + else + value = ((mantissa << 4) + 0x108) << (exponent - 1); + return (int16_t)(sign ? -value : value); +} + +static bool_t decode_wav(uint8_t const *file, uint32_t size, + AXaudiospec *spec, uint8_t **audio_buf, uint32_t *audio_len) +{ + uint32_t off = 12; + int got_fmt = 0; + uint16_t tag = 0, channels = 0, bits = 0, block_align = 0; + uint32_t rate = 0; + uint8_t const *data = NULL; + uint32_t data_len = 0; + uint32_t frames, out_len, i, s; + int out_bps; + AXaudioformat format; + uint8_t *out; + + if (!file || size < 12 || !spec || !audio_buf || !audio_len) { + audio_set_error("invalid WAV arguments"); + return FALSE; + } + if (memcmp(file, "RIFF", 4) != 0 || memcmp(file + 8, "WAVE", 4) != 0) { + audio_set_error("not a RIFF WAVE file"); + return FALSE; + } + + while (off + 8 <= size) { + uint32_t chunk = rd_le32(file + off + 4); + uint8_t const *body = file + off + 8; + uint32_t avail = size - (off + 8); + if (chunk > avail) chunk = avail; /* SDL-style lenient truncation */ + if (memcmp(file + off, "fmt ", 4) == 0 && chunk >= 16) { + tag = rd_le16(body); + channels = rd_le16(body + 2); + rate = rd_le32(body + 4); + block_align = rd_le16(body + 12); + bits = rd_le16(body + 14); + if (tag == 0xFFFE && chunk >= 40) + tag = rd_le16(body + 24); /* WAVEFORMATEXTENSIBLE subformat tag */ + got_fmt = 1; + } else if (memcmp(file + off, "data", 4) == 0 && !data) { + data = body; + data_len = chunk; + } + off += 8 + chunk + (chunk & 1); + } + + if (!got_fmt || !data || channels < 1 || channels > 2 || rate < 8000 || rate > 192000) { + audio_set_error("WAV is missing fmt/data or has an unsupported layout"); + return FALSE; + } + + if (tag == 1 && bits == 8) format = AX_AUDIO_U8; + else if (tag == 1 && bits == 16) format = AX_AUDIO_S16; + else if (tag == 1 && (bits == 24 || bits == 32)) format = AX_AUDIO_S32; + else if (tag == 3 && bits == 32) format = AX_AUDIO_F32; + else if (tag == 6 || tag == 7) format = AX_AUDIO_S16; /* A-law / mu-law */ + else { + audio_set_error("unsupported WAV encoding (PCM, float, A-law, mu-law only)"); + return FALSE; + } + + if (tag == 1 || tag == 3) { + uint32_t expect = (uint32_t)channels * (bits / 8); + if (block_align && block_align != expect) { + audio_set_error("WAV block align does not match bit depth"); + return FALSE; + } + block_align = (uint16_t)expect; + } else if (block_align == 0) { + block_align = channels; + } + + frames = data_len / block_align; + if (frames == 0) { + audio_set_error("WAV data chunk is empty"); + return FALSE; + } + out_bps = format_bytes(format); + out_len = frames * channels * (uint32_t)out_bps; + out = (uint8_t *)malloc(out_len ? out_len : 1); + if (!out) { + audio_set_error("out of memory loading WAV"); + return FALSE; + } + + for (i = 0; i < frames; i++) { + uint8_t const *frame = data + i * block_align; + for (s = 0; s < channels; s++) { + uint8_t const *p = frame + s * (tag == 6 || tag == 7 ? 1 : (bits / 8)); + uint32_t o = (i * channels + s) * (uint32_t)out_bps; + if (tag == 6) { + int16_t v = alaw_decode(*p); + memcpy(out + o, &v, 2); + } else if (tag == 7) { + int16_t v = mulaw_decode(*p); + memcpy(out + o, &v, 2); + } else if (bits == 8) { + out[o] = *p; + } else if (bits == 16) { + int16_t v = (int16_t)rd_le16(p); + memcpy(out + o, &v, 2); + } else if (bits == 24) { + int32_t v = (int32_t)(p[0] | (p[1] << 8) | (p[2] << 16)); + if (v & 0x800000) v |= ~0xFFFFFF; + v <<= 8; + memcpy(out + o, &v, 4); + } else { + memcpy(out + o, p, 4); /* 32-bit PCM or IEEE float, little-endian host */ + } + } + } + + memset(spec, 0, sizeof(*spec)); + spec->freq = (int)rate; + spec->format = format; + spec->channels = (uint8_t)channels; + spec->samples = 1024; + *audio_buf = out; + *audio_len = out_len; + return TRUE; +} + +bool_t +axAudioInit(void) +{ + if (g_audio_ready) return TRUE; + g_dev.lock = axMutexCreate(); + if (!g_dev.lock) { + audio_set_error("failed to create audio mutex"); + return FALSE; + } + g_audio_ready = 1; + g_audio_error[0] = '\0'; + return TRUE; +} + +void +axAudioShutdown(void) +{ + if (!g_audio_ready) return; + axAudioClose(AX_AUDIO_DEV_ID); + axMutexDestroy(g_dev.lock); + g_dev.lock = NULL; + g_audio_ready = 0; +} + +char const * +axAudioGetError(void) +{ + return g_audio_error[0] ? g_audio_error : NULL; +} + +int +axAudioOpen(AXaudiospec const *desired, AXaudiospec *obtained) +{ + AXaudiospec spec; + int frames; + + if (!axAudioInit()) return 0; + axMutexLock(g_dev.lock); + if (g_dev.open) { + axMutexUnlock(g_dev.lock); + audio_set_error("audio device already open"); + return 0; + } + memset(&spec, 0, sizeof(spec)); + spec.freq = 44100; + spec.format = AX_AUDIO_S16; + spec.channels = 2; + spec.samples = 1024; + if (desired) { + spec.freq = desired->freq ? desired->freq : spec.freq; + spec.format = desired->format ? desired->format : spec.format; + spec.channels = desired->channels ? desired->channels : spec.channels; + spec.samples = desired->samples ? desired->samples : spec.samples; + spec.callback = desired->callback; + spec.userdata = desired->userdata; + } + if (!valid_spec(&spec)) { + axMutexUnlock(g_dev.lock); + audio_set_error("unsupported audio spec"); + return 0; + } + frames = spec.samples > 0 ? spec.samples : 1024; + g_dev.spec = spec; + g_dev.paused = 1; + g_dev.len = 0; + g_dev.rd = 0; + g_dev.cap = 0; + g_dev.queue = NULL; + g_dev.open = 1; + axMutexUnlock(g_dev.lock); + + if (!ax_audio_hw_open(spec.freq, spec.channels, frames)) { + axMutexLock(g_dev.lock); + g_dev.open = 0; + axMutexUnlock(g_dev.lock); + audio_set_error("failed to open audio device"); + return 0; + } + g_dev.hw_open = 1; + ax_audio_hw_pause(TRUE); + if (obtained) { + *obtained = spec; + obtained->callback = NULL; + obtained->userdata = NULL; + } + g_audio_error[0] = '\0'; + return AX_AUDIO_DEV_ID; +} + +void +axAudioClose(int dev) +{ + uint8_t *queue = NULL; + if (!g_audio_ready || dev != AX_AUDIO_DEV_ID) return; + if (g_dev.hw_open) { + ax_audio_hw_close(); + g_dev.hw_open = 0; + } + axMutexLock(g_dev.lock); + queue = g_dev.queue; + g_dev.queue = NULL; + g_dev.cap = g_dev.len = g_dev.rd = 0; + g_dev.open = 0; + g_dev.paused = 1; + axMutexUnlock(g_dev.lock); + free(queue); +} + +void +axAudioPause(int dev, bool_t pause) +{ + if (!g_audio_ready || dev != AX_AUDIO_DEV_ID) return; + axMutexLock(g_dev.lock); + if (g_dev.open) g_dev.paused = pause ? 1 : 0; + axMutexUnlock(g_dev.lock); + if (g_dev.hw_open) ax_audio_hw_pause(pause ? TRUE : FALSE); +} + +void +axAudioLock(int dev) +{ + if (g_audio_ready && dev == AX_AUDIO_DEV_ID && g_dev.lock) + axMutexLock(g_dev.lock); +} + +void +axAudioUnlock(int dev) +{ + if (g_audio_ready && dev == AX_AUDIO_DEV_ID && g_dev.lock) + axMutexUnlock(g_dev.lock); +} + +bool_t +axAudioQueue(int dev, void const *data, uint32_t len) +{ + uint32_t space, first; + if (!data && len) return FALSE; + if (!g_audio_ready || dev != AX_AUDIO_DEV_ID) return FALSE; + axMutexLock(g_dev.lock); + if (!g_dev.open || g_dev.spec.callback) { + axMutexUnlock(g_dev.lock); + audio_set_error("queue requires an open callback-less device"); + return FALSE; + } + if (g_dev.len + len > AX_AUDIO_QUEUE_MAX) { + axMutexUnlock(g_dev.lock); + audio_set_error("audio queue is full"); + return FALSE; + } + if (g_dev.cap < g_dev.len + len) { + uint32_t cap = g_dev.cap ? g_dev.cap : 4096; + uint8_t *grown; + while (cap < g_dev.len + len) { + if (cap > AX_AUDIO_QUEUE_MAX / 2) { cap = AX_AUDIO_QUEUE_MAX; break; } + cap *= 2; + } + grown = (uint8_t *)realloc(g_dev.queue, cap); + if (!grown) { + axMutexUnlock(g_dev.lock); + audio_set_error("out of memory queueing audio"); + return FALSE; + } + if (g_dev.len && g_dev.rd) { + /* Compact the live bytes to the front after growth. */ + memmove(grown, grown + g_dev.rd, g_dev.len); + g_dev.rd = 0; + } + g_dev.queue = grown; + g_dev.cap = cap; + } + if (g_dev.rd + g_dev.len > g_dev.cap) { + memmove(g_dev.queue, g_dev.queue + g_dev.rd, g_dev.len); + g_dev.rd = 0; + } + space = g_dev.cap - (g_dev.rd + g_dev.len); + first = len < space ? len : space; + memcpy(g_dev.queue + g_dev.rd + g_dev.len, data, first); + if (first < len) + memcpy(g_dev.queue, (uint8_t const *)data + first, len - first); + g_dev.len += len; + axMutexUnlock(g_dev.lock); + return TRUE; +} + +uint32_t +axAudioQueued(int dev) +{ + uint32_t n = 0; + if (!g_audio_ready || dev != AX_AUDIO_DEV_ID) return 0; + axMutexLock(g_dev.lock); + n = g_dev.len; + axMutexUnlock(g_dev.lock); + return n; +} + +void +axAudioClearQueue(int dev) +{ + if (!g_audio_ready || dev != AX_AUDIO_DEV_ID) return; + axMutexLock(g_dev.lock); + g_dev.len = 0; + g_dev.rd = 0; + axMutexUnlock(g_dev.lock); +} + +static void take_user_bytes(uint8_t *dst, int nbytes) +{ + int got = 0; + if (g_dev.spec.callback) { + axMutexUnlock(g_dev.lock); + g_dev.spec.callback(g_dev.spec.userdata, dst, nbytes); + axMutexLock(g_dev.lock); + return; + } + if (g_dev.paused || g_dev.len == 0) { + memset(dst, 0, (size_t)nbytes); + return; + } + if ((uint32_t)nbytes > g_dev.len) got = (int)g_dev.len; + else got = nbytes; + if (g_dev.rd + g_dev.len > g_dev.cap) { + memmove(g_dev.queue, g_dev.queue + g_dev.rd, g_dev.len); + g_dev.rd = 0; + } + memcpy(dst, g_dev.queue + g_dev.rd, (size_t)got); + g_dev.rd += (uint32_t)got; + g_dev.len -= (uint32_t)got; + if (g_dev.len == 0) g_dev.rd = 0; + if (got < nbytes) memset(dst + got, 0, (size_t)(nbytes - got)); +} + +static int16_t sample_to_s16(uint8_t const *p, AXaudioformat format) +{ + switch (format) { + case AX_AUDIO_U8: + return (int16_t)(((int)*p - 128) << 8); + case AX_AUDIO_S16: { + int16_t v; + memcpy(&v, p, 2); + return v; + } + case AX_AUDIO_S32: { + int32_t v; + memcpy(&v, p, 4); + return (int16_t)(v >> 16); + } + case AX_AUDIO_F32: { + float f; + int s; + memcpy(&f, p, 4); + if (f > 1.f) f = 1.f; + if (f < -1.f) f = -1.f; + s = (int)(f * 32767.f); + return (int16_t)s; + } + default: + return 0; + } +} + +void +ax_audio_device_fill(void *dst, int nbytes) +{ + int16_t *out = (int16_t *)dst; + int channels, bps, frames, i, c; + uint8_t *user = NULL; + int user_bytes; + + if (!dst || nbytes <= 0) return; + if (!g_audio_ready || !g_dev.open) { + memset(dst, 0, (size_t)nbytes); + return; + } + axMutexLock(g_dev.lock); + channels = g_dev.spec.channels; + bps = format_bytes(g_dev.spec.format); + frames = nbytes / (channels * 2); + user_bytes = frames * channels * bps; + if (frames <= 0 || bps <= 0) { + memset(dst, 0, (size_t)nbytes); + axMutexUnlock(g_dev.lock); + return; + } + if (g_dev.paused) { + memset(dst, 0, (size_t)nbytes); + axMutexUnlock(g_dev.lock); + return; + } + user = (uint8_t *)malloc((size_t)user_bytes); + if (!user) { + memset(dst, 0, (size_t)nbytes); + axMutexUnlock(g_dev.lock); + return; + } + take_user_bytes(user, user_bytes); + for (i = 0; i < frames; i++) { + for (c = 0; c < channels; c++) { + out[i * channels + c] = sample_to_s16(user + (i * channels + c) * bps, g_dev.spec.format); + } + } + free(user); + axMutexUnlock(g_dev.lock); +} + +bool_t +axLoadWAV(char const *path, AXaudiospec *spec, uint8_t **audio_buf, uint32_t *audio_len) +{ + FILE *f; + long sz; + uint8_t *file = NULL; + bool_t ok; + if (!path || !spec || !audio_buf || !audio_len) { + audio_set_error("invalid WAV arguments"); + return FALSE; + } + f = fopen(path, "rb"); + if (!f) { + audio_set_error("failed to open WAV file"); + return FALSE; + } + if (fseek(f, 0, SEEK_END) != 0 || (sz = ftell(f)) < 12 || sz > 64 * 1024 * 1024) { + fclose(f); + audio_set_error("WAV file is missing or too large"); + return FALSE; + } + rewind(f); + file = (uint8_t *)malloc((size_t)sz); + if (!file || fread(file, 1, (size_t)sz, f) != (size_t)sz) { + free(file); + fclose(f); + audio_set_error("failed to read WAV file"); + return FALSE; + } + fclose(f); + ok = decode_wav(file, (uint32_t)sz, spec, audio_buf, audio_len); + free(file); + return ok; +} + +bool_t +axLoadWAVMem(void const *data, uint32_t size, AXaudiospec *spec, + uint8_t **audio_buf, uint32_t *audio_len) +{ + return decode_wav((uint8_t const *)data, size, spec, audio_buf, audio_len); +} + +void +axFreeWAV(uint8_t *audio_buf) +{ + free(audio_buf); +} + +static int clamp_s16(int v) +{ + if (v > 32767) return 32767; + if (v < -32768) return -32768; + return v; +} + +void +axAudioMix(uint8_t *dst, uint8_t const *src, AXaudioformat format, uint32_t len, int volume) +{ + uint32_t i; + if (!dst || !src || len == 0) return; + if (volume < 0) volume = 0; + if (volume > 128) volume = 128; + switch (format) { + case AX_AUDIO_U8: + for (i = 0; i < len; i++) { + int mixed = ((int)dst[i] - 128) + ((((int)src[i] - 128) * volume) / 128); + dst[i] = (uint8_t)(clamp_s16(mixed) + 128); + } + break; + case AX_AUDIO_S16: + for (i = 0; i + 1 < len; i += 2) { + int16_t d, s; + memcpy(&d, dst + i, 2); + memcpy(&s, src + i, 2); + d = (int16_t)clamp_s16(d + (s * volume) / 128); + memcpy(dst + i, &d, 2); + } + break; + case AX_AUDIO_S32: + for (i = 0; i + 3 < len; i += 4) { + int32_t d, s; + int64_t mixed; + memcpy(&d, dst + i, 4); + memcpy(&s, src + i, 4); + mixed = (int64_t)d + ((int64_t)s * volume) / 128; + if (mixed > 2147483647LL) mixed = 2147483647LL; + if (mixed < -2147483648LL) mixed = -2147483648LL; + d = (int32_t)mixed; + memcpy(dst + i, &d, 4); + } + break; + case AX_AUDIO_F32: + for (i = 0; i + 3 < len; i += 4) { + float d, s; + memcpy(&d, dst + i, 4); + memcpy(&s, src + i, 4); + d += s * ((float)volume / 128.f); + if (d > 1.f) d = 1.f; + if (d < -1.f) d = -1.f; + memcpy(dst + i, &d, 4); + } + break; + default: + break; + } +} diff --git a/ios/ios_window.m b/ios/ios_window.m index 9111a1f..e6a0aa9 100644 --- a/ios/ios_window.m +++ b/ios/ios_window.m @@ -304,13 +304,6 @@ bool_t axCreateWindow(const char *title, uint32_t w, uint32_t h, uint32_t flags) ios_view.contentScaleFactor = ios_scale; CAEAGLLayer *layer = (CAEAGLLayer *)ios_view.layer; layer.opaque = YES; - CGColorSpaceRef color_space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); - if (color_space) { - layer.colorspace = color_space; - CGColorSpaceRelease(color_space); - } else { - IOS_TRACE("sRGB drawable color space unavailable; using platform default"); - } layer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking: @YES, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8}; glGenFramebuffers(1, &ios_framebuffer); glGenRenderbuffers(1, &ios_color); diff --git a/platform.h b/platform.h index 2853d1e..3b16ddf 100644 --- a/platform.h +++ b/platform.h @@ -4,7 +4,7 @@ * * This header defines the public API for the platform library, providing * a unified interface for window management, event handling, OpenGL/EGL - * rendering context management, file dialogs, and system utilities across + * rendering context management, WAV playback, file dialogs, and system utilities across * macOS, Linux (Wayland or X11), QNX, and WebGL (Emscripten) targets. * * All public symbols are exported via the #AX_API visibility macro. @@ -1633,4 +1633,143 @@ axTlsRecv(AXtlsctx *ctx, void *buf, int len); /** @} */ + +/** + * @defgroup audio Audio + * @brief SDL-style WAV loading and playback. + * + * WAV support covers RIFF PCM (8, 16, 24, and 32-bit), IEEE float 32, A-law, + * and mu-law. Compressed ADPCM is not supported. Samples are returned in + * host endianness. Playback devices start paused, matching SDL: queue or + * install a callback, then call #axAudioPause with `FALSE`. + * + * The hardware backend consumes signed 16-bit frames. #axAudioQueue and the + * device callback use the format requested at #axAudioOpen; conversion happens + * inside the device. Volume for #axAudioMix is SDL's 0–128 scale, where 128 + * is unity. + * @{ + */ + +/** @brief Unsigned 8-bit sample, biased at 128. */ +#define AX_AUDIO_U8 0x0008 +/** @brief Signed 16-bit host-endian sample. */ +#define AX_AUDIO_S16 0x8010 +/** @brief Signed 32-bit host-endian sample. */ +#define AX_AUDIO_S32 0x8020 +/** @brief 32-bit IEEE float sample in the range [-1, 1]. */ +#define AX_AUDIO_F32 0x8120 + +/** @brief Sample format passed to the device and mixer. */ +typedef int AXaudioformat; + +/** + * @brief Desired or obtained playback format. + * + * @p samples is the hardware period in frames. @p callback may be NULL, in + * which case the device pulls bytes queued with #axAudioQueue. A callback + * must fill @p stream completely and must not call #axAudioLock, + * #axAudioQueue, or #axAudioClose. + */ +typedef struct AXaudiospec { + int freq; /**< Sample frames per second. */ + AXaudioformat format;/**< One of AX_AUDIO_U8, S16, S32, or F32. */ + uint8_t channels; /**< 1 or 2. */ + uint16_t samples; /**< Period size in sample frames. */ + void (*callback)(void *userdata, uint8_t *stream, int len); + void *userdata; /**< Passed through to @p callback. */ +} AXaudiospec; + +/** @brief Prepare the audio subsystem. Safe to call more than once. */ +AX_API bool_t +axAudioInit(void); + +/** @brief Close the device and release audio subsystem resources. */ +AX_API void +axAudioShutdown(void); + +/** + * @brief Last audio error, or NULL if the last call succeeded. + * The pointer is invalidated by the next audio call. + */ +AX_API char const * +axAudioGetError(void); + +/** + * @brief Open the default playback device. + * + * Starts paused. Pass NULL @p desired for 44100 Hz stereo S16. On success + * returns a device id (currently always 1). Only one device may be open. + * + * @param[in] desired Requested format, or NULL for defaults. + * @param[out] obtained Filled with the accepted format; may be NULL. + * @return Device id, or 0 on failure. + */ +AX_API int +axAudioOpen(AXaudiospec const *desired, AXaudiospec *obtained); + +/** @brief Close a device opened by #axAudioOpen. Passing an unknown id is safe. */ +AX_API void +axAudioClose(int dev); + +/** + * @brief Pause or resume playback. + * @param pause `TRUE` to pause, `FALSE` to start the callback or queued audio. + */ +AX_API void +axAudioPause(int dev, bool_t pause); + +/** @brief Lock the device. Do not call from the audio callback. */ +AX_API void +axAudioLock(int dev); + +/** @brief Unlock a device locked with #axAudioLock. */ +AX_API void +axAudioUnlock(int dev); + +/** + * @brief Queue interleaved samples in the device format. + * Fails if the device was opened with a callback, or if the queue would + * exceed 4 MiB. + * @return `TRUE` if the bytes were copied into the queue. + */ +AX_API bool_t +axAudioQueue(int dev, void const *data, uint32_t len); + +/** @brief Bytes still waiting in the playback queue. */ +AX_API uint32_t +axAudioQueued(int dev); + +/** @brief Discard queued samples. Does not affect a callback device. */ +AX_API void +axAudioClearQueue(int dev); + +/** + * @brief Load a RIFF WAVE file from disk. + * @param[out] spec Format of the decoded buffer. + * @param[out] audio_buf Allocated sample bytes. Free with #axFreeWAV. + * @param[out] audio_len Length of @p audio_buf in bytes. + * @return `TRUE` on success. + */ +AX_API bool_t +axLoadWAV(char const *path, AXaudiospec *spec, uint8_t **audio_buf, uint32_t *audio_len); + +/** @brief Load a RIFF WAVE image already in memory. Same contract as #axLoadWAV. */ +AX_API bool_t +axLoadWAVMem(void const *data, uint32_t size, AXaudiospec *spec, + uint8_t **audio_buf, uint32_t *audio_len); + +/** @brief Free a buffer returned by #axLoadWAV or #axLoadWAVMem. NULL is safe. */ +AX_API void +axFreeWAV(uint8_t *audio_buf); + +/** + * @brief Mix @p src onto @p dst with clipping. + * @param volume 0 is silence, 128 is unity, matching SDL_MixAudioFormat. + */ +AX_API void +axAudioMix(uint8_t *dst, uint8_t const *src, AXaudioformat format, uint32_t len, int volume); + +/** @} */ + + #endif diff --git a/tests/test_audio.c b/tests/test_audio.c new file mode 100644 index 0000000..caeff5e --- /dev/null +++ b/tests/test_audio.c @@ -0,0 +1,157 @@ +/* + * test_audio.c - WAV loader and software mixer tests. + * + * Hardware playback is optional: CI hosts often have no audio device, so + * axAudioOpen is allowed to fail. Loading, decoding, and mixing are not. + */ + +#include "platform.h" +#include +#include +#include +#include +#include + +static void wr_le16(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)(v & 0xff); + p[1] = (uint8_t)(v >> 8); +} + +static void wr_le32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)(v & 0xff); + p[1] = (uint8_t)((v >> 8) & 0xff); + p[2] = (uint8_t)((v >> 16) & 0xff); + p[3] = (uint8_t)(v >> 24); +} + +static uint32_t build_pcm16(uint8_t *dst, int frames, int channels, int freq) +{ + uint32_t data = (uint32_t)(frames * channels * 2); + uint32_t riff = 36 + data; + int i; + wr_le32(dst, 0x46464952); /* RIFF */ + memcpy(dst, "RIFF", 4); + wr_le32(dst + 4, riff); + memcpy(dst + 8, "WAVE", 4); + memcpy(dst + 12, "fmt ", 4); + wr_le32(dst + 16, 16); + wr_le16(dst + 20, 1); + wr_le16(dst + 22, (uint16_t)channels); + wr_le32(dst + 24, (uint32_t)freq); + wr_le32(dst + 28, (uint32_t)(freq * channels * 2)); + wr_le16(dst + 32, (uint16_t)(channels * 2)); + wr_le16(dst + 34, 16); + memcpy(dst + 36, "data", 4); + wr_le32(dst + 40, data); + for (i = 0; i < frames * channels; i++) + wr_le16(dst + 44 + i * 2, (uint16_t)(i == 0 ? 1000 : -2000)); + return 44 + data; +} + +static void test_pcm_and_mix(void) +{ + uint8_t file[128]; + uint32_t size = build_pcm16(file, 4, 1, 22050); + AXaudiospec spec; + uint8_t *buf = NULL; + uint32_t len = 0; + int16_t sample; + int16_t dst[2]; + int16_t src[2]; + char path[] = "ax_test_tone.wav"; + FILE *f; + + assert(axLoadWAVMem(file, size, &spec, &buf, &len) == TRUE); + assert(spec.freq == 22050); + assert(spec.format == AX_AUDIO_S16); + assert(spec.channels == 1); + assert(len == 8); + memcpy(&sample, buf, 2); + assert(sample == 1000); + axFreeWAV(buf); + + f = fopen(path, "wb"); + assert(f); + assert(fwrite(file, 1, size, f) == size); + fclose(f); + assert(axLoadWAV(path, &spec, &buf, &len) == TRUE); + assert(spec.freq == 22050); + axFreeWAV(buf); + remove(path); + + dst[0] = 1000; + dst[1] = -1000; + src[0] = 500; + src[1] = 500; + axAudioMix((uint8_t *)dst, (uint8_t *)src, AX_AUDIO_S16, sizeof(dst), 128); + assert(dst[0] == 1500); + assert(dst[1] == -500); + axAudioMix((uint8_t *)dst, (uint8_t *)src, AX_AUDIO_S16, sizeof(dst), 0); + assert(dst[0] == 1500); +} + +static void test_mulaw_and_reject(void) +{ + uint8_t file[64]; + AXaudiospec spec; + uint8_t *buf = NULL; + uint32_t len = 0; + int16_t sample; + uint8_t bad[] = "not a wav file!!"; + + memcpy(file, "RIFF", 4); + wr_le32(file + 4, 36); + memcpy(file + 8, "WAVE", 4); + memcpy(file + 12, "fmt ", 4); + wr_le32(file + 16, 16); + wr_le16(file + 20, 7); /* mu-law */ + wr_le16(file + 22, 1); + wr_le32(file + 24, 8000); + wr_le32(file + 28, 8000); + wr_le16(file + 32, 1); + wr_le16(file + 34, 8); + memcpy(file + 36, "data", 4); + wr_le32(file + 40, 1); + file[44] = 0xff; /* mu-law silence */ + assert(axLoadWAVMem(file, 45, &spec, &buf, &len) == TRUE); + assert(spec.format == AX_AUDIO_S16); + assert(spec.freq == 8000); + assert(len == 2); + memcpy(&sample, buf, 2); + assert(sample == 0); + axFreeWAV(buf); + + assert(axLoadWAVMem(bad, sizeof(bad), &spec, &buf, &len) == FALSE); + assert(axAudioGetError() != NULL); +} + +int main(void) +{ + test_pcm_and_mix(); + test_mulaw_and_reject(); + /* Opening a device is best-effort. Headless CI has no sound card. */ + { + AXaudiospec want; + int dev; + memset(&want, 0, sizeof(want)); + want.freq = 22050; + want.format = AX_AUDIO_S16; + want.channels = 1; + want.samples = 512; + dev = axAudioOpen(&want, NULL); + if (dev) { + uint8_t silence[64]; + memset(silence, 0, sizeof(silence)); + assert(axAudioQueue(dev, silence, sizeof(silence)) == TRUE); + assert(axAudioQueued(dev) == sizeof(silence)); + axAudioClearQueue(dev); + assert(axAudioQueued(dev) == 0); + axAudioClose(dev); + } + } + axAudioShutdown(); + printf("Audio WAV tests passed.\n"); + return 0; +}