feat(desktop): add opt-in Darwin audio output
This commit is contained in:
parent
902c892d3f
commit
2ea7164d9a
21 changed files with 1311 additions and 30 deletions
618
Cargo.lock
generated
618
Cargo.lock
generated
|
|
@ -2,6 +2,34 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
|
@ -20,6 +48,12 @@ version = "3.20.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.5.1"
|
||||
|
|
@ -30,12 +64,28 @@ dependencies = [
|
|||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
|
|
@ -46,6 +96,68 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
|
|
@ -108,7 +220,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -144,6 +256,7 @@ dependencies = [
|
|||
name = "gameboy-rust-desktop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"gba-core",
|
||||
"gba-demos",
|
||||
"minifb",
|
||||
|
|
@ -165,6 +278,22 @@ dependencies = [
|
|||
"gba-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
|
|
@ -183,6 +312,50 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
|
||||
dependencies = [
|
||||
"jni-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.106"
|
||||
|
|
@ -218,6 +391,21 @@ dependencies = [
|
|||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
|
|
@ -247,6 +435,149 @@ dependencies = [
|
|||
"x11-dl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.6.0+11769913"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
|
||||
dependencies = [
|
||||
"jni-sys 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-audio-toolbox"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
|
|
@ -282,6 +613,15 @@ version = "0.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
|
|
@ -321,6 +661,15 @@ version = "1.0.23"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdl2"
|
||||
version = "0.38.0"
|
||||
|
|
@ -371,7 +720,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -399,6 +748,17 @@ version = "0.4.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
|
|
@ -410,6 +770,26 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
|
|
@ -419,6 +799,36 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.15+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
|
|
@ -431,6 +841,16 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
|
|
@ -476,7 +896,7 @@ dependencies = [
|
|||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.6",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
|
@ -515,12 +935,204 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11-dl"
|
||||
version = "2.21.0"
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ Use `--test-suite suite.json` for [repeatable headless ROM assertions](docs/rom-
|
|||
| [docs/hardware/bios.md](docs/hardware/bios.md) | The original BIOS replacement and each supported service |
|
||||
| [docs/hardware/timers-irq.md](docs/hardware/timers-irq.md) | Timers, interrupt registers, HALT/STOP, and power control |
|
||||
| [docs/hardware/dma.md](docs/hardware/dma.md) | DMA channels, triggers, and timing |
|
||||
| [docs/hardware/audio.md](docs/hardware/audio.md) | Disabled sound initialization and remaining audio work |
|
||||
| [docs/hardware/audio.md](docs/hardware/audio.md) | Sound devices, stereo capture, optional Darwin output, and limits |
|
||||
| [docs/hardware/serial.md](docs/hardware/serial.md) | Disconnected serial initialization and transfer limits |
|
||||
| [docs/hardware/display-timing.md](docs/hardware/display-timing.md) | Display clock, status, IRQs, and scanline capture |
|
||||
| [docs/hardware/video.md](docs/hardware/video.md) | Backgrounds, sprites, mosaic, windows, color effects, and input |
|
||||
|
|
|
|||
|
|
@ -15,3 +15,6 @@ serde = { version = "1", features = ["derive"] }
|
|||
serde_json = "1"
|
||||
# Pinned because src/desktop.rs corrects a macOS focus bug in this version.
|
||||
minifb = { version = "=0.28.0", default-features = false, features = ["x11"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cpal = { version = "=0.16.0", default-features = false }
|
||||
|
|
|
|||
30
crates/desktop/src/audio.rs
Normal file
30
crates/desktop/src/audio.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
//! Opt-in desktop audio. The emulator core never owns a host device.
|
||||
#[cfg(target_os = "macos")]
|
||||
mod device;
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
mod playback;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use device::AudioOutput;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub struct AudioOutput;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
impl AudioOutput {
|
||||
pub fn new() -> std::io::Result<Self> {
|
||||
Err(std::io::Error::other(
|
||||
"--audio currently requires macOS; omit --audio to run muted",
|
||||
))
|
||||
}
|
||||
pub fn submit(&self, _: &[gba_core::audio::StereoLevel]) -> std::io::Result<()> {
|
||||
unreachable!()
|
||||
}
|
||||
pub fn clear(&self) -> std::io::Result<()> {
|
||||
unreachable!()
|
||||
}
|
||||
pub fn description(&self) -> String {
|
||||
unreachable!()
|
||||
}
|
||||
pub fn report(&self, _: &mut impl std::io::Write) -> std::io::Result<()> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
188
crates/desktop/src/audio/device.rs
Normal file
188
crates/desktop/src/audio/device.rs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
//! macOS CoreAudio through CPAL. No blocking or allocation in the data callback.
|
||||
use super::playback::Playback;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use gba_core::audio::StereoLevel;
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
struct Shared {
|
||||
playback: Mutex<Playback>,
|
||||
failed: AtomicBool,
|
||||
callbacks: AtomicU64,
|
||||
nonzero: AtomicU64,
|
||||
contention: AtomicU64,
|
||||
}
|
||||
pub struct AudioOutput {
|
||||
_stream: cpal::Stream,
|
||||
shared: Arc<Shared>,
|
||||
rate: u32,
|
||||
channels: u16,
|
||||
}
|
||||
impl AudioOutput {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
let device = cpal::default_host()
|
||||
.default_output_device()
|
||||
.ok_or_else(|| {
|
||||
io::Error::other("no default audio output device; omit --audio to run muted")
|
||||
})?;
|
||||
let config = device.default_output_config().map_err(io::Error::other)?;
|
||||
let rate = config.sample_rate().0;
|
||||
let channels = config.channels();
|
||||
if !(8000..=192000).contains(&rate) || !(1..=32).contains(&channels) {
|
||||
return Err(io::Error::other(
|
||||
"unsupported audio rate/channel count; omit --audio to run muted",
|
||||
));
|
||||
}
|
||||
let shared = Arc::new(Shared {
|
||||
playback: Mutex::new(Playback::new(rate, usize::from(channels))),
|
||||
failed: AtomicBool::new(false),
|
||||
callbacks: AtomicU64::new(0),
|
||||
nonzero: AtomicU64::new(0),
|
||||
contention: AtomicU64::new(0),
|
||||
});
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => build::<f32>(&device, &config.into(), shared.clone()),
|
||||
cpal::SampleFormat::I16 => build::<i16>(&device, &config.into(), shared.clone()),
|
||||
cpal::SampleFormat::U16 => build::<u16>(&device, &config.into(), shared.clone()),
|
||||
format => {
|
||||
return Err(io::Error::other(format!(
|
||||
"unsupported output sample format {format}; omit --audio to run muted"
|
||||
)))
|
||||
}
|
||||
}?;
|
||||
stream.play().map_err(io::Error::other)?;
|
||||
Ok(Self {
|
||||
_stream: stream,
|
||||
shared,
|
||||
rate,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
fn check(&self) -> io::Result<()> {
|
||||
if self.shared.failed.load(Ordering::Relaxed) {
|
||||
Err(io::Error::other(
|
||||
"audio output stream failed; restart without --audio to run muted",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
pub fn submit(&self, samples: &[StereoLevel]) -> io::Result<()> {
|
||||
self.check()?;
|
||||
self.shared
|
||||
.playback
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("audio queue poisoned"))?
|
||||
.submit(samples);
|
||||
Ok(())
|
||||
}
|
||||
pub fn clear(&self) -> io::Result<()> {
|
||||
self.check()?;
|
||||
self.shared
|
||||
.playback
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("audio queue poisoned"))?
|
||||
.clear();
|
||||
Ok(())
|
||||
}
|
||||
pub fn description(&self) -> String {
|
||||
format!(
|
||||
"CoreAudio: {} Hz, {} channels; linear conversion, 25% gain, DC removal",
|
||||
self.rate, self.channels
|
||||
)
|
||||
}
|
||||
pub fn report(&self, writer: &mut impl Write) -> io::Result<()> {
|
||||
let p = self
|
||||
.shared
|
||||
.playback
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("audio queue poisoned"))?;
|
||||
writeln!(writer,"Audio callbacks: {}; nonzero output frames: {}; underruns: {}; dropped input frames: {}; callback lock misses: {}", self.shared.callbacks.load(Ordering::Relaxed),self.shared.nonzero.load(Ordering::Relaxed),p.underruns,p.dropped,self.shared.contention.load(Ordering::Relaxed))?;
|
||||
writeln!(
|
||||
writer,
|
||||
"Audio submitted frames: {}; nonzero input frames: {}",
|
||||
p.submitted, p.nonzero_input
|
||||
)?;
|
||||
self.check()
|
||||
}
|
||||
}
|
||||
fn build<T>(
|
||||
device: &cpal::Device,
|
||||
config: &cpal::StreamConfig,
|
||||
shared: Arc<Shared>,
|
||||
) -> io::Result<cpal::Stream>
|
||||
where
|
||||
T: cpal::SizedSample + cpal::FromSample<f32>,
|
||||
{
|
||||
let errors = shared.clone();
|
||||
device
|
||||
.build_output_stream(
|
||||
config,
|
||||
move |data: &mut [T], _| {
|
||||
data.fill(T::from_sample(0.));
|
||||
shared.callbacks.fetch_add(1, Ordering::Relaxed);
|
||||
match shared.playback.try_lock() {
|
||||
Ok(mut p) => {
|
||||
let nonzero = p.render(data, T::from_sample);
|
||||
shared.nonzero.fetch_add(nonzero as u64, Ordering::Relaxed);
|
||||
}
|
||||
Err(std::sync::TryLockError::WouldBlock) => {
|
||||
shared.contention.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Err(std::sync::TryLockError::Poisoned(_)) => {
|
||||
shared.failed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
},
|
||||
move |_| {
|
||||
errors.failed.store(true, Ordering::Relaxed);
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
#[ignore = "requires a native default audio device; plays a quiet original test tone"]
|
||||
fn native_audio_output_consumes_original_samples() {
|
||||
let output = AudioOutput::new().unwrap();
|
||||
// Exercise the actual core capture path without depending on window focus.
|
||||
let mut memory = gba_core::memory::Memory::new(vec![]).unwrap();
|
||||
for (address, value) in [
|
||||
(0x04000084, 0x80),
|
||||
(0x04000088, 0x200),
|
||||
(0x04000082, 2),
|
||||
(0x04000080, 0x1177),
|
||||
(0x04000062, 0x3080),
|
||||
(0x04000064, 0x86d6),
|
||||
] {
|
||||
memory.write16(address, value).unwrap();
|
||||
}
|
||||
memory.set_audio_capture(true);
|
||||
memory.advance_cycles(4096 * 512);
|
||||
let mut samples = vec![StereoLevel::default(); 4096];
|
||||
assert_eq!(memory.drain_audio_samples(&mut samples), 4096);
|
||||
assert_eq!(memory.audio_dropped_samples(), 0);
|
||||
assert!(samples.iter().any(|s| s.left > 0));
|
||||
assert!(samples.iter().any(|s| s.left < 0));
|
||||
output.submit(&samples).unwrap();
|
||||
let start = std::time::Instant::now();
|
||||
while output.shared.nonzero.load(Ordering::Relaxed) == 0
|
||||
&& start.elapsed() < std::time::Duration::from_secs(3)
|
||||
{
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
output.check().unwrap();
|
||||
}
|
||||
output.report(&mut std::io::stdout()).unwrap();
|
||||
assert!(output.shared.nonzero.load(Ordering::Relaxed) > 0);
|
||||
output.clear().unwrap();
|
||||
}
|
||||
}
|
||||
193
crates/desktop/src/audio/playback.rs
Normal file
193
crates/desktop/src/audio/playback.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
//! Bounded host buffering and linear rate conversion. No device access here.
|
||||
use gba_core::audio::{StereoLevel, AUDIO_SAMPLE_RATE};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub(super) const CAPACITY: usize = 8192;
|
||||
const PREFILL: usize = 1024;
|
||||
|
||||
pub(super) struct Playback {
|
||||
queue: VecDeque<StereoLevel>,
|
||||
rate: u32,
|
||||
channels: usize,
|
||||
phase: u32,
|
||||
pair: Option<[StereoLevel; 2]>,
|
||||
previous: [f32; 2],
|
||||
filtered: [f32; 2],
|
||||
decay: f32,
|
||||
pub(super) submitted: u64,
|
||||
pub(super) nonzero_input: u64,
|
||||
pub(super) dropped: u64,
|
||||
pub(super) underruns: u64,
|
||||
}
|
||||
impl Playback {
|
||||
pub(super) fn new(rate: u32, channels: usize) -> Self {
|
||||
assert!((8000..=192000).contains(&rate) && (1..=32).contains(&channels));
|
||||
Self {
|
||||
queue: VecDeque::with_capacity(CAPACITY),
|
||||
rate,
|
||||
channels,
|
||||
phase: 0,
|
||||
pair: None,
|
||||
previous: [0.; 2],
|
||||
filtered: [0.; 2],
|
||||
decay: (-std::f32::consts::TAU * 20. / rate as f32).exp(),
|
||||
submitted: 0,
|
||||
nonzero_input: 0,
|
||||
dropped: 0,
|
||||
underruns: 0,
|
||||
}
|
||||
}
|
||||
pub(super) fn submit(&mut self, samples: &[StereoLevel]) {
|
||||
self.submitted = self.submitted.saturating_add(samples.len() as u64);
|
||||
self.nonzero_input = self.nonzero_input.saturating_add(
|
||||
samples
|
||||
.iter()
|
||||
.filter(|s| s.left != 0 || s.right != 0)
|
||||
.count() as u64,
|
||||
);
|
||||
for &sample in samples {
|
||||
if self.queue.len() == CAPACITY {
|
||||
self.dropped = self.dropped.saturating_add(1);
|
||||
} else {
|
||||
self.queue.push_back(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(super) fn clear(&mut self) {
|
||||
self.queue.clear();
|
||||
self.reset();
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.pair = None;
|
||||
self.phase = 0;
|
||||
self.previous = [0.; 2];
|
||||
self.filtered = [0.; 2];
|
||||
}
|
||||
fn next(&mut self) -> [f32; 2] {
|
||||
if self.pair.is_none() {
|
||||
if self.queue.len() < PREFILL {
|
||||
return [0.; 2];
|
||||
}
|
||||
self.pair = Some([
|
||||
self.queue.pop_front().unwrap(),
|
||||
self.queue.pop_front().unwrap(),
|
||||
]);
|
||||
}
|
||||
let [a, b] = self.pair.unwrap();
|
||||
let fraction = self.phase as f32 / self.rate as f32;
|
||||
let mut output = [0.; 2];
|
||||
for (side, (a, b)) in [(a.left, b.left), (a.right, b.right)]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
// Conservative 25% host gain and 20 Hz DC removal; not a hardware analog model.
|
||||
let input = (f32::from(a) + (f32::from(b) - f32::from(a)) * fraction) / 512. * 0.25;
|
||||
self.filtered[side] = input - self.previous[side] + self.decay * self.filtered[side];
|
||||
self.previous[side] = input;
|
||||
output[side] = self.filtered[side].clamp(-1., 1.);
|
||||
}
|
||||
self.phase += AUDIO_SAMPLE_RATE;
|
||||
while self.phase >= self.rate {
|
||||
self.phase -= self.rate;
|
||||
if let Some(next) = self.queue.pop_front() {
|
||||
let old = self.pair.unwrap()[1];
|
||||
self.pair = Some([old, next]);
|
||||
} else {
|
||||
self.underruns = self.underruns.saturating_add(1);
|
||||
self.reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
pub(super) fn render<T: Copy>(
|
||||
&mut self,
|
||||
output: &mut [T],
|
||||
convert: impl Fn(f32) -> T,
|
||||
) -> usize {
|
||||
output.fill(convert(0.));
|
||||
let mut nonzero = 0;
|
||||
for frame in output.chunks_exact_mut(self.channels) {
|
||||
let [left, right] = self.next();
|
||||
if self.channels == 1 {
|
||||
frame[0] = convert((left + right) * 0.5);
|
||||
} else {
|
||||
frame[0] = convert(left);
|
||||
frame[1] = convert(right);
|
||||
}
|
||||
nonzero += usize::from(left != 0. || right != 0.);
|
||||
}
|
||||
nonzero
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn sample(left: i16, right: i16) -> StereoLevel {
|
||||
StereoLevel { left, right }
|
||||
}
|
||||
#[test]
|
||||
fn prefill_silence_stereo_mapping_extra_channels_and_clear() {
|
||||
let mut p = Playback::new(48000, 4);
|
||||
let mut out = [9.; 16];
|
||||
p.render(&mut out, |x| x);
|
||||
assert_eq!(out, [0.; 16]);
|
||||
p.submit(&vec![sample(256, -256); PREFILL]);
|
||||
p.render(&mut out, |x| x);
|
||||
assert_eq!(&out[..4], &[0.125, -0.125, 0., 0.]);
|
||||
assert!(out[4] > 0. && out[4] < 0.125);
|
||||
p.clear();
|
||||
p.render(&mut out, |x| x);
|
||||
assert_eq!(out, [0.; 16]);
|
||||
}
|
||||
#[test]
|
||||
fn split_callbacks_keep_phase_and_rate_conversion_consumption() {
|
||||
for rate in [8000, 32768, 44100, 48000, 192000] {
|
||||
let mut a = Playback::new(rate, 2);
|
||||
let mut b = Playback::new(rate, 2);
|
||||
let data: Vec<_> = (0..4096)
|
||||
.map(|i| sample((i % 1024) as i16 - 512, 0))
|
||||
.collect();
|
||||
a.submit(&data);
|
||||
b.submit(&data);
|
||||
let mut one = vec![0.; 2000];
|
||||
let mut split = one.clone();
|
||||
a.render(&mut one, |x| x);
|
||||
for part in split.chunks_mut(14) {
|
||||
b.render(part, |x| x);
|
||||
}
|
||||
assert_eq!(one, split);
|
||||
assert_eq!(a.phase, b.phase);
|
||||
assert_eq!(a.queue, b.queue);
|
||||
if rate >= 32768 {
|
||||
assert_eq!(
|
||||
a.queue.len(),
|
||||
4096 - 2 - (1000 * AUDIO_SAMPLE_RATE / rate) as usize
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn overflow_underflow_and_recovery_are_bounded() {
|
||||
let mut p = Playback::new(32768, 1);
|
||||
p.submit(&vec![sample(100, -100); CAPACITY + 7]);
|
||||
assert_eq!(p.dropped, 7);
|
||||
let mut out = vec![1.; CAPACITY + 100];
|
||||
p.render(&mut out, |x| x);
|
||||
assert!(out.iter().all(|x| *x == 0.));
|
||||
assert_eq!(p.underruns, 1);
|
||||
p.submit(&vec![sample(256, 256); PREFILL]);
|
||||
p.render(&mut out[..1], |x| x);
|
||||
assert_eq!(out[0], 0.125);
|
||||
}
|
||||
#[test]
|
||||
fn dc_offset_decays_without_out_of_range_or_nonfinite_samples() {
|
||||
let mut p = Playback::new(32768, 2);
|
||||
p.submit(&vec![sample(-512, 511); CAPACITY]);
|
||||
let mut out = vec![0.; 12000];
|
||||
p.render(&mut out, |x| x);
|
||||
assert!(out.iter().all(|x| x.is_finite() && x.abs() <= 0.25));
|
||||
assert!(out[11999].abs() < 0.000001);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ use gba_core::{
|
|||
|
||||
const MAX_STEPS: u64 = 100_000_000;
|
||||
const MAX_FRAMES: u64 = 100_000;
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] (--steps COUNT | --window [--frames COUNT]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] (--steps COUNT | --window [--frames COUNT] [--audio]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
|
|
@ -35,6 +35,7 @@ pub struct Options {
|
|||
mode: Mode,
|
||||
hardware: CartridgeHardware,
|
||||
save_device: SaveDevice,
|
||||
audio: bool,
|
||||
}
|
||||
|
||||
pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
||||
|
|
@ -43,6 +44,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
let mut steps = None;
|
||||
let mut frames = None;
|
||||
let mut window = false;
|
||||
let mut audio = false;
|
||||
let mut hardware = CartridgeHardware::None;
|
||||
let mut save_device = SaveDevice::None;
|
||||
let mut args = args.iter();
|
||||
|
|
@ -61,6 +63,8 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
Some("flash128") => SaveDevice::Flash128,
|
||||
_ => return Err(invalid()),
|
||||
};
|
||||
} else if arg == "--audio" && !audio {
|
||||
audio = true;
|
||||
} else if arg == "--window" && !window {
|
||||
window = true;
|
||||
} else if (arg == "--steps" && steps.is_none()) || (arg == "--frames" && frames.is_none()) {
|
||||
|
|
@ -89,6 +93,9 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
return Err(invalid());
|
||||
}
|
||||
}
|
||||
if audio && !window {
|
||||
return Err(invalid());
|
||||
}
|
||||
let mode = match (window, steps, frames) {
|
||||
(false, Some(steps), None) => Mode::Terminal { steps },
|
||||
(true, None, frames) => Mode::Window { frames },
|
||||
|
|
@ -99,6 +106,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
mode,
|
||||
hardware,
|
||||
save_device,
|
||||
audio,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -284,6 +292,7 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
frames,
|
||||
options.hardware,
|
||||
options.save_device,
|
||||
options.audio,
|
||||
writer,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,13 +28,36 @@ fn options_require_both_flags_in_either_order() {
|
|||
path: "original program.gba".into(),
|
||||
mode: Mode::Terminal { steps: count },
|
||||
hardware: CartridgeHardware::None,
|
||||
save_device: SaveDevice::None
|
||||
save_device: SaveDevice::None,
|
||||
audio: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_is_opt_in_window_only_and_cannot_be_duplicated() {
|
||||
assert!(
|
||||
parse_args(&args(&["--audio", "--rom", "test.gba", "--window"]))
|
||||
.unwrap()
|
||||
.audio
|
||||
);
|
||||
assert!(
|
||||
!parse_args(&args(&["--rom", "test.gba", "--window"]))
|
||||
.unwrap()
|
||||
.audio
|
||||
);
|
||||
for tail in [
|
||||
vec!["--audio", "--steps", "10"],
|
||||
vec!["--audio", "--window", "--audio"],
|
||||
] {
|
||||
let mut arguments = args(&["--rom", "test.gba"]);
|
||||
arguments.extend(args(&tail));
|
||||
assert!(parse_args(&arguments).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_options_accept_any_order_and_optional_frame_limits() {
|
||||
for arguments in [
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
//! Main-thread ROM presentation and physical keyboard input.
|
||||
//! Main-thread ROM presentation, physical keyboard input, and optional host audio.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use super::*;
|
||||
use crate::cartridge::window::{Session, Update};
|
||||
|
||||
fn next_frame_start(previous: Instant, now: Instant, period: Duration, audio: bool) -> Instant {
|
||||
let target = previous + period;
|
||||
if audio && now.saturating_duration_since(target) <= period {
|
||||
target
|
||||
} else {
|
||||
now
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
bytes: Vec<u8>,
|
||||
frame_limit: Option<u64>,
|
||||
hardware: gba_core::cartridge::CartridgeHardware,
|
||||
save_device: gba_core::cartridge::SaveDevice,
|
||||
audio_enabled: bool,
|
||||
writer: &mut impl Write,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut session = Session::new(bytes)?;
|
||||
|
|
@ -28,7 +38,7 @@ pub fn run(
|
|||
)?;
|
||||
writeln!(
|
||||
writer,
|
||||
"Input releases on focus loss. No audio or saves. Startup frames are not suppressed."
|
||||
"Input releases and audio mutes on focus loss. No saves. Startup frames are not suppressed."
|
||||
)?;
|
||||
if let Some(limit) = frame_limit {
|
||||
writeln!(
|
||||
|
|
@ -37,10 +47,22 @@ pub fn run(
|
|||
)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
let mut audio = None;
|
||||
let mut samples = [gba_core::audio::StereoLevel::default(); 1024];
|
||||
let result = (|| -> Result<&str, Box<dyn Error>> {
|
||||
let title = "GBA Rust | ROM | Esc: exit | No audio or saves";
|
||||
let title = "GBA Rust | ROM | Esc: exit | No saves";
|
||||
let mut window = create_window(title)?;
|
||||
window.set_target_fps(0);
|
||||
if audio_enabled {
|
||||
let output = crate::audio::AudioOutput::new()?;
|
||||
writeln!(writer, "Audio enabled: {}", output.description())?;
|
||||
audio = Some(output);
|
||||
session.memory_mut().set_audio_capture(true);
|
||||
} else {
|
||||
writeln!(writer, "Audio muted; use --audio for macOS output.")?;
|
||||
}
|
||||
writer.flush()?;
|
||||
let mut was_audio_active = false;
|
||||
let period = Duration::from_secs_f64(f64::from(CYCLES_PER_FRAME) / f64::from(CPU_HZ));
|
||||
let mut frame_started = Instant::now();
|
||||
let mut was_stopped = false;
|
||||
|
|
@ -50,9 +72,30 @@ pub fn run(
|
|||
if let Some(clock) = rtc_clock.as_mut() {
|
||||
clock.sync(session.memory_mut())?;
|
||||
}
|
||||
let buttons = read_buttons(window.is_active(), |key| window.is_key_down(key));
|
||||
let reported_active = window.is_active();
|
||||
let focused = is_focused(reported_active);
|
||||
let buttons = read_buttons(reported_active, |key| window.is_key_down(key));
|
||||
let update = session.update(buttons)?;
|
||||
let stopped = update == Update::Stopped;
|
||||
if let Some(output) = audio.as_ref() {
|
||||
let active = focused && !stopped;
|
||||
if !active && was_audio_active {
|
||||
output.clear()?;
|
||||
}
|
||||
was_audio_active = active;
|
||||
loop {
|
||||
let count = session.memory_mut().drain_audio_samples(&mut samples);
|
||||
output.submit(if active { &samples[..count] } else { &[] })?;
|
||||
if count < samples.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if session.memory_mut().audio_dropped_samples() != 0 {
|
||||
return Err(
|
||||
io::Error::other("core audio capture overflow; reduce host load").into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if stopped != was_stopped {
|
||||
window.set_title(if stopped {
|
||||
"GBA Rust | ROM STOP: waiting for enabled keypad input | Esc: exit"
|
||||
|
|
@ -71,9 +114,12 @@ pub fn run(
|
|||
if frame_limit.is_some_and(|limit| session.frames() >= limit) {
|
||||
return Ok("window frame limit reached");
|
||||
}
|
||||
// Slow hosts do not skip emulated frames or accumulate catch-up work.
|
||||
thread::sleep(period.saturating_sub(frame_started.elapsed()));
|
||||
frame_started = Instant::now();
|
||||
// With audio, retain an absolute frame deadline so sleep overshoot
|
||||
// does not steadily starve the output device. Catch up at most one
|
||||
// frame; a slow host never accumulates unbounded work or skips emulation.
|
||||
frame_started =
|
||||
next_frame_start(frame_started, Instant::now(), period, audio_enabled);
|
||||
}
|
||||
Update::Stopped => {
|
||||
if frame_limit.is_some() {
|
||||
|
|
@ -95,5 +141,26 @@ pub fn run(
|
|||
writer,
|
||||
result.as_ref().copied().unwrap_or("ROM window diagnostic"),
|
||||
)?;
|
||||
if let Some(output) = audio.as_ref() {
|
||||
output.report(writer)?;
|
||||
}
|
||||
result.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pacing_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn audio_deadlines_absorb_sleep_overshoot_but_bound_catch_up() {
|
||||
let start = Instant::now();
|
||||
let period = Duration::from_millis(16);
|
||||
assert_eq!(
|
||||
next_frame_start(start, start + Duration::from_millis(17), period, true),
|
||||
start + period
|
||||
);
|
||||
let late = start + Duration::from_millis(50);
|
||||
assert_eq!(next_frame_start(start, late, period, true), late);
|
||||
let now = start + Duration::from_millis(17);
|
||||
assert_eq!(next_frame_start(start, now, period, false), now);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod audio;
|
||||
mod cartridge;
|
||||
mod desktop;
|
||||
|
||||
|
|
@ -53,6 +54,7 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
|
|||
"--window",
|
||||
"--frames",
|
||||
"--rtc",
|
||||
"--audio",
|
||||
"--save-type",
|
||||
]
|
||||
.iter()
|
||||
|
|
@ -86,7 +88,7 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
|
|||
[arg] if arg == "--help" || arg == "-h" => Ok(RunMode::Help),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] (--steps COUNT | --window [--frames COUNT]); or gameboy-rust --test-suite PATH.json",
|
||||
"usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] (--steps COUNT | --window [--frames COUNT] [--audio]); or gameboy-rust --test-suite PATH.json",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -132,6 +134,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
--rom PATH --steps COUNT Run a raw ROM in the terminal (1..=100000000 steps)\n\
|
||||
--rom PATH --window Open a raw ROM with keyboard input\n\
|
||||
--frames COUNT Exit ROM window after 1..=100000 captured frames\n\
|
||||
--audio Enable macOS output for ROM windows (default: muted)\n\
|
||||
--rtc Select cartridge RTC (UTC startup, host elapsed time)\n\
|
||||
--save-type flash64|flash128 Select Flash identification/read subset (no saving yet)\n\
|
||||
--test-suite PATH.json Run bounded ROM assertions; write a JSON report\n\
|
||||
|
|
@ -167,7 +170,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
Affine backgrounds: Arrows pan; Q rotates; W zooms; Z disables wrapping; Enter resets; Escape exits.\n\
|
||||
Bitmap pages: Arrows pan; Q rotates; W zooms; Z forces page1; Enter resets; Escape exits.\n\
|
||||
All demos use original test content. ROM mode uses our limited BIOS replacement.\n\
|
||||
Terminal ROM mode has no window or input. ROM modes have no audio or saves.\n\
|
||||
Terminal ROM mode has no window, input, or audio. ROM windows offer --audio on macOS. No saves.\n\
|
||||
ROM window: Arrows=D-pad; Z/X=A/B; Q/W=L/R; Enter=Start; Backspace=Select; Escape exits.\n\
|
||||
Commercial games are not supported."
|
||||
);
|
||||
|
|
|
|||
|
|
@ -82,10 +82,13 @@ fn wave_setup() -> Vec<u32> {
|
|||
let mut code = Vec::new();
|
||||
for (address, value) in [
|
||||
(0x04000084, 0x80),
|
||||
(0x04000088, 0x200),
|
||||
(0x04000080, 0x4477),
|
||||
(0x04000082, 2),
|
||||
(0x04000090, 0xf012), // CPU bank 1 before selecting playback bank 1.
|
||||
(0x04000070, 0xc0),
|
||||
(0x04000072, 0x2000),
|
||||
(0x04000074, 0x87ff),
|
||||
(0x04000074, 0x87e0),
|
||||
] {
|
||||
code.extend([0xe59f0000, 0xea000000, address]);
|
||||
code.extend([0xe59f1000, 0xea000000, value]);
|
||||
|
|
@ -95,6 +98,29 @@ fn wave_setup() -> Vec<u32> {
|
|||
code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_options_fail_before_file_access_unless_a_window_is_selected() {
|
||||
for tail in [
|
||||
vec!["--audio", "--steps", "1"],
|
||||
vec!["--audio", "--window", "--audio"],
|
||||
] {
|
||||
let output = command()
|
||||
.args(["--rom", "missing-original.gba"])
|
||||
.args(tail)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(stderr(&output).contains("usage:"));
|
||||
assert!(!stderr(&output).contains("cannot load ROM"));
|
||||
}
|
||||
let output = command()
|
||||
.args(["--audio", "--window", "--rom", "missing-original.gba"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(stderr(&output).contains("cannot load ROM"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_wave_program_can_activate_channel3_without_host_audio() {
|
||||
let fixture = Fixture::new();
|
||||
|
|
@ -401,7 +427,7 @@ fn help_describes_terminal_rom_mode_and_existing_cpu_demo_still_runs() {
|
|||
assert!(output.status.success());
|
||||
assert!(stdout(&output).contains("--rom PATH --steps COUNT"));
|
||||
assert!(stdout(&output).contains("--rom PATH --window"));
|
||||
assert!(stdout(&output).contains("ROM modes have no audio or saves"));
|
||||
assert!(stdout(&output).contains("ROM windows offer --audio on macOS. No saves."));
|
||||
let output = command().arg("--cpu-demo").output().unwrap();
|
||||
assert!(output.status.success());
|
||||
assert!(stdout(&output).contains("Exception demo"));
|
||||
|
|
@ -440,6 +466,56 @@ fn window_options_and_bad_files_fail_without_opening_a_window() {
|
|||
assert!(output.stdout.is_empty());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
#[ignore = "requires a focused native window and audio device; plays an original wave tone"]
|
||||
fn native_rom_audio_window_routes_core_samples_to_output() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.rom(&wave_setup());
|
||||
let mut child = command()
|
||||
.arg("--rom")
|
||||
.arg(&path)
|
||||
.args(["--window", "--audio", "--frames", "120"])
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let start = std::time::Instant::now();
|
||||
while child.try_wait().unwrap().is_none() {
|
||||
if start.elapsed() > std::time::Duration::from_secs(30) {
|
||||
child.kill().unwrap();
|
||||
let output = child.wait_with_output().unwrap();
|
||||
panic!(
|
||||
"audio window timed out: {} {}",
|
||||
stdout(&output),
|
||||
stderr(&output)
|
||||
);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
let output = child.wait_with_output().unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {}",
|
||||
stdout(&output),
|
||||
stderr(&output)
|
||||
);
|
||||
let text = stdout(&output);
|
||||
assert!(text.contains("Audio enabled: CoreAudio:"), "{text}");
|
||||
assert!(text.contains("Captured ROM frames: 120"), "{text}");
|
||||
let nonzero: u64 = text
|
||||
.split("nonzero output frames: ")
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert!(nonzero > 0, "{text}");
|
||||
assert!(text.contains("dropped input frames: 0"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a logged-in native desktop session"]
|
||||
fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors() {
|
||||
|
|
|
|||
|
|
@ -152,6 +152,9 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-demos/src/mosaic_demo.rs` | CPU-driven background/sprite mosaic demo runner |
|
||||
| `crates/gba-demos/src/raster_demo.rs` | Original ARM HBlank DMA raster program and color table |
|
||||
| `crates/gba-core/src/video/effects.rs` | Region masks and RGB555 alpha/brightness arithmetic |
|
||||
| `crates/desktop/src/audio.rs` | Host-audio platform boundary and explicit non-Darwin diagnostic |
|
||||
| `crates/desktop/src/audio/device.rs` | CPAL/CoreAudio lifecycle, nonblocking output callback, runtime errors, and counters |
|
||||
| `crates/desktop/src/audio/playback.rs` | Bounded host queue, prefill, stereo mapping, linear rate conversion, gain, and DC removal |
|
||||
| `crates/desktop/src/desktop.rs` | Native window, display test, and keyboard controls |
|
||||
| `crates/desktop/src/desktop/affine.rs` | Affine background window and smoke-test pixel checks |
|
||||
| `crates/desktop/src/desktop/affine_raster.rs` | Affine raster window and every-pixel smoke checks |
|
||||
|
|
@ -169,7 +172,7 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/desktop/src/cartridge/rtc_clock.rs` | Host-only UTC seed and monotonic elapsed-time adapter, with injected epoch/duration tests |
|
||||
| `crates/desktop/src/cartridge/window.rs` | ROM-window session, bounded slices, capture, STOP/input handling, and progress limits |
|
||||
| `crates/desktop/src/cartridge/window/tests.rs` | Original ROM pixels, keyboard mapping, STOP wake, HALT, frame budgets, and diagnostics |
|
||||
| `crates/desktop/src/desktop/rom.rs` | Main-thread ROM window, input polling, presentation pacing, and frame-limited exit |
|
||||
| `crates/desktop/src/desktop/rom.rs` | Main-thread ROM window, input polling, optional audio draining/muting, bounded pacing, and frame-limited exit |
|
||||
| `crates/desktop/examples/write_rom_demo.rs` | Non-overwriting export of the original input-test ROM |
|
||||
| `crates/desktop/src/cartridge/tests.rs` | ROM options, bounded readers, instruction/DMA/IRQ budgets, HALT/STOP, and diagnostic reports |
|
||||
| `crates/desktop/tests/rom_cli.rs` | File loading and exit-status checks; opt-in native ROM-window integration test |
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ Verified locally on macOS 15.7.3:
|
|||
- The headless suite runner passes generated ARM/Thumb/BIOS cases, produces repeatable JSON, and rejects deliberate assertion mismatches.
|
||||
|
||||
These results cover the CPU core, memory, Mode 0–5 snapshots and row capture, input mapping, and desktop windows.
|
||||
They do not verify a complete GBA display controller or audio.
|
||||
They do not verify a complete GBA display controller or hardware audio fidelity.
|
||||
CPAL 0.16 is a macOS-only dependency of the desktop crate. The existing Nix Darwin shell builds its CoreAudio backend without extra shell packages.
|
||||
Native tests submit an original tone and original ROM-generated samples to the default output device.
|
||||
Use `--audio` with a ROM window to opt in. Ordinary tests and muted windows do not require an audio device.
|
||||
Other host audio platforms are not implemented. Underruns remain possible; device callbacks do not prove listening quality.
|
||||
Arrow-key movement in the original host display test was confirmed manually on this Mac.
|
||||
CPU demos and ROM windows have automated input-mapping tests; their physical keyboard behavior still needs manual confirmation.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Audio device subset
|
||||
|
||||
The core models Direct Sound A/B, bounded sound DMA, and programmable sound generator (PSG) channels 1–4.
|
||||
Wave channel 3 supports single-bank playback only. The core does not produce desktop audio.
|
||||
Wave channel 3 supports single-bank playback only. Optional fixed-rate capture supplies samples to the separate Darwin output adapter.
|
||||
`Memory::audio_level()` exposes the current digital stereo level for deterministic inspection, not a continuous sample stream.
|
||||
|
||||
## Registers and PSG state
|
||||
|
|
@ -262,6 +262,43 @@ Capture is disabled by default. Changing enable state clears queued frames, phas
|
|||
Repeating the current enable state preserves capture state. Configure capture between machine steps.
|
||||
Original tests cover exact sample boundaries, timer consumption, pulse phases, batching, queue overflow, HALT/STOP, and transactional rollback.
|
||||
|
||||
## Darwin output adapter
|
||||
|
||||
Use `--audio` with `--rom ... --window` to enable CPAL 0.16/CoreAudio output on macOS.
|
||||
Windows remain muted by default. Terminal runs, ROM suites, and demos do not open an audio device.
|
||||
The flag is invalid in terminal mode and cannot be repeated. Other platforms currently report an explicit unsupported-platform error.
|
||||
|
||||
The adapter opens the default output device at its default configuration.
|
||||
Supported formats are float32, signed16, and unsigned16, with 8–192 kHz rates and 1–32 channels.
|
||||
Stereo maps to the first two channels. Mono averages left/right; additional channels receive silence.
|
||||
Device initialization, unsupported formats, and stream errors fail the audio-enabled run rather than silently switching to muted execution.
|
||||
Omit `--audio` to run without an audio device.
|
||||
|
||||
The core's 32,768 Hz frames enter a bounded 8,192-frame host queue.
|
||||
Playback waits for 1,024 frames before starting or resuming after an underrun.
|
||||
Linear interpolation converts to the device rate. A 20 Hz high-pass filter removes DC, followed by conservative 25% host gain and clipping.
|
||||
These are presentation choices, not a measured GBA analog model or band-limited reconstruction.
|
||||
High-frequency aliases, startup transients, and channel-model inaccuracies remain possible.
|
||||
|
||||
The data callback performs no allocation, blocking lock, emulation step, or log write.
|
||||
It tries the queue lock once; contention produces silence and increments a counter.
|
||||
An underrun produces silence and returns to the prefill state. Overflow discards new input frames and records the loss.
|
||||
The final report includes submitted/nonzero-input frames, callbacks, nonzero-output frames, underruns, dropped input, and lock misses.
|
||||
Zero submitted frames can indicate an unfocused window, not a failed output device.
|
||||
The window treats a core capture overflow as an error; it drains samples between bounded CPU slices.
|
||||
The callback cannot advance emulator clocks or request extra emulation work.
|
||||
|
||||
Focus loss and STOP clear queued host audio and conversion state. Muted frames are drained without being submitted.
|
||||
The device may still finish its already-submitted buffer. Resume waits for fresh prefill instead of replaying stale samples.
|
||||
Audio-enabled windows use absolute frame deadlines with at most one frame of catch-up.
|
||||
This reduces sleep drift without skipping emulated frames or accumulating unbounded catch-up work.
|
||||
Slow hosts can still underrun. Sample delivery is not a claim of uninterrupted or hardware-faithful sound.
|
||||
|
||||
Native checks on Darwin arm64 exercised a quiet original tone and an original wave-ROM window through CoreAudio.
|
||||
A 900-frame local Emerald run reached its limit with 554,484 nonzero device-output frames, zero dropped input frames, and 13 underruns.
|
||||
Those counters demonstrate output submission, not human listening, correct music, or game compatibility.
|
||||
Audio quality and physical speaker/headphone output still require a manual listening check.
|
||||
|
||||
## Validation and remaining work
|
||||
|
||||
Original tests cover signed samples, byte order, queue capacity, partial writes, resets, underflow, stereo routing, and clipping.
|
||||
|
|
@ -278,13 +315,16 @@ CPU/DMA tests include noise register writes at bus completion and failed-step ro
|
|||
BIOS reset tests verify that selected sound reset stops all supported PSG channels, while unselected sound state remains active.
|
||||
|
||||
These tests validate the documented nominal model, not hardware audio fidelity.
|
||||
Remaining audio work: two-bank wave playback, verified live bank changes, hardware PWM sampling, filtering/resampling, and a Darwin host backend.
|
||||
Remaining audio work: two-bank wave playback, verified live bank changes, hardware PWM sampling, improved reconstruction, and lower-underrun pacing.
|
||||
A local Emerald input probe exposed a wave trigger with the gate disabled. This is now accepted without activating playback.
|
||||
The same scheduled-input probe passes that point and completes its extended frame budget; audible output and gameplay remain unverified.
|
||||
The same scheduled-input probe passes that point and completes its extended frame budget; gameplay remains unverified.
|
||||
Later native output checks exercise the host audio path separately; they do not establish game-audio accuracy.
|
||||
See [the local runtime result](../research/emerald-reset.md).
|
||||
|
||||
## References
|
||||
|
||||
- [CPAL 0.16.0](https://docs.rs/cpal/0.16.0/cpal/): default device/configuration, typed output callbacks, sample formats, CoreAudio backend, and stream lifecycle. CPAL is a macOS-only desktop dependency here.
|
||||
|
||||
- [GBATEK wave channel 3](https://problemkaputt.de/gbatek-gba-sound-channel-3-wave-output.htm): bank selection, nibble order, rotating RAM, sample rate, length, and force volume.
|
||||
- [NanoBoyAdvance wave at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/channel/wave_channel.cc) and [header](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/channel/wave_channel.hh): signed gain and rate comparison; pointer-based RAM and two-bank/retrigger differences are not copied.
|
||||
|
||||
|
|
|
|||
|
|
@ -544,6 +544,7 @@ Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbac
|
|||
The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op.
|
||||
Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction.
|
||||
Prior boot/service steps remain committed on failure.
|
||||
Two-bank wave playback, host audio output, connected serial links, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
|
||||
Two-bank wave playback, connected serial links, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
|
||||
Darwin ROM windows can enable host audio with `--audio`; this does not extend BIOS coverage.
|
||||
Disconnected normal serial transfers use the [bounded serial model](serial.md); cancel active transfers before BIOS serial reset.
|
||||
This subset is not sufficient for Pokémon Emerald compatibility.
|
||||
|
|
|
|||
|
|
@ -135,5 +135,9 @@ The CPU writes the palette: blue at rest, red while Z is held, and green while R
|
|||
Right takes priority over Z. Other buttons have no visible effect in this small program.
|
||||
No game or firmware content is included.
|
||||
|
||||
Both ROM modes lack audio, saves, and per-step tracing. Commercial-game compatibility remains unverified and unsupported.
|
||||
ROM windows can enable Darwin output with `--audio`; they are muted by default.
|
||||
The option requires a usable default audio device. Device/configuration errors fail explicitly; omit the flag to run muted.
|
||||
Focus loss and STOP clear queued audio. See [audio output and its limits](audio.md#darwin-output-adapter).
|
||||
Terminal mode has no audio. Both modes lack save writing/persistence and per-step tracing.
|
||||
Commercial-game compatibility remains unverified and unsupported.
|
||||
Only load files that you may lawfully use. Do not commit game ROMs or firmware.
|
||||
|
|
|
|||
|
|
@ -125,7 +125,8 @@ This result does not establish gameplay, linked transfers, or cartridge/serial G
|
|||
Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels.
|
||||
Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.
|
||||
All supported channels use the instantaneous digital-level inspection interface.
|
||||
Wave channel 3 now supports single-bank playback. Two-bank playback, a continuous sample stream, and desktop output are still missing.
|
||||
Wave channel 3 now supports single-bank playback. Two-bank playback remains missing.
|
||||
Fixed-rate capture and optional Darwin `--audio` output were added later; their counters do not establish correct game music.
|
||||
Do not treat digital-level tests as audible-game validation or suppress remaining diagnostics without implementing their behavior.
|
||||
Cartridge saves remain a separate required feature. SRAM alone will not support Emerald's Flash save protocol.
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ A separate 600-frame Emerald native run still reaches its frame limit without in
|
|||
## Remaining requirements
|
||||
|
||||
- Verify visible menu text and gameplay through direct window testing or explicit independently justified assertions.
|
||||
- Implement continuous sample production and a Darwin output backend before claiming audible sound.
|
||||
- Fixed-rate capture and opt-in Darwin `--audio` output were added later. Verify listening quality and reduce observed underruns.
|
||||
- Research two-bank wave playback and live bank changes separately. Both remain diagnostic during active playback.
|
||||
- Implement Flash programming/erase and safe persistence before claiming saving.
|
||||
|
||||
|
|
@ -93,5 +93,6 @@ For a manual native check, use:
|
|||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --window
|
||||
```
|
||||
|
||||
Enter maps to Start. Z maps to A. Escape exits. This command does not supply the deterministic probe schedule or fixed RTC.
|
||||
Enter maps to Start. Z maps to A. Escape exits. Add `--audio` for optional Darwin output.
|
||||
This command does not supply the deterministic probe schedule or fixed RTC.
|
||||
Hardware sources and implementation comparisons are recorded in [audio references](../hardware/audio.md#references).
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ Update this file when a feature lands or a limit is removed.
|
|||
- Wave channel 3 supports single-bank nibble playback, rotating RAM, volume/force-volume, length, activity status, and digital mixing.
|
||||
- Two-bank wave playback, active bank changes, and unsupported serial operations remain diagnostic with whole-access and block-store rollback.
|
||||
- ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP.
|
||||
- Opt-in macOS CoreAudio output, with tested original tones and wave-ROM playback; focus loss and STOP mute queued audio.
|
||||
- Optional ROM-window frame limits and an original input-test ROM generator.
|
||||
- Headless JSON ROM suites with PC or VBlank completion, register/CPSR/memory/pixel assertions, scheduled buttons, bounded budgets, and structured failure reports.
|
||||
- Hash-pinned preparation of public `jsmolka/gba-tests` ARM, Thumb, memory, and BIOS ROMs, with verified result-register checkpoints.
|
||||
|
|
@ -146,7 +147,7 @@ Internal affine origins, vertical window flags, and vertical mosaic counters tra
|
|||
Horizontal window comparators retain four-cycle event history.
|
||||
Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits.
|
||||
Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented.
|
||||
There is no host audio output or save writing/persistence.
|
||||
ROM windows offer opt-in Darwin audio output with `--audio`. Save writing/persistence remains unavailable.
|
||||
The [Flash subset](hardware/saves.md) supports explicit device selection, identification, array reads, and banking.
|
||||
Flash programming, erase, busy polling, wider accesses, unverified aliases, and other save-device protocols remain diagnostic or unimplemented.
|
||||
The optional [cartridge GPIO/RTC subset](hardware/cartridge-gpio.md) supports calendar/control transfers and explicit clock updates, selected with `--rtc` in ROM modes.
|
||||
|
|
@ -170,7 +171,9 @@ The presentation framebuffer is separate from emulated video RAM.
|
|||
All named graphics demos connect desktop input and rendering to the emulated CPU. The default display test remains host-generated.
|
||||
ROM files can run in bounded terminal mode or a window with the original BIOS replacement.
|
||||
ROM windows have no demo-specific startup checks. Input is sampled between bounded execution slices.
|
||||
Audio, saves, and external BIOS loading remain unavailable. ROM loading does not establish commercial-game compatibility.
|
||||
Audio is muted by default; `--audio` opens the macOS output device for ROM windows only. Saves and external BIOS loading remain unavailable.
|
||||
The host output has bounded buffers, linear rate conversion, DC removal, and underrun/drop counters. Slow hosts can stutter.
|
||||
ROM loading and nonzero audio callbacks do not establish commercial-game compatibility or correct music.
|
||||
A local Emerald run now completes its all-device reset and master sound enable.
|
||||
It also passes the pulse/noise triggers and normal-mode RCNT configuration.
|
||||
It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior.
|
||||
|
|
|
|||
|
|
@ -81,7 +81,25 @@ An original desktop ROM installs its Flash byte reader in IWRAM; terminal/native
|
|||
They verify explicit activation errors and atomic byte/word/block-store/DMA failure behavior.
|
||||
Wave tests cover every frequency and bank, independent nibble rotation, full rotations, bounded maximum batches, and fractional gains.
|
||||
They also check inactive triggers, gates, length, status, master/BIOS reset, HALT/STOP, live-write limits, and CPU/DMA rollback.
|
||||
Original terminal/native programs activate wave playback without external assets or host audio.
|
||||
Original terminal/native programs activate wave playback without external assets.
|
||||
Fixed-rate capture tests verify sample boundaries, timer/PSG ordering, rollback, queue limits, drop counts, and HALT/STOP.
|
||||
Host conversion tests require no device and cover rates, stereo/mono mapping, prefill, overflow, underflow, DC removal, and callback splitting.
|
||||
CLI tests keep output muted by default and reject `--audio` outside ROM-window mode.
|
||||
|
||||
The following ignored tests require a native audio device and play original test tones:
|
||||
|
||||
```sh
|
||||
direnv exec . cargo test --locked --release -p gameboy-rust-desktop native_audio_output -- --ignored --nocapture --test-threads=1
|
||||
direnv exec . cargo test --locked --release -p gameboy-rust-desktop --test rom_cli native_rom_audio_window -- --ignored --test-threads=1
|
||||
```
|
||||
|
||||
The device test generates samples with the actual core and does not require window focus.
|
||||
The window test requires focus. It checks that ROM-generated samples reach a nonzero CoreAudio callback.
|
||||
An unfocused window correctly submits no samples, so that test fails rather than claiming playback success.
|
||||
The focused window check passed locally; a later unattended run remained unfocused and did not validate playback.
|
||||
Use the reported submitted-frame count to distinguish this condition from an output-device failure.
|
||||
These checks do not prove physical speaker output, correct game music, or hardware audio fidelity.
|
||||
Normal Cargo/Python tests do not open an audio device. Underruns are reported and can occur on slow hosts.
|
||||
The optional [local Emerald input probe](research/emerald-wave.md) now completes its scheduled Start/A frame budget.
|
||||
Frame completion and different pixels are not assertions about readable menus, correct graphics, or gameplay.
|
||||
[Jev evidence review](jev-debugging.md) is optional and never changes deterministic test verdicts.
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ A [fixed-time input probe](research/emerald-wave.md) then exposed an unsupported
|
|||
Single-bank wave support now passes that point and completes 1,320 frames with scheduled Start/A input.
|
||||
Captured output differs from a no-input control; readable menus and gameplay remain unverified.
|
||||
The core has tested Direct Sound, both pulse channels, single-bank wave channel 3, and noise channel 4.
|
||||
Two-bank wave playback, a continuous sample stream, and desktop audio output remain missing.
|
||||
Fixed-rate stereo capture and opt-in Darwin `--audio` output now work with original native probes.
|
||||
A local Emerald run submits nonzero device frames but records underruns; listening quality and correct music remain unverified.
|
||||
Two-bank wave playback, hardware PWM sampling, and improved reconstruction/pacing remain missing.
|
||||
See [the runtime result](research/emerald-reset.md). Next, verify visible menu progress rather than treating frame counts as playability.
|
||||
The separate save requirement includes Flash programming/erase and safe host persistence. Identification/read support is not successful saving.
|
||||
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue