feat(audio,windows): real Raw Input + low-level hook global PTT (SDD-083 / SDD-084)
The v1.0.0-rc.7 Windows backends were thread::sleep stubs that
reported optimistic L2GlobalHoldToTalk / L3GlobalWithMouseButtons
descriptors without actually registering for any global key events.
Surfaced on Windows verification as:
* 'even press to talk key was set, still only the hold to talk
button is work for talk'
* 'and displayed as L2GlobalHoldToTalk(raw-input)'
* 'cannot continuous transmission'
This commit implements the real backends:
WindowsRawInputBackend (preferred Windows rung, SDD-083):
* Hidden message-only window via
CreateWindowExW(..., HWND_MESSAGE, ...).
* RegisterRawInputDevices with RIDEV_INPUTSINK on
Usage Page 0x01 / Usage 0x06 (keyboard) and 0x02 (mouse) so
events fire globally — including when Chanora is unfocused.
* WndProc handling WM_INPUT: GetRawInputData ->
keyboard.VKey vs bound vk, or mouse.usButtonFlags vs bound
side-button index. Down -> gate.set(true); up -> gate.set(false).
* Dedicated chanora-rawinput thread runs GetMessageW /
TranslateMessage / DispatchMessageW until stop() posts
WM_QUIT via PostThreadMessageW.
WindowsHookBackend (fallback rung, SDD-084):
* SetWindowsHookExW(WH_KEYBOARD_LL) + WH_MOUSE_LL on a
dedicated chanora-llhook thread.
* Hook procs translate KBDLLHOOKSTRUCT.vkCode and
MSLLHOOKSTRUCT.mouseData against the same shared
AtomicBinding.
* UnhookWindowsHookEx on teardown.
Both backends:
* Honest descriptor() reporting: backends start reporting
L0Focused; level upgrades to L2 / L3 only after a real
arming success (RegisterRawInputDevices or SetWindowsHookEx
returning Ok). This fixes the 'L2 reported but doesn't fire'
complaint by making the badge tell the truth — if Raw Input
registration fails at runtime the user sees the L0Focused
info-icon explanation sheet instead of being told L2 works.
* AtomicBinding (class / vk / mouse_btn) for lock-free hot
path. Translation lives in
crates/chanora_audio/src/ptt_backends/windows_keymap.rs
which maps Flutter LogicalKeyboardKey.keyLabel strings
(e.g. 'Space', 'F10', 'A') to Win32 VK_* codes; mouse
side-button bitmask strings ('mouse-side-button:8' /
':16') to RawInput button indices (4 / 5).
* Per-thread context (thread_local RefCell) carries the
gate + binding to the WndProc / hook proc without needing
raw-pointer user-data plumbing.
Diagnostic logging:
* AudioEngine::start now logs default_input_config and
default_output_config explicitly with the channels /
sample_rate / sample_format that cpal reports, so a
build_*_stream failure on locale-specific Windows hosts
(reported on ko-KR Windows 11 as 'Start Audio Button not
work') becomes diagnosable from the stderr log alone.
* build_output_stream surfaces the requested config in the
tracing::error! record on failure.
Privacy (DEC-027 / SDD-090): the windows.rs and
windows_keymap.rs hot paths NEVER log raw VKs, scan codes,
keysyms, key labels, or button identifiers. Only the
platform-neutral input class ('keyboard' /
'mouse-side-button') and the backend id appear in the tracing
stream. The SDD-090 PttSanitizer Layer is the defence-in-depth
net but this code does not rely on it.
Tests: 4 new windows-only unit tests in windows_keymap (ASCII
letters / digits / Space + Fn / unknown / mouse button index).
They compile only under cfg(target_os = "windows") so the
Linux workspace test count is unchanged at 59/0/3.
Cargo deps: adds windows = '0.54' (target_os = windows) with
the feature set needed for RawInput + hooks. 0.54 matches
the version already transitive through the workspace.
Verified on Linux: cargo check --workspace clean, cargo test
--workspace 59/0/3 (windows-gated tests skip on Linux). The
real exercise of this commit will happen on the Korean Windows
11 host (100.84.219.45) at the next build.
This commit is contained in:
@@ -142,6 +142,11 @@ impl AudioEngine {
|
||||
mut voice_in_rx: mpsc::Receiver<InboundVoice>,
|
||||
) -> Result<Self, AudioError> {
|
||||
let host = cpal::default_host();
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
host_id = ?host.id(),
|
||||
"starting audio engine: cpal host selected"
|
||||
);
|
||||
let in_dev = host
|
||||
.default_input_device()
|
||||
.ok_or(AudioError::NoInputDevice)?;
|
||||
@@ -156,6 +161,45 @@ impl AudioEngine {
|
||||
"starting audio engine"
|
||||
);
|
||||
|
||||
// Log the cpal-reported default configs *before* trying to
|
||||
// open streams, so a downstream stream-build failure can
|
||||
// be cross-referenced against what the platform reported
|
||||
// as its default format. Some locale / driver combinations
|
||||
// on Windows have been observed to expose configs that
|
||||
// accept device enumeration but reject `default_*_config`
|
||||
// afterwards (reported on the ko-KR Windows 11 host as
|
||||
// "Start audio button not work"). Promote what would
|
||||
// otherwise be silent or laconic errors into structured
|
||||
// log records the user can paste back.
|
||||
match in_dev.default_input_config() {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
channels = c.channels(),
|
||||
sample_rate = c.sample_rate().0,
|
||||
sample_format = ?c.sample_format(),
|
||||
"default_input_config reported"
|
||||
),
|
||||
Err(e) => warn!(
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
"default_input_config FAILED — capture will be disabled"
|
||||
),
|
||||
}
|
||||
match out_dev.default_output_config() {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
channels = c.channels(),
|
||||
sample_rate = c.sample_rate().0,
|
||||
sample_format = ?c.sample_format(),
|
||||
"default_output_config reported"
|
||||
),
|
||||
Err(e) => warn!(
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
"default_output_config FAILED — output stream will fail to build"
|
||||
),
|
||||
}
|
||||
|
||||
// A.5 mobile-only preset acknowledgement. On Linux desktop
|
||||
// the flag is ignored; on Android we log it so a future cpal
|
||||
// / Oboe wiring can be observed in the diagnostic export.
|
||||
@@ -702,7 +746,16 @@ where
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map_err(|e| AudioError::Backend(format!("build_output_stream: {e}")))?;
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
requested_channels = config.channels,
|
||||
requested_sample_rate = config.sample_rate.0,
|
||||
"build_output_stream FAILED"
|
||||
);
|
||||
AudioError::Backend(format!("build_output_stream: {e}"))
|
||||
})?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user