From a1aa7c05a1b8c526abc6b8d57932823407b8c608 Mon Sep 17 00:00:00 2001 From: sethvincent Date: Fri, 4 Jul 2025 10:45:31 -0700 Subject: [PATCH 1/8] implement audio devices --- .github/workflows/prebuild.yml | 23 +- .github/workflows/test.yml | 25 +- README.md | 199 +++++++++++++++- binding.cc | 288 +++++++++++++++++++++++- examples/audio-device-list.js | 29 +++ {example => examples}/package.json | 2 +- {example => examples}/video-playback.js | 0 index.js | 1 + lib/audio-device.js | 218 ++++++++++++++++++ package.json | 3 + test.js | 1 + test/audio-device.js | 89 ++++++++ 12 files changed, 871 insertions(+), 7 deletions(-) create mode 100644 examples/audio-device-list.js rename {example => examples}/package.json (85%) rename {example => examples}/video-playback.js (100%) create mode 100644 lib/audio-device.js create mode 100644 test/audio-device.js diff --git a/.github/workflows/prebuild.yml b/.github/workflows/prebuild.yml index 8a47f01..afdab65 100644 --- a/.github/workflows/prebuild.yml +++ b/.github/workflows/prebuild.yml @@ -31,6 +31,16 @@ jobs: - uses: actions/setup-node@v4 with: node-version: lts/* + - uses: sethvincent/sound-ci-helpers@windows-updates + - run: | + choco install vb-cable -y + choco upgrade llvm + if: ${{ matrix.platform == 'win32' }} + - shell: pwsh + if: ${{ matrix.platform == 'win32' }} + run: | + New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy\' -Force + New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy\' -Name 'LetAppsAccessMicrophone' -Value 1 -PropertyType DWord - run: | sudo apt update sudo apt install -y \ @@ -41,8 +51,19 @@ jobs: libxi-dev \ libxfixes-dev \ libwayland-dev \ - xvfb + xvfb \ + libpulse-dev \ + pulseaudio + if: ${{ matrix.platform == 'linux' }} + - run: pulseaudio -D --start --exit-idle-time=-1 if: ${{ matrix.platform == 'linux' }} + - run: | + brew install switchaudio-osx blackhole-2ch + sudo pkill coreaudiod || true + sleep 10 + SwitchAudioSource -s 'BlackHole 2ch' -t input + SwitchAudioSource -s 'BlackHole 2ch' -t output + if: ${{ matrix.platform == 'darwin' }} - run: npm install -g bare-runtime bare-make - run: npm install - run: bare-make generate --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} ${{ matrix.flags }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29ea5b3..9e60b6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,16 @@ jobs: - uses: actions/setup-node@v4 with: node-version: lts/* + - uses: sethvincent/sound-ci-helpers@windows-updates + - run: | + choco install vb-cable -y + choco upgrade llvm + if: ${{ matrix.platform == 'win32' }} + - shell: pwsh + if: ${{ matrix.platform == 'win32' }} + run: | + New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy\' -Force + New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy\' -Name 'LetAppsAccessMicrophone' -Value 1 -PropertyType DWord - run: | sudo apt update sudo apt install -y \ @@ -37,10 +47,19 @@ jobs: libxi-dev \ libxfixes-dev \ libwayland-dev \ - xvfb + xvfb \ + libpulse-dev \ + pulseaudio if: ${{ matrix.platform == 'linux' }} - - run: choco upgrade llvm - if: ${{ matrix.platform == 'win32' }} + - run: pulseaudio -D --start --exit-idle-time=-1 + if: ${{ matrix.platform == 'linux' }} + - run: | + brew install switchaudio-osx blackhole-2ch + sudo pkill coreaudiod || true + sleep 10 + SwitchAudioSource -s 'BlackHole 2ch' -t input + SwitchAudioSource -s 'BlackHole 2ch' -t output + if: ${{ matrix.platform == 'darwin' }} - run: npm install -g bare-runtime bare-make - run: npm install - run: bare-make generate --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --debug ${{ matrix.flags }} diff --git a/README.md b/README.md index c030560..f5557db 100644 --- a/README.md +++ b/README.md @@ -199,9 +199,206 @@ Parameters: **Returns**: `boolean` indicating if an event was polled +### `AudioDevice` + +The `AudioDevice` API provides functionality to manage SDL audio devices for playback and recording. + +```js +const device = await sdl.AudioDevice.open(deviceId[, spec]) +``` + +Parameters: + +- `deviceId` (`number`): The audio device ID. Use `constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK` or `constants.SDL_AUDIO_DEVICE_DEFAULT_RECORDING` for defaults +- `spec` (`object`, optional): Audio specification with the following properties: + - `format` (`number`): Audio format (e.g., `constants.SDL_AUDIO_F32`) + - `channels` (`number`): Number of audio channels (e.g., 2 for stereo) + - `freq` (`number`): Sample rate in Hz (e.g., 48000) + +**Returns**: A Promise that resolves to a new `AudioDevice` instance + +#### Static Methods + +##### `AudioDevice.playbackDeviceFormats()` + +Gets the audio formats supported by all playback devices. + +**Returns**: `AudioDeviceFormat[]` - Array of audio device formats + +##### `AudioDevice.recordingDeviceFormats()` + +Gets the audio formats supported by all recording devices. + +**Returns**: `AudioDeviceFormat[]` - Array of audio device formats + +##### `AudioDevice.playbackDevices()` + +Gets all available playback devices. + +**Returns**: `Promise` - Promise that resolves to an array of audio devices + +##### `AudioDevice.recordingDevices()` + +Gets all available recording devices. + +**Returns**: `Promise` - Promise that resolves to an array of audio devices + +##### `AudioDevice.defaultPlaybackDevice([spec])` + +Opens the default playback device. + +Parameters: + +- `spec` (`object`, optional): Audio specification (same as `open()`) + +**Returns**: `Promise` - Promise that resolves to the default playback device + +##### `AudioDevice.defaultRecordingDevice([spec])` + +Opens the default recording device. + +Parameters: + +- `spec` (`object`, optional): Audio specification (same as `open()`) + +**Returns**: `Promise` - Promise that resolves to the default recording device + +#### Properties + +##### `AudioDevice.name` + +Gets the device name. + +**Returns**: `string` + +##### `AudioDevice.format` + +Gets the audio device format. + +**Returns**: `AudioDeviceFormat` instance + +##### `AudioDevice.isPlaybackDevice` + +Indicates if this is a playback device. + +**Returns**: `boolean` + +##### `AudioDevice.isPhysicalDevice` + +Indicates if this is a physical device. + +**Returns**: `boolean` + +##### `AudioDevice.isPaused` + +Indicates if the device is paused. + +**Returns**: `boolean` + +##### `AudioDevice.gain` + +Gets or sets the device gain (volume). + +**Returns**: `number` (0.0 to 1.0) + +#### Methods + +##### `AudioDevice.ready()` + +Wait for the device to be ready. + +**Returns**: `Promise` + +##### `AudioDevice.pause()` + +Pauses audio playback/recording. + +**Returns**: `boolean` indicating success + +##### `AudioDevice.resume()` + +Resumes audio playback/recording. + +**Returns**: `boolean` indicating success + +##### `AudioDevice.close()` + +Closes the audio device. + +**Returns**: `Promise` + +### `AudioDevice.AudioDeviceFormat` + +Represents the format of an audio device. + +```js +const format = new sdl.AudioDevice.AudioDeviceFormat(deviceId) +``` + +Parameters: + +- `deviceId` (`number`): The audio device ID + +**Returns**: A new `AudioDeviceFormat` instance + +#### Properties + +##### `AudioDeviceFormat.valid` + +Indicates if the format is valid. + +**Returns**: `boolean` + +##### `AudioDeviceFormat.sampleFrames` + +Gets the number of sample frames. + +**Returns**: `number` + +##### `AudioDeviceFormat.spec` + +Gets the audio specification. + +**Returns**: `AudioSpec` instance + +### `AudioDevice.AudioSpec` + +Represents an audio specification. + +```js +const spec = new sdl.AudioDevice.AudioSpec(format) +``` + +Parameters: + +- `format` (`AudioDeviceFormat`): The audio device format + +**Returns**: A new `AudioSpec` instance + +#### Properties + +##### `AudioSpec.format` + +Gets the audio format. + +**Returns**: `number` (e.g., `constants.SDL_AUDIO_F32`) + +##### `AudioSpec.channels` + +Gets the number of channels. + +**Returns**: `number` + +##### `AudioSpec.freq` + +Gets the sample rate in Hz. + +**Returns**: `number` + ## Examples -- [Video playback with `bare-ffmpeg`](./example/video-playback.js) +- [Video playback with `bare-ffmpeg`](./examples/video-playback.js) +- [List available audio playback and recording devices](./examples/audio-device-list.js) ## License diff --git a/binding.cc b/binding.cc index 40b37b9..e25dab5 100644 --- a/binding.cc +++ b/binding.cc @@ -24,13 +24,24 @@ typedef struct { SDL_KeyboardEvent handle; } bare_sdl_keyboard_event_t; +typedef struct { + SDL_AudioDeviceID *devices; + int count; +} bare_sdl_audio_device_list_t; + +typedef struct { + SDL_AudioSpec spec; + int sample_frames; + bool valid; +} bare_sdl_audio_device_format_t; + static uv_once_t bare_sdl__init_guard = UV_ONCE_INIT; static void bare_sdl__on_init(void) { // Note: This is a way to prevent SDL to handle signals SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1"); - SDL_Init(SDL_INIT_VIDEO); + SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); } // Window @@ -263,6 +274,246 @@ bare_sdl_get_event_key_scancode( return key->handle.scancode; } +static uint32_t +bare_sdl_open_audio_device( + js_env_t *env, + js_receiver_t, + uint32_t requested_device_id, + std::optional format, + std::optional channels, + std::optional freq +) { + int err; + + SDL_AudioSpec *spec_ptr = nullptr; + if (format.has_value() && channels.has_value() && freq.has_value()) { + SDL_AudioSpec spec = { + .format = (SDL_AudioFormat) format.value(), + .channels = channels.value(), + .freq = freq.value() + }; + + spec_ptr = &spec; + } + + auto logical_device_id = SDL_OpenAudioDevice((SDL_AudioDeviceID) requested_device_id, spec_ptr); + + if (logical_device_id == 0) { + const char *sdl_error = SDL_GetError(); + err = js_throw_error(env, nullptr, sdl_error); + assert(err == 0); + + throw js_pending_exception; + } + + return logical_device_id; +} + +static void +bare_sdl_close_audio_device( + js_env_t *, + js_receiver_t, + uint32_t device_id +) { + SDL_CloseAudioDevice((SDL_AudioDeviceID) device_id); +} + +static bool +bare_sdl_pause_audio_device( + js_env_t *env, + js_receiver_t, + uint32_t device_id +) { + return SDL_PauseAudioDevice(device_id); +} + +static bool +bare_sdl_resume_audio_device( + js_env_t *env, + js_receiver_t, + uint32_t device_id +) { + return SDL_ResumeAudioDevice(device_id); +} + +static std::vector +bare_sdl_get_audio_playback_devices( + js_env_t *env, + js_receiver_t +) { + int count = 0; + SDL_AudioDeviceID *devices = SDL_GetAudioPlaybackDevices(&count); + + std::vector list; + + if (devices != nullptr) { + for (int i = 0; i < count; i++) { + list.push_back(devices[i]); + } + SDL_free(devices); + } + + return list; +} + +static std::vector +bare_sdl_get_audio_recording_devices( + js_env_t *env, + js_receiver_t +) { + int count = 0; + SDL_AudioDeviceID *devices = SDL_GetAudioRecordingDevices(&count); + + std::vector list; + + if (devices != nullptr) { + for (int i = 0; i < count; i++) { + list.push_back(devices[i]); + } + SDL_free(devices); + } + + return list; +} + +static std::optional +bare_sdl_get_audio_device_list_item( + js_env_t *env, + js_receiver_t, + js_arraybuffer_span_of_t list, + int index +) { + if (index < 0 || index >= list->count || !list->devices) { + return NULL; + } + + return (uint32_t) list->devices[index]; +} + +static std::string +bare_sdl_get_audio_device_name( + js_env_t *env, + js_receiver_t, + uint32_t device_id +) { + const char *name = SDL_GetAudioDeviceName((SDL_AudioDeviceID) device_id); + return name ? std::string(name) : std::string(""); +} + +static double +bare_sdl_get_audio_device_gain( + js_env_t *env, + js_receiver_t, + uint32_t device_id +) { + return SDL_GetAudioDeviceGain((SDL_AudioDeviceID) device_id); +} + +static void +bare_sdl_set_audio_device_gain( + js_env_t *env, + js_receiver_t, + uint32_t device_id, + double gain +) { + SDL_SetAudioDeviceGain((SDL_AudioDeviceID) device_id, (float) gain); +} + +static js_arraybuffer_t +bare_sdl_get_audio_device_format( + js_env_t *env, + js_receiver_t, + uint32_t device_id +) { + int err; + + js_arraybuffer_t handle; + + bare_sdl_audio_device_format_t *format; + err = js_create_arraybuffer(env, format, handle); + assert(err == 0); + + format->valid = SDL_GetAudioDeviceFormat( + (SDL_AudioDeviceID) device_id, + &format->spec, + &format->sample_frames + ); + + return handle; +} + +static bool +bare_sdl_get_audio_device_format_valid( + js_env_t *env, + js_receiver_t, + js_arraybuffer_span_of_t format +) { + return format->valid; +} + +static uint32_t +bare_sdl_get_audio_device_format_format( + js_env_t *env, + js_receiver_t, + js_arraybuffer_span_of_t format +) { + return (uint32_t) format->spec.format; +} + +static int +bare_sdl_get_audio_device_format_channels( + js_env_t *env, + js_receiver_t, + js_arraybuffer_span_of_t format +) { + return format->spec.channels; +} + +static int +bare_sdl_get_audio_device_format_freq( + js_env_t *env, + js_receiver_t, + js_arraybuffer_span_of_t format +) { + return format->spec.freq; +} + +static int +bare_sdl_get_audio_device_format_sample_frames( + js_env_t *env, + js_receiver_t, + js_arraybuffer_span_of_t format +) { + return format->sample_frames; +} + +static bool +bare_sdl_is_audio_device_physical( + js_env_t *env, + js_receiver_t, + uint32_t deviceId +) { + return SDL_IsAudioDevicePhysical((SDL_AudioDeviceID) deviceId); +} + +static bool +bare_sdl_is_audio_device_playback( + js_env_t *env, + js_receiver_t, + uint32_t deviceId +) { + return SDL_IsAudioDevicePlayback((SDL_AudioDeviceID) deviceId); +} + +static bool +bare_sdl_is_audio_device_paused( + js_env_t *env, + js_receiver_t, + uint32_t deviceId +) { + return SDL_AudioDevicePaused((SDL_AudioDeviceID) deviceId); +} + // Exports static js_value_t * @@ -690,6 +941,21 @@ bare_sdl_exports(js_env_t *env, js_value_t *exports) { V(SDL_SCANCODE_ENDCALL) V(SDL_SCANCODE_RESERVED) V(SDL_SCANCODE_COUNT) + + V(SDL_AUDIO_U8) + V(SDL_AUDIO_S8) + V(SDL_AUDIO_S16LE) + V(SDL_AUDIO_S16BE) + V(SDL_AUDIO_S32LE) + V(SDL_AUDIO_S32BE) + V(SDL_AUDIO_F32LE) + V(SDL_AUDIO_F32BE) + V(SDL_AUDIO_S16) + V(SDL_AUDIO_S32) + V(SDL_AUDIO_F32) + + V(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK) + V(SDL_AUDIO_DEVICE_DEFAULT_RECORDING) #undef V #define V(name, function) \ @@ -714,6 +980,26 @@ bare_sdl_exports(js_env_t *env, js_value_t *exports) { V("getEventType", bare_sdl_get_event_type) V("getEventKey", bare_sdl_get_event_key) V("getEventKeyScancode", bare_sdl_get_event_key_scancode) + + V("openAudioDevice", bare_sdl_open_audio_device) + V("closeAudioDevice", bare_sdl_close_audio_device) + V("pauseAudioDevice", bare_sdl_pause_audio_device) + V("resumeAudioDevice", bare_sdl_resume_audio_device) + V("getAudioPlaybackDevices", bare_sdl_get_audio_playback_devices) + V("getAudioRecordingDevices", bare_sdl_get_audio_recording_devices) + V("getAudioDeviceListItem", bare_sdl_get_audio_device_list_item) + V("getAudioDeviceName", bare_sdl_get_audio_device_name) + V("getAudioDeviceGain", bare_sdl_get_audio_device_gain) + V("setAudioDeviceGain", bare_sdl_set_audio_device_gain) + V("getAudioDeviceFormat", bare_sdl_get_audio_device_format) + V("getAudioDeviceFormatValid", bare_sdl_get_audio_device_format_valid) + V("getAudioDeviceFormatFormat", bare_sdl_get_audio_device_format_format) + V("getAudioDeviceFormatChannels", bare_sdl_get_audio_device_format_channels) + V("getAudioDeviceFormatFreq", bare_sdl_get_audio_device_format_freq) + V("getAudioDeviceFormatSampleFrames", bare_sdl_get_audio_device_format_sample_frames) + V("isAudioDevicePhysical", bare_sdl_is_audio_device_physical) + V("isAudioDevicePlayback", bare_sdl_is_audio_device_playback) + V("isAudioDevicePaused", bare_sdl_is_audio_device_paused) #undef V return exports; diff --git a/examples/audio-device-list.js b/examples/audio-device-list.js new file mode 100644 index 0000000..4da9db7 --- /dev/null +++ b/examples/audio-device-list.js @@ -0,0 +1,29 @@ +const sdl = require('..') + +async function main() { + const recording = await sdl.AudioDevice.recordingDevices() + const playback = await sdl.AudioDevice.playbackDevices() + + console.log('\n# Recording devices:\n') + for (const device of recording) { + printDevice(device) + } + + console.log('\n# Playback devices:\n') + for (const device of recording) { + printDevice(device) + } +} + +function printDevice(device) { + console.log('Device:', device.name) + console.log('ID:', device.id) + console.log('Sample frames:', device.format.sampleFrames) + console.log('Spec:') + console.log(' Format:', device.format.spec.format) + console.log(' Channels:', device.format.spec.channels) + console.log(' Freq:', device.format.spec.freq) + console.log() +} + +main().catch(console.error) diff --git a/example/package.json b/examples/package.json similarity index 85% rename from example/package.json rename to examples/package.json index d068f50..4646ab3 100644 --- a/example/package.json +++ b/examples/package.json @@ -4,7 +4,7 @@ "author": "Holepunch", "license": "Apache-2.0", "dependencies": { - "bare-ffmpeg": "1.0.0-8", + "bare-ffmpeg": "1.0.0-14", "bare-sdl": "file:.." } } diff --git a/example/video-playback.js b/examples/video-playback.js similarity index 100% rename from example/video-playback.js rename to examples/video-playback.js diff --git a/index.js b/index.js index c4cc70d..9ee2b40 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ exports.constants = require('./lib/constants') +exports.AudioDevice = require('./lib/audio-device') exports.Event = require('./lib/event') exports.Poller = require('./lib/poller') exports.Renderer = require('./lib/renderer') diff --git a/lib/audio-device.js b/lib/audio-device.js new file mode 100644 index 0000000..d93680e --- /dev/null +++ b/lib/audio-device.js @@ -0,0 +1,218 @@ +const ReadyResource = require('ready-resource') +const binding = require('../binding') +const constants = binding.constants + +class SDLAudioSpec { + constructor(format) { + this._format = format + } + + get format() { + return binding.getAudioDeviceFormatFormat(this._format._handle) + } + + get channels() { + return binding.getAudioDeviceFormatChannels(this._format._handle) + } + + get freq() { + return binding.getAudioDeviceFormatFreq(this._format._handle) + } + + toJSON() { + return { + format: this.format, + channels: this.channels, + freq: this.freq + } + } +} + +class SDLAudioDeviceFormat { + constructor(deviceId) { + this._handle = binding.getAudioDeviceFormat(deviceId) + } + + get valid() { + return binding.getAudioDeviceFormatValid(this._handle) + } + + get sampleFrames() { + return binding.getAudioDeviceFormatSampleFrames(this._handle) + } + + get spec() { + return new SDLAudioSpec(this) + } + + toJSON() { + return { + valid: this.valid, + sampleFrames: this.sampleFrames, + spec: this.spec.toJSON() + } + } +} + +class SDLAudioDevice extends ReadyResource { + static AudioSpec = SDLAudioSpec + static AudioDeviceFormat = SDLAudioDeviceFormat + + /** + * @return []SDLAudioDeviceFormat + */ + static playbackDeviceFormats() { + const list = binding.getAudioPlaybackDevices() + return list.map((deviceId) => { + return new SDLAudioDeviceFormat(deviceId) + }) + } + + /** + * @return []SDLAudioDeviceFormat + */ + static recordingDeviceFormats() { + const list = binding.getAudioRecordingDevices() + return list.map((deviceId) => { + return new SDLAudioDeviceFormat(deviceId) + }) + } + + /** + * @return []SDLAudioDevice + */ + static async recordingDevices() { + const list = binding.getAudioRecordingDevices() + return Promise.all( + list.map(async (deviceId) => { + const device = await SDLAudioDevice.open(deviceId) + return device + }) + ) + } + + /** + * @return []SDLAudioDevice + */ + static async playbackDevices() { + const list = binding.getAudioPlaybackDevices() + return Promise.all( + list.map(async (deviceId) => { + return SDLAudioDevice.open(deviceId) + }) + ) + } + + /** + * @return SDLAudioDevice + */ + static async defaultRecordingDevice(spec) { + return SDLAudioDevice.open( + constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, + spec + ) + } + + /** + * @return SDLAudioDevice + */ + static async defaultPlaybackDevice(spec) { + return SDLAudioDevice.open( + constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, + spec + ) + } + + /** + * @return SDLAudioDevice + */ + static async open(deviceId, spec) { + const device = new SDLAudioDevice(deviceId, spec) + await device.ready() + return device + } + + constructor(deviceId, spec) { + super() + this._requestedDeviceId = deviceId + this.spec = spec + this.id = null + } + + _open() { + if (this.opening || this.opened) { + return + } + + const format = this.spec?.format + const channels = this.spec?.channels + const freq = this.spec?.freq + + this.id = binding.openAudioDevice( + this._requestedDeviceId, + format, + channels, + freq + ) + } + + _close() { + if (this.closing) return + binding.closeAudioDevice(this.id) + this.id = null + } + + get name() { + return binding.getAudioDeviceName(this.id) + } + + get format() { + return new SDLAudioDeviceFormat(this.id) + } + + get isPlaybackDevice() { + return binding.isAudioDevicePlayback(this.id) + } + + get isPhysicalDevice() { + return binding.isAudioDevicePhysical(this.id) + } + + get isPaused() { + return binding.isAudioDevicePaused(this.id) + } + + get gain() { + return binding.getAudioDeviceGain(this.id) + } + + set gain(volume) { + binding.setAudioDeviceGain(this.id, volume) + } + + pause() { + return binding.pauseAudioDevice(this.id) + } + + resume() { + return binding.resumeAudioDevice(this.id) + } + + toJSON() { + return { + id: this.id, + name: this.name, + format: this.format, + isPlaybackDevice: this.isPlaybackDevice, + isPhysicalDevice: this.isPhysicalDevice, + isPaused: this.isPaused, + gain: this.gain, + opening: this.opening, + opened: this.opened, + closing: this.closing, + closed: this.closed + } + } +} + +module.exports = SDLAudioDevice diff --git a/package.json b/package.json index 9564938..7f5a4da 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,8 @@ "cmake-harden": "^1.1.2", "prettier": "^3.5.3", "prettier-config-standard": "^7.0.0" + }, + "dependencies": { + "ready-resource": "^1.1.2" } } diff --git a/test.js b/test.js index 74c605f..464c6c8 100644 --- a/test.js +++ b/test.js @@ -1,3 +1,4 @@ +require('./test/audio-device') require('./test/event') require('./test/poller') require('./test/renderer') diff --git a/test/audio-device.js b/test/audio-device.js new file mode 100644 index 0000000..de4c858 --- /dev/null +++ b/test/audio-device.js @@ -0,0 +1,89 @@ +const test = require('brittle') +const sdl = require('..') + +test('sdl.AudioDevice - playbackDeviceFormats', (t) => { + const formats = sdl.AudioDevice.playbackDeviceFormats() + t.ok(Array.isArray(formats), 'returns an array') +}) + +test('sdl.AudioDevice - recordingDeviceFormats', (t) => { + const formats = sdl.AudioDevice.recordingDeviceFormats() + t.ok(Array.isArray(formats), 'returns an array') +}) + +test('sdl.AudioDevice - playbackDevices', async (t) => { + const devices = await sdl.AudioDevice.playbackDevices() + t.ok(Array.isArray(devices), 'returns an array') +}) + +test('sdl.AudioDevice - recordingDevices', async (t) => { + const devices = await sdl.AudioDevice.recordingDevices() + t.ok(Array.isArray(devices), 'returns an array') +}) + +test('sdl.AudioDevice - defaultRecordingDevice', async (t) => { + const device = await sdl.AudioDevice.defaultRecordingDevice() + t.ok(device instanceof sdl.AudioDevice, 'returns sdl.AudioDevice instance') +}) + +test('sdl.AudioDevice - defaultPlaybackDevice', async (t) => { + const device = await sdl.AudioDevice.defaultPlaybackDevice() + t.ok(device instanceof sdl.AudioDevice, 'returns sdl.AudioDevice instance') +}) + +test('sdl.AudioDevice - properties', async (t) => { + const spec = { format: sdl.constants.SDL_AUDIO_F32, channels: 2, freq: 48000 } + const device = await sdl.AudioDevice.defaultPlaybackDevice(spec) + await device.ready() + + t.ok(typeof device.name === 'string') + t.ok(device.format instanceof sdl.AudioDevice.AudioDeviceFormat) + t.is(typeof device.isPlaybackDevice, 'boolean') + t.is(typeof device.isPhysicalDevice, 'boolean') + t.is(typeof device.isPaused, 'boolean') + t.is(typeof device.gain, 'number') +}) + +test('sdl.AudioDevice - set gain', async (t) => { + const device = await sdl.AudioDevice.defaultPlaybackDevice() + await device.ready() + device.gain = 0.5 + t.is(device.gain, 0.5, 'sets gain correctly') +}) + +test('sdl.AudioDevice - pause/resume', async (t) => { + const device = await sdl.AudioDevice.defaultPlaybackDevice() + await device.ready() + { + const result = device.pause() + t.ok(typeof result === 'boolean') + } + + { + const result = device.resume() + t.ok(typeof result === 'boolean', 'returns boolean') + } +}) + +test('SDLAudioSpec', async (t) => { + const device = await sdl.AudioDevice.defaultPlaybackDevice() + const format = new sdl.AudioDevice.AudioDeviceFormat(device.id) + const spec = new sdl.AudioDevice.AudioSpec(format) + t.ok(spec && spec._format) +}) + +test('sdl.AudioDeviceFormat', (t) => { + const deviceId = 1 + const format = new sdl.AudioDevice.AudioDeviceFormat(deviceId) + + t.ok(format._handle) + + const valid = format.valid + t.ok(typeof valid === 'boolean', 'returns boolean') + + const sampleFrames = format.sampleFrames + t.ok(typeof sampleFrames === 'number', 'returns number') + + const spec = format.spec + t.ok(spec instanceof sdl.AudioDevice.AudioSpec, 'returns AudioSpec instance') +}) From c92913af4fad4e95e0a7d35af0f2dc759ea9a965 Mon Sep 17 00:00:00 2001 From: sethvincent Date: Wed, 9 Jul 2025 16:52:13 -0700 Subject: [PATCH 2/8] inline, sound ci helpers, remove ready-resources, other small fixes --- .github/actions/virtual-audio-device/LICENSE | 29 +++++ .../actions/virtual-audio-device/README.md | 3 + .../actions/virtual-audio-device/action.yml | 18 +++ .../virtual-audio-device/linux/setup_sound.sh | 9 ++ .../virtual-audio-device/macos/setup_sound.sh | 6 + .../virtual-audio-device/windows/devcon.exe | Bin 0 -> 81408 bytes .../windows/setup_sound.ps1 | 73 ++++++++++++ .../virtual-audio-device/windows/vbcable.cer | Bin 0 -> 1422 bytes .github/workflows/prebuild.yml | 5 +- .github/workflows/test.yml | 4 +- README.md | 104 +++++++++--------- binding.cc | 9 +- lib/audio-device.js | 78 ++++--------- package.json | 3 - test/audio-device.js | 36 +++--- 15 files changed, 235 insertions(+), 142 deletions(-) create mode 100644 .github/actions/virtual-audio-device/LICENSE create mode 100644 .github/actions/virtual-audio-device/README.md create mode 100644 .github/actions/virtual-audio-device/action.yml create mode 100755 .github/actions/virtual-audio-device/linux/setup_sound.sh create mode 100755 .github/actions/virtual-audio-device/macos/setup_sound.sh create mode 100755 .github/actions/virtual-audio-device/windows/devcon.exe create mode 100755 .github/actions/virtual-audio-device/windows/setup_sound.ps1 create mode 100644 .github/actions/virtual-audio-device/windows/vbcable.cer diff --git a/.github/actions/virtual-audio-device/LICENSE b/.github/actions/virtual-audio-device/LICENSE new file mode 100644 index 0000000..b788a7c --- /dev/null +++ b/.github/actions/virtual-audio-device/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2019, Laboratory for Auditory Brain Science and Neuroengineering +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/.github/actions/virtual-audio-device/README.md b/.github/actions/virtual-audio-device/README.md new file mode 100644 index 0000000..6f850f3 --- /dev/null +++ b/.github/actions/virtual-audio-device/README.md @@ -0,0 +1,3 @@ +# virtual-audio-device + +Adapted from sound-ci-helpers: https://github.com/LABSN/sound-ci-helpers diff --git a/.github/actions/virtual-audio-device/action.yml b/.github/actions/virtual-audio-device/action.yml new file mode 100644 index 0000000..9ffecfd --- /dev/null +++ b/.github/actions/virtual-audio-device/action.yml @@ -0,0 +1,18 @@ +name: 'virtual-audio-device' +description: 'set up virtual audio devices for win32, linux, darwin' +inputs: + platform: + description: 'Target platform' + required: true +runs: + using: 'composite' + steps: + - run: '${{ github.action_path }}/linux/setup_sound.sh' + shell: bash -e {0} + if: ${{ inputs.platform == 'linux' }} + - run: '${{ github.action_path }}/macos/setup_sound.sh' + shell: bash -e {0} + if: ${{ inputs.platform == 'darwin' }} + - run: '${{ github.action_path }}/windows/setup_sound.ps1' + shell: powershell -Command {0} + if: ${{ inputs.platform == 'win32' }} diff --git a/.github/actions/virtual-audio-device/linux/setup_sound.sh b/.github/actions/virtual-audio-device/linux/setup_sound.sh new file mode 100755 index 0000000..e48a8fe --- /dev/null +++ b/.github/actions/virtual-audio-device/linux/setup_sound.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -xeo pipefail + +sudo apt update +sudo apt install -y pulseaudio libportaudio2 dbus-x11 libasound-dev +systemctl --user restart pulseaudio.service +systemctl --user restart pulseaudio.socket +pactl list diff --git a/.github/actions/virtual-audio-device/macos/setup_sound.sh b/.github/actions/virtual-audio-device/macos/setup_sound.sh new file mode 100755 index 0000000..1067f95 --- /dev/null +++ b/.github/actions/virtual-audio-device/macos/setup_sound.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -xeo pipefail +brew fetch --retry --cask background-music +brew install --cask background-music +sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod || sudo killall coreaudiod diff --git a/.github/actions/virtual-audio-device/windows/devcon.exe b/.github/actions/virtual-audio-device/windows/devcon.exe new file mode 100755 index 0000000000000000000000000000000000000000..ae9a38963a779c1b0f79b3d51d309a6ed42a2e42 GIT binary patch literal 81408 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 ZmWTE<=`OwBs96(3^rt4Bc)WFY=s%#Z;F$mb literal 0 HcmV?d00001 diff --git a/.github/workflows/prebuild.yml b/.github/workflows/prebuild.yml index afdab65..ad00280 100644 --- a/.github/workflows/prebuild.yml +++ b/.github/workflows/prebuild.yml @@ -31,7 +31,10 @@ jobs: - uses: actions/setup-node@v4 with: node-version: lts/* - - uses: sethvincent/sound-ci-helpers@windows-updates + - uses: ./.github/actions/virtual-audio-device + with: + platform: ${{ matrix.platform }} + arch: ${{ matrix.arch }} - run: | choco install vb-cable -y choco upgrade llvm diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e60b6e..b1621d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,9 @@ jobs: - uses: actions/setup-node@v4 with: node-version: lts/* - - uses: sethvincent/sound-ci-helpers@windows-updates + - uses: ./.github/actions/virtual-audio-device + with: + platform: ${{ matrix.platform }} - run: | choco install vb-cable -y choco upgrade llvm diff --git a/README.md b/README.md index f5557db..fef009e 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ Parameters: The `AudioDevice` API provides functionality to manage SDL audio devices for playback and recording. ```js -const device = await sdl.AudioDevice.open(deviceId[, spec]) +const device = new sdl.AudioDevice(deviceId[, spec]) ``` Parameters: @@ -215,53 +215,7 @@ Parameters: - `channels` (`number`): Number of audio channels (e.g., 2 for stereo) - `freq` (`number`): Sample rate in Hz (e.g., 48000) -**Returns**: A Promise that resolves to a new `AudioDevice` instance - -#### Static Methods - -##### `AudioDevice.playbackDeviceFormats()` - -Gets the audio formats supported by all playback devices. - -**Returns**: `AudioDeviceFormat[]` - Array of audio device formats - -##### `AudioDevice.recordingDeviceFormats()` - -Gets the audio formats supported by all recording devices. - -**Returns**: `AudioDeviceFormat[]` - Array of audio device formats - -##### `AudioDevice.playbackDevices()` - -Gets all available playback devices. - -**Returns**: `Promise` - Promise that resolves to an array of audio devices - -##### `AudioDevice.recordingDevices()` - -Gets all available recording devices. - -**Returns**: `Promise` - Promise that resolves to an array of audio devices - -##### `AudioDevice.defaultPlaybackDevice([spec])` - -Opens the default playback device. - -Parameters: - -- `spec` (`object`, optional): Audio specification (same as `open()`) - -**Returns**: `Promise` - Promise that resolves to the default playback device - -##### `AudioDevice.defaultRecordingDevice([spec])` - -Opens the default recording device. - -Parameters: - -- `spec` (`object`, optional): Audio specification (same as `open()`) - -**Returns**: `Promise` - Promise that resolves to the default recording device +**Returns**: A new `AudioDevice` instance #### Properties @@ -303,12 +257,6 @@ Gets or sets the device gain (volume). #### Methods -##### `AudioDevice.ready()` - -Wait for the device to be ready. - -**Returns**: `Promise` - ##### `AudioDevice.pause()` Pauses audio playback/recording. @@ -325,7 +273,53 @@ Resumes audio playback/recording. Closes the audio device. -**Returns**: `Promise` +**Returns**: `void` + +#### Static Methods + +##### `AudioDevice.playbackDeviceFormats()` + +Gets the audio formats supported by all playback devices. + +**Returns**: `AudioDeviceFormat[]` - Array of audio device formats + +##### `AudioDevice.recordingDeviceFormats()` + +Gets the audio formats supported by all recording devices. + +**Returns**: `AudioDeviceFormat[]` - Array of audio device formats + +##### `AudioDevice.playbackDevices()` + +Gets all available playback devices. + +**Returns**: `AudioDevice[]` - An array of audio devices + +##### `AudioDevice.recordingDevices()` + +Gets all available recording devices. + +**Returns**: `AudioDevice[]` - An array of audio devices + +##### `AudioDevice.defaultPlaybackDevice([spec])` + +Creates a new instance of `AudioDevice` for the default audio playback device. Equivalent to calling `new AudioDevice(constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, spec)` + +Parameters: + +- `spec` (`object`, optional): Audio specification (same as `open()`) + +**Returns**: `AudioDevice` - The default playback device + +##### `AudioDevice.defaultRecordingDevice([spec])` + +Creates a new instance of `AudioDevice` for the default audio recording device. Equivalent to calling `new AudioDevice(constants.SDL_AUDIO_DEVICE_DEFAULT_RECORDING, spec)` + +Parameters: + +- `spec` (`object`, optional): Audio specification (same as `open()`) + +**Returns**: `AudioDevice` - The default recording device ### `AudioDevice.AudioDeviceFormat` diff --git a/binding.cc b/binding.cc index e25dab5..3b83532 100644 --- a/binding.cc +++ b/binding.cc @@ -285,10 +285,11 @@ bare_sdl_open_audio_device( ) { int err; + SDL_AudioSpec spec; SDL_AudioSpec *spec_ptr = nullptr; if (format.has_value() && channels.has_value() && freq.has_value()) { - SDL_AudioSpec spec = { - .format = (SDL_AudioFormat) format.value(), + spec = { + .format = static_cast(format.value()), .channels = channels.value(), .freq = freq.value() }; @@ -390,14 +391,14 @@ bare_sdl_get_audio_device_list_item( return (uint32_t) list->devices[index]; } -static std::string +static std::optional bare_sdl_get_audio_device_name( js_env_t *env, js_receiver_t, uint32_t device_id ) { const char *name = SDL_GetAudioDeviceName((SDL_AudioDeviceID) device_id); - return name ? std::string(name) : std::string(""); + return name; } static double diff --git a/lib/audio-device.js b/lib/audio-device.js index d93680e..76233e2 100644 --- a/lib/audio-device.js +++ b/lib/audio-device.js @@ -1,4 +1,3 @@ -const ReadyResource = require('ready-resource') const binding = require('../binding') const constants = binding.constants @@ -54,13 +53,10 @@ class SDLAudioDeviceFormat { } } -class SDLAudioDevice extends ReadyResource { +class SDLAudioDevice { static AudioSpec = SDLAudioSpec static AudioDeviceFormat = SDLAudioDeviceFormat - /** - * @return []SDLAudioDeviceFormat - */ static playbackDeviceFormats() { const list = binding.getAudioPlaybackDevices() return list.map((deviceId) => { @@ -68,9 +64,6 @@ class SDLAudioDevice extends ReadyResource { }) } - /** - * @return []SDLAudioDeviceFormat - */ static recordingDeviceFormats() { const list = binding.getAudioRecordingDevices() return list.map((deviceId) => { @@ -78,71 +71,34 @@ class SDLAudioDevice extends ReadyResource { }) } - /** - * @return []SDLAudioDevice - */ - static async recordingDevices() { + static recordingDevices() { const list = binding.getAudioRecordingDevices() - return Promise.all( - list.map(async (deviceId) => { - const device = await SDLAudioDevice.open(deviceId) - return device - }) - ) + return list.map((deviceId) => { + return new SDLAudioDevice(deviceId) + }) } - /** - * @return []SDLAudioDevice - */ - static async playbackDevices() { + static playbackDevices() { const list = binding.getAudioPlaybackDevices() - return Promise.all( - list.map(async (deviceId) => { - return SDLAudioDevice.open(deviceId) - }) - ) - } - - /** - * @return SDLAudioDevice - */ - static async defaultRecordingDevice(spec) { - return SDLAudioDevice.open( - constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, - spec - ) + return list.map((deviceId) => { + return new SDLAudioDevice(deviceId) + }) } - /** - * @return SDLAudioDevice - */ - static async defaultPlaybackDevice(spec) { - return SDLAudioDevice.open( - constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, + static defaultRecordingDevice(spec) { + return new SDLAudioDevice( + constants.SDL_AUDIO_DEVICE_DEFAULT_RECORDING, spec ) } - /** - * @return SDLAudioDevice - */ - static async open(deviceId, spec) { - const device = new SDLAudioDevice(deviceId, spec) - await device.ready() - return device + static defaultPlaybackDevice(spec) { + return new SDLAudioDevice(constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, spec) } constructor(deviceId, spec) { - super() this._requestedDeviceId = deviceId this.spec = spec - this.id = null - } - - _open() { - if (this.opening || this.opened) { - return - } const format = this.spec?.format const channels = this.spec?.channels @@ -156,12 +112,16 @@ class SDLAudioDevice extends ReadyResource { ) } - _close() { + destroy() { if (this.closing) return binding.closeAudioDevice(this.id) this.id = null } + [Symbol.dispose]() { + this.destroy() + } + get name() { return binding.getAudioDeviceName(this.id) } diff --git a/package.json b/package.json index 7f5a4da..9564938 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,5 @@ "cmake-harden": "^1.1.2", "prettier": "^3.5.3", "prettier-config-standard": "^7.0.0" - }, - "dependencies": { - "ready-resource": "^1.1.2" } } diff --git a/test/audio-device.js b/test/audio-device.js index de4c858..0224a1a 100644 --- a/test/audio-device.js +++ b/test/audio-device.js @@ -11,30 +11,29 @@ test('sdl.AudioDevice - recordingDeviceFormats', (t) => { t.ok(Array.isArray(formats), 'returns an array') }) -test('sdl.AudioDevice - playbackDevices', async (t) => { - const devices = await sdl.AudioDevice.playbackDevices() +test('sdl.AudioDevice - playbackDevices', (t) => { + const devices = sdl.AudioDevice.playbackDevices() t.ok(Array.isArray(devices), 'returns an array') }) -test('sdl.AudioDevice - recordingDevices', async (t) => { - const devices = await sdl.AudioDevice.recordingDevices() +test('sdl.AudioDevice - recordingDevices', (t) => { + const devices = sdl.AudioDevice.recordingDevices() t.ok(Array.isArray(devices), 'returns an array') }) -test('sdl.AudioDevice - defaultRecordingDevice', async (t) => { - const device = await sdl.AudioDevice.defaultRecordingDevice() +test('sdl.AudioDevice - defaultRecordingDevice', (t) => { + const device = sdl.AudioDevice.defaultRecordingDevice() t.ok(device instanceof sdl.AudioDevice, 'returns sdl.AudioDevice instance') }) -test('sdl.AudioDevice - defaultPlaybackDevice', async (t) => { - const device = await sdl.AudioDevice.defaultPlaybackDevice() +test('sdl.AudioDevice - defaultPlaybackDevice', (t) => { + const device = sdl.AudioDevice.defaultPlaybackDevice() t.ok(device instanceof sdl.AudioDevice, 'returns sdl.AudioDevice instance') }) -test('sdl.AudioDevice - properties', async (t) => { +test('sdl.AudioDevice - properties', (t) => { const spec = { format: sdl.constants.SDL_AUDIO_F32, channels: 2, freq: 48000 } - const device = await sdl.AudioDevice.defaultPlaybackDevice(spec) - await device.ready() + using device = sdl.AudioDevice.defaultPlaybackDevice(spec) t.ok(typeof device.name === 'string') t.ok(device.format instanceof sdl.AudioDevice.AudioDeviceFormat) @@ -44,16 +43,15 @@ test('sdl.AudioDevice - properties', async (t) => { t.is(typeof device.gain, 'number') }) -test('sdl.AudioDevice - set gain', async (t) => { - const device = await sdl.AudioDevice.defaultPlaybackDevice() - await device.ready() +test('sdl.AudioDevice - set gain', (t) => { + using device = sdl.AudioDevice.defaultPlaybackDevice() device.gain = 0.5 t.is(device.gain, 0.5, 'sets gain correctly') }) -test('sdl.AudioDevice - pause/resume', async (t) => { - const device = await sdl.AudioDevice.defaultPlaybackDevice() - await device.ready() +test('sdl.AudioDevice - pause/resume', (t) => { + const device = sdl.AudioDevice.defaultPlaybackDevice() + { const result = device.pause() t.ok(typeof result === 'boolean') @@ -65,8 +63,8 @@ test('sdl.AudioDevice - pause/resume', async (t) => { } }) -test('SDLAudioSpec', async (t) => { - const device = await sdl.AudioDevice.defaultPlaybackDevice() +test('SDLAudioSpec', (t) => { + using device = sdl.AudioDevice.defaultPlaybackDevice() const format = new sdl.AudioDevice.AudioDeviceFormat(device.id) const spec = new sdl.AudioDevice.AudioSpec(format) t.ok(spec && spec._format) From f32d1bd4ba35d2a60452dc14d9c2e1cb1c74233b Mon Sep 17 00:00:00 2001 From: Seth Vincent Date: Wed, 9 Jul 2025 17:43:42 -0700 Subject: [PATCH 3/8] Update binding.cc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kasper Isager Dalsgarð --- binding.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/binding.cc b/binding.cc index 3b83532..b44936a 100644 --- a/binding.cc +++ b/binding.cc @@ -300,8 +300,7 @@ bare_sdl_open_audio_device( auto logical_device_id = SDL_OpenAudioDevice((SDL_AudioDeviceID) requested_device_id, spec_ptr); if (logical_device_id == 0) { - const char *sdl_error = SDL_GetError(); - err = js_throw_error(env, nullptr, sdl_error); + err = js_throw_error(env, nullptr, SDL_GetError()); assert(err == 0); throw js_pending_exception; From 88983bc83ac261d8a81864bfb009c43bc37edd4e Mon Sep 17 00:00:00 2001 From: Seth Vincent Date: Wed, 9 Jul 2025 17:44:00 -0700 Subject: [PATCH 4/8] Update binding.cc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kasper Isager Dalsgarð --- binding.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binding.cc b/binding.cc index b44936a..49c058a 100644 --- a/binding.cc +++ b/binding.cc @@ -384,7 +384,7 @@ bare_sdl_get_audio_device_list_item( int index ) { if (index < 0 || index >= list->count || !list->devices) { - return NULL; + return std::nullopt; } return (uint32_t) list->devices[index]; From 24fd2ac25ccc228b1590a133ffbb20068c2518b3 Mon Sep 17 00:00:00 2001 From: Seth Vincent Date: Wed, 9 Jul 2025 17:44:17 -0700 Subject: [PATCH 5/8] Update binding.cc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kasper Isager Dalsgarð --- binding.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binding.cc b/binding.cc index 49c058a..85738a5 100644 --- a/binding.cc +++ b/binding.cc @@ -387,7 +387,7 @@ bare_sdl_get_audio_device_list_item( return std::nullopt; } - return (uint32_t) list->devices[index]; + return uint32_t(list->devices[index]); } static std::optional From 882550d9bb710ed6ce98d43903e0a3294663502b Mon Sep 17 00:00:00 2001 From: Seth Vincent Date: Wed, 9 Jul 2025 17:44:41 -0700 Subject: [PATCH 6/8] Update README.md Co-authored-by: Tony Gorez --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fef009e..fdaaa5c 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ const device = new sdl.AudioDevice(deviceId[, spec]) Parameters: -- `deviceId` (`number`): The audio device ID. Use `constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK` or `constants.SDL_AUDIO_DEVICE_DEFAULT_RECORDING` for defaults +- `deviceId` (`number`): The audio device ID. Use `constants.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK` or `constants.SDL_AUDIO_DEVICE_DEFAULT_RECORDING` for defaults. - `spec` (`object`, optional): Audio specification with the following properties: - `format` (`number`): Audio format (e.g., `constants.SDL_AUDIO_F32`) - `channels` (`number`): Number of audio channels (e.g., 2 for stereo) From 2bc31aaac8addc6390cb468a67b4c7b89aff3a01 Mon Sep 17 00:00:00 2001 From: sethvincent Date: Wed, 23 Jul 2025 23:22:42 -0700 Subject: [PATCH 7/8] remove unused properties from AudioDevice.toJSON() --- lib/audio-device.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/audio-device.js b/lib/audio-device.js index 76233e2..c28df38 100644 --- a/lib/audio-device.js +++ b/lib/audio-device.js @@ -166,11 +166,7 @@ class SDLAudioDevice { isPlaybackDevice: this.isPlaybackDevice, isPhysicalDevice: this.isPhysicalDevice, isPaused: this.isPaused, - gain: this.gain, - opening: this.opening, - opened: this.opened, - closing: this.closing, - closed: this.closed + gain: this.gain } } } From 3f3af9df1b67ef91fb090fd96ede5f75ab0e28d0 Mon Sep 17 00:00:00 2001 From: sethvincent Date: Wed, 23 Jul 2025 23:26:22 -0700 Subject: [PATCH 8/8] dont run certain audio device tests on linux --- package.json | 3 ++- test/audio-device.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9564938..cfa75f2 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "cmake-fetch": "^1.4.6", "cmake-harden": "^1.1.2", "prettier": "^3.5.3", - "prettier-config-standard": "^7.0.0" + "prettier-config-standard": "^7.0.0", + "which-runtime": "^1.3.0" } } diff --git a/test/audio-device.js b/test/audio-device.js index 0224a1a..ec07b32 100644 --- a/test/audio-device.js +++ b/test/audio-device.js @@ -1,4 +1,5 @@ const test = require('brittle') +const { isLinux } = require('which-runtime') const sdl = require('..') test('sdl.AudioDevice - playbackDeviceFormats', (t) => { @@ -22,16 +23,31 @@ test('sdl.AudioDevice - recordingDevices', (t) => { }) test('sdl.AudioDevice - defaultRecordingDevice', (t) => { + if (isLinux) { + t.pass() + return + } + const device = sdl.AudioDevice.defaultRecordingDevice() t.ok(device instanceof sdl.AudioDevice, 'returns sdl.AudioDevice instance') }) test('sdl.AudioDevice - defaultPlaybackDevice', (t) => { + if (isLinux) { + t.pass() + return + } + const device = sdl.AudioDevice.defaultPlaybackDevice() t.ok(device instanceof sdl.AudioDevice, 'returns sdl.AudioDevice instance') }) test('sdl.AudioDevice - properties', (t) => { + if (isLinux) { + t.pass() + return + } + const spec = { format: sdl.constants.SDL_AUDIO_F32, channels: 2, freq: 48000 } using device = sdl.AudioDevice.defaultPlaybackDevice(spec) @@ -44,12 +60,22 @@ test('sdl.AudioDevice - properties', (t) => { }) test('sdl.AudioDevice - set gain', (t) => { + if (isLinux) { + t.pass() + return + } + using device = sdl.AudioDevice.defaultPlaybackDevice() device.gain = 0.5 t.is(device.gain, 0.5, 'sets gain correctly') }) test('sdl.AudioDevice - pause/resume', (t) => { + if (isLinux) { + t.pass() + return + } + const device = sdl.AudioDevice.defaultPlaybackDevice() { @@ -64,6 +90,11 @@ test('sdl.AudioDevice - pause/resume', (t) => { }) test('SDLAudioSpec', (t) => { + if (isLinux) { + t.pass() + return + } + using device = sdl.AudioDevice.defaultPlaybackDevice() const format = new sdl.AudioDevice.AudioDeviceFormat(device.id) const spec = new sdl.AudioDevice.AudioSpec(format)