Skip to content

Commit 203e3e0

Browse files
committed
wasapi: tighten format-tag mapping and parity tests
1 parent 0ada539 commit 203e3e0

File tree

4 files changed

+133
-61
lines changed

4 files changed

+133
-61
lines changed

.beads/issues.jsonl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
{"id":"moon_cpal-44r","title":"Release: publish 0.11.0","description":"Bump moon_cpal to 0.11.0 (breaking-change bump within 0.x policy) and publish.","acceptance_criteria":"- moon.mod.json version is 0.11.0\\n- moon publish returns 200 OK","status":"closed","priority":2,"issue_type":"chore","owner":"842376130@qq.com","created_at":"2026-02-15T19:08:45.58169+08:00","created_by":"Milky2018","updated_at":"2026-02-15T19:09:30.370778+08:00","closed_at":"2026-02-15T19:09:30.370778+08:00","close_reason":"0.11.0 published","comments":[{"id":53,"issue_id":"moon_cpal-44r","author":"Milky2018","text":"Attempted 1.0.0 publish first, but Moon registry rejected it because this package must remain in 0.x.y at this stage. Published 0.11.0 successfully instead (Server status 200 OK).","created_at":"2026-02-15T11:09:30Z"}]}
77
{"id":"moon_cpal-5sm","title":"JACK backend: device + stream","description":"Implement JACK backend on Linux using the unified callback-thread model. Provide a CI-validated smoke test using jackd dummy driver.","notes":"2026-02-04: Implemented JACK backend (Linux) with callback-thread model (JACK process callback + per-channel ringbuffers + MoonBit worker thread). Added cmd/jack_stream_smoke and wired platform dynamic dispatch + available_hosts. CI ubuntu now installs jackd2/libjack and runs jackd dummy + jack smoke.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-04T11:13:02.714524+08:00","updated_at":"2026-02-04T11:27:51.20402+08:00","closed_at":"2026-02-04T11:27:51.20402+08:00","close_reason":"Closed","dependencies":[{"issue_id":"moon_cpal-5sm","depends_on_id":"moon_cpal-uao.5","type":"parent-child","created_at":"2026-02-04T11:13:14.802061+08:00","created_by":"Milky2018"}]}
88
{"id":"moon_cpal-77d","title":"Release: publish 0.10.12","description":"Publish Milky2018/moon_cpal version 0.10.12.","acceptance_criteria":"- moon.mod.json version is 0.10.12\\n- moon publish returns 200 OK","status":"closed","priority":3,"issue_type":"chore","owner":"842376130@qq.com","created_at":"2026-02-14T19:54:16.898304+08:00","created_by":"Milky2018","updated_at":"2026-02-14T19:54:41.615592+08:00","closed_at":"2026-02-14T19:54:41.615592+08:00","close_reason":"0.10.12 published","comments":[{"id":51,"issue_id":"moon_cpal-77d","author":"Milky2018","text":"Published successfully: moon publish returned Server status 200 OK for version 0.10.12.","created_at":"2026-02-14T11:54:41Z"}]}
9+
{"id":"moon_cpal-bqj","title":"WASAPI: stop coercing unknown format tags to F32","description":"Align with upstream format_from_waveformatex_ptr behavior: unknown mix/supported format tags should not silently map to F32. Use explicit tag-\u003eSampleFormat option mapping and error/skip on unknown tags.","status":"closed","priority":1,"issue_type":"task","owner":"842376130@qq.com","created_at":"2026-02-17T13:18:47.714369+08:00","created_by":"Milky2018","updated_at":"2026-02-17T13:20:03.183916+08:00","closed_at":"2026-02-17T13:20:03.183916+08:00","close_reason":"Completed: added explicit WASAPI format-tag mapping helper; unknown tags are no longer coerced to F32 in supported/default config paths.","labels":["parity","wasapi","windows"]}
910
{"id":"moon_cpal-c8w","title":"Project hygiene (bd/CI/docs)","description":"Repository-level work: bd issue workflow, CI for MoonBit targets, docs parity notes, and release hygiene.","status":"closed","priority":2,"issue_type":"epic","owner":"842376130@qq.com","created_at":"2026-02-02T17:12:10.474054+08:00","created_by":"Milky2018","updated_at":"2026-02-02T17:32:30.55971+08:00","closed_at":"2026-02-02T17:32:30.55971+08:00","close_reason":"Closed"}
1011
{"id":"moon_cpal-c8w.1","title":"CI: run moon test on core + macOS","description":"Add GitHub Actions:\\n- ubuntu-latest: run Total tests: 4, passed: 4, failed: 0. (pure core only)\\n- macos-latest: run Total tests: 8, passed: 8, failed: 0. (includes macos package tests)\\nOptionally also run for faster feedback.","status":"closed","priority":2,"issue_type":"chore","owner":"842376130@qq.com","created_at":"2026-02-02T17:12:40.068169+08:00","created_by":"Milky2018","updated_at":"2026-02-02T17:32:13.263994+08:00","closed_at":"2026-02-02T17:32:13.263994+08:00","close_reason":"Closed","dependencies":[{"issue_id":"moon_cpal-c8w.1","depends_on_id":"moon_cpal-c8w","type":"parent-child","created_at":"2026-02-02T17:12:40.068673+08:00","created_by":"Milky2018"},{"issue_id":"moon_cpal-c8w.1","depends_on_id":"moon_cpal-c8w.3","type":"blocks","created_at":"2026-02-02T17:13:16.70621+08:00","created_by":"Milky2018"}]}
1112
{"id":"moon_cpal-c8w.2","title":"Install bd git hooks","description":"Run ✓ Git hooks installed successfully\n\nInstalled hooks:\n - post-checkout\n - prepare-commit-msg\n - pre-commit\n - post-merge\n - pre-push so issues.jsonl stays in sync and merge driver is active for .beads/*.jsonl.","status":"closed","priority":2,"issue_type":"chore","owner":"842376130@qq.com","created_at":"2026-02-02T17:12:40.243809+08:00","created_by":"Milky2018","updated_at":"2026-02-02T17:31:39.067859+08:00","closed_at":"2026-02-02T17:31:39.067859+08:00","close_reason":"Closed","dependencies":[{"issue_id":"moon_cpal-c8w.2","depends_on_id":"moon_cpal-c8w","type":"parent-child","created_at":"2026-02-02T17:12:40.244366+08:00","created_by":"Milky2018"},{"issue_id":"moon_cpal-c8w.2","depends_on_id":"moon_cpal-c8w.3","type":"blocks","created_at":"2026-02-02T17:13:16.886028+08:00","created_by":"Milky2018"}]}
@@ -107,5 +108,5 @@
107108
{"id":"moon_cpal-uao.7","title":"CoreAudio: improve callback timestamps","description":"Input/Output callback timestamps are currently derived from mach_absolute_time at callback time. Use AudioQueue-provided timestamps (input start time) and/or device timing to better match cpal semantics.","acceptance_criteria":"- InputCallbackInfo.capture uses AudioQueue inStartTime when available\\n- OutputCallbackInfo.playback uses best available estimate\\n- Monotonicity preserved","status":"closed","priority":3,"issue_type":"task","owner":"842376130@qq.com","created_at":"2026-02-03T19:40:05.271989+08:00","created_by":"Milky2018","updated_at":"2026-02-03T19:52:51.028133+08:00","closed_at":"2026-02-03T19:52:51.028133+08:00","close_reason":"Use AudioQueue timestamps when available: input capture uses inStartTime hostTime (or sampleTime fallback); output playback uses AudioQueueGetCurrentTime hostTime when available.","dependencies":[{"issue_id":"moon_cpal-uao.7","depends_on_id":"moon_cpal-uao","type":"parent-child","created_at":"2026-02-03T19:40:05.272719+08:00","created_by":"Milky2018"}]}
108109
{"id":"moon_cpal-uao.8","title":"CoreAudio: reduce memcpy in callback bridge","description":"Callback bridge still memcpy()s between MoonBit buffer and AudioQueueBuffer each callback (output) and from AudioQueueBuffer into MoonBit buffer (input). Investigate zero-copy or lower-copy approaches while preserving callback safety.","acceptance_criteria":"- Reduce per-callback memcpy cost where possible (document limits)\\n- Preserve current callback semantics (buffer valid only during callback)\\n- Keep tests and macos_stream_smoke working","status":"closed","priority":3,"issue_type":"task","owner":"842376130@qq.com","created_at":"2026-02-03T19:52:51.192331+08:00","created_by":"Milky2018","updated_at":"2026-02-03T20:04:25.588465+08:00","closed_at":"2026-02-03T20:04:25.588465+08:00","close_reason":"No true zero-copy path found with current MoonBit ABI. Reduced callback bridge memory work: removed per-callback output memset (buffers are pre-zeroed once, callback expected to fully fill) and removed input remainder clearing; kept necessary memcpy between AudioQueue buffers and MoonBit buffers.","dependencies":[{"issue_id":"moon_cpal-uao.8","depends_on_id":"moon_cpal-uao","type":"parent-child","created_at":"2026-02-03T19:52:51.193004+08:00","created_by":"Milky2018"}]}
109110
{"id":"moon_cpal-uao.9","title":"CoreAudio: explore zero-copy Data via external memory (blocked by MoonBit ABI)","description":"Investigate whether MoonBit runtime can support creating a Byte array/Bytes view backed by an external pointer (AudioQueueBuffer mAudioData) with lifetime limited to callback. If not supported, document as limitation and keep memcpy.","acceptance_criteria":"- Determine feasibility in MoonBit C backend/runtime\\n- If feasible: prototype and measure\\n- If infeasible: document clearly and close","status":"closed","priority":3,"issue_type":"task","owner":"842376130@qq.com","created_at":"2026-02-03T20:04:25.674904+08:00","created_by":"Milky2018","updated_at":"2026-02-03T20:11:05.41978+08:00","closed_at":"2026-02-03T20:11:05.41978+08:00","close_reason":"MoonBit C backend lacks a safe external-pointer-backed Bytes/FixedArray representation; documented constraints and kept memcpy (see macos/ZERO_COPY.md).","dependencies":[{"issue_id":"moon_cpal-uao.9","depends_on_id":"moon_cpal-uao","type":"parent-child","created_at":"2026-02-03T20:04:25.675472+08:00","created_by":"Milky2018"}]}
110-
{"id":"moon_cpal-uhc","title":"WASAPI: add tests for raw build prevalidation parity","description":"Add regression tests to ensure stream build paths do not reject configs solely due to pre-enumeration gaps; keep assertions best-effort for CI variability.","status":"open","priority":1,"issue_type":"task","owner":"842376130@qq.com","created_at":"2026-02-17T13:13:45.613777+08:00","created_by":"Milky2018","updated_at":"2026-02-17T13:13:45.613777+08:00","labels":["test","wasapi","windows"]}
111+
{"id":"moon_cpal-uhc","title":"WASAPI: add tests for raw build prevalidation parity","description":"Add regression tests to ensure stream build paths do not reject configs solely due to pre-enumeration gaps; keep assertions best-effort for CI variability.","notes":"Added regression test raw_build_parity_wbtest.mbt for raw output build prevalidation parity.","status":"closed","priority":1,"issue_type":"task","owner":"842376130@qq.com","created_at":"2026-02-17T13:13:45.613777+08:00","created_by":"Milky2018","updated_at":"2026-02-17T13:18:47.789003+08:00","closed_at":"2026-02-17T13:18:47.789008+08:00","labels":["test","wasapi","windows"]}
111112
{"id":"moon_cpal-zeu","title":"Release: publish 0.10.10","description":"Publish Milky2018/moon_cpal version 0.10.10 to resolve duplicate 0.10.9 version conflict.","acceptance_criteria":"- moon.mod.json version bumped to 0.10.10\\n- moon publish returns 200 OK","status":"closed","priority":3,"issue_type":"chore","owner":"842376130@qq.com","created_at":"2026-02-13T19:57:17.551865+08:00","created_by":"Milky2018","updated_at":"2026-02-13T19:57:23.82405+08:00","closed_at":"2026-02-13T19:57:23.82405+08:00","close_reason":"0.10.10 published","comments":[{"id":49,"issue_id":"moon_cpal-zeu","author":"Milky2018","text":"Published successfully: moon publish returned Server status 200 OK after bumping version to 0.10.10 and fixing deprecated closure syntax in cmd/enumerate.","created_at":"2026-02-13T11:57:23Z"}]}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2025 International Digital Economy Academy
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
///|
16+
test "wasapi sample format tag mapping is explicit (unit)" {
17+
inspect(wasapi_sample_format_from_tag((1 : UInt)), content="Some(F32)")
18+
inspect(wasapi_sample_format_from_tag((2 : UInt)), content="Some(I16)")
19+
inspect(wasapi_sample_format_from_tag((3 : UInt)), content="Some(U16)")
20+
inspect(wasapi_sample_format_from_tag((4 : UInt)), content="Some(U8)")
21+
inspect(wasapi_sample_format_from_tag((6 : UInt)), content="Some(I24)")
22+
inspect(wasapi_sample_format_from_tag((7 : UInt)), content="Some(U24)")
23+
inspect(wasapi_sample_format_from_tag((8 : UInt)), content="Some(I32)")
24+
inspect(wasapi_sample_format_from_tag((10 : UInt)), content="Some(I64)")
25+
inspect(wasapi_sample_format_from_tag((0 : UInt)) == None, content="true")
26+
inspect(wasapi_sample_format_from_tag((5 : UInt)) == None, content="true")
27+
inspect(wasapi_sample_format_from_tag((11 : UInt)) == None, content="true")
28+
inspect(wasapi_sample_format_from_tag((12 : UInt)) == None, content="true")
29+
}

wasapi/host.mbt

Lines changed: 39 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,21 @@ fn clamp_u32_to_frame_count(u : UInt) -> Int {
285285
}
286286
}
287287

288+
///|
289+
fn wasapi_sample_format_from_tag(fmt_tag : UInt) -> @core.SampleFormat? {
290+
match fmt_tag {
291+
1 => Some(@core.SampleFormat::F32)
292+
2 => Some(@core.SampleFormat::I16)
293+
3 => Some(@core.SampleFormat::U16)
294+
4 => Some(@core.SampleFormat::U8)
295+
6 => Some(@core.SampleFormat::I24)
296+
7 => Some(@core.SampleFormat::U24)
297+
8 => Some(@core.SampleFormat::I32)
298+
10 => Some(@core.SampleFormat::I64)
299+
_ => None
300+
}
301+
}
302+
288303
///|
289304
pub fn Device::supported_input_configs(
290305
_self : Device,
@@ -301,16 +316,9 @@ pub fn Device::supported_input_configs(
301316
match default_config_ex_u32(_self.id, true) {
302317
None => []
303318
Some((ch, sr, fmt_tag, bmin, bmax)) => {
304-
let fmt = match fmt_tag {
305-
1 => @core.SampleFormat::F32
306-
2 => @core.SampleFormat::I16
307-
3 => @core.SampleFormat::U16
308-
4 => @core.SampleFormat::U8
309-
6 => @core.SampleFormat::I24
310-
7 => @core.SampleFormat::U24
311-
8 => @core.SampleFormat::I32
312-
10 => @core.SampleFormat::I64
313-
_ => @core.SampleFormat::F32
319+
let fmt = match wasapi_sample_format_from_tag(fmt_tag) {
320+
Some(v) => v
321+
None => return []
314322
}
315323
let buf = @core.SupportedBufferSize::Range(
316324
min=clamp_u32_to_frame_count(bmin),
@@ -338,16 +346,9 @@ pub fn Device::supported_input_configs(
338346
let fmt_tag = buf[off + 2]
339347
let bmin = buf[off + 3]
340348
let bmax = buf[off + 4]
341-
let fmt = match fmt_tag {
342-
1 => @core.SampleFormat::F32
343-
2 => @core.SampleFormat::I16
344-
3 => @core.SampleFormat::U16
345-
4 => @core.SampleFormat::U8
346-
6 => @core.SampleFormat::I24
347-
7 => @core.SampleFormat::U24
348-
8 => @core.SampleFormat::I32
349-
10 => @core.SampleFormat::I64
350-
_ => @core.SampleFormat::F32
349+
let fmt = match wasapi_sample_format_from_tag(fmt_tag) {
350+
Some(v) => v
351+
None => continue
351352
}
352353
let b = @core.SupportedBufferSize::Range(
353354
min=clamp_u32_to_frame_count(bmin),
@@ -384,16 +385,9 @@ pub fn Device::supported_output_configs(
384385
match default_config_ex_u32(_self.id, false) {
385386
None => []
386387
Some((ch, sr, fmt_tag, bmin, bmax)) => {
387-
let fmt = match fmt_tag {
388-
1 => @core.SampleFormat::F32
389-
2 => @core.SampleFormat::I16
390-
3 => @core.SampleFormat::U16
391-
4 => @core.SampleFormat::U8
392-
6 => @core.SampleFormat::I24
393-
7 => @core.SampleFormat::U24
394-
8 => @core.SampleFormat::I32
395-
10 => @core.SampleFormat::I64
396-
_ => @core.SampleFormat::F32
388+
let fmt = match wasapi_sample_format_from_tag(fmt_tag) {
389+
Some(v) => v
390+
None => return []
397391
}
398392
let buf = @core.SupportedBufferSize::Range(
399393
min=clamp_u32_to_frame_count(bmin),
@@ -421,16 +415,9 @@ pub fn Device::supported_output_configs(
421415
let fmt_tag = buf[off + 2]
422416
let bmin = buf[off + 3]
423417
let bmax = buf[off + 4]
424-
let fmt = match fmt_tag {
425-
1 => @core.SampleFormat::F32
426-
2 => @core.SampleFormat::I16
427-
3 => @core.SampleFormat::U16
428-
4 => @core.SampleFormat::U8
429-
6 => @core.SampleFormat::I24
430-
7 => @core.SampleFormat::U24
431-
8 => @core.SampleFormat::I32
432-
10 => @core.SampleFormat::I64
433-
_ => @core.SampleFormat::F32
418+
let fmt = match wasapi_sample_format_from_tag(fmt_tag) {
419+
Some(v) => v
420+
None => continue
434421
}
435422
let b = @core.SupportedBufferSize::Range(
436423
min=clamp_u32_to_frame_count(bmin),
@@ -471,16 +458,12 @@ pub fn Device::default_input_config(
471458
"wasapi default_input_config: query failed",
472459
)
473460
Some((ch, sr, fmt_tag, bmin, bmax)) => {
474-
let fmt = match fmt_tag {
475-
1 => @core.SampleFormat::F32
476-
2 => @core.SampleFormat::I16
477-
3 => @core.SampleFormat::U16
478-
4 => @core.SampleFormat::U8
479-
6 => @core.SampleFormat::I24
480-
7 => @core.SampleFormat::U24
481-
8 => @core.SampleFormat::I32
482-
10 => @core.SampleFormat::I64
483-
_ => @core.SampleFormat::F32
461+
let fmt = match wasapi_sample_format_from_tag(fmt_tag) {
462+
Some(v) => v
463+
None =>
464+
raise @core.default_stream_config_error_backend_specific(
465+
"wasapi default_input_config: unsupported format tag",
466+
)
484467
}
485468
@core.SupportedStreamConfig::new(
486469
ch.reinterpret_as_int(),
@@ -515,16 +498,12 @@ pub fn Device::default_output_config(
515498
"wasapi default_output_config: query failed",
516499
)
517500
Some((ch, sr, fmt_tag, bmin, bmax)) => {
518-
let fmt = match fmt_tag {
519-
1 => @core.SampleFormat::F32
520-
2 => @core.SampleFormat::I16
521-
3 => @core.SampleFormat::U16
522-
4 => @core.SampleFormat::U8
523-
6 => @core.SampleFormat::I24
524-
7 => @core.SampleFormat::U24
525-
8 => @core.SampleFormat::I32
526-
10 => @core.SampleFormat::I64
527-
_ => @core.SampleFormat::F32
501+
let fmt = match wasapi_sample_format_from_tag(fmt_tag) {
502+
Some(v) => v
503+
None =>
504+
raise @core.default_stream_config_error_backend_specific(
505+
"wasapi default_output_config: unsupported format tag",
506+
)
528507
}
529508
@core.SupportedStreamConfig::new(
530509
ch.reinterpret_as_int(),

wasapi/raw_build_parity_wbtest.mbt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2025 International Digital Economy Academy
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
///|
16+
test "wasapi raw output build on capture endpoint is not pre-rejected by config ranges (best-effort)" {
17+
if @core.native_os() != @core.NativeOs::Windows {
18+
inspect(true, content="true")
19+
return
20+
}
21+
22+
let host = default_host()
23+
let in_dev = match host.default_input_device() {
24+
None => {
25+
inspect(true, content="true")
26+
return
27+
}
28+
Some(d) => d
29+
}
30+
let in_cfg = try in_dev.default_input_config() catch {
31+
_ => {
32+
inspect(true, content="true")
33+
return
34+
}
35+
} noraise {
36+
x => x
37+
}
38+
let sc = in_cfg.config()
39+
40+
try
41+
in_dev.build_output_stream_raw(
42+
sc,
43+
in_cfg.sample_format(),
44+
fn(_d, _info) { () },
45+
fn(_e) { () },
46+
None,
47+
)
48+
catch {
49+
e =>
50+
match e {
51+
// This case historically indicated MoonBit-side prevalidation against
52+
// supported_output_configs() on capture endpoints; keep it rejected.
53+
@core.BuildStreamError::StreamConfigNotSupported =>
54+
inspect(false, content="true")
55+
_ => inspect(true, content="true")
56+
}
57+
} noraise {
58+
s => {
59+
s.close()
60+
inspect(true, content="true")
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)