From 1bebb6dac35dfdf910d4452bb42d5ceb643a2a9f Mon Sep 17 00:00:00 2001 From: Andy Russell Date: Sat, 2 Jan 2021 00:44:21 -0500 Subject: [PATCH 1/5] improve cycle documentation --- src/cpu/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index 37c30da..95f74b9 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -16,6 +16,10 @@ use log::*; pub use self::instructions::Instruction; pub use self::registers::{Flags, Registers}; +/// Machine cycles. The minimum number of cycles that must occur before another instruction can be +/// decoded. +/// +/// All instructions take 1-6 whole M-cycles to complete. An M-cycle is equivalent to 4 T-cycles. #[derive( Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Add, AddAssign, Sub, SubAssign, )] @@ -33,6 +37,7 @@ impl From for MCycles { } } +/// Time cycles. An individual clock of the CPU. #[derive( Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Add, AddAssign, Sub, SubAssign, )] From 7a92c1fc93118e99b88c26ce0262395a11cc6947 Mon Sep 17 00:00:00 2001 From: Andy Russell Date: Tue, 22 Dec 2020 16:31:51 -0500 Subject: [PATCH 2/5] implement square wave output, without sweep --- Cargo.lock | 896 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/audio/mod.rs | 517 ++++++++++++++++++------- src/audio/output.rs | 72 +++- src/cpu/mod.rs | 3 + src/lib.rs | 14 + src/main.rs | 8 +- 7 files changed, 1358 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91f7506..c6e853b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,27 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9fe5e32de01730eb1f6b7f5b51c17e03e2325bf40a74f754f04f130043affff" +[[package]] +name = "addr2line" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aho-corasick" version = "0.7.15" @@ -15,6 +36,28 @@ dependencies = [ "memchr", ] +[[package]] +name = "alsa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix 0.15.0", +] + +[[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 = "andrew" version = "0.3.1" @@ -49,6 +92,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + [[package]] name = "ash" version = "0.31.0" @@ -75,6 +124,51 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + [[package]] name = "bit-set" version = "0.5.2" @@ -132,6 +226,12 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + [[package]] name = "calloop" version = "0.6.5" @@ -139,7 +239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" dependencies = [ "log", - "nix", + "nix 0.18.0", ] [[package]] @@ -151,6 +251,21 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -163,6 +278,23 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chunked_transfer" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7477065d45a8fe57167bf3cf8bcd3729b54cfcb81cca49bda2d038ea89ae82ca" + +[[package]] +name = "clang-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0659001ab56b791be01d4b729c44376edc6718cf389a502e579b77b758f3296c" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "2.33.3" @@ -218,6 +350,63 @@ dependencies = [ "objc", ] +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + +[[package]] +name = "combine" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9417a0c314565e2abffaece67e95a8cb51f9238cd39f3764d9dfdf09e72b20c" +dependencies = [ + "bytes", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "const_fn" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826" + +[[package]] +name = "cookie" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" +dependencies = [ + "cookie", + "idna", + "log", + "publicsuffix", + "serde", + "serde_json", + "time", + "url", +] + [[package]] name = "copyless" version = "0.1.5" @@ -244,6 +433,12 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" + [[package]] name = "core-foundation-sys" version = "0.7.0" @@ -306,6 +501,59 @@ dependencies = [ "objc", ] +[[package]] +name = "coreaudio-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491" +dependencies = [ + "bitflags", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05631e2089dfa5d3b6ea1cfbbfd092e2ee5deeb69698911bc976b28b746d3657" +dependencies = [ + "alsa", + "core-foundation-sys 0.6.2", + "coreaudio-rs", + "jni 0.17.0", + "js-sys", + "lazy_static", + "libc", + "mach", + "ndk", + "ndk-glue", + "nix 0.15.0", + "oboe", + "parking_lot", + "stdweb 0.1.3", + "thiserror", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "csv" version = "1.1.4" @@ -417,6 +665,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "dispatch" version = "0.2.0" @@ -457,6 +711,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + [[package]] name = "feo-boy" version = "0.1.0" @@ -464,7 +728,9 @@ dependencies = [ "anyhow", "bitflags", "byteorder", + "cpal", "csv", + "derivative", "derive_more", "indoc", "itertools", @@ -473,6 +739,7 @@ dependencies = [ "num_enum 0.5.1", "pixels", "pretty_env_logger", + "proptest", "quickcheck", "quote", "rand", @@ -486,6 +753,29 @@ dependencies = [ "winit_input_helper", ] +[[package]] +name = "fetch_unroll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d44807d562d137f063cbfe209da1c3f9f2fa8375e11166ef495daab7b847f9" +dependencies = [ + "libflate", + "tar", + "ureq", +] + +[[package]] +name = "filetime" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + [[package]] name = "fnv" version = "1.0.7" @@ -507,6 +797,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -784,6 +1084,18 @@ dependencies = [ "slab", ] +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "heck" version = "0.3.1" @@ -817,6 +1129,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indoc" version = "1.0.3" @@ -865,6 +1188,34 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +[[package]] +name = "jni" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402" +dependencies = [ + "cesu8", + "combine 3.8.1", + "error-chain", + "jni-sys", + "log", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bcc950632e48b86da402c5c077590583da5ac0d480103611d5374e7c967a3c" +dependencies = [ + "cesu8", + "combine 4.4.0", + "error-chain", + "jni-sys", + "log", + "walkdir", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -917,6 +1268,24 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "libflate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", + "rle-decode-fast", +] + +[[package]] +name = "libflate_lz77" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" + [[package]] name = "libloading" version = "0.6.5" @@ -945,6 +1314,15 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -954,6 +1332,12 @@ dependencies = [ "libc", ] +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -990,6 +1374,16 @@ dependencies = [ "objc", ] +[[package]] +name = "miniz_oxide" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.6.22" @@ -1103,6 +1497,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nix" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + [[package]] name = "nix" version = "0.18.0" @@ -1125,6 +1532,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -1197,6 +1615,35 @@ dependencies = [ "cc", ] +[[package]] +name = "object" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" + +[[package]] +name = "oboe" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aadc2b0867bdbb9a81c4d99b9b682958f49dbea1295a81d2f646cca2afdd9fc" +dependencies = [ + "jni 0.14.0", + "ndk", + "ndk-glue", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82" +dependencies = [ + "fetch_unroll", +] + [[package]] name = "once_cell" version = "1.5.2" @@ -1238,6 +1685,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1371,6 +1824,48 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "proptest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12e6c80c1139113c28ee4670dc50cc42915228b51f56a9e407f0ec60f966646f" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", +] + +[[package]] +name = "publicsuffix" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" +dependencies = [ + "error-chain", + "idna", + "lazy_static", + "regex", + "url", +] + +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1439,6 +1934,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rand_xorshift" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" +dependencies = [ + "rand_core", +] + [[package]] name = "range-alloc" version = "0.1.1" @@ -1497,6 +2001,70 @@ version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ring" +version = "0.16.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "rle-decode-fast" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" + +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" +dependencies = [ + "base64", + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "rusttype" version = "0.9.2" @@ -1507,6 +2075,18 @@ dependencies = [ "owned_ttf_parser", ] +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "rustyline" version = "6.3.0" @@ -1518,7 +2098,7 @@ dependencies = [ "libc", "log", "memchr", - "nix", + "nix 0.18.0", "scopeguard", "unicode-segmentation", "unicode-width", @@ -1553,6 +2133,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.117" @@ -1573,6 +2178,29 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "slab" version = "0.4.2" @@ -1599,12 +2227,18 @@ dependencies = [ "lazy_static", "log", "memmap", - "nix", + "nix 0.18.0", "wayland-client", "wayland-cursor", "wayland-protocols", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spirv_cross" version = "0.22.0" @@ -1626,6 +2260,70 @@ dependencies = [ "num-traits", ] +[[package]] +name = "standback" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf906c8b8fc3f6ecd1046e01da1d8ddec83e48c8b08b84dcc02b585a6bedf5a8" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "storage-map" version = "0.3.0" @@ -1682,6 +2380,32 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tar" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489997b7557e9a43e192c527face4feacc78bfbe6eed67fd55c4c9e381cba290" +dependencies = [ + "filetime", + "libc", + "redox_syscall", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + [[package]] name = "termcolor" version = "1.1.0" @@ -1735,6 +2459,59 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7572415bd688d401c52f6e36f4c8e805b9ae1622619303b9fa835d531db0acae" +[[package]] +name = "time" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcdaeea317915d59b2b4cd3b5efcd156c309108664277793f5351700c02ce98b" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb 0.4.20", + "time-macros", + "version_check", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "toml" version = "0.5.7" @@ -1785,6 +2562,24 @@ dependencies = [ "wide", ] +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.7.0" @@ -1809,6 +2604,52 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" +dependencies = [ + "base64", + "chunked_transfer", + "cookie", + "cookie_store", + "log", + "once_cell", + "qstring", + "rustls", + "url", + "webpki", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.0" @@ -1827,6 +2668,21 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.3.1" @@ -1919,7 +2775,7 @@ dependencies = [ "bitflags", "downcast-rs", "libc", - "nix", + "nix 0.18.0", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -1932,7 +2788,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230b3ffeda101f877ff8ecb8573f5d26e7beb345b197807c4df34ec06879a3e6" dependencies = [ - "nix", + "nix 0.18.0", "once_cell", "smallvec", "wayland-sys", @@ -1944,7 +2800,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0aad1b4301cdccfb5f64056a4736e8155a5f4734bac41fdbca80b1fdbe1ab3e1" dependencies = [ - "nix", + "nix 0.18.0", "wayland-client", "xcursor", ] @@ -1993,6 +2849,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" +dependencies = [ + "webpki", +] + [[package]] name = "wgpu" version = "0.6.0" @@ -2185,6 +3060,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + [[package]] name = "xcursor" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 8ee2f15..67ec323 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ path = "src/main.rs" anyhow = "1.0.31" bitflags = "1.2.1" byteorder = "1.3.4" +cpal = "0.13.1" +derivative = "2.1.1" derive_more = "0.99.7" itertools = "0.9.0" lazy_static = "1.4.0" @@ -41,5 +43,6 @@ serde = { version = "1.0.111", features = ["derive"] } [dev-dependencies] indoc = "1.0.3" +proptest = "0.10.1" quickcheck = "0.9.2" rand = "0.7.3" diff --git a/src/audio/mod.rs b/src/audio/mod.rs index d170c1e..a76238a 100644 --- a/src/audio/mod.rs +++ b/src/audio/mod.rs @@ -1,10 +1,73 @@ -//! Audio-related functionality. +//! Implementation of the Game Boy sound hardware. //! -//! Contains an implementation of the Game Boy sound hardware. +//! Detailed information about this component can be found on the [Gameboy sound hardware page of +//! the GB dev wiki][sound hardware wiki]. +//! +//! [sound hardware wiki]: https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware -use crate::bytes::ByteExt; +use std::collections::VecDeque; +use std::convert::TryInto; +use std::sync::{Arc, Mutex}; + +use anyhow::Result; +use crate::bytes::ByteExt; +use crate::cpu; use crate::memory::Addressable; +use crate::TCycles; + +mod output; + +use output::Output; + +/// Shared buffer containing PCM audio samples generated by the emulator to be sent to the audio +/// device. +type SampleBuffer = Arc>>; + +/// Four available square wave duty cycles. +const DUTY: [[u8; 8]; 4] = [ + [0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 0], +]; + +/// Generates clocks for the other modulation units. +#[derive(Debug)] +struct FrameSequencer { + timer: u32, +} + +impl FrameSequencer { + fn new() -> FrameSequencer { + FrameSequencer { timer: 0 } + } + + /// Ticks the frame sequencer a single cycle. + /// + /// If the step value of the sequencer has changed since the last call, returns the new value. + fn step(&mut self) -> Option { + const FREQUENCY: u32 = cpu::FREQUENCY / 512; + + let step = if self.timer % FREQUENCY == 0 { + Some( + (self.timer / FREQUENCY) + .try_into() + .expect("step must be between 0 and 7"), + ) + } else { + None + }; + + self.timer += 1; + + if self.timer == FREQUENCY * 8 { + self.timer = 0; + } + + step + } +} /// The sweep register data for a channel. #[derive(Debug, Default)] @@ -44,22 +107,41 @@ pub struct Wave { pub pattern: u8, /// Sound length (0-63). - pub length: u8, + pub length_load: u8, + + length: u8, + + enabled: bool, } impl Wave { /// Gets the result of reading the wave register for the current register state. pub fn read(&self) -> u8 { let mut byte = self.pattern << 6; - byte |= 0x3F; + byte |= self.length_load; byte } /// Modifies the wave state according to the written byte. pub fn write(&mut self, byte: u8) { - self.length = byte & 0x3F; self.pattern = byte >> 6; + self.length_load = byte & 0x3F; + self.length = 64 - self.length_load; + } + + /// Clocks the length counter. Returns `true` if the counter reaches zero. + fn step(&mut self) -> bool { + if self.length > 0 && self.enabled { + self.length -= 1; + } + + if self.length == 0 { + self.enabled = false; + return true; + } else { + return false; + } } } @@ -74,6 +156,13 @@ pub struct Envelope { /// Number of envelope sweep (0-7). The length of one step of the sweep is n * (1/64). pub number: u8, + + /// Whether the unit should update the volume on the next step. + disabled: bool, + + counter: u8, + + volume: u8, } impl Envelope { @@ -92,6 +181,27 @@ impl Envelope { self.direction_increase = byte.has_bit_set(3); self.number = byte & 0x7; } + + fn step(&mut self) { + if self.number != 0 { + self.counter += 1; + + if self.counter >= self.number { + let new_volume = if self.direction_increase { + Some(self.volume + 1) + } else { + self.volume.checked_sub(1) + }; + + match new_volume { + Some(new_volume) if new_volume <= 15 => self.volume = new_volume, + _ => self.disabled = true, + } + + self.counter = 0; + } + } + } } /// The frequency data for a channel. @@ -175,6 +285,8 @@ impl OutputLevel { pub struct Length { /// Sound length (0-63). pub length: u8, + + enabled: bool, } // TODO test @@ -250,9 +362,9 @@ impl InitialCounterConsecutive { } } -/// A single Game Boy sound channel. +/// A single Game Boy square channel. #[derive(Debug, Default)] -pub struct Sound { +pub struct SquareChannel { /// Whether or not the sound is enabled. pub is_on: bool, @@ -273,6 +385,62 @@ pub struct Sound { /// The frequency data. pub frequency: Frequency, + + timer: u16, + + cycle_position: usize, + + output: u32, +} + +impl SquareChannel { + fn step(&mut self) { + if self.timer > 0 { + self.timer -= 1; + } else { + self.cycle_position += 1; + if self.cycle_position == 8 { + self.cycle_position = 0; + } + + self.timer = (2048 - self.frequency.frequency) * 4; + } + + self.output = if self.is_on { + let wave = u32::from(DUTY[usize::from(self.wave.pattern)][self.cycle_position]); + wave * u32::from(self.envelope.volume) + } else { + 0 + }; + } + + /// Executes the [trigger event]. + /// + /// [trigger event]: https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Trigger_Event + fn trigger(&mut self) { + self.is_on = true; + + if self.wave.length == 0 { + self.wave.length = 64; + } + + // TODO: reload frequency timer + + self.envelope.counter = self.envelope.number; + self.envelope.volume = self.envelope.initial_vol; + + // TODO: Handle disabled channel DAC + } + + fn clock_length(&mut self) { + if self.wave.step() { + self.is_on = false; + } + } + + fn clock_envelope(&mut self) { + self.envelope.step(); + } } /// Sound channel 3. @@ -325,14 +493,20 @@ pub struct Sound4 { pub initial_counter_consecutive: InitialCounterConsecutive, } -/// The controller for the four sound channels output by the Game Boy. -#[derive(Debug, Default)] +/// The controller for the four sound channels output by the Game Boy. Also known as the APU (Audio +/// Processing Unit) or PAPU (Pseudo-Audio Processing Unit). +/// +/// Generates [PCM audio samples] at the frequency of the APU and stores them in a ring buffer. +/// These samples are then downsampled to a rate acceptable by the computer hardware. +/// +/// [PCM audio samples]: https://en.wikipedia.org/wiki/Pulse-code_modulation +#[derive(Debug)] pub struct SoundController { - /// Sound 1: Rectangle waveform with sweep and envelope functions. - pub sound_1: Sound, + /// Square 1: Rectangle waveform with sweep and envelope functions. + pub square_1: SquareChannel, - /// Sound 2: Rectangle waveform with envelope function. - pub sound_2: Sound, + /// Square 2: Rectangle waveform with envelope function. + pub square_2: SquareChannel, /// Sound 3: A waveform specificed by the waveform RAM. pub sound_3: Sound3, @@ -340,6 +514,8 @@ pub struct SoundController { /// Sound 4: White noise with an envelope function. pub sound_4: Sound4, + frame_sequencer: FrameSequencer, + /// Toggle whether or not sound is enabled. pub sound_enabled: bool, @@ -354,11 +530,70 @@ pub struct SoundController { /// Whether to output Vin to SO2. pub vin_so2: bool, + + /// Audio output. `None` if no output is desired. + out: Option, } impl SoundController { + /// Creates an emulated sound controller with no output. pub fn new() -> SoundController { - SoundController::default() + SoundController { + square_1: SquareChannel::default(), + square_2: SquareChannel::default(), + sound_3: Sound3::default(), + sound_4: Sound4::default(), + frame_sequencer: FrameSequencer::new(), + sound_enabled: false, + so1_vol: 0, + so2_vol: 0, + vin_so1: false, + vin_so2: false, + out: None, + } + } + + /// Creates an emulated sound controller tha + pub fn new_with_playback() -> Result { + Ok(SoundController { + out: Some(Output::new()?), + ..Default::default() + }) + } + + /// Ticks the audio output by a number of T-cycles. + pub fn step(&mut self, cycles: TCycles) { + for _ in 0..cycles.0 { + if let Some(step) = self.frame_sequencer.step() { + if step % 2 == 0 { + self.square_1.clock_length(); + self.square_2.clock_length(); + } + + if step == 7 { + self.square_1.clock_envelope(); + self.square_2.clock_envelope(); + } + } + + self.square_1.step(); + self.square_2.step(); + + if let Some(out) = &self.out { + let mut sample = 0_f32; + + sample += self.square_1.output as f32 / 100_f32; + sample += self.square_2.output as f32 / 100_f32; + + out.sample_buffer.lock().unwrap().push_back(sample); + } + } + } +} + +impl Default for SoundController { + fn default() -> Self { + SoundController::new() } } @@ -382,18 +617,18 @@ impl Addressable for SoundController { // 0: Addition (frequency increases) // 1: Subtraction (frequency decreases) // Bit 2-0 - Number of sweep shift (n: 0-7) - 0xFF10 => self.sound_1.sweep.read(), + 0xFF10 => self.square_1.sweep.read(), // NR11: Sound 1 Sound length/Wave pattern duty // Bit 7-6 - Wave pattern duty // Bit 5-0 - Sound length data (Write only) - 0xFF11 => self.sound_1.wave.read(), + 0xFF11 => self.square_1.wave.read(), // NR12: Channel 1 volume envelope // Bit 7-4 - Initial volume of the envelope (0-15) (0 = no sound) // Bit 3 - Envelope direction (0 = decrease, 1 = increase) // Bit 2-0 - Number of envelope sweep (n: 0-7) (If 0, stop the envelope operation) - 0xFF12 => self.sound_1.envelope.read(), + 0xFF12 => self.square_1.envelope.read(), // NR13: Channel 1 frequency low // Unreadable. @@ -404,18 +639,18 @@ impl Addressable for SoundController { // Bit 6 - Counter/consecutive selection (1 = stop output when length in NR11 // expires) // Bit 2-0 - Frequency's higher 3 bits (write only) - 0xFF14 => self.sound_1.frequency.read_hi(), + 0xFF14 => self.square_1.frequency.read_hi(), // NR21: Sound 2 Sound length/Wave pattern duty // Bit 7-6 - Wave pattern duty // Bit 5-0 - Sound length data (Write only) - 0xFF16 => self.sound_2.wave.read(), + 0xFF16 => self.square_2.wave.read(), // NR22: Channel 2 volume envelope // Bit 7-4 - Initial volume of the envelope (0-15) (0 = no sound) // Bit 3 - Envelope direction (0 = decrease, 1 = increase) // Bit 2-0 - Number of envelope sweep (n: 0-7) (If 0, stop the envelope operation) - 0xFF17 => self.sound_2.envelope.read(), + 0xFF17 => self.square_2.envelope.read(), // NR23: Channel 2 frequency low // Unreadable. @@ -426,7 +661,7 @@ impl Addressable for SoundController { // Bit 6 - Counter/consecutive selection (1 = stop output when length in NR21 // expires) // Bit 2-0 - Frequency's higher 3 bits (write only) - 0xFF19 => self.sound_2.frequency.read_hi(), + 0xFF19 => self.square_2.frequency.read_hi(), // NR30: Channel 3 sound on/off // Bit 7 - Sound channel 3 off (0=Stop, 1=Playback) @@ -512,11 +747,11 @@ impl Addressable for SoundController { 0xFF25 => { let mut byte: u8 = 0; - byte.set_bit(0, self.sound_1.so1_enabled); - byte.set_bit(4, self.sound_1.so2_enabled); + byte.set_bit(0, self.square_1.so1_enabled); + byte.set_bit(4, self.square_1.so2_enabled); - byte.set_bit(1, self.sound_2.so1_enabled); - byte.set_bit(5, self.sound_2.so2_enabled); + byte.set_bit(1, self.square_2.so1_enabled); + byte.set_bit(5, self.square_2.so2_enabled); byte.set_bit(2, self.sound_3.so1_enabled); byte.set_bit(6, self.sound_3.so2_enabled); @@ -539,8 +774,8 @@ impl Addressable for SoundController { 0xFF26 => { let mut byte: u8 = 0xFF; - byte.set_bit(0, self.sound_1.is_on); - byte.set_bit(1, self.sound_2.is_on); + byte.set_bit(0, self.square_1.is_on); + byte.set_bit(1, self.square_2.is_on); byte.set_bit(2, self.sound_3.is_on); byte.set_bit(3, self.sound_4.is_on); @@ -582,51 +817,65 @@ impl Addressable for SoundController { // 0: Addition (frequency increases) // 1: Subtraction (frequency decreases) // Bit 2-0 - Number of sweep shift (n: 0-7) - 0xFF10 => self.sound_1.sweep.write(byte), + 0xFF10 => self.square_1.sweep.write(byte), // NR11: Sound 1 Sound length/Wave pattern duty // Bit 7-6 - Wave pattern duty // Bit 5-0 - Sound length data (Write only) - 0xFF11 => self.sound_1.wave.write(byte), + 0xFF11 => self.square_1.wave.write(byte), // NR12: Channel 1 volume envelope // Bit 7-4 - Initial volume of the envelope (0-15) (0 = no sound) // Bit 3 - Envelope direction (0 = decrease, 1 = increase) // Bit 2-0 - Number of envelope sweep (n: 0-7) (If 0, stop the envelope operation) - 0xFF12 => self.sound_1.envelope.write(byte), + 0xFF12 => self.square_1.envelope.write(byte), // NR13: Channel 1 Frequency low // Lower 8 bits of the 11-bit frequency - 0xFF13 => self.sound_1.frequency.write_lo(byte), + 0xFF13 => self.square_1.frequency.write_lo(byte), // NR14: Channel 1 frequency high // Bit 7 - Initial (1 = restart sound) (write only) // Bit 6 - Counter/consecutive selection (1 = stop output when length in NR11 // expires) // Bit 2-0 - Frequency's higher 3 bits (write only) - 0xFF14 => self.sound_1.frequency.write_hi(byte), + 0xFF14 => { + if byte.has_bit_set(7) { + self.square_1.trigger(); + } + + self.square_1.wave.enabled = byte.has_bit_set(6); + self.square_1.frequency.write_hi(byte); + } // NR21: Sound 2 Sound length/Wave pattern duty // Bit 7-6 - Wave pattern duty // Bit 5-0 - Sound length data (Write only) - 0xFF16 => self.sound_2.wave.write(byte), + 0xFF16 => self.square_2.wave.write(byte), // NR22: Channel 2 volume envelope // Bit 7-4 - Initial volume of the envelope (0-15) (0 = no sound) // Bit 3 - Envelope direction (0 = decrease, 1 = increase) // Bit 2-0 - Number of envelope sweep (n: 0-7) (If 0, stop the envelope operation) - 0xFF17 => self.sound_2.envelope.write(byte), + 0xFF17 => self.square_2.envelope.write(byte), // NR23: Channel 2 frequency low // Lower 8 bits of the 11-bit frequency - 0xFF18 => self.sound_2.frequency.write_lo(byte), + 0xFF18 => self.square_2.frequency.write_lo(byte), // NR24: Channel 2 frequency high // Bit 7 - Initial (1 = restart sound) (write only) // Bit 6 - Counter/consecutive selection (1 = stop output when length in NR21 // expires) // Bit 2-0 - Frequency's higher 3 bits (write only) - 0xFF19 => self.sound_2.frequency.write_hi(byte), + 0xFF19 => { + if byte.has_bit_set(7) { + self.square_2.trigger(); + } + self.square_2.wave.enabled = byte.has_bit_set(6); + + self.square_2.frequency.write_hi(byte); + } // NR30: Channel 3 sound on/off // Bit 7 - Sound channel 3 off (0=Stop, 1=Playback) @@ -702,11 +951,11 @@ impl Addressable for SoundController { // Bit 1 - Output sound 2 to SO1 terminal // Bit 0 - Output sound 1 to SO1 terminal 0xFF25 => { - self.sound_1.so1_enabled = byte.has_bit_set(0); - self.sound_1.so2_enabled = byte.has_bit_set(4); + self.square_1.so1_enabled = byte.has_bit_set(0); + self.square_1.so2_enabled = byte.has_bit_set(4); - self.sound_2.so1_enabled = byte.has_bit_set(1); - self.sound_2.so2_enabled = byte.has_bit_set(5); + self.square_2.so1_enabled = byte.has_bit_set(1); + self.square_2.so2_enabled = byte.has_bit_set(5); self.sound_3.so1_enabled = byte.has_bit_set(2); self.sound_3.so2_enabled = byte.has_bit_set(6); @@ -728,8 +977,8 @@ impl Addressable for SoundController { self.sound_enabled = byte.has_bit_set(7); if !self.sound_enabled { - self.sound_1 = Sound::default(); - self.sound_2 = Sound::default(); + self.square_1 = SquareChannel::default(); + self.square_2 = SquareChannel::default(); self.sound_3 = Sound3::default(); self.sound_4 = Sound4::default(); } @@ -755,11 +1004,12 @@ impl Addressable for SoundController { mod tests { use std::u8; - use crate::bytes::ByteExt; + use proptest::proptest; + use crate::bytes::ByteExt; use crate::memory::Addressable; - use super::{Envelope, Frequency, SoundController, Sweep, Wave}; + use super::{Frequency, SoundController, Sweep}; #[test] fn sweep_read() { @@ -805,85 +1055,6 @@ mod tests { } } - #[test] - fn wave_read() { - let mut wave = Wave::default(); - - for pattern in 0..4 { - for length in 0..64 { - let expected = (pattern << 6) | 0x3F; - - wave.pattern = pattern; - wave.length = length; - - assert_eq!(wave.read(), expected); - } - } - } - - #[test] - fn wave_write() { - let mut wave = Wave::default(); - - for pattern in 0..4 { - for length in 0..64 { - let byte = (pattern << 6) | length; - - let expected_pattern = pattern; - let expected_length = length; - - wave.write(byte); - - assert_eq!(wave.pattern, expected_pattern); - assert_eq!(wave.length, expected_length); - } - } - } - - #[test] - fn envelope_read() { - let mut envelope = Envelope::default(); - - for initial_vol in 0..16 { - for direction_increase in 0..2 { - for number in 0..8 { - let expected = (initial_vol << 4) | (direction_increase << 3) | number; - - envelope.initial_vol = initial_vol; - envelope.direction_increase = - if direction_increase == 1 { true } else { false }; - envelope.number = number; - - assert_eq!(envelope.read(), expected); - } - } - } - } - - #[test] - fn envelope_write() { - let mut envelope = Envelope::default(); - - for initial_vol in 0..16 { - for direction_increase in 0..2 { - for number in 0..8 { - let byte = (initial_vol << 4) | (direction_increase << 3) | number; - - let expected_initial_vol = initial_vol; - let expected_direction_increase = - if direction_increase == 1 { true } else { false }; - let expected_number = number; - - envelope.write(byte); - - assert_eq!(envelope.initial_vol, expected_initial_vol); - assert_eq!(envelope.direction_increase, expected_direction_increase); - assert_eq!(envelope.number, expected_number); - } - } - } - } - #[test] fn frequency_write_low() { let mut frequency = Frequency::default(); @@ -1030,11 +1201,11 @@ mod tests { for i_large in 0usize..256 { let i = i_large as u8; - sc.sound_1.so1_enabled = i.has_bit_set(0); - sc.sound_1.so2_enabled = i.has_bit_set(4); + sc.square_1.so1_enabled = i.has_bit_set(0); + sc.square_1.so2_enabled = i.has_bit_set(4); - sc.sound_2.so1_enabled = i.has_bit_set(1); - sc.sound_2.so2_enabled = i.has_bit_set(5); + sc.square_2.so1_enabled = i.has_bit_set(1); + sc.square_2.so2_enabled = i.has_bit_set(5); sc.sound_3.so1_enabled = i.has_bit_set(2); sc.sound_3.so2_enabled = i.has_bit_set(6); @@ -1058,11 +1229,11 @@ mod tests { let i = i_large as u8; // Make up a default state - writing with sound disabled shouldn't change this - sc.sound_1.so1_enabled = false; - sc.sound_1.so2_enabled = false; + sc.square_1.so1_enabled = false; + sc.square_1.so2_enabled = false; - sc.sound_2.so1_enabled = true; - sc.sound_2.so2_enabled = false; + sc.square_2.so1_enabled = true; + sc.square_2.so2_enabled = false; sc.sound_3.so1_enabled = false; sc.sound_3.so2_enabled = true; @@ -1073,11 +1244,11 @@ mod tests { sc.sound_enabled = false; sc.write_byte(0xFF25, i); - assert_eq!(sc.sound_1.so1_enabled, false); - assert_eq!(sc.sound_1.so2_enabled, false); + assert_eq!(sc.square_1.so1_enabled, false); + assert_eq!(sc.square_1.so2_enabled, false); - assert_eq!(sc.sound_2.so1_enabled, true); - assert_eq!(sc.sound_2.so2_enabled, false); + assert_eq!(sc.square_2.so1_enabled, true); + assert_eq!(sc.square_2.so2_enabled, false); assert_eq!(sc.sound_3.so1_enabled, false); assert_eq!(sc.sound_3.so2_enabled, true); @@ -1088,11 +1259,11 @@ mod tests { sc.sound_enabled = true; sc.write_byte(0xFF25, i); - assert_eq!(sc.sound_1.so1_enabled, i.has_bit_set(0)); - assert_eq!(sc.sound_1.so2_enabled, i.has_bit_set(4)); + assert_eq!(sc.square_1.so1_enabled, i.has_bit_set(0)); + assert_eq!(sc.square_1.so2_enabled, i.has_bit_set(4)); - assert_eq!(sc.sound_2.so1_enabled, i.has_bit_set(1)); - assert_eq!(sc.sound_2.so2_enabled, i.has_bit_set(5)); + assert_eq!(sc.square_2.so1_enabled, i.has_bit_set(1)); + assert_eq!(sc.square_2.so2_enabled, i.has_bit_set(5)); assert_eq!(sc.sound_3.so1_enabled, i.has_bit_set(2)); assert_eq!(sc.sound_3.so2_enabled, i.has_bit_set(6)); @@ -1110,8 +1281,8 @@ mod tests { let mut expected: u8 = i | 0xF0; expected.set_bit(7, i.has_bit_set(4)); - sc.sound_1.is_on = i.has_bit_set(0); - sc.sound_2.is_on = i.has_bit_set(1); + sc.square_1.is_on = i.has_bit_set(0); + sc.square_2.is_on = i.has_bit_set(1); sc.sound_3.is_on = i.has_bit_set(2); sc.sound_4.is_on = i.has_bit_set(3); sc.sound_enabled = i.has_bit_set(4); @@ -1133,4 +1304,66 @@ mod tests { assert_eq!(sc.sound_enabled, i.has_bit_set(7)); } } + + proptest! { + #[test] + fn nr11(duty in 0u8..4, length_load in 0u8..64) { + let mut apu = SoundController::new(); + apu.write_byte(0xFF26, 0xFF); + + let byte = duty << 6 | length_load; + + apu.write_byte(0xFF11, byte); + assert_eq!(apu.square_1.wave.pattern, duty); + assert_eq!(apu.square_1.wave.length_load, length_load); + assert_eq!(apu.square_1.wave.length, 64 - length_load); + + assert_eq!(apu.read_byte(0xFF11), byte); + } + + #[test] + fn nr12(starting_volume in 0u8..16, add_mode in 0u8..2, period in 0u8..8) { + let mut apu = SoundController::new(); + apu.write_byte(0xFF26, 0xFF); + + let byte = starting_volume << 4 | add_mode << 3 | period; + + apu.write_byte(0xFF12, byte); + assert_eq!(apu.square_1.envelope.initial_vol, starting_volume); + assert_eq!(apu.square_1.envelope.direction_increase as u8, add_mode); + assert_eq!(apu.square_1.envelope.number, period); + + assert_eq!(apu.read_byte(0xFF12), byte); + } + + #[test] + fn nr21(duty in 0u8..4, length_load in 0u8..64) { + let mut apu = SoundController::new(); + apu.write_byte(0xFF26, 0xFF); + + let byte = duty << 6 | length_load; + + apu.write_byte(0xFF16, byte); + assert_eq!(apu.square_2.wave.pattern, duty); + assert_eq!(apu.square_2.wave.length_load, length_load); + assert_eq!(apu.square_2.wave.length, 64 - length_load); + + assert_eq!(apu.read_byte(0xFF16), byte); + } + + #[test] + fn nr22(starting_volume in 0u8..16, add_mode in 0u8..2, period in 0u8..8) { + let mut apu = SoundController::new(); + apu.write_byte(0xFF26, 0xFF); + + let byte = starting_volume << 4 | add_mode << 3 | period; + + apu.write_byte(0xFF17, byte); + assert_eq!(apu.square_2.envelope.initial_vol, starting_volume); + assert_eq!(apu.square_2.envelope.direction_increase as u8, add_mode); + assert_eq!(apu.square_2.envelope.number, period); + + assert_eq!(apu.read_byte(0xFF17), byte); + } + } } diff --git a/src/audio/output.rs b/src/audio/output.rs index 99f4d40..f52615c 100644 --- a/src/audio/output.rs +++ b/src/audio/output.rs @@ -2,11 +2,71 @@ //! //! Plays the audio based on the state of the sound hardware. -use audio::SoundController; +use std::sync::Arc; -/// Outputs the GameBoy audio for a given sound controller. -#[derive(Debug, Default)] -pub struct AudioOutput { - /// The sound controller which determines the audio to output. - sound_controller: SoundController, +use anyhow::{anyhow, Result}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{OutputCallbackInfo, SampleFormat, SampleRate, Stream}; +use derivative::Derivative; +use log::*; + +use crate::cpu; + +use super::SampleBuffer; + +/// Outputs PCM audio generated by the sound controller. +#[derive(Derivative)] +#[derivative(Debug)] +pub struct Output { + /// The audio output stream. + #[derivative(Debug = "ignore")] + stream: Stream, + + /// Queued raw emulated PCM audio samples. + pub sample_buffer: SampleBuffer, +} + +impl Output { + pub fn new() -> Result { + let device = cpal::default_host() + .default_output_device() + .ok_or_else(|| anyhow!("no audio output devices found"))?; + + let sample_buffer = SampleBuffer::default(); + + let config = device + .supported_output_configs()? + .filter(|config| config.channels() == 1 && config.sample_format() == SampleFormat::F32) + .next() + .map(|config| config.with_sample_rate(SampleRate(44_100))) // TODO: use max? + .ok_or_else(|| anyhow!("no supported audio output configuration found"))? + .config(); + + info!("initializing audio playback with {:?}", config); + + let skipped_samples = (cpu::FREQUENCY / config.sample_rate.0) as usize; + + let stream_buffer = Arc::clone(&sample_buffer); + let stream = device.build_output_stream( + &config, + move |dst: &mut [f32], _: &OutputCallbackInfo| { + let mut src = stream_buffer.lock().unwrap(); + + // Naive nearest-neighbor downsampling + for (i, sample) in dst.iter_mut().enumerate() { + *sample = *src.get(i * skipped_samples).unwrap_or(&0.0); + } + + src.clear(); + }, + |err| panic!("{}", err), + )?; + + stream.play()?; + + Ok(Output { + stream, + sample_buffer, + }) + } } diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index 95f74b9..e8cd42a 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -16,6 +16,9 @@ use log::*; pub use self::instructions::Instruction; pub use self::registers::{Flags, Registers}; +/// CPU frequency in Hz. +pub const FREQUENCY: u32 = 4_194_304; + /// Machine cycles. The minimum number of cycles that must occur before another instruction can be /// decoded. /// diff --git a/src/lib.rs b/src/lib.rs index 6be6664..49b7d28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,18 @@ impl Emulator { } } + /// Create a new emulator with audio playback enabled. + pub fn new_with_playback() -> Self { + let mut emulator = Emulator::new(); + + match SoundController::new_with_playback() { + Ok(controller) => emulator.bus.audio = controller, + Err(err) => error!("unable to initialize audio playback: {}", err), + } + + emulator + } + /// Create a new emulator with the debugger enabled. pub fn new_with_debug() -> Self { let mut emulator = Emulator::new(); @@ -262,6 +274,8 @@ impl Emulator { self.cpu.step(&mut self.bus); cycles += self.bus.timer.diff(); + self.bus.audio.step(cycles.into()); + if let Some(ref mut debugger) = self.debug { let pc = self.cpu.reg.pc; if debugger.breakpoints.contains(&pc) { diff --git a/src/main.rs b/src/main.rs index f2f84e8..ff934b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,10 @@ struct Opt { #[structopt(long, default_value = "1")] scaling: u8, + /// Disable audio playback. + #[structopt(long)] + disable_audio: bool, + /// Enable debug mode. #[structopt(short, long)] debug: bool, @@ -36,8 +40,10 @@ struct Opt { fn run(opt: Opt) -> Result<()> { let mut emulator = if opt.debug { Emulator::new_with_debug() - } else { + } else if opt.disable_audio { Emulator::new() + } else { + Emulator::new_with_playback() }; if let Some(bios) = &opt.bios { From e04ea5991b1773a53abab68b228fe59837c8cb1b Mon Sep 17 00:00:00 2001 From: Andy Russell Date: Sat, 2 Jan 2021 00:40:21 -0500 Subject: [PATCH 3/5] use slightly more accurate cycle timing --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 49b7d28..738dd90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,9 @@ pub use crate::input::Button; /// The amount of time it takes for a physical Game Boy to complete a single cycle. /// /// Sourced from this [timing document](http://gameboy.mongenel.com/dmg/gbc_cpu_timing.txt). -const CYCLE_DURATION: Duration = Duration::from_nanos(234); +/// +/// See also [`crate::cpu::Frequency`]. +const CYCLE_DURATION: Duration = Duration::from_nanos(238); /// The emulator itself. Contains all components required to emulate the Game Boy. #[derive(Debug)] From d8c409336230f153b279cc41ddd84846864e56d0 Mon Sep 17 00:00:00 2001 From: Andy Russell Date: Wed, 6 Jan 2021 21:11:53 -0500 Subject: [PATCH 4/5] reduce audio crackling --- src/audio/output.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/audio/output.rs b/src/audio/output.rs index f52615c..5a982b4 100644 --- a/src/audio/output.rs +++ b/src/audio/output.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{OutputCallbackInfo, SampleFormat, SampleRate, Stream}; +use cpal::{OutputCallbackInfo, SampleFormat, Stream}; use derivative::Derivative; use log::*; @@ -38,7 +38,7 @@ impl Output { .supported_output_configs()? .filter(|config| config.channels() == 1 && config.sample_format() == SampleFormat::F32) .next() - .map(|config| config.with_sample_rate(SampleRate(44_100))) // TODO: use max? + .map(|config| config.with_max_sample_rate()) .ok_or_else(|| anyhow!("no supported audio output configuration found"))? .config(); @@ -53,11 +53,17 @@ impl Output { let mut src = stream_buffer.lock().unwrap(); // Naive nearest-neighbor downsampling - for (i, sample) in dst.iter_mut().enumerate() { - *sample = *src.get(i * skipped_samples).unwrap_or(&0.0); + for sample in dst.iter_mut() { + *sample = src.pop_front().unwrap_or(0.0); + + for _ in 0..skipped_samples { + src.pop_front(); + } } - src.clear(); + // FIXME: There are extra samples left over here, and the number of skipped + // samples isn't quite accurate. If we were a bit more accurate here, we might be + // able to fix some crackles in the output. }, |err| panic!("{}", err), )?; From b8a0f0ab222b671326d5b62fe8beb364d98e52e6 Mon Sep 17 00:00:00 2001 From: Andy Russell Date: Wed, 6 Jan 2021 21:36:55 -0500 Subject: [PATCH 5/5] install alsa development headers in CI --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f42cefd..e063b49 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,6 +7,9 @@ jobs: name: Build runs-on: ubuntu-latest steps: + - name: Install system libraries + run: sudo apt-get update && sudo apt-get install -yq libasound2-dev + - name: Checkout sources uses: actions/checkout@v2