Feed the microphone to the ocarina's input streams, with a picker in the Insert menu #937

Merged
AR51AurA merged 11 commits from audio/microphone into main 2026-11-09 17:39:22 +00:00
AR51AurA commented 2026-11-09 02:03:12 +00:00 (Migrated from github.com)

What this adds

The ocarina's microphone mode (play an instrument, or hum) didn't work: the game's microphone functions were placeholder stubs, so the game never received any sound. This PR implements them on SDL's recording devices (SDL3's, or SDL2's capture devices, as the Linux AppImage carries SDL2), and adds a Microphone row to the Insert menu's Gameplay tab:

  • Off: the game receives silence.
  • Default: follows Windows' default recording device.
  • Any named microphone: each recording device Windows knows about is listed by name.

The choice applies straight away and is saved in hookshot_options.ini by device name (microphone_device=), so it survives devices being reordered.

What the game does (from how it calls these functions)

  • At startup, the game calls nn::audio::ListVirtualInputDevices. With the stub returning 0 devices, it never opened an input stream.
    • When it finds a device, it opens two streams: one on that device, one on GetTrueRawAudioInputDevice.
    • Both use the same StreamFormat {PcmInt16, 1 channel, 48000 Hz} and a capacity of 4800 frames.
  • With the ocarina's microphone mode on:
    • It starts the streams with StartInputStream when GetInputStreamState != 2, so 2 means started.
    • Each frame it calls GetInputStreamFilledFrameCount, then ReadInputStream for up to 256 frames.
    • It also calls GetInputStreamStatistics(stream, true). The result comes back through x8, and the game checks the second 64-bit word against 1.
  • The game works out the notes itself, so the host only has to supply raw samples.

