commit 8820089289c1d69c46f12e7416f2caec2fee4240 Author: Ondřej Hruška Date: Sun Oct 25 19:36:00 2020 +0100 add some stuff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/beeptestrs.iml b/.idea/beeptestrs.iml new file mode 100644 index 0000000..c254557 --- /dev/null +++ b/.idea/beeptestrs.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..abd3fb5 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..e1d001f --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1603627779596 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1526224 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,596 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "alsa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44581add1add74ade32aca327b550342359ec00191672c23c1caa3d492b85930" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix", +] + +[[package]] +name = "alsa-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a0559bcd3f7a482690d98be41c08a43e92f669b179433e95ddf5e8b8fd36a3" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "anyhow" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" + +[[package]] +name = "beeptestrs" +version = "0.1.0" +dependencies = [ + "anyhow", + "cpal", + "parking_lot 0.11.0", +] + +[[package]] +name = "bindgen" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72a978d268b1d70b0e963217e60fdabd9523a941457a6c42a7315d15c7e89e5" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if 0.1.10", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "cc" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6570ee6e089131e928d5ec9236db9e818aa3cf850f48b0eec6ef700571271d4" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4410231e181e24e3e4612dfca601601e40942003e6a52a12d9d80f19fd9737" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "js-sys", + "lazy_static", + "libc", + "mach", + "nix", + "parking_lot 0.9.0", + "stdweb", + "thiserror", + "web-sys", + "winapi", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "instant" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb1fc4429a33e1f80d41dc9fea4d108a88bec1de8053878898ae448a0b52f613" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "js-sys" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743" + +[[package]] +name = "libloading" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +dependencies = [ + "cc", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lock_api" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +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 = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[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 = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "parking_lot" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.6.2", + "rustc_version", +] + +[[package]] +name = "parking_lot" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" +dependencies = [ + "instant", + "lock_api 0.4.1", + "parking_lot_core 0.8.0", +] + +[[package]] +name = "parking_lot_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi 0.0.3", + "libc", + "redox_syscall", + "rustc_version", + "smallvec 0.6.13", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi 0.1.0", + "instant", + "libc", + "redox_syscall", + "smallvec 1.4.2", + "winapi", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "regex" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8963b85b8ce3074fecffde43b4b0dded83ce2f367dc8d363afc56679f3ee820b" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cab7a364d15cde1e505267766a2d3c4e22a843e1a601f0fa7564c0f82ced11c" + +[[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 = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[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 = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "smallvec" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "smallvec" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" + +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + +[[package]] +name = "syn" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "318234ffa22e0920fe9a40d7b8369b5f649d490980cf7aadcf1eb91594869b42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae2447b6282786c3493999f40a9be2a6ad20cb8bd268b0a0dbf5a065535c0ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "version_check" +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 = "wasm-bindgen" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" +dependencies = [ + "cfg-if 0.1.10", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" + +[[package]] +name = "web-sys" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..eea7a5c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "beeptestrs" +version = "0.1.0" +authors = ["Ondřej Hruška "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cpal = "0.12.1" +anyhow = "1.0.33" +parking_lot = "0.11.0" diff --git a/src/beep.rs b/src/beep.rs new file mode 100644 index 0000000..419b23c --- /dev/null +++ b/src/beep.rs @@ -0,0 +1,152 @@ +use std::sync::Arc; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use std::ops::{Add, AddAssign, Sub}; +use parking_lot::RwLock; +use std::time::Duration; +use std::thread; +use cpal::SampleFormat; +use crate::beep::osc::{Instrument, Oscillator}; + +pub mod osc; + +struct AudioContext { + device: cpal::Device, + sample_format: SampleFormat, + config: cpal::StreamConfig +} + +fn al_init() -> Result { + let host = cpal::default_host(); + + let device = host.default_output_device() + .ok_or_else(|| anyhow::anyhow!("No audio output device!"))?; + + let config = device.default_output_config()?; + + Ok(AudioContext { + device, + sample_format: config.sample_format(), + config: config.into() + }) +} + +pub fn beep() -> Result<(), anyhow::Error> { + let ac = al_init()?; + + match ac.sample_format { + cpal::SampleFormat::F32 => run::(&ac.device, &ac.config.into())?, + cpal::SampleFormat::I16 => run::(&ac.device, &ac.config.into())?, + cpal::SampleFormat::U16 => run::(&ac.device, &ac.config.into())?, + } + + Ok(()) +} + + +fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error> + where + T: cpal::Sample, +{ + let sample_rate = config.sample_rate.0 as f32; + let channels = config.channels as usize; + + println!("SR={}",sample_rate); + + // Produce a sinusoid of maximum amplitude. + + let mut ins = Instrument::new(vec![ + Oscillator::new(440.0, 1.0, sample_rate), + //Oscillator::new(441.0, 1.0, sample_rate), + ], sample_rate); + + let instrument = Arc::new(parking_lot::RwLock::new(ins)); + + let err_fn = |err| eprintln!("an error occurred on stream: {}", err); + + let m_instrument = instrument.clone(); + let stream = device.build_output_stream( + config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + write_data(data, channels, &m_instrument) + }, + err_fn, + )?; + stream.play()?; + + // instrument.write().normalize(false); + // instrument.write().set_amp(1.0); + // thread::sleep(Duration::from_millis(500)); + // + // instrument.write().set_amp(0.0); + // thread::sleep(Duration::from_millis(250)); + + instrument.write().set_amp(1.0); + instrument.write().normalize(true); + thread::sleep(Duration::from_millis(500)); + + instrument.write().set_amp(0.0); + thread::sleep(Duration::from_millis(250)); + + instrument.write().set_amp(0.5); + thread::sleep(Duration::from_millis(500)); + + instrument.write().set_amp(0.0); + thread::sleep(Duration::from_millis(250)); + + instrument.write().fade_amp(1.0, Duration::from_millis(10)); + instrument.write().waveforms[0].fade_balance(-1.0, Duration::from_millis(10)); + thread::sleep(Duration::from_millis(500)); + instrument.write().fade_amp(0.0, Duration::from_millis(10)); + thread::sleep(Duration::from_millis(500)); + + + /* + + //instrument.write().set_amp_fade(0.0, 4.0); + + thread::sleep(Duration::from_millis(1000)); + instrument.write().waveforms[1].fade_amp(0.0, Duration::from_millis(500)); + thread::sleep(Duration::from_millis(500)); + + instrument.write().waveforms[0].fade_balance(-1.0, Duration::from_millis(1000)); + thread::sleep(Duration::from_millis(1000)); + instrument.write().waveforms[0].fade_balance(1.0, Duration::from_millis(2000)); + instrument.write().waveforms[0].fade_freq(880.0, Duration::from_millis(1000)); + thread::sleep(Duration::from_millis(2000)); + instrument.write().waveforms[0].fade_balance(-1.0, Duration::from_millis(1000)); + instrument.write().waveforms[0].fade_amp(0.0, Duration::from_millis(1000)); + thread::sleep(Duration::from_millis(1000)); +*/ + + // for _ in 0..10 + // { + // instrument.write().harmonics[0].set_balance_fade(-1.0, 0.1); + // thread::sleep(Duration::from_millis(100)); + // instrument.write().harmonics[0].set_balance_fade(1.0, 0.1); + // thread::sleep(Duration::from_millis(100)); + // } + // + // instrument.write().harmonics[0].set_balance_fade(0.0, 0.5); + // instrument.write().harmonics[0].set_amp_fade(0.0, 0.05); + // thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +fn write_data(output: &mut [T], channels: usize, instrument: &Arc>) + where + T: cpal::Sample, +{ + let mut instrument = instrument.write(); + for frame in output.chunks_mut(channels) { + let sample = instrument.sample(); + + if channels == 1 { + frame[0] = cpal::Sample::from::(&((sample.left + sample.right) / 2.0)); + } else { + frame[0] = cpal::Sample::from::(&sample.left); + frame[1] = cpal::Sample::from::(&sample.right); + } + } +} diff --git a/src/beep/osc.rs b/src/beep/osc.rs new file mode 100644 index 0000000..15c3371 --- /dev/null +++ b/src/beep/osc.rs @@ -0,0 +1,424 @@ +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign}; +use std::sync::Arc; +use std::time::Duration; + +use parking_lot::RwLock; + +use hack::StableClamp; + +mod hack { + /// Work-around for `f32::clamp()` being unstable + pub(super) trait StableClamp { + fn clamp_(self, min: f32, max: f32) -> f32; + } + + impl StableClamp for f32 { + fn clamp_(self, min: f32, max: f32) -> f32 { + assert!(min <= max); + let mut x = self; + if x < min { + x = min; + } + if x > max { + x = max; + } + x + } + } +} + +/// Value fader +#[derive(Clone,Debug)] +struct Tween { + /// Actual value + actual: f32, + /// Target value, approached by adding "step" to :actual" every sample + target: f32, + /// Value added to "actual" per sample. None = set to target at the end of a cycle + step: Option, + // Min value + min: f32, + // Max value + max: f32, +} + +/// Scale function amplitude by a gain +trait GainScale { + fn scale_gain(self, gain: f32) -> Self; +} + +impl GainScale for f32 { + /// Scale by gain 0-1 + fn scale_gain(self, gain: f32) -> Self { + // TODO what scaling to use? + // self * (1.0 + gain * 9.0).log10() + // self * gain.sqrt() + self * gain + } +} + +impl Tween { + /// Update values for the next sample + pub fn tick(&mut self) { + if let Some(step) = self.step { + let actual0 = self.actual; + self.actual += step; + if (self.target > actual0 && self.target <= self.actual) + || (self.target <= actual0 && self.target > self.actual) + { + self.step = None; + self.actual = self.target; + } + } + } + + /// Get actual value + pub fn actual(&self) -> f32 { + self.actual + } + + /// Check if the change from actual to target is scheduled to happen instantly + /// at the end of a cycle. + pub fn is_change_scheduled_at_crossover(&self) -> bool { + self.target != self.actual + && self.step.is_none() + } + + /// Check if the fading is in progress + pub fn is_fading(&self) -> bool { + self.step.is_some() + } + + /// Schedule change at crossover + fn set_at_crossover(&mut self, val: f32) { + self.target = val.clamp_(self.min, self.max); + self.step = None; // change at the earliest convenience + } + + /// Start fading + fn fade(&mut self, val: f32, time: Duration, sample_rate: f32) { + self.target = val.clamp_(self.min, self.max); + let nsamples = (time.as_secs_f32() * sample_rate).ceil(); + self.step = Some((self.target - self.actual) / nsamples); + } + + /// Set value immediately + fn set_immediate(&mut self, val: f32) { + self.target = val; + self.actual = val; + self.step = None; + } +} + +#[derive(Clone,Debug)] +pub struct Oscillator { + /// Amplitude fader 0..1 + gain: Tween, + /// Frequency fader 1..22050 + freq: Tween, + /// Balance fader -1..1 + balance: Tween, + /// Left channel multiplier + gain_left: f32, + /// Right channel multiplier + gain_right: f32, + /// Sample rate (Hz) + sample_rate: f32, + /// Phase increment per sample + phase_step: f32, + /// Phase 0-TAU + phase: f32, +} + +impl Oscillator { + /// New oscillator with a frequency and amplitude + pub fn new(freq: f32, amp: f32, sample_rate : f32) -> Self { + let mut o = Self { + gain: Tween { + actual: amp, + target: amp, + min: 0.0, + max: 1.0, + step: None + }, + freq: Tween { + actual: freq, + target: freq, + min: 1.0, + max: sample_rate / 2.0, + step: None + }, + balance: Tween { + actual: 0.0, + target: 0.0, + min: -1.0, + max: 1.0, + step: None + }, + phase_step: 0.0, + gain_left: 0.0, + gain_right: 0.0, + sample_rate, + phase: 0.0 + }; + o.update_step(); + o.update_gains(1.0); + o + } + + /// Update the step variable + fn update_step(&mut self) { + self.phase_step = (self.freq.actual / self.sample_rate) * std::f32::consts::TAU; + } + + /// Update the pre-computed channel gain variables + fn update_gains(&mut self, gain_scale: f32) { + let angle = ((1.0 + self.balance.actual) / 2.0) * std::f32::consts::FRAC_PI_2; + let act = self.gain.actual * gain_scale; + self.gain_left = act.scale_gain(angle.sin()); + self.gain_right = act.scale_gain(angle.cos()); + } + + /// Get actual amplitude (0..1) + pub fn get_amp(&self) -> f32 { + self.gain.actual + } + + /// Set amplitude (0..1), change at the end of the current waveform to reduce popping + pub fn set_amp(&mut self, amp : f32) { + self.gain.set_at_crossover(amp); + } + + /// Fade to amplitude (0..1) over a given time + pub fn fade_amp(&mut self, amp : f32, time: Duration) { + self.gain.fade(amp, time, self.sample_rate); + } + + /// Get balance value (-1..1) + pub fn get_balance(&self) -> f32 { + self.balance.actual + } + + /// Set balance (-1..1), change at the end of the current waveform to reduce popping + pub fn set_balance(&mut self, balance : f32) { + self.balance.set_at_crossover(balance); + } + + /// Fade to balance (-1..1) over a given time + pub fn fade_balance(&mut self, balance : f32, time: Duration) { + self.balance.fade(balance, time, self.sample_rate); + } + + /// Get frequency value (1..22050) + pub fn get_freq(&self) -> f32 { + self.freq.actual + } + + /// Set frequency (1..22050), change at the end of the current waveform to reduce popping + pub fn set_freq(&mut self, freq : f32) { + self.freq.set_at_crossover(freq); + } + + /// Fade to frequency (1..22050) over a given time + pub fn fade_freq(&mut self, freq : f32, time: Duration) { + self.freq.fade(freq, time, self.sample_rate); + } + + /// Take a sample. Mutable to update faders. + pub fn sample(&mut self, gain_scale: f32) -> StereoSample { + if self.freq.is_fading() { + self.freq.tick(); + self.update_step(); + } + + if self.balance.is_fading() || self.gain.is_fading() { + self.balance.tick(); + self.gain.tick(); + self.update_gains(gain_scale); + } + + let ph0 = self.phase; + self.phase = (self.phase + self.phase_step) % std::f32::consts::TAU; + + // "zero crossing detection" + if self.phase < ph0 { + if self.gain.is_change_scheduled_at_crossover() { + self.gain.actual = self.gain.target; + self.update_gains(gain_scale); + } + + if self.balance.is_change_scheduled_at_crossover() { + self.balance.actual = self.balance.target; + self.update_gains(gain_scale); + } + + if self.freq.is_change_scheduled_at_crossover() { + self.freq.actual = self.freq.target; + } + } + + let y = self.phase.sin() * self.gain.actual; + + StereoSample { + left: self.gain_left * y, + right: self.gain_right * y, + } + } +} + +/// Waveform generator with multiple frequencies / waveforms +#[derive(Clone,Debug)] +pub struct Instrument { + /// Waveforms to combine to produce the final sound + pub waveforms: Vec, + /// Normalize the output to produce constant amplitude of 1.0 (instead of clipping) + normalize: bool, + sample_rate : f32, + master: Tween, +} + +impl Instrument { + pub fn new(waveforms: Vec, sample_rate: f32) -> Self { + let mut o = Self { + waveforms, + normalize: false, + sample_rate, + master: Tween { + actual: 1.0, + target: 1.0, + step: None, + min: 0.0, + max: 1.0 + } + }; + o + } + + pub fn normalize(&mut self, normalize: bool) { + self.normalize = normalize; + + // This is an artifact of how Oscillator works internally: + // The left/right gains are pre-computed and updated only when they change + if normalize { + self.force_normalize(); + } else { + self.waveforms.iter_mut() + .for_each(|item| item.update_gains(1.0)); + } + } + + pub fn get_amp(&self) -> f32 { + self.master.actual() + } + + pub fn set_amp(&mut self, amp : f32) { + self.master.set_immediate(amp); + } + + pub fn fade_amp(&mut self, amp : f32, time: Duration) { + self.master.fade(amp, time, self.sample_rate); + } + + fn force_normalize(&mut self) { + let gain_scale = 1.0 / self.waveforms.iter() + .fold((0.0), |acc, item| acc + item.gain.actual); + + println!("Gain scale {}", gain_scale); + self.waveforms.iter_mut() + .for_each(|item| item.update_gains(gain_scale)); + } + + /// Get a sample (left, right) + pub fn sample(&mut self) -> StereoSample { + self.master.tick(); + + let gain_scaling = if self.normalize { + 1.0 / self.waveforms.iter() + .fold((0.0), |acc, item| acc + item.gain.actual) + } else { + 1.0 + }; + + let (mut value, gains)= self.waveforms.iter_mut() + .fold((StereoSample::default(), 0.0), |mut acc, item| { + acc.0 += item.sample(gain_scaling); + acc.1 += item.gain.actual; + acc + }); + + if self.normalize { + value.left /= gains; + value.right /= gains; + } + + value.left = value.left.scale_gain(self.master.actual).clamp_(-1.0, 1.0); + value.right = value.right.scale_gain(self.master.actual).clamp_(-1.0, 1.0); + value.clip() + } +} + +#[derive(Default, Clone, Copy)] +pub struct StereoSample { + pub left: f32, + pub right: f32, +} + +impl StereoSample { + pub fn clip(self) -> Self { + Self { + left: self.left.clamp_(-1.0, 1.0), + right: self.right.clamp_(-1.0, 1.0), + } + } +} + +impl Add for StereoSample { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self { + left: self.left + rhs.left, + right: self.right + rhs.right, + } + } +} + +impl Div for StereoSample { + type Output = Self; + + fn div(self, rhs: f32) -> Self::Output { + Self { + left: self.left / rhs, + right: self.right / rhs, + } + } +} + +impl Mul for StereoSample { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + left: self.left * rhs, + right: self.right * rhs, + } + } +} + +impl AddAssign for StereoSample { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs + } +} + +impl DivAssign for StereoSample { + fn div_assign(&mut self, rhs: f32) { + *self = *self / rhs + } +} + +impl MulAssign for StereoSample { + fn mul_assign(&mut self, rhs: f32) { + *self = *self * rhs + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7d94cd --- /dev/null +++ b/src/main.rs @@ -0,0 +1,7 @@ +mod beep; +extern crate anyhow; +extern crate cpal; + +fn main() -> Result<(), anyhow::Error> { + beep::beep() +}