diff --git a/Cargo.lock b/Cargo.lock index 6678aa6..d198fba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,9 +26,9 @@ dependencies = [ [[package]] name = "nalgebra" -version = "0.32.5" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea4908d4f23254adda3daa60ffef0f1ac7b8c3e9a864cf3cc154b251908a2ef" +checksum = "3c4b5f057b303842cf3262c27e465f4c303572e7f6b0648f60e16248ac3397f4" dependencies = [ "approx", "num-complex", @@ -84,9 +84,9 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "simba" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" dependencies = [ "approx", "num-complex", diff --git a/Cargo.toml b/Cargo.toml index b2848c5..4c0b4e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "firefly-rust" version = "0.3.0" edition = "2021" +rust-version = "1.81.0" authors = ["Firefly Zero team"] description = "Rust SDK for making Firefly Zero games" repository = "https://github.com/firefly-zero/firefly-rust" @@ -17,4 +18,4 @@ sudo = [] nalgebra_support = ["nalgebra"] [dependencies] -nalgebra = { version = "0.32.5", optional = true, default-features = false } +nalgebra = { version = "0.33.0", optional = true, default-features = false } diff --git a/Taskfile.yml b/Taskfile.yml index 33a2ad5..0971555 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,5 +1,5 @@ # https://taskfile.dev -version: '3' +version: "3" tasks: release: @@ -19,6 +19,7 @@ tasks: test: desc: "run tests" cmds: + - rustup target add wasm32-unknown-unknown - cargo test --all-features --lib - > cargo build diff --git a/examples/audio_chord/firefly.toml b/examples/audio_chord/firefly.toml new file mode 100644 index 0000000..680267d --- /dev/null +++ b/examples/audio_chord/firefly.toml @@ -0,0 +1,5 @@ +author_id = "demo" +app_id = "rust-chord" +author_name = "Demo" +app_name = "Chord Demo (Rust)" +compile_args = ["--no-default-features"] diff --git a/examples/audio_chord/main.rs b/examples/audio_chord/main.rs new file mode 100644 index 0000000..ad10a6b --- /dev/null +++ b/examples/audio_chord/main.rs @@ -0,0 +1,10 @@ +#![no_std] +#![no_main] +use firefly_rust::audio; + +#[no_mangle] +extern fn boot() { + audio::OUT.add_sine(audio::Freq::C4, 0.); + audio::OUT.add_sine(audio::Freq::E4, 0.); + audio::OUT.add_sine(audio::Freq::G4, 0.); +} diff --git a/examples/audio_modulator/firefly.toml b/examples/audio_modulator/firefly.toml new file mode 100644 index 0000000..b2a7324 --- /dev/null +++ b/examples/audio_modulator/firefly.toml @@ -0,0 +1,5 @@ +author_id = "demo" +app_id = "rust-mod" +author_name = "Demo" +app_name = "Modulator Demo (Rust)" +compile_args = ["--no-default-features"] diff --git a/examples/audio_modulator/main.rs b/examples/audio_modulator/main.rs new file mode 100644 index 0000000..30da982 --- /dev/null +++ b/examples/audio_modulator/main.rs @@ -0,0 +1,15 @@ +#![no_std] +#![no_main] +use firefly_rust::audio; + +#[no_mangle] +extern fn boot() { + let gain = audio::OUT.add_gain(0.); + gain.modulate(audio::LinearModulator { + start: 0., + end: 1., + start_at: audio::Time::ZERO, + end_at: audio::Time::seconds(2), + }); + gain.add_sine(audio::Freq::A4, 0.); +} diff --git a/examples/audio_noise/firefly.toml b/examples/audio_noise/firefly.toml new file mode 100644 index 0000000..cd59da2 --- /dev/null +++ b/examples/audio_noise/firefly.toml @@ -0,0 +1,5 @@ +author_id = "demo" +app_id = "rust-noise" +author_name = "Demo" +app_name = "Noise Demo (Rust)" +compile_args = ["--no-default-features"] diff --git a/examples/audio_noise/main.rs b/examples/audio_noise/main.rs new file mode 100644 index 0000000..be6a97b --- /dev/null +++ b/examples/audio_noise/main.rs @@ -0,0 +1,8 @@ +#![no_std] +#![no_main] +use firefly_rust::audio; + +#[no_mangle] +extern fn boot() { + audio::OUT.add_noise(0); +} diff --git a/examples/audio_sawtooth/firefly.toml b/examples/audio_sawtooth/firefly.toml new file mode 100644 index 0000000..45ba45e --- /dev/null +++ b/examples/audio_sawtooth/firefly.toml @@ -0,0 +1,5 @@ +author_id = "demo" +app_id = "rust-sawtooth" +author_name = "Demo" +app_name = "Sawtooth Wave Demo (Rust)" +compile_args = ["--no-default-features"] diff --git a/examples/audio_sawtooth/main.rs b/examples/audio_sawtooth/main.rs new file mode 100644 index 0000000..df4b99c --- /dev/null +++ b/examples/audio_sawtooth/main.rs @@ -0,0 +1,8 @@ +#![no_std] +#![no_main] +use firefly_rust::audio; + +#[no_mangle] +extern fn boot() { + audio::OUT.add_sawtooth(audio::Freq::A4, 0.); +} diff --git a/examples/audio_sine/firefly.toml b/examples/audio_sine/firefly.toml new file mode 100644 index 0000000..aeb2cd8 --- /dev/null +++ b/examples/audio_sine/firefly.toml @@ -0,0 +1,5 @@ +author_id = "demo" +app_id = "rust-sine" +author_name = "Demo" +app_name = "Sine Demo (Rust)" +compile_args = ["--no-default-features"] diff --git a/examples/audio_sine/main.rs b/examples/audio_sine/main.rs new file mode 100644 index 0000000..c08bdff --- /dev/null +++ b/examples/audio_sine/main.rs @@ -0,0 +1,8 @@ +#![no_std] +#![no_main] +use firefly_rust::audio; + +#[no_mangle] +extern fn boot() { + audio::OUT.add_sine(audio::Freq::A4, 0.); +} diff --git a/examples/audio_square/firefly.toml b/examples/audio_square/firefly.toml new file mode 100644 index 0000000..5aaa684 --- /dev/null +++ b/examples/audio_square/firefly.toml @@ -0,0 +1,5 @@ +author_id = "demo" +app_id = "rust-square" +author_name = "Demo" +app_name = "Square Wave Demo (Rust)" +compile_args = ["--no-default-features"] diff --git a/examples/audio_square/main.rs b/examples/audio_square/main.rs new file mode 100644 index 0000000..384176f --- /dev/null +++ b/examples/audio_square/main.rs @@ -0,0 +1,8 @@ +#![no_std] +#![no_main] +use firefly_rust::audio; + +#[no_mangle] +extern fn boot() { + audio::OUT.add_square(audio::Freq::A4, 0.); +} diff --git a/examples/audio_triangle/firefly.toml b/examples/audio_triangle/firefly.toml new file mode 100644 index 0000000..354d722 --- /dev/null +++ b/examples/audio_triangle/firefly.toml @@ -0,0 +1,5 @@ +author_id = "demo" +app_id = "rust-aud-tri" +author_name = "Demo" +app_name = "Triangle Wave Demo (Rust)" +compile_args = ["--no-default-features"] diff --git a/examples/audio_triangle/main.rs b/examples/audio_triangle/main.rs new file mode 100644 index 0000000..f1deb80 --- /dev/null +++ b/examples/audio_triangle/main.rs @@ -0,0 +1,8 @@ +#![no_std] +#![no_main] +use firefly_rust::audio; + +#[no_mangle] +extern fn boot() { + audio::OUT.add_triangle(audio::Freq::A4, 0.); +} diff --git a/rustfmt.toml b/rustfmt.toml index 0b7b70e..932b641 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,3 @@ -imports_granularity = "Module" newline_style = "Unix" use_field_init_shorthand = true force_explicit_abi = false @@ -6,6 +5,7 @@ force_explicit_abi = false ## Uncomment when stabilized: # group_imports = "One" # hex_literal_case = "Lower" +# imports_granularity = "Module" # normalize_comments = true # normalize_doc_attributes = true # reorder_impl_items = true diff --git a/src/audio/freq.rs b/src/audio/freq.rs new file mode 100644 index 0000000..a3707d1 --- /dev/null +++ b/src/audio/freq.rs @@ -0,0 +1,234 @@ +pub const SAMPLE_RATE: u32 = 44_100; + +#[derive(Copy, Clone)] +pub enum Pitch { + C, + Cs, + D, + Ds, + E, + F, + Fs, + G, + Gs, + A, + As, + B, +} + +impl TryFrom for Pitch { + type Error = (); + + fn try_from(value: char) -> Result { + match value { + 'A' => Ok(Self::A), + 'B' => Ok(Self::B), + 'C' => Ok(Self::C), + 'D' => Ok(Self::D), + 'E' => Ok(Self::E), + 'F' => Ok(Self::F), + 'G' => Ok(Self::G), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone)] +pub struct Freq(pub(super) f32); + +impl Freq { + pub const ZERO: Self = Self(0.); + + // https://www.liutaiomottola.com/formulae/freqtab.htm + + /// C0, MIDI note #12 + pub const C0: Self = Self(16.351); + pub const CS0: Self = Self(17.324); + pub const D0: Self = Self(18.354); + pub const DS0: Self = Self(19.445); + pub const E0: Self = Self(20.601); + pub const F0: Self = Self(21.827); + pub const FS0: Self = Self(23.124); + pub const G0: Self = Self(24.499); + pub const GS0: Self = Self(25.956); + /// A0, the lowest note of a piano + pub const A0: Self = Self(27.5); + pub const AS0: Self = Self(29.135); + /// B0, the lowest note of a 5 string bass + pub const B0: Self = Self(30.868); + /// C1, the lowest note of double bass with C extension + pub const C1: Self = Self(32.703); + pub const CS1: Self = Self(34.648); + pub const D1: Self = Self(36.708); + pub const DS1: Self = Self(38.891); + /// E1, the lowest note of a bass + pub const E1: Self = Self(41.203); + pub const F1: Self = Self(43.654); + pub const FS1: Self = Self(46.249); + pub const G1: Self = Self(48.999); + pub const GS1: Self = Self(51.913); + pub const A1: Self = Self(55.); + pub const AS1: Self = Self(58.27); + pub const B1: Self = Self(61.735); + pub const C2: Self = Self(65.406); + pub const CS2: Self = Self(69.296); + pub const D2: Self = Self(73.416); + pub const DS2: Self = Self(77.782); + /// E2, the lowest note of a guitar. + pub const E2: Self = Self(82.407); + pub const F2: Self = Self(87.307); + pub const FS2: Self = Self(92.499); + pub const G2: Self = Self(97.999); + pub const GS2: Self = Self(103.826); + pub const A2: Self = Self(110.); + pub const AS2: Self = Self(116.541); + pub const B2: Self = Self(123.471); + pub const C3: Self = Self(130.813); + pub const CS3: Self = Self(138.591); + pub const D3: Self = Self(146.832); + pub const DS3: Self = Self(155.563); + pub const E3: Self = Self(164.814); + pub const F3: Self = Self(174.614); + pub const FS3: Self = Self(184.997); + /// G3, the lowest note of a violin. + pub const G3: Self = Self(195.998); + pub const GS3: Self = Self(207.652); + pub const A3: Self = Self(220.); + pub const AS3: Self = Self(233.082); + pub const B3: Self = Self(246.942); + /// C4, the "middle C". + pub const C4: Self = Self(261.626); + pub const CS4: Self = Self(277.183); + pub const D4: Self = Self(293.665); + pub const DS4: Self = Self(311.127); + pub const E4: Self = Self(329.628); + pub const F4: Self = Self(349.228); + pub const FS4: Self = Self(369.994); + pub const G4: Self = Self(391.995); + pub const GS4: Self = Self(415.305); + /// A4, the tuning reference note. + pub const A4: Self = Self(440.); + pub const AS4: Self = Self(466.164); + pub const B4: Self = Self(493.883); + pub const C5: Self = Self(523.251); + pub const CS5: Self = Self(554.365); + pub const D5: Self = Self(587.33); + pub const DS5: Self = Self(622.254); + pub const E5: Self = Self(659.255); + pub const F5: Self = Self(698.456); + pub const FS5: Self = Self(739.989); + pub const G5: Self = Self(783.991); + pub const GS5: Self = Self(830.609); + pub const A5: Self = Self(880.); + pub const AS5: Self = Self(932.328); + pub const B5: Self = Self(987.767); + pub const C6: Self = Self(1046.502); + pub const CS6: Self = Self(1108.731); + pub const D6: Self = Self(1174.659); + pub const DS6: Self = Self(1244.508); + pub const E6: Self = Self(1318.51); + pub const F6: Self = Self(1396.913); + pub const FS6: Self = Self(1479.978); + pub const G6: Self = Self(1567.982); + pub const GS6: Self = Self(1661.219); + pub const A6: Self = Self(1760.); + pub const AS6: Self = Self(1864.655); + pub const B6: Self = Self(1975.533); + pub const C7: Self = Self(2093.005); + pub const CS7: Self = Self(2217.461); + pub const D7: Self = Self(2349.318); + pub const DS7: Self = Self(2489.016); + pub const E7: Self = Self(2637.021); + pub const F7: Self = Self(2793.826); + pub const FS7: Self = Self(2959.955); + pub const G7: Self = Self(3135.964); + pub const GS7: Self = Self(3322.438); + pub const A7: Self = Self(3520.); + pub const AS7: Self = Self(3729.31); + pub const B7: Self = Self(3951.066); + /// C8, the highest note of a piano. + pub const C8: Self = Self(4186.009); + pub const CS8: Self = Self(4434.922); + pub const D8: Self = Self(4698.636); + pub const DS8: Self = Self(4978.032); + pub const E8: Self = Self(5274.042); + pub const F8: Self = Self(5587.652); + pub const FS8: Self = Self(5919.91); + pub const G8: Self = Self(6271.928); + pub const GS8: Self = Self(6644.876); + pub const A8: Self = Self(7040.); + pub const AS8: Self = Self(7458.62); + pub const B8: Self = Self(7902.132); + pub const C9: Self = Self(8372.018); + pub const CS9: Self = Self(8869.844); + pub const D9: Self = Self(9397.272); + pub const DS9: Self = Self(9956.064); + pub const E9: Self = Self(10548.084); + pub const F9: Self = Self(11175.304); + pub const FS9: Self = Self(11839.82); + pub const G9: Self = Self(12543.856); + /// G#9, MIDI note #128, the top of the MIDI tuning range. + pub const GS9: Self = Self(13289.752); + pub const A9: Self = Self(14080.); + pub const AS9: Self = Self(14917.24); + /// B9. For most of adults, it is already beyond the hearing range. + pub const B9: Self = Self(15804.264); + + #[must_use] + pub fn hz(hz: f32) -> Self { + Self(hz) + } + + #[must_use] + #[expect(clippy::cast_precision_loss)] + pub fn midi(note: u8) -> Self { + // https://inspiredacoustics.com/en/MIDI_note_numbers_and_center_frequencies + // https://en.wikipedia.org/wiki/Musical_note#MIDI + let mut f: f32 = match note % 12 { + 0 => 8.1758, + 1 => 8.66, + 2 => 9.18, + 3 => 9.72, + 4 => 10.30, + 5 => 10.91, + 6 => 11.56, + 7 => 12.25, + 8 => 12.98, + 9 => 13.75, + 10 => 14.57, + _ => 15.43, + }; + let oct = note / 12; + f *= (1 << oct) as f32; + Self(f) + } + + #[must_use] + #[expect(clippy::cast_precision_loss)] + pub fn note(pitch: Pitch, octave: u8) -> Self { + // https://github.com/crbulakites/hum/blob/master/src/hum_process/hum_math.rs + // https://en.wikipedia.org/wiki/Musical_note#Pitch_frequency_in_hertz + let mut f: f32 = match pitch { + Pitch::C => 16.351, + Pitch::Cs => 17.324, + Pitch::D => 18.354, + Pitch::Ds => 19.445, + Pitch::E => 20.601, + Pitch::F => 21.827, + Pitch::Fs => 23.124, + Pitch::G => 24.499, + Pitch::Gs => 25.956, + Pitch::A => 27.5, + Pitch::As => 29.135, + Pitch::B => 30.868, + }; + f *= (1 << octave) as f32; + Freq(f) + } +} + +impl From for Freq { + fn from(value: f32) -> Self { + Self(value) + } +} diff --git a/src/audio/mod.rs b/src/audio/mod.rs new file mode 100644 index 0000000..d1e36a9 --- /dev/null +++ b/src/audio/mod.rs @@ -0,0 +1,9 @@ +mod freq; +mod modulators; +mod nodes; +mod time; + +pub use freq::*; +pub use modulators::*; +pub use nodes::*; +pub use time::*; diff --git a/src/audio/modulators.rs b/src/audio/modulators.rs new file mode 100644 index 0000000..4583b8c --- /dev/null +++ b/src/audio/modulators.rs @@ -0,0 +1,88 @@ +use super::*; + +pub trait Modulator { + fn modulate(self, node_id: u32, param: u32); +} + +/// Linear (ramp up or down) envelope. +/// +/// It looks like this: `⎽╱⎺` or `⎺╲⎽`. +/// +/// The value before `start_at` is `start`, the value after `end_at` is `end`, +/// and the value between `start_at` and `end_at` changes linearly from `start` to `end`. +pub struct LinearModulator { + pub start: f32, + pub end: f32, + pub start_at: Time, + pub end_at: Time, +} + +impl Modulator for LinearModulator { + fn modulate(self, node_id: u32, param: u32) { + unsafe { + bindings::mod_linear( + node_id, + param, + self.start, + self.end, + self.start_at.0, + self.end_at.0, + ); + } + } +} + +/// Hold envelope. +/// +/// It looks like this: `⎽│⎺` or `⎺│⎽`. +/// +/// The value before `time` is `before` and the value after `time` is `after`. +/// Equivalent to [`LinearModulator`] with `start_at` being equal to `end_at`. +pub struct HoldModulator { + pub before: f32, + pub after: f32, + pub time: Time, +} + +impl Modulator for HoldModulator { + fn modulate(self, node_id: u32, param: u32) { + unsafe { + bindings::mod_hold(node_id, param, self.before, self.after, self.time.0); + } + } +} + +/// Sine wave low-frequency oscillator. +/// +/// It looks like this: `∿`. +/// +/// `low` is the lowest produced value, `high` is the highest. +pub struct SineModulator { + pub freq: Freq, + pub low: f32, + pub high: f32, +} + +impl Modulator for SineModulator { + fn modulate(self, node_id: u32, param: u32) { + unsafe { + bindings::mod_sine(node_id, param, self.freq.0, self.low, self.high); + } + } +} + +mod bindings { + #[link(wasm_import_module = "audio")] + extern { + pub(super) fn mod_linear( + node_id: u32, + param: u32, + start: f32, + end: f32, + start_at: u32, + end_at: u32, + ); + pub(super) fn mod_hold(node_id: u32, param: u32, v1: f32, v2: f32, time: u32); + pub(super) fn mod_sine(node_id: u32, param: u32, freq: f32, low: f32, high: f32); + } +} diff --git a/src/audio/nodes.rs b/src/audio/nodes.rs new file mode 100644 index 0000000..de404ef --- /dev/null +++ b/src/audio/nodes.rs @@ -0,0 +1,347 @@ +use core::marker::PhantomData; + +use super::*; + +/// A marker for a specific node type. See [`Node::add_sine`]. +pub struct Sine {} +/// A marker for a specific node type. See [`Node::add_mix`]. +pub struct Mix {} +/// A marker for a specific node type. See [`Node::add_all_for_one`]. +pub struct AllForOne {} +/// A marker for a specific node type. See [`Node::add_gain`]. +pub struct Gain {} +/// A marker for a specific node type. See [`Node::add_loop`]. +pub struct Loop {} +/// A marker for a specific node type. See [`Node::add_concat`]. +pub struct Concat {} +/// A marker for a specific node type. See [`Node::add_pan`]. +pub struct Pan {} +/// A marker for a specific node type. See [`Node::add_mute`]. +pub struct Mute {} +/// A marker for a specific node type. See [`Node::add_pause`]. +pub struct Pause {} +/// A marker for a specific node type. See [`Node::add_track_position`]. +pub struct TrackPosition {} +/// A marker for a specific node type. See [`Node::add_low_pass`]. +pub struct LowPass {} +/// A marker for a specific node type. See [`Node::add_high_pass`]. +pub struct HighPass {} +/// A marker for a specific node type. See [`Node::add_take_left`]. +pub struct TakeLeft {} +/// A marker for a specific node type. See [`Node::add_take_right`]. +pub struct TakeRight {} +/// A marker for a specific node type. See [`Node::add_swap`]. +pub struct Swap {} +/// A marker for a specific node type. See [`Node::add_clip`]. +pub struct Clip {} +/// A marker for a specific node type. See [`Node::add_square`]. +pub struct Square {} +/// A marker for a specific node type. See [`Node::add_sawtooth`]. +pub struct Sawtooth {} +/// A marker for a specific node type. See [`Node::add_triangle`]. +pub struct Triangle {} +/// A marker for a specific node type. See [`Node::add_noise`]. +pub struct Noise {} +/// A marker for a specific node type. See [`Node::add_empty`]. +pub struct Empty {} +/// A marker for a specific node type. See [`Node::add_zero`]. +pub struct Zero {} + +/// An audio node: a source, a sink, a filter, an effect, etc. +pub struct Node { + id: u32, + /// A marker for a specific node type. Used to control which parameters can be modulated. + _flavor: PhantomData, +} + +/// The output audio node. Mixes all inputs and plays them on the device's speaker. +pub const OUT: Node = Node::new(0); + +#[expect(clippy::must_use_candidate)] +impl Node { + #[must_use] + const fn new(id: u32) -> Self { + Self { + id, + _flavor: PhantomData, + } + } + + /// Add sine wave oscillator source (`∿`). + pub fn add_sine(&self, f: Freq, phase: f32) -> Node { + let id = unsafe { bindings::add_sine(self.id, f.0, phase) }; + Node::new(id) + } + + /// Add square wave oscillator source (`⎍`). + pub fn add_square(&self, f: Freq, phase: f32) -> Node { + let id = unsafe { bindings::add_square(self.id, f.0, phase) }; + Node::new(id) + } + + /// Add sawtooth wave oscillator source (`╱│`). + pub fn add_sawtooth(&self, f: Freq, phase: f32) -> Node { + let id = unsafe { bindings::add_sawtooth(self.id, f.0, phase) }; + Node::new(id) + } + + /// Add triangle wave oscillator source (`╱╲`). + pub fn add_triangle(&self, f: Freq, phase: f32) -> Node { + let id = unsafe { bindings::add_triangle(self.id, f.0, phase) }; + Node::new(id) + } + + /// Add white noise source (amplitude on each tick is random). + pub fn add_noise(&self, seed: i32) -> Node { + let id = unsafe { bindings::add_noise(self.id, seed) }; + Node::new(id) + } + + /// Add always stopped source. + pub fn add_empty(&self) -> Node { + let id = unsafe { bindings::add_empty(self.id) }; + Node::new(id) + } + + /// Add silent source producing zeros. + pub fn add_zero(&self) -> Node { + let id = unsafe { bindings::add_zero(self.id) }; + Node::new(id) + } + + /// Add node simply mixing all inputs. + pub fn add_mix(&self) -> Node { + let id = unsafe { bindings::add_mix(self.id) }; + Node::new(id) + } + + /// Add mixer node that stops if any of the sources stops. + pub fn add_all_for_one(&self) -> Node { + let id = unsafe { bindings::add_all_for_one(self.id) }; + Node::new(id) + } + + /// Add gain control node. + pub fn add_gain(&self, lvl: f32) -> Node { + let id = unsafe { bindings::add_gain(self.id, lvl) }; + Node::new(id) + } + + /// Add a loop node that resets the input if it stops. + pub fn add_loop(&self) -> Node { + let id = unsafe { bindings::add_loop(self.id) }; + Node::new(id) + } + + /// Add a node that plays the inputs one after the other, in the order as they added. + pub fn add_concat(&self) -> Node { + let id = unsafe { bindings::add_concat(self.id) }; + Node::new(id) + } + + /// Add node panning the audio to the left (0.), right (1.), or something in between. + pub fn add_pan(&self, lvl: f32) -> Node { + let id = unsafe { bindings::add_pan(self.id, lvl) }; + Node::new(id) + } + + /// Add node that can be muted using modulation. + pub fn add_mute(&self) -> Node { + let id = unsafe { bindings::add_mute(self.id) }; + Node::new(id) + } + + /// Add node that can be paused using modulation. + pub fn add_pause(&self) -> Node { + let id = unsafe { bindings::add_pause(self.id) }; + Node::new(id) + } + + /// Add node tracking the elapsed playback time. + pub fn add_track_position(&self) -> Node { + let id = unsafe { bindings::add_track_position(self.id) }; + Node::new(id) + } + + /// Add lowpass filter node. + pub fn add_low_pass(&self, freq: f32, q: f32) -> Node { + let id = unsafe { bindings::add_low_pass(self.id, freq, q) }; + Node::new(id) + } + + /// Add highpass filter node. + pub fn add_high_pass(&self, freq: f32, q: f32) -> Node { + let id = unsafe { bindings::add_high_pass(self.id, freq, q) }; + Node::new(id) + } + + /// Add node converting stereo to mono by taking the left channel. + pub fn add_take_left(&self) -> Node { + let id = unsafe { bindings::add_take_left(self.id) }; + Node::new(id) + } + + /// Add node converting stereo to mono by taking the right channel. + pub fn add_take_right(&self) -> Node { + let id = unsafe { bindings::add_take_right(self.id) }; + Node::new(id) + } + + /// Add node swapping left and right channels of the stereo input. + pub fn add_swap(&self) -> Node { + let id = unsafe { bindings::add_swap(self.id) }; + Node::new(id) + } + + /// Add node clamping the input amplitude. Can be used for hard distortion. + pub fn add_clip(&self, low: f32, high: f32) -> Node { + let id = unsafe { bindings::add_clip(self.id, low, high) }; + Node::new(id) + } + + /// Reset the node state to how it was when it was just added. + pub fn reset(&self) { + unsafe { bindings::reset(self.id) } + } + + /// Reset the node and all child nodes to the state to how it was when they were just added. + pub fn reset_all(&self) { + unsafe { bindings::reset_all(self.id) } + } + + /// Remove all child nodes. + /// + /// After it is called, you should make sure to discard all references to the old + /// child nodes. + pub fn clear(&self) { + unsafe { bindings::clear(self.id) } + } +} + +impl Node { + /// Modulate oscillation frequency. + pub fn modulate(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate oscillation frequency. + pub fn modulate(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate oscillation frequency. + pub fn modulate(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate oscillation frequency. + pub fn modulate(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate the gain level. + pub fn modulate(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate the pan value (from 0. to 1.: 0. is only left, 1. is only right). + pub fn modulate(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate the muted state. + /// + /// Below 0.5 is muted, above is unmuted. + pub fn modulate(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate the paused state. + /// + /// Below 0.5 is paused, above is playing. + pub fn modulate(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate the cut-off frequency. + pub fn modulate_freq(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate the cut-off frequency. + pub fn modulate_freq(&self, m: M) { + m.modulate(self.id, 0); + } +} + +impl Node { + /// Modulate the low cut amplitude and adjust the high amplitude to keep the gap. + /// + /// In other words, the difference between low and high cut points will stay the same. + pub fn modulate_both(&self, m: M) { + m.modulate(self.id, 0); + } + + /// Modulate the low cut amplitude. + pub fn modulate_low(&self, m: M) { + m.modulate(self.id, 0); + } + + /// Modulate the high cut amplitude. + pub fn modulate_high(&self, m: M) { + m.modulate(self.id, 0); + } +} + +mod bindings { + #[link(wasm_import_module = "audio")] + extern { + // generators + pub(super) fn add_sine(parent_id: u32, freq: f32, phase: f32) -> u32; + pub(super) fn add_square(parent_id: u32, freq: f32, phase: f32) -> u32; + pub(super) fn add_sawtooth(parent_id: u32, freq: f32, phase: f32) -> u32; + pub(super) fn add_triangle(parent_id: u32, freq: f32, phase: f32) -> u32; + pub(super) fn add_noise(parent_id: u32, seed: i32) -> u32; + pub(super) fn add_empty(parent_id: u32) -> u32; + pub(super) fn add_zero(parent_id: u32) -> u32; + + // nodes + pub(super) fn add_mix(parent_id: u32) -> u32; + pub(super) fn add_all_for_one(parent_id: u32) -> u32; + pub(super) fn add_gain(parent_id: u32, lvl: f32) -> u32; + pub(super) fn add_loop(parent_id: u32) -> u32; + pub(super) fn add_concat(parent_id: u32) -> u32; + pub(super) fn add_pan(parent_id: u32, lvl: f32) -> u32; + pub(super) fn add_mute(parent_id: u32) -> u32; + pub(super) fn add_pause(parent_id: u32) -> u32; + pub(super) fn add_track_position(parent_id: u32) -> u32; + pub(super) fn add_low_pass(parent_id: u32, freq: f32, q: f32) -> u32; + pub(super) fn add_high_pass(parent_id: u32, freq: f32, q: f32) -> u32; + pub(super) fn add_take_left(parent_id: u32) -> u32; + pub(super) fn add_take_right(parent_id: u32) -> u32; + pub(super) fn add_swap(parent_id: u32) -> u32; + pub(super) fn add_clip(parent_id: u32, low: f32, high: f32) -> u32; + + pub(super) fn reset(node_id: u32); + pub(super) fn reset_all(node_id: u32); + pub(super) fn clear(node_id: u32); + } +} diff --git a/src/audio/time.rs b/src/audio/time.rs new file mode 100644 index 0000000..e6f4224 --- /dev/null +++ b/src/audio/time.rs @@ -0,0 +1,30 @@ +use super::*; + +pub struct Time(pub(super) u32); + +impl Time { + pub const ZERO: Self = Self(0); + + #[must_use] + pub fn samples(s: u32) -> Self { + Self(s) + } + + #[must_use] + pub fn seconds(s: u32) -> Self { + Self(s * SAMPLE_RATE) + } + + #[must_use] + pub fn ms(s: u32) -> Self { + Self(s * SAMPLE_RATE / 1000) + } + + #[must_use] + pub fn duration(s: core::time::Duration) -> Self { + let s = s.as_secs_f32() * 44_100.; + #[expect(clippy::cast_sign_loss)] + let s = s as u32; + Self(s) + } +} diff --git a/src/bindings.rs b/src/bindings.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/bindings.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/graphics/angle.rs b/src/graphics/angle.rs index 0caa8d7..973fbd5 100644 --- a/src/graphics/angle.rs +++ b/src/graphics/angle.rs @@ -140,7 +140,7 @@ mod tests { } #[test] - #[allow(clippy::float_cmp)] + #[expect(clippy::float_cmp)] fn test_to_degrees() { let a = Angle::from_degrees; assert_eq!(a(47.).to_degrees(), 47.); diff --git a/src/input.rs b/src/input.rs index fb6e7b0..aaa1c1e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -33,7 +33,7 @@ impl Pad { #[must_use] pub fn radius(self) -> f32 { let r = self.x * self.x + self.y * self.y; - #[allow(clippy::cast_precision_loss)] + #[expect(clippy::cast_precision_loss)] math::sqrt(r as f32) } @@ -42,7 +42,7 @@ impl Pad { /// [polar coordinate]: https://en.wikipedia.org/wiki/Polar_coordinate_system #[must_use] pub fn azimuth(self) -> Angle { - #[allow(clippy::cast_precision_loss)] + #[expect(clippy::cast_precision_loss)] let r = math::atan(self.y as f32 / self.x as f32); Angle::from_radians(r) } diff --git a/src/lib.rs b/src/lib.rs index 704d4a2..1b49916 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,20 @@ #![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] -#![deny(clippy::pedantic)] +#![deny( + clippy::pedantic, + clippy::alloc_instead_of_core, + clippy::allow_attributes, + clippy::std_instead_of_alloc, + clippy::std_instead_of_core +)] #![allow(clippy::wildcard_imports)] -#![allow(clippy::struct_excessive_bools)] -#![allow(clippy::cast_possible_truncation)] -#![allow(clippy::iter_without_into_iter)] +#![expect( + clippy::struct_excessive_bools, + clippy::cast_possible_truncation, + clippy::iter_without_into_iter +)] -mod bindings; +pub mod audio; mod fs; pub mod graphics; mod input; diff --git a/src/math.rs b/src/math.rs index 9f4f237..bcac6f2 100644 --- a/src/math.rs +++ b/src/math.rs @@ -37,7 +37,7 @@ pub fn cos(x: f32) -> f32 { #[must_use] pub fn floor(x: f32) -> f32 { // https://github.com/tarcieri/micromath/blob/main/src/float/floor.rs - #[allow(clippy::cast_precision_loss)] + #[expect(clippy::cast_precision_loss)] let mut res = (x as i32) as f32; if x < res { res -= 1.0; @@ -110,7 +110,7 @@ mod tests { use super::*; #[test] - #[allow(clippy::float_cmp)] + #[expect(clippy::float_cmp)] fn test_sqrt() { assert_eq!(sqrt(4.), 2.); assert_eq!(sqrt(9.), 3.125); diff --git a/src/menu.rs b/src/menu.rs index 29601c2..ce47ef0 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -22,7 +22,7 @@ pub fn remove_menu_item(i: u8) { /// /// It will be opened before the next update. /// The current update and then render will proceed as planned. -#[allow(clippy::module_name_repetitions)] +#[expect(clippy::module_name_repetitions)] pub fn open_menu() { unsafe { bindings::open_menu(); diff --git a/src/net.rs b/src/net.rs index cd8bf83..40bf5fa 100644 --- a/src/net.rs +++ b/src/net.rs @@ -34,7 +34,7 @@ impl Peers { /// /// Never zero. 1 for local single-player game. 2 or more for multiplayer. #[must_use] - #[allow(clippy::len_without_is_empty)] // always non-empty + #[expect(clippy::len_without_is_empty)] // always non-empty pub fn len(&self) -> usize { self.0.count_ones() as usize }