Implementation

  • host/nn_audio_in.c (new):
    • Device functions: ListVirtualInputDevices (always 1 device, as on the console), GetDefaultVirtualInputDevice, GetTrueRawAudioInputDevice and IsVirtualInputDeviceStreamFormatSupported.
    • Stream functions: Open (the overload the game uses), Close, Start, Stop, Purge, GetState, Get{Filled,Free,Capacity}FrameCount, Read, GetStatistics (all zeros: nothing lost), and gain get/set.
    • Each stream keeps a handle in the first 8 bytes of the game's InputStreamType, as nn_audio.c does for its own objects.
    • A started stream reads an SDL recording stream, converted to the stream's format (SDL resamples and downmixes).
    • If the game stops reading, anything beyond the stream's capacity is dropped, so audio doesn't build up seconds of delay.
    • The started streams share one source: the microphone is opened once and each stream gets its own copy of what it records.
    • A chosen microphone is always looked up by name in a list made just before opening it (SDL2's ids are places in its list, and the menu's list can be old). Opened again under a new id, the source keeps that id.
    • A chosen microphone that is missing, does not open, stops sending, or is no longer listed: the default stands in (silence if there is none) and it is tried again after 3 s, then twice as long each time up to a minute, said once in the log.
    • While open, a chosen microphone is looked for in a fresh list every 5 s, and at once when the menu shows it as not connected: one that is pulled out but goes on sending zeros (SDL3 on Windows) is treated as gone.
    • With the microphone Off or no SDL, the stream produces silence in real time, so the game keeps reading as it would on the console.
  • host/hos_sdl.c / .h: hos_sdl_mic_*, loaded as optional: on SDL3 a recording stream per microphone; on SDL2 (the Linux AppImage's) a capture device whose callback fills a ring of about a second, read under the device's lock. Both convert to the stream's format.
  • gpu/vk/vk_api.h: MkwVkHost gains mic_count, mic_name and mic_select (the menu's choice: off, the default, or a microphone by its whole name and place in the list), and MKWVK_API_VERSION goes up by one.
  • host/rx_backend.c: connects hos_mic_count, hos_mic_name and hos_mic_select to the menu.
  • gpu/vk/vk_menu.cpp, vk_menu.h:
    • The new row lists the devices again each time the menu opens, keeping the chosen one by name. At the start they're only listed when one was chosen by name (Off and Default don't start SDL audio for it).
    • The name saved in the ini is cut to 60 bytes at a UTF-8 boundary (other readers of the file read 80-byte lines); the host is told the listed device's whole name, and takes a kept name as the start of the device's.
    • A chosen microphone that is unplugged stays chosen, shown as "(not connected)", and is used again when it's back.
    • Changing it doesn't switch the quality preset to Custom.
  • readme.md: the row in the Insert menu's table, and a note under it.
  • HOS_MIC_LOG=1 (off by default, documented at the top of nn_audio_in.c and in the readme) logs a line a second while a stream runs: frames read and peak level.

Testing

  • Game-free check: tests/test_audio_in.py (run by tests/run_all.py, with --cc gcc as the other host C checks). It compiles host/nn_audio_in.c with stand-ins for SDL, the menu and the clock, and drives the streams as the game does. It covers the device list, open, start (state 2), silence at the stream's rate while Off, the microphone's samples once a device is picked, dropping past the capacity, purge, stop and close, a shared source, two devices with one name, names past 60 bytes, a device pulled out that keeps sending zeros, one that stalls, and the retries (46 cases). It also builds the menu's microphone code from vk_menu.cpp and checks duplicates, unplug and replug, Off/Default, and the UTF-8 cut.
  • In game: v2.0.5 plus this change, Windows, AMD Radeon RX 9070 XT (Vulkan driver 2.0.406), microphone: a Focusrite USB audio interface (input "Analogue 1 + 2").
  • Unplugging and replugging, the stand-in and the zero-sending device are covered by the test, not yet tried in game.
  • Linux: the SDL2 path compiles against the POSIX shims and its ring buffer was checked on its own, but it has not been played on a Linux machine yet.
  • Humming and whistling: the ocarina plays back the song hummed or whistled, in place of playing it on the instrument.
  • The game opens both streams at its start (and again later, after closing them), but the microphone itself only opens when the game starts listening, and closes when it stops: several start/stop cycles in one session, and nothing recorded in between:
audio in: stream 0 opened: format 2, 1 channel, 48000 Hz, 4800 frames
audio in: stream 1 opened (raw): format 2, 1 channel, 48000 Hz, 4800 frames
...
audio in: stream 0: Analogue 1 + 2 (Focusrite USB Audio) open (48000 Hz, 1 channel)
audio in: stream 1 (raw): Analogue 1 + 2 (Focusrite USB Audio) open (48000 Hz, 1 channel)
audio in: stream 0 stopped (641726 frames read)
audio in: stream 1 stopped (638430 frames read)

Notes

  • The menu's default is Default rather than Off, because the game itself only opens the microphone while its ocarina microphone mode is in use: nothing is recorded until the player turns that on in the game, which is what the console does. Off is one step to the left.
  • Windows must allow desktop apps to use the microphone (Settings → Privacy & security → Microphone). Otherwise SDL gets silence.
  • The streams' state values (1 stopped, 2 started) and the statistics layout come from how the game uses them, not from SDK documentation.

Fixes #189

### What this adds The ocarina's microphone mode (play an instrument, or hum) didn't work: the game's microphone functions were placeholder stubs, so the game never received any sound. This PR implements them on SDL's recording devices (SDL3's, or SDL2's capture devices, as the Linux AppImage carries SDL2), and adds a **Microphone** row to the Insert menu's Gameplay tab: - **Off:** the game receives silence. - **Default:** follows Windows' default recording device. - **Any named microphone:** each recording device Windows knows about is listed by name. The choice applies straight away and is saved in `hookshot_options.ini` by device name (`microphone_device=`), so it survives devices being reordered. ### What the game does (from how it calls these functions) - At startup, the game calls `nn::audio::ListVirtualInputDevices`. With the stub returning 0 devices, it never opened an input stream. - When it finds a device, it opens two streams: one on that device, one on `GetTrueRawAudioInputDevice`. - Both use the same `StreamFormat` `{PcmInt16, 1 channel, 48000 Hz}` and a capacity of 4800 frames. - With the ocarina's microphone mode on: - It starts the streams with `StartInputStream` when `GetInputStreamState != 2`, so 2 means started. - Each frame it calls `GetInputStreamFilledFrameCount`, then `ReadInputStream` for up to 256 frames. - It also calls `GetInputStreamStatistics(stream, true)`. The result comes back through x8, and the game checks the second 64-bit word against 1. - The game works out the notes itself, so the host only has to supply raw samples. ### Implementation - **`host/nn_audio_in.c` (new):** - Device functions: `ListVirtualInputDevices` (always 1 device, as on the console), `GetDefaultVirtualInputDevice`, `GetTrueRawAudioInputDevice` and `IsVirtualInputDeviceStreamFormatSupported`. - Stream functions: `Open` (the overload the game uses), `Close`, `Start`, `Stop`, `Purge`, `GetState`, `Get{Filled,Free,Capacity}FrameCount`, `Read`, `GetStatistics` (all zeros: nothing lost), and gain get/set. - Each stream keeps a handle in the first 8 bytes of the game's `InputStreamType`, as `nn_audio.c` does for its own objects. - A started stream reads an SDL recording stream, converted to the stream's format (SDL resamples and downmixes). - If the game stops reading, anything beyond the stream's capacity is dropped, so audio doesn't build up seconds of delay. - The started streams share one source: the microphone is opened once and each stream gets its own copy of what it records. - A chosen microphone is always looked up by name in a list made just before opening it (SDL2's ids are places in its list, and the menu's list can be old). Opened again under a new id, the source keeps that id. - A chosen microphone that is missing, does not open, stops sending, or is no longer listed: the default stands in (silence if there is none) and it is tried again after 3 s, then twice as long each time up to a minute, said once in the log. - While open, a chosen microphone is looked for in a fresh list every 5 s, and at once when the menu shows it as not connected: one that is pulled out but goes on sending zeros (SDL3 on Windows) is treated as gone. - With the microphone Off or no SDL, the stream produces silence in real time, so the game keeps reading as it would on the console. - **`host/hos_sdl.c` / `.h`:** `hos_sdl_mic_*`, loaded as optional: on SDL3 a recording stream per microphone; on SDL2 (the Linux AppImage's) a capture device whose callback fills a ring of about a second, read under the device's lock. Both convert to the stream's format. - **`gpu/vk/vk_api.h`:** `MkwVkHost` gains `mic_count`, `mic_name` and `mic_select` (the menu's choice: off, the default, or a microphone by its whole name and place in the list), and `MKWVK_API_VERSION` goes up by one. - **`host/rx_backend.c`:** connects `hos_mic_count`, `hos_mic_name` and `hos_mic_select` to the menu. - **`gpu/vk/vk_menu.cpp`, `vk_menu.h`:** - The new row lists the devices again each time the menu opens, keeping the chosen one by name. At the start they're only listed when one was chosen by name (Off and Default don't start SDL audio for it). - The name saved in the ini is cut to 60 bytes at a UTF-8 boundary (other readers of the file read 80-byte lines); the host is told the listed device's whole name, and takes a kept name as the start of the device's. - A chosen microphone that is unplugged stays chosen, shown as "(not connected)", and is used again when it's back. - Changing it doesn't switch the quality preset to Custom. - `readme.md`: the row in the Insert menu's table, and a note under it. - `HOS_MIC_LOG=1` (off by default, documented at the top of `nn_audio_in.c` and in the readme) logs a line a second while a stream runs: frames read and peak level. ### Testing - Game-free check: `tests/test_audio_in.py` (run by `tests/run_all.py`, with `--cc gcc` as the other host C checks). It compiles `host/nn_audio_in.c` with stand-ins for SDL, the menu and the clock, and drives the streams as the game does. It covers the device list, open, start (state 2), silence at the stream's rate while Off, the microphone's samples once a device is picked, dropping past the capacity, purge, stop and close, a shared source, two devices with one name, names past 60 bytes, a device pulled out that keeps sending zeros, one that stalls, and the retries (46 cases). It also builds the menu's microphone code from `vk_menu.cpp` and checks duplicates, unplug and replug, Off/Default, and the UTF-8 cut. - In game: v2.0.5 plus this change, Windows, AMD Radeon RX 9070 XT (Vulkan driver 2.0.406), microphone: a Focusrite USB audio interface (input "Analogue 1 + 2"). - Unplugging and replugging, the stand-in and the zero-sending device are covered by the test, not yet tried in game. - Linux: the SDL2 path compiles against the POSIX shims and its ring buffer was checked on its own, but it has not been played on a Linux machine yet. - Humming and whistling: the ocarina plays back the song hummed or whistled, in place of playing it on the instrument. - The game opens both streams at its start (and again later, after closing them), but the microphone itself only opens when the game starts listening, and closes when it stops: several start/stop cycles in one session, and nothing recorded in between: ``` audio in: stream 0 opened: format 2, 1 channel, 48000 Hz, 4800 frames audio in: stream 1 opened (raw): format 2, 1 channel, 48000 Hz, 4800 frames ... audio in: stream 0: Analogue 1 + 2 (Focusrite USB Audio) open (48000 Hz, 1 channel) audio in: stream 1 (raw): Analogue 1 + 2 (Focusrite USB Audio) open (48000 Hz, 1 channel) audio in: stream 0 stopped (641726 frames read) audio in: stream 1 stopped (638430 frames read) ``` ### Notes - The menu's default is **Default** rather than Off, because the game itself only opens the microphone while its ocarina microphone mode is in use: nothing is recorded until the player turns that on in the game, which is what the console does. Off is one step to the left. - Windows must allow desktop apps to use the microphone (Settings → Privacy & security → Microphone). Otherwise SDL gets silence. - The streams' state values (1 stopped, 2 started) and the statistics layout come from how the game uses them, not from SDK documentation. Fixes #189
hckrman101 (Migrated from github.com) reviewed 2026-11-09 07:40:34 +00:00
hckrman101 (Migrated from github.com) left a comment

Fun feature! A few issues, the first three being real bugs in the device handling: same-name devices, a mic unplugged mid-game, and the saved device being lost when it's missing at start. The rest are smaller.

Fun feature! A few issues, the first three being real bugs in the device handling: same-name devices, a mic unplugged mid-game, and the saved device being lost when it's missing at start. The rest are smaller.
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:34 +00:00

The chosen mic is matched by name and the first match wins, so of two devices with the same name (two USB Audio Devices is common) the second can never stay selected: pick it (opts.mic=3), and the next menu open (or the start's refresh_mics(&mic_saved_name)) moves it back to index 0, quietly switching the ocarina to the other mic. Preferring the current index when its name still matches, and only searching otherwise, would fix it.

The chosen mic is matched by name and the first match wins, so of two devices with the same name (two `USB Audio Device`s is common) the second can never stay selected: pick it (`opts.mic=3`), and the next menu open (or the start's `refresh_mics(&mic_saved_name)`) moves it back to index 0, quietly switching the ocarina to the other mic. Preferring the current index when its name still matches, and only searching otherwise, would fix it.
@ -426,6 +461,52 @@ int detect_pad_layout() {
if (a == HOS_PAD_SOUTH && b == HOS_PAD_EAST && x == HOS_PAD_NORTH && y == HOS_PAD_WEST) return 2;
return 3;
}
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:34 +00:00

If the saved mic is absent at start, refresh_mics falls back to Default (opts.mic = 1), and the next save() (any option change) writes no microphone_device line, so the choice is lost for good: plug the headset back in and the game stays on Default. Keeping mic_saved_name and writing it back while the device is missing would preserve it.

If the saved mic is absent at start, `refresh_mics` falls back to Default (`opts.mic = 1`), and the next `save()` (any option change) writes no `microphone_device` line, so the choice is lost for good: plug the headset back in and the game stays on Default. Keeping `mic_saved_name` and writing it back while the device is missing would preserve it.
@ -429,0 +490,4 @@
if (mic_key(host->mic_name(i)) == mic_want) at = i;
opts.mic = at >= 0 ? at + 2 : 2 + mic_listed;
} else if (opts.mic >= 2) {
opts.mic = 1;
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:35 +00:00

nit: this chain of "not the preset's" pointer comparisons grows by one per non-graphics option, and forgetting one silently resets the preset to Custom. A preset_exempt flag on Row, set in the rows[] table, would scale better.

nit: this chain of "not the preset's" pointer comparisons grows by one per non-graphics option, and forgetting one silently resets the preset to Custom. A `preset_exempt` flag on `Row`, set in the `rows[]` table, would scale better.
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:35 +00:00

This reader got a bigger buffer for microphone_device, but the ini's other readers still use char line[80] (host/nn_hid.c binds_load, host/nn_applet.c, host/hos_main.c's ultrawide read). A long device name (PipeWire/Windows names easily pass 80 chars) gets split by fgets and its tail read as a separate line, and nn_applet.c's key reader has no whole-line guard. Bumping those buffers too (or skipping over-long lines) would avoid misparses.

This reader got a bigger buffer for `microphone_device`, but the ini's other readers still use `char line[80]` (`host/nn_hid.c` `binds_load`, `host/nn_applet.c`, `host/hos_main.c`'s ultrawide read). A long device name (PipeWire/Windows names easily pass 80 chars) gets split by `fgets` and its tail read as a separate line, and `nn_applet.c`'s key reader has no whole-line guard. Bumping those buffers too (or skipping over-long lines) would avoid misparses.
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:35 +00:00

nit: option 18 is a bare number here, in nn_audio_in.c (rx_option(18, 1)), the vk_api.h comment and the test. Adding it to the HOS_OPT_* enum like its neighbours would keep them from drifting apart.

nit: option `18` is a bare number here, in `nn_audio_in.c` (`rx_option(18, 1)`), the `vk_api.h` comment and the test. Adding it to the `HOS_OPT_*` enum like its neighbours would keep them from drifting apart.
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:34 +00:00

Once the mic is open, this only reports what SDL has buffered. If the device is unplugged mid-game, hos_sdl_mic_available returns 0 forever and ReadInputStream returns 0 frames each frame instead of the promised steady silence, until the user happens to open the Insert menu. Likewise a first open that fails (device busy) is never retried. Falling back to silence when the device stops delivering (or SDL reports it removed), and retrying the open now and then, would cover both.

Once the mic is open, this only reports what SDL has buffered. If the device is unplugged mid-game, `hos_sdl_mic_available` returns 0 forever and `ReadInputStream` returns 0 frames each frame instead of the promised steady silence, until the user happens to open the Insert menu. Likewise a first open that fails (device busy) is never retried. Falling back to silence when the device stops delivering (or SDL reports it removed), and retrying the open now and then, would cover both.
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:34 +00:00

The game starts both its default and its raw input stream, and each opens the mic itself, so the same physical device is opened twice: twice the conversion work, and on some SDL2 backends a second open of a capture device fails, leaving one stream silent. Opening it once and feeding both streams would be safer.

The game starts both its default and its raw input stream, and each opens the mic itself, so the same physical device is opened twice: twice the conversion work, and on some SDL2 backends a second open of a capture device fails, leaving one stream silent. Opening it once and feeding both streams would be safer.
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:34 +00:00

This overload takes the os::SystemEvent but never signals it. If the game waits on that event before ReadInputStream, it will block (or never be woken). Is the event version used by OoT? If so, it should be signalled when frames are available.

This overload takes the `os::SystemEvent` but never signals it. If the game waits on that event before `ReadInputStream`, it will block (or never be woken). Is the event version used by OoT? If so, it should be signalled when frames are available.
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:34 +00:00

hos_mic_count re-enumerates the devices while holding mic_lock, and the menu calls it on the render thread every time the menu opens. On SDL2, re-detecting capture devices can take tens of ms with WASAPI/PulseAudio, which stalls the presented frame, and the game's audio-in calls block on the same lock meanwhile. Enumerating outside the lock and swapping the list in under it would avoid both.

`hos_mic_count` re-enumerates the devices while holding `mic_lock`, and the menu calls it on the render thread every time the menu opens. On SDL2, re-detecting capture devices can take tens of ms with WASAPI/PulseAudio, which stalls the presented frame, and the game's audio-in calls block on the same lock meanwhile. Enumerating outside the lock and swapping the list in under it would avoid both.
hckrman101 (Migrated from github.com) commented 2026-11-09 07:40:34 +00:00

nit: the lazily initialised CRITICAL_SECTION with a spin on lock_state can be replaced by static SRWLOCK lock = SRWLOCK_INIT; with AcquireSRWLockExclusive/ReleaseSRWLockExclusive, as hos_thread.c / hos_svc.c already do. No init race to manage.

nit: the lazily initialised `CRITICAL_SECTION` with a spin on `lock_state` can be replaced by `static SRWLOCK lock = SRWLOCK_INIT;` with `AcquireSRWLockExclusive`/`ReleaseSRWLockExclusive`, as `hos_thread.c` / `hos_svc.c` already do. No init race to manage.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:07:04 +00:00
AR51AurA (Migrated from github.com) commented 2026-11-09 08:07:04 +00:00

Fixed. The row keeps its place when the device there still has the chosen name, so the second of two same-named microphones stays chosen across menu opens and restarts. The host now opens the exact device chosen (by the id it was chosen as), not the first of its name, and only falls back to the first of its name when that device is gone and comes back under a new id. tests/test_audio_in.py covers both sides with two same-named devices. I only have one microphone, so I haven’t been able to try two same-named ones on real hardware.

Fixed. The row keeps its place when the device there still has the chosen name, so the second of two same-named microphones stays chosen across menu opens and restarts. The host now opens the exact device chosen (by the id it was chosen as), not the first of its name, and only falls back to the first of its name when that device is gone and comes back under a new id. `tests/test_audio_in.py` covers both sides with two same-named devices. I only have one microphone, so I haven’t been able to try two same-named ones on real hardware.
Cretezy commented 2026-11-09 08:08:04 +00:00 (Migrated from github.com)

Also, does this support mics on controllers, e.g. Switch Pro Controller?

Also, does this support mics on controllers, e.g. Switch Pro Controller?
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:08:12 +00:00
@ -426,6 +461,52 @@ int detect_pad_layout() {
if (a == HOS_PAD_SOUTH && b == HOS_PAD_EAST && x == HOS_PAD_NORTH && y == HOS_PAD_WEST) return 2;
return 3;
}
AR51AurA (Migrated from github.com) commented 2026-11-09 08:08:11 +00:00

Fixed: the chosen name is kept while the device is missing. Default stands in, the row says “(name: not found)”, the name stays in the ini, and it’s re-selected when the device is back.

Fixed: the chosen name is kept while the device is missing. Default stands in, the row says “(name: not found)”, the name stays in the ini, and it’s re-selected when the device is back.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:13:59 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:13:58 +00:00

Fixed: a microphone that sends nothing for 2 s is closed, the streams get silence at their rate, and the open is retried every 3 s, by name. A failed first open goes the same way.

Fixed: a microphone that sends nothing for 2 s is closed, the streams get silence at their rate, and the open is retried every 3 s, by name. A failed first open goes the same way.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 08:14:13 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks for the quick turnaround, this is much better: the shared source, the SRW lock, HOS_OPT_MIC, preset_exempt, keeping the saved name and the 60-byte cut all look good to me. Dropping the SystemEvent overload makes sense too. A few things in the new retry path still stand out, mainly the first two.

Thanks for the quick turnaround, this is much better: the shared source, the SRW lock, `HOS_OPT_MIC`, `preset_exempt`, keeping the saved name and the 60-byte cut all look good to me. Dropping the SystemEvent overload makes sense too. A few things in the new retry path still stand out, mainly the first two.
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:14 +00:00

nit: cutting at 60 bytes can split a UTF-8 character (non-ASCII device names get a broken character in the ini and in "(name: not found)"). Backing up to a character boundary would avoid that.

nit: cutting at 60 bytes can split a UTF-8 character (non-ASCII device names get a broken character in the ini and in "(name: not found)"). Backing up to a character boundary would avoid that.
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:14 +00:00

refresh_mics only runs when the menu opens, which gives two different behaviours for the same unplug. With the menu closed, the host keeps retrying the missing mic and the game gets silence (not the default the readme mentions). With the menu opened meanwhile, the row falls back to Default (choice 1), so the host stops wanting the mic and it never comes back on its own when plugged in again. Having the host fall back to the default device while the chosen one is missing, and keep retrying the chosen one, would match the readme in both cases.

`refresh_mics` only runs when the menu opens, which gives two different behaviours for the same unplug. With the menu closed, the host keeps retrying the missing mic and the game gets silence (not the default the readme mentions). With the menu opened meanwhile, the row falls back to Default (choice 1), so the host stops wanting the mic and it never comes back on its own when plugged in again. Having the host fall back to the default device while the chosen one is missing, and keep retrying the chosen one, would match the readme in both cases.
@ -335,0 +426,4 @@
/* (no changes allowed: SDL converts the device's sound to this. SDL2's id is a place in its list, so the caller takes
it from a list made just before (host/nn_audio_in.c find_device). SDL2 opens a device by its name: of two with one
name it takes the first, whichever was chosen; telling them apart is SDL3's) */
const char *name = id ? ((const char *(*)(int, int))rec.DeviceName2)((int)id - 1, 1) : NULL;
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:14 +00:00

On SDL2 (the Linux AppImage) this opens the device by name, so with two same-named mics SDL opens the first one even though want_id points at the second: the menu says one mic, the game hears the other. That's an SDL2 limitation, so maybe just worth a comment, or noting that the same-name case is SDL3-only.

On SDL2 (the Linux AppImage) this opens the device by *name*, so with two same-named mics SDL opens the first one even though `want_id` points at the second: the menu says one mic, the game hears the other. That's an SDL2 limitation, so maybe just worth a comment, or noting that the same-name case is SDL3-only.
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:14 +00:00

The retry looks in the cached list first and only lists again when the name isn't there. After an unplug/replug, the cached list (only refreshed by hos_mic_count, i.e. on menu open) still has the mic's name with its old id, so pick returns that id, list_now never runs, and hos_sdl_mic_open(old id) fails every 3 s until the menu is opened. On SDL2 the id is an index, so it can open whichever device now sits at that index. Listing again when the cached id fails to open (or always on a retry) would fix it.

The retry looks in the cached list first and only lists again when the name isn't there. After an unplug/replug, the cached list (only refreshed by `hos_mic_count`, i.e. on menu open) still has the mic's name with its *old* id, so `pick` returns that id, `list_now` never runs, and `hos_sdl_mic_open(old id)` fails every 3 s until the menu is opened. On SDL2 the id is an index, so it can open whichever device now sits at that index. Listing again when the cached id fails to open (or always on a retry) would fix it.
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:14 +00:00

With no recording device at all (choice 1, the default), this retries every 3 s for the whole session once the streams are started: hos_sdl_mic_open runs on the game's thread under the exclusive lock (a WASAPI activation can take tens of ms), and hos_sdl.c logs "sdl: the microphone did not open" on every attempt. The failures gate here only covers this file's own line. Backing the retry off (e.g. doubling up to a minute) and gating the SDL log line too would avoid the stalls and the log growth.

With no recording device at all (choice 1, the default), this retries every 3 s for the whole session once the streams are started: `hos_sdl_mic_open` runs on the game's thread under the exclusive lock (a WASAPI activation can take tens of ms), and `hos_sdl.c` logs "sdl: the microphone did not open" on every attempt. The `failures` gate here only covers this file's own line. Backing the retry off (e.g. doubling up to a minute) and gating the SDL log line too would avoid the stalls and the log growth.
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:14 +00:00

A device that opens but never sends data (a failed ResumeAudioStreamDevice is only logged) loops forever: 2 s of quiet closes it, the 3 s retry reopens it, and failures is reset to 0 on the successful open, so it logs two lines and reopens the device every ~5 s. Not resetting failures until data has actually been heard would keep it quiet.

A device that opens but never sends data (a failed `ResumeAudioStreamDevice` is only logged) loops forever: 2 s of quiet closes it, the 3 s retry reopens it, and `failures` is reset to 0 on the successful open, so it logs two lines and reopens the device every ~5 s. Not resetting `failures` until data has actually been heard would keep it quiet.
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:14 +00:00

nit: silence_from_ns is never moved forward while a source stays silent, so (now - silence_from_ns) * rate overflows int64 after ~53 h at 48 kHz. Unlikely, but rebasing silence_from_ns/silence_made every so often (or computing in seconds + remainder) avoids it.

nit: `silence_from_ns` is never moved forward while a source stays silent, so `(now - silence_from_ns) * rate` overflows int64 after ~53 h at 48 kHz. Unlikely, but rebasing `silence_from_ns`/`silence_made` every so often (or computing in seconds + remainder) avoids it.
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:14 +00:00

minor: if the game stops polling for a while, the whole SDL backlog is pushed through every stream ring in 4 KB pieces under the lock, though only the newest capacity is kept. Dropping everything beyond the largest stream capacity first would make catching up cheap.

minor: if the game stops polling for a while, the whole SDL backlog is pushed through every stream ring in 4 KB pieces under the lock, though only the newest capacity is kept. Dropping everything beyond the largest stream capacity first would make catching up cheap.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 08:14:17 +00:00
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:17 +00:00

Thanks, the menu side looks right now, and the exact id fixes it on SDL3. On SDL2 the open is still by name (see the new comment on hos_sdl.c), so there it still opens the first of the two.

Thanks, the menu side looks right now, and the exact id fixes it on SDL3. On SDL2 the open is still by name (see the new comment on `hos_sdl.c`), so there it still opens the first of the two.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 08:14:19 +00:00
@ -426,6 +461,52 @@ int detect_pad_layout() {
if (a == HOS_PAD_SOUTH && b == HOS_PAD_EAST && x == HOS_PAD_NORTH && y == HOS_PAD_WEST) return 2;
return 3;
}
hckrman101 (Migrated from github.com) commented 2026-11-09 08:14:19 +00:00

Confirmed, keeping mic_want and writing it back while the device is missing works. Thanks!

Confirmed, keeping `mic_want` and writing it back while the device is missing works. Thanks!
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:14:21 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:14:21 +00:00

Fixed: started streams of the same format share one source, opened once, and each stream gets its own buffer of what it records.

Fixed: started streams of the same format share one source, opened once, and each stream gets its own buffer of what it records.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:15:55 +00:00
AR51AurA (Migrated from github.com) commented 2026-11-09 08:15:55 +00:00

Went the other way: microphone_device= is cut to 60 bytes, so the line always fits the 80-byte readers without touching them. Names are matched on that prefix.

Went the other way: microphone_device= is cut to 60 bytes, so the line always fits the 80-byte readers without touching them. Names are matched on that prefix.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:15:56 +00:00
@ -429,0 +490,4 @@
if (mic_key(host->mic_name(i)) == mic_want) at = i;
opts.mic = at >= 0 ? at + 2 : 2 + mic_listed;
} else if (opts.mic >= 2) {
opts.mic = 1;
AR51AurA (Migrated from github.com) commented 2026-11-09 08:15:56 +00:00

Done: Row::preset_exempt, set on the existing exempt rows and the Microphone row. test_mouse_settings and test_gpu_choice now check the flag.

Done: Row::preset_exempt, set on the existing exempt rows and the Microphone row. test_mouse_settings and test_gpu_choice now check the flag.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:15:57 +00:00
AR51AurA (Migrated from github.com) commented 2026-11-09 08:15:57 +00:00

Done: HOS_OPT_MIC in vk_api.h.

Done: HOS_OPT_MIC in vk_api.h.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:15:58 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:15:58 +00:00

Done: static SRWLOCK_INIT locks.

Done: static SRWLOCK_INIT locks.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:16:00 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:16:00 +00:00

Fixed: enumeration happens with no lock held, and the device list has its own lock, separate from the streams’.

Fixed: enumeration happens with no lock held, and the device list has its own lock, separate from the streams’.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:16:01 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:16:01 +00:00

OoT only imports the plain overload, so I removed the event one. Its stub will log a call if anything ever uses it.

OoT only imports the plain overload, so I removed the event one. Its stub will log a call if anything ever uses it.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:37:10 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:37:09 +00:00

Fixed: a retry always looks the device up in a freshly made list, so a re-plugged device under a new id (or a new SDL2 index) is found without opening the menu.

Fixed: a retry always looks the device up in a freshly made list, so a re-plugged device under a new id (or a new SDL2 index) is found without opening the menu.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:38:37 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:38:37 +00:00

Fixed: retries back off from 3 s, doubling up to 60 s, including for the default. hos_sdl_mic_open no longer logs; the caller logs once with hos_sdl_error(). The open still runs on the game’s thread, but at most once a minute when nothing is there. Moving it to a worker could be a follow-up.

Fixed: retries back off from 3 s, doubling up to 60 s, including for the default. hos_sdl_mic_open no longer logs; the caller logs once with hos_sdl_error(). The open still runs on the game’s thread, but at most once a minute when nothing is there. Moving it to a worker could be a follow-up.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:39:35 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:39:35 +00:00

Fixed: the counter and the waits only start over when the wanted microphone actually gives sound, so a silent device backs off like a missing one and is logged once. A device whose ResumeAudioStreamDevice fails now counts as not opened.

Fixed: the counter and the waits only start over when the wanted microphone actually gives sound, so a silent device backs off like a missing one and is logged once. A device whose ResumeAudioStreamDevice fails now counts as not opened.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:40:12 +00:00
AR51AurA (Migrated from github.com) commented 2026-11-09 08:40:12 +00:00

Fixed by telling the host the choice by name (mic_select) instead of option 18. An unplugged choice stays chosen as a ‘(not connected)’ entry, and the host lets the default stand in and reconnects on its own, with or without the menu.

Fixed by telling the host the choice by name (mic_select) instead of option 18. An unplugged choice stays chosen as a ‘(not connected)’ entry, and the host lets the default stand in and reconnects on its own, with or without the menu.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:40:50 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:40:50 +00:00

Fixed: the start moves forward in whole seconds, and the sum is done in seconds plus the rest.

Fixed: the start moves forward in whole seconds, and the sum is done in seconds plus the rest.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:41:02 +00:00
AR51AurA (Migrated from github.com) commented 2026-11-09 08:41:02 +00:00

Fixed: cut at a character’s start.

Fixed: cut at a character’s start.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 08:41:16 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 08:41:16 +00:00

Fixed: the backlog beyond the largest stream’s capacity is read and dropped before anything is copied into the streams.

Fixed: the backlog beyond the largest stream’s capacity is read and dropped before anything is copied into the streams.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 09:40:28 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks, the retry path is a lot more solid now: the fresh list on retries, the backoff with one log line per streak, failures resetting only when sound arrives, the default standing in while the chosen mic is missing, and the silence clock all look good. One regression from the name cut stands out, plus a few gaps in the unplug handling.

Thanks, the retry path is a lot more solid now: the fresh list on retries, the backoff with one log line per streak, `failures` resetting only when sound arrives, the default standing in while the chosen mic is missing, and the silence clock all look good. One regression from the name cut stands out, plus a few gaps in the unplug handling.
hckrman101 (Migrated from github.com) commented 2026-11-09 09:40:28 +00:00

This now hands the host mic_want, which is cut to kMicNameKept (60) bytes, but the host still compares it to the full SDL name with strcmp (nn_audio_in.c pick at 129, and the want_id check at 205). So any mic whose name is over 60 bytes is never found. For example "Microphone Array (Intel® Smart Sound Technology for Digital Microphones)" is 72 bytes. The host logs it as not plugged in, the default stands in, and retries back off to every 60 s, while the menu shows it as chosen and connected. Before this commit the host took the full name from dev_names. Passing the full host->mic_name(index) when the mic is listed (keeping the cut only for the ini), or matching by prefix when want is exactly kMicNameKept long, would fix it.

This now hands the host `mic_want`, which is cut to `kMicNameKept` (60) bytes, but the host still compares it to the full SDL name with `strcmp` (`nn_audio_in.c` `pick` at 129, and the `want_id` check at 205). So any mic whose name is over 60 bytes is never found. For example "Microphone Array (Intel® Smart Sound Technology for Digital Microphones)" is 72 bytes. The host logs it as not plugged in, the default stands in, and retries back off to every 60 s, while the menu shows it as chosen and connected. Before this commit the host took the full name from `dev_names`. Passing the full `host->mic_name(index)` when the mic is listed (keeping the cut only for the ini), or matching by prefix when `want` is exactly `kMicNameKept` long, would fix it.
hckrman101 (Migrated from github.com) commented 2026-11-09 09:40:28 +00:00

minor: refresh_mics() now runs on every start, not just when a mic name was saved, so every launch with an ini loads SDL audio and lists recording devices on the render thread during start-up, even with the mic Off or on Default. Calling only mic_tell_host() when mic_want is empty would keep that cost to the people who chose a mic.

minor: `refresh_mics()` now runs on every start, not just when a mic name was saved, so every launch with an ini loads SDL audio and lists recording devices on the render thread during start-up, even with the mic Off or on Default. Calling only `mic_tell_host()` when `mic_want` is empty would keep that cost to the people who chose a mic.
@ -335,0 +426,4 @@
/* (no changes allowed: SDL converts the device's sound to this. SDL2's id is a place in its list, so the caller takes
it from a list made just before (host/nn_audio_in.c find_device). SDL2 opens a device by its name: of two with one
name it takes the first, whichever was chosen; telling them apart is SDL3's) */
const char *name = id ? ((const char *(*)(int, int))rec.DeviceName2)((int)id - 1, 1) : NULL;
hckrman101 (Migrated from github.com) commented 2026-11-09 09:40:28 +00:00

On SDL2 the id is a list index, and the first open after a menu change or stream start (connect(src, 0)) takes it from the menu's cached list, which can be hours old. If the device order changed since (a headset connected, a webcam unplugged), index id-1 is now another device, or out of range and so NULL, which opens the default. Either way it opens as the wanted mic, gives sound, and is never retried. Opening by the name pick matched, or always listing fresh in connect, would avoid it.

On SDL2 the id is a list index, and the first open after a menu change or stream start (`connect(src, 0)`) takes it from the menu's cached list, which can be hours old. If the device order changed since (a headset connected, a webcam unplugged), index `id-1` is now another device, or out of range and so `NULL`, which opens the default. Either way it opens as the wanted mic, gives sound, and is never retried. Opening by the name `pick` matched, or always listing fresh in `connect`, would avoid it.
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
hckrman101 (Migrated from github.com) commented 2026-11-09 09:40:28 +00:00

The default only stands in when the wanted mic fails to open. A mic that still opens but stops sending (a stalled driver) goes quiet, then closes into silence, then reopens on retry and goes quiet again, and never gets the default the header comment promises. Opening the stand-in in the quiet branch (line ~251) when mode == 2 would match the "not plugged in" case.

The default only stands in when the wanted mic fails to *open*. A mic that still opens but stops sending (a stalled driver) goes quiet, then closes into silence, then reopens on retry and goes quiet again, and never gets the default the header comment promises. Opening the stand-in in the quiet branch (line ~251) when `mode == 2` would match the "not plugged in" case.
@ -0,0 +227,4 @@
}
src->mode = mode, src->want_id = want_id;
snprintf(src->want, sizeof src->want, "%s", want);
src->failures = 0, src->retry_wait = RETRY_NS;
hckrman101 (Migrated from github.com) commented 2026-11-09 09:40:28 +00:00

When a retry reopens the wanted mic under a new id from the fresh list, src->want_id keeps the old one, as it does when the source was created while the mic was missing (0). The next menu open then sends the new id, same is false, and the working mic is closed and reopened: a short audio gap, a new log line, and the backoff reset. Updating src->want_id to the id pick returned whenever the wanted mic opens would avoid that.

When a retry reopens the wanted mic under a new id from the fresh list, `src->want_id` keeps the old one, as it does when the source was created while the mic was missing (0). The next menu open then sends the new id, `same` is false, and the working mic is closed and reopened: a short audio gap, a new log line, and the backoff reset. Updating `src->want_id` to the id `pick` returned whenever the wanted mic opens would avoid that.
@ -0,0 +267,4 @@
hos_log("audio in: %s is back", wanted(src));
src->failures = 0, src->retry_wait = RETRY_NS;
}
} else if (now - src->heard_ns > QUIET_NS) { /* (pulled out, or its driver stopped) */
hckrman101 (Migrated from github.com) commented 2026-11-09 09:40:28 +00:00

Unplug detection still relies only on "no bytes for 2 s". If I remember SDL3 right, a removed recording device becomes a zombie that keeps delivering zeros, so on Windows heard stays set, the stand-in never opens and there's no retry: the ocarina just gets silence. Opening the menu doesn't help either, because choose() treats index -1 ("not connected") with the same name as the same choice. Handling SDL_EVENT_AUDIO_DEVICE_REMOVED, or checking now and then that the open device's id is still in a fresh list, would cover it. Treating sel_index == -1 as "missing" while that mic is open would help too.

Unplug detection still relies only on "no bytes for 2 s". If I remember SDL3 right, a removed recording device becomes a zombie that keeps delivering zeros, so on Windows `heard` stays set, the stand-in never opens and there's no retry: the ocarina just gets silence. Opening the menu doesn't help either, because `choose()` treats index -1 ("not connected") with the same name as the same choice. Handling `SDL_EVENT_AUDIO_DEVICE_REMOVED`, or checking now and then that the open device's id is still in a fresh list, would cover it. Treating `sel_index == -1` as "missing" while that mic is open would help too.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 16:41:52 +00:00
AR51AurA (Migrated from github.com) commented 2026-11-09 16:41:52 +00:00

Good catch, thanks. Fixed in e77bd79. The 60-byte cut now only applies to what’s saved in the ini. For a device that’s in the list, the menu sends the host its whole name. For one that isn’t (unplugged, so the menu only has the saved name), the host treats the saved name as the start of the device’s name. The test covers a 72-byte name found both ways.

Good catch, thanks. Fixed in e77bd79. The 60-byte cut now only applies to what’s saved in the ini. For a device that’s in the list, the menu sends the host its whole name. For one that isn’t (unplugged, so the menu only has the saved name), the host treats the saved name as the start of the device’s name. The test covers a 72-byte name found both ways.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 16:41:54 +00:00
@ -0,0 +267,4 @@
hos_log("audio in: %s is back", wanted(src));
src->failures = 0, src->retry_wait = RETRY_NS;
}
} else if (now - src->heard_ns > QUIET_NS) { /* (pulled out, or its driver stopped) */
AR51AurA (Migrated from github.com) commented 2026-11-09 16:41:54 +00:00

Fixed in e77bd79. While a chosen microphone is open, the host checks for it in a fresh device list every 5 s. When the menu shows it as “not connected”, the host checks straight away. If it’s gone, it’s closed, Default stands in, and the usual retries start. The test has a device that’s pulled out but keeps sending zeros.

Fixed in e77bd79. While a chosen microphone is open, the host checks for it in a fresh device list every 5 s. When the menu shows it as “not connected”, the host checks straight away. If it’s gone, it’s closed, Default stands in, and the usual retries start. The test has a device that’s pulled out but keeps sending zeros.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 16:41:55 +00:00
@ -335,0 +426,4 @@
/* (no changes allowed: SDL converts the device's sound to this. SDL2's id is a place in its list, so the caller takes
it from a list made just before (host/nn_audio_in.c find_device). SDL2 opens a device by its name: of two with one
name it takes the first, whichever was chosen; telling them apart is SDL3's) */
const char *name = id ? ((const char *(*)(int, int))rec.DeviceName2)((int)id - 1, 1) : NULL;
AR51AurA (Migrated from github.com) commented 2026-11-09 16:41:55 +00:00

Fixed in e77bd79. The host no longer trusts a list position from earlier. Every open looks the device up by name in a list made just before opening, so an old position from the menu can’t open the wrong device.

Fixed in e77bd79. The host no longer trusts a list position from earlier. Every open looks the device up by name in a list made just before opening, so an old position from the menu can’t open the wrong device.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 16:41:57 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
AR51AurA (Migrated from github.com) commented 2026-11-09 16:41:57 +00:00

Fixed in e77bd79. A chosen microphone that stops sending is now handled like one that doesn’t open: Default stands in and it’s retried with the same backoff.

Fixed in e77bd79. A chosen microphone that stops sending is now handled like one that doesn’t open: Default stands in and it’s retried with the same backoff.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 16:41:58 +00:00
@ -0,0 +227,4 @@
}
src->mode = mode, src->want_id = want_id;
snprintf(src->want, sizeof src->want, "%s", want);
src->failures = 0, src->retry_wait = RETRY_NS;
AR51AurA (Migrated from github.com) commented 2026-11-09 16:41:58 +00:00

Fixed in e77bd79. When the microphone opens again under a new id, the source keeps the new id, so the menu’s next message about it counts as the same choice and doesn’t reopen it. There’s a test case for this.

Fixed in e77bd79. When the microphone opens again under a new id, the source keeps the new id, so the menu’s next message about it counts as the same choice and doesn’t reopen it. There’s a test case for this.
AR51AurA (Migrated from github.com) reviewed 2026-11-09 16:41:59 +00:00
AR51AurA (Migrated from github.com) commented 2026-11-09 16:41:59 +00:00

Fixed in e77bd79. At startup the menu only lists microphones when one was chosen by name. With Off or Default it just tells the host the choice, so SDL audio isn’t started for this.

Fixed in e77bd79. At startup the menu only lists microphones when one was chosen by name. With Off or Default it just tells the host the choice, so SDL audio isn’t started for this.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 17:31:15 +00:00
hckrman101 (Migrated from github.com) commented 2026-11-09 17:31:15 +00:00

Confirmed, thanks. The whole name goes to the host when the mic is listed, and same_name takes the cut one as a prefix when it isn't, so long names are found both ways.

Confirmed, thanks. The whole name goes to the host when the mic is listed, and `same_name` takes the cut one as a prefix when it isn't, so long names are found both ways.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 17:31:16 +00:00
@ -0,0 +267,4 @@
hos_log("audio in: %s is back", wanted(src));
src->failures = 0, src->retry_wait = RETRY_NS;
}
} else if (now - src->heard_ns > QUIET_NS) { /* (pulled out, or its driver stopped) */
hckrman101 (Migrated from github.com) commented 2026-11-09 17:31:16 +00:00

Looks good. The 5 s check against a fresh list plus the immediate check when the menu shows it as not connected covers the zombie case. One small leftover: the check matches by name, so with two mics of the same name, pulling out the chosen one isn't noticed while the other is still listed. On SDL3 comparing the id with want_id would catch it, but that's an edge case and not blocking.

Looks good. The 5 s check against a fresh list plus the immediate check when the menu shows it as not connected covers the zombie case. One small leftover: the check matches by name, so with two mics of the same name, pulling out the chosen one isn't noticed while the other is still listed. On SDL3 comparing the id with `want_id` would catch it, but that's an edge case and not blocking.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 17:31:18 +00:00
@ -335,0 +426,4 @@
/* (no changes allowed: SDL converts the device's sound to this. SDL2's id is a place in its list, so the caller takes
it from a list made just before (host/nn_audio_in.c find_device). SDL2 opens a device by its name: of two with one
name it takes the first, whichever was chosen; telling them apart is SDL3's) */
const char *name = id ? ((const char *(*)(int, int))rec.DeviceName2)((int)id - 1, 1) : NULL;
hckrman101 (Migrated from github.com) commented 2026-11-09 17:31:18 +00:00

Confirmed, find_device always lists fresh now, so a stale index from the menu can't open the wrong device.

Confirmed, `find_device` always lists fresh now, so a stale index from the menu can't open the wrong device.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 17:31:20 +00:00
@ -0,0 +1,501 @@
/* (hookshot) nn::audio's input streams: the microphone, as the ocarina's "play with your voice / an instrument" option uses it.
hckrman101 (Migrated from github.com) commented 2026-11-09 17:31:20 +00:00

Confirmed, lost() puts the default in for a stalled mic the same way as one that doesn't open.

Confirmed, `lost()` puts the default in for a stalled mic the same way as one that doesn't open.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 17:31:21 +00:00
@ -0,0 +227,4 @@
}
src->mode = mode, src->want_id = want_id;
snprintf(src->want, sizeof src->want, "%s", want);
src->failures = 0, src->retry_wait = RETRY_NS;
hckrman101 (Migrated from github.com) commented 2026-11-09 17:31:21 +00:00

Confirmed, want_id follows the id it was last opened under, and the test covers the menu's next message.

Confirmed, `want_id` follows the id it was last opened under, and the test covers the menu's next message.
hckrman101 (Migrated from github.com) reviewed 2026-11-09 17:31:23 +00:00
hckrman101 (Migrated from github.com) commented 2026-11-09 17:31:23 +00:00

Confirmed, thanks.

Confirmed, thanks.
hckrman101 (Migrated from github.com) approved these changes 2026-11-09 17:31:35 +00:00
hckrman101 (Migrated from github.com) left a comment

All the points from the last round are fixed in e77bd79, and the merge doesn't touch the mic code. LGTM.

Leftover nits, none blocking:

  • the 5 s unplug check matches by name, so with two same-named mics, pulling out the chosen one goes unnoticed while the other one is still listed (on SDL3, comparing against want_id would catch it)
  • on SDL2, if the device order changes while the mic is open, the menu's next message has a different index-based id, so the mic is closed and reopened once (short gap)

Cretezy's question about controller mics is still unanswered above, might be worth a word.

All the points from the last round are fixed in e77bd79, and the merge doesn't touch the mic code. LGTM. Leftover nits, none blocking: - the 5 s unplug check matches by name, so with two same-named mics, pulling out the chosen one goes unnoticed while the other one is still listed (on SDL3, comparing against `want_id` would catch it) - on SDL2, if the device order changes while the mic is open, the menu's next message has a different index-based id, so the mic is closed and reopened once (short gap) Cretezy's question about controller mics is still unanswered above, might be worth a word.
AR51AurA commented 2026-11-09 17:52:32 +00:00 (Migrated from github.com)

Also, does this support mics on controllers, e.g. Switch Pro Controller?

It should, I got it working on my dualsense, but that's the only controller I have that has a microphone to test, in game reports the volume being low unless I'm right against it, though that could be down to the controller. As long as the device has a microphone that's registered, it should work.

> Also, does this support mics on controllers, e.g. Switch Pro Controller? It should, I got it working on my dualsense, but that's the only controller I have that has a microphone to test, in game reports the volume being low unless I'm right against it, though that could be down to the controller. As long as the device has a microphone that's registered, it should work.
Commenting is not possible because the repository is archived.
No description provided.