├── .gitignore ├── examples ├── ambidumbi.snt ├── poseidon.snt ├── microscope.snt ├── lovely_drive.snt ├── writer.rs └── player.rs ├── src ├── consts.rs ├── lib.rs ├── song.rs └── synth.rs ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .DS_store 4 | *~ 5 | -------------------------------------------------------------------------------- /examples/ambidumbi.snt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parasyte/sonant-rs/HEAD/examples/ambidumbi.snt -------------------------------------------------------------------------------- /examples/poseidon.snt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parasyte/sonant-rs/HEAD/examples/poseidon.snt -------------------------------------------------------------------------------- /examples/microscope.snt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parasyte/sonant-rs/HEAD/examples/microscope.snt -------------------------------------------------------------------------------- /examples/lovely_drive.snt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parasyte/sonant-rs/HEAD/examples/lovely_drive.snt -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const NUM_CHANNELS: usize = 2; 2 | pub(crate) const NUM_INSTRUMENTS: usize = 8; 3 | pub(crate) const NUM_PATTERNS: usize = 10; 4 | 5 | pub(crate) const MAX_OVERLAPPING_NOTES: usize = 8; 6 | 7 | pub(crate) const HEADER_LENGTH: usize = 4; 8 | pub(crate) const INSTRUMENT_LENGTH: usize = 0x1a0; 9 | pub(crate) const FOOTER_LENGTH: usize = 1; 10 | pub(crate) const SONG_LENGTH: usize = 11 | HEADER_LENGTH + INSTRUMENT_LENGTH * NUM_INSTRUMENTS + FOOTER_LENGTH; 12 | pub(crate) const OSCILLATOR_LENGTH: usize = 6; 13 | 14 | pub(crate) const SEQUENCE_LENGTH: usize = 48; 15 | pub(crate) const PATTERN_LENGTH: usize = 32; 16 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Rust port of the [Sonant 4K synth](http://www.pouet.net/prod.php?which=53615) with streaming 2 | //! support. 3 | //! 4 | //! Sonant [(C) 2008-2009 Jake Taylor](https://creativecommons.org/licenses/by-nc-sa/2.5/) 5 | //! [ Ferris / Youth Uprising ] 6 | //! 7 | //! # Crate features 8 | //! 9 | //! - `std` (default) - Allow `std::error::Error`. Disable default features to use `sonant` in a 10 | //! `no_std` environment. 11 | 12 | #![cfg_attr(not(feature = "std"), no_std)] 13 | #![deny(clippy::all)] 14 | #![deny(clippy::pedantic)] 15 | #![allow(clippy::cast_possible_truncation)] 16 | #![allow(clippy::cast_precision_loss)] 17 | #![allow(clippy::cast_sign_loss)] 18 | #![forbid(unsafe_code)] 19 | 20 | mod consts; 21 | mod song; 22 | mod synth; 23 | 24 | pub use song::{Error, Song}; 25 | pub use synth::Synth; 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sonant" 3 | description = "A Rust port of the Sonant 4K synth with streaming support." 4 | repository = "https://github.com/parasyte/sonant-rs" 5 | version = "0.2.0" 6 | authors = ["Jay Oster "] 7 | readme = "README.md" 8 | license = "MIT" 9 | categories = ["embedded", "game-engines", "multimedia", "no-std"] 10 | keywords = ["audio", "no_std", "sound", "synth", "synthesizer"] 11 | edition = "2021" 12 | 13 | [dependencies] 14 | arrayvec = { version = "0.7", default-features = false } 15 | byteorder = { version = "1", default-features = false } 16 | libm = "0.2" 17 | randomize = "5" 18 | thiserror = { version = "1", optional = true } 19 | 20 | [dev-dependencies] 21 | colored = "2" 22 | cpal = "0.15" 23 | error-iter = "0.4" 24 | getrandom = "0.2" 25 | riff-wave = "0.1" 26 | 27 | [features] 28 | default = ["std"] 29 | std = ["thiserror"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018-2019 Jay Oster 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /examples/writer.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(clippy::pedantic)] 3 | #![allow(clippy::cast_possible_truncation)] 4 | #![forbid(unsafe_code)] 5 | 6 | use arrayvec::ArrayVec; 7 | use byteorder::{ByteOrder as _, NativeEndian}; 8 | use colored::Colorize; 9 | use error_iter::ErrorIter as _; 10 | use riff_wave::{WaveWriter, WriteError}; 11 | use sonant::{Error as SonantError, Song, Synth}; 12 | use std::{fs::File, io::BufWriter, process::ExitCode}; 13 | use thiserror::Error; 14 | 15 | #[derive(Debug, Error)] 16 | pub enum Error { 17 | #[error("Missing snt-file argument\nUsage: player ")] 18 | MissingSntFilename, 19 | 20 | #[error("Missing wav-file argument\nUsage: player ")] 21 | MissingWavFilename, 22 | 23 | #[error("Sonant error")] 24 | Sonant(#[from] SonantError), 25 | 26 | #[error("I/O error")] 27 | Io(#[from] std::io::Error), 28 | 29 | #[error("Wave writer error")] 30 | Writer(#[from] WriteError), 31 | } 32 | 33 | fn main() -> ExitCode { 34 | match writer() { 35 | Err(e) => { 36 | eprintln!("{} {}", "error:".red(), e); 37 | 38 | for cause in e.sources().skip(1) { 39 | eprintln!("{} {}", "caused by:".bright_red(), cause); 40 | } 41 | 42 | ExitCode::FAILURE 43 | } 44 | Ok(()) => ExitCode::SUCCESS, 45 | } 46 | } 47 | 48 | fn writer() -> Result<(), Error> { 49 | let mut args = std::env::args().skip(1); 50 | let snt_filename = args.next().ok_or(Error::MissingSntFilename)?; 51 | let wav_filename = args.next().ok_or(Error::MissingWavFilename)?; 52 | 53 | // Read the snt file 54 | let data = std::fs::read(snt_filename)?; 55 | 56 | // Create a seed for the PRNG 57 | let mut seed = [0_u8; 16]; 58 | getrandom::getrandom(&mut seed).expect("failed to getrandom"); 59 | let seed = ( 60 | NativeEndian::read_u64(&seed[0..8]), 61 | NativeEndian::read_u64(&seed[8..16]), 62 | ); 63 | 64 | // Load a sonant song and create a synth 65 | let song = Song::from_slice(&data)?; 66 | let synth = Synth::new(&song, seed, 44100.0); 67 | 68 | // Write the wav file 69 | let file = File::create(wav_filename)?; 70 | let writer = BufWriter::new(file); 71 | let mut wave_writer = WaveWriter::new(2, 44100, 16, writer)?; 72 | 73 | for sample in synth.flat_map(ArrayVec::from) { 74 | let sample = (sample * f32::from(i16::MAX)).round() as i16; 75 | wave_writer.write_sample_i16(sample)?; 76 | } 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | jobs: 8 | checks: 9 | name: Check 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | rust: 14 | - stable 15 | - beta 16 | - 1.70.0 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v3 20 | - name: Update apt repos 21 | run: sudo apt -y update 22 | - name: Install dependencies 23 | run: sudo apt -y install libasound2-dev 24 | - name: Install toolchain 25 | uses: dtolnay/rust-toolchain@master 26 | with: 27 | toolchain: ${{ matrix.rust }} 28 | - name: Rust cache 29 | uses: Swatinem/rust-cache@v2 30 | with: 31 | shared-key: common 32 | - name: Cargo check 33 | run: cargo check --workspace 34 | 35 | lints: 36 | name: Lints 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v3 41 | - name: Update apt repos 42 | run: sudo apt -y update 43 | - name: Install dependencies 44 | run: sudo apt -y install libasound2-dev 45 | - name: Install toolchain 46 | uses: dtolnay/rust-toolchain@master 47 | with: 48 | toolchain: stable 49 | components: clippy, rustfmt 50 | - name: Rust cache 51 | uses: Swatinem/rust-cache@v2 52 | with: 53 | shared-key: common 54 | - name: Install cargo-machete 55 | uses: baptiste0928/cargo-install@v2 56 | with: 57 | crate: cargo-machete 58 | - name: Cargo fmt 59 | run: cargo fmt --all -- --check 60 | - name: Cargo doc 61 | run: cargo doc --workspace --no-deps 62 | - name: Cargo clippy 63 | run: cargo clippy --workspace --tests -- -D warnings 64 | - name: Cargo machete 65 | run: cargo machete 66 | 67 | tests: 68 | name: Test 69 | runs-on: ubuntu-latest 70 | needs: [checks, lints] 71 | strategy: 72 | matrix: 73 | rust: 74 | - stable 75 | - beta 76 | - 1.70.0 77 | steps: 78 | - name: Checkout sources 79 | uses: actions/checkout@v3 80 | - name: Update apt repos 81 | run: sudo apt -y update 82 | - name: Install dependencies 83 | run: sudo apt -y install libasound2-dev 84 | - name: Install toolchain 85 | uses: dtolnay/rust-toolchain@master 86 | with: 87 | toolchain: ${{ matrix.rust }} 88 | - name: Rust cache 89 | uses: Swatinem/rust-cache@v2 90 | with: 91 | shared-key: common 92 | - name: Cargo test 93 | run: cargo test --workspace 94 | -------------------------------------------------------------------------------- /examples/player.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(clippy::pedantic)] 3 | #![allow(clippy::cast_precision_loss)] 4 | #![forbid(unsafe_code)] 5 | 6 | use arrayvec::ArrayVec; 7 | use byteorder::{ByteOrder, NativeEndian}; 8 | use colored::Colorize; 9 | use cpal::traits::{DeviceTrait as _, HostTrait as _, StreamTrait as _}; 10 | use cpal::{FromSample, SampleFormat, SizedSample}; 11 | use error_iter::ErrorIter as _; 12 | use sonant::{Song, Synth}; 13 | use std::process::ExitCode; 14 | use std::sync::mpsc; 15 | use thiserror::Error; 16 | 17 | #[derive(Debug, Error)] 18 | pub enum Error { 19 | #[error("Missing filename argument")] 20 | MissingFilename, 21 | 22 | #[error("Sonant error")] 23 | Sonant(#[from] sonant::Error), 24 | 25 | #[error("I/O error")] 26 | Io(#[from] std::io::Error), 27 | 28 | #[error("CPAL audio stream config error")] 29 | AudioConfig(#[from] cpal::DefaultStreamConfigError), 30 | 31 | #[error("CPAL audio stream builder error")] 32 | AudioStream(#[from] cpal::BuildStreamError), 33 | 34 | #[error("CPAL audio stream play error")] 35 | AudioPlay(#[from] cpal::PlayStreamError), 36 | } 37 | 38 | fn main() -> ExitCode { 39 | match player() { 40 | Err(e) => { 41 | eprintln!("{} {}", "error:".red(), e); 42 | 43 | for cause in e.sources().skip(1) { 44 | eprintln!("{} {}", "caused by:".bright_red(), cause); 45 | } 46 | 47 | ExitCode::FAILURE 48 | } 49 | Ok(()) => ExitCode::SUCCESS, 50 | } 51 | } 52 | 53 | fn player() -> Result<(), Error> { 54 | let mut args = std::env::args().skip(1); 55 | let filename = args.next().ok_or(Error::MissingFilename)?; 56 | 57 | // cpal boilerplate 58 | let host = cpal::default_host(); 59 | let device = host 60 | .default_output_device() 61 | .expect("no output device available"); 62 | 63 | let stream_config = device.default_output_config()?; 64 | let sample_rate = stream_config.sample_rate(); 65 | let format = stream_config.sample_format(); 66 | 67 | // Read the file 68 | let data = std::fs::read(filename)?; 69 | 70 | // Create a seed for the PRNG 71 | let mut seed = [0_u8; 16]; 72 | getrandom::getrandom(&mut seed).expect("failed to getrandom"); 73 | let seed = ( 74 | NativeEndian::read_u64(&seed[0..8]), 75 | NativeEndian::read_u64(&seed[8..16]), 76 | ); 77 | 78 | // Load a sonant song and create a synth 79 | let song = Song::from_slice(&data)?; 80 | let synth = Synth::new(&song, seed, sample_rate.0 as f32); 81 | 82 | match format { 83 | SampleFormat::I8 => run::(&device, &stream_config.into(), synth), 84 | SampleFormat::I16 => run::(&device, &stream_config.into(), synth), 85 | SampleFormat::I32 => run::(&device, &stream_config.into(), synth), 86 | SampleFormat::I64 => run::(&device, &stream_config.into(), synth), 87 | SampleFormat::U8 => run::(&device, &stream_config.into(), synth), 88 | SampleFormat::U16 => run::(&device, &stream_config.into(), synth), 89 | SampleFormat::U32 => run::(&device, &stream_config.into(), synth), 90 | SampleFormat::U64 => run::(&device, &stream_config.into(), synth), 91 | SampleFormat::F32 => run::(&device, &stream_config.into(), synth), 92 | SampleFormat::F64 => run::(&device, &stream_config.into(), synth), 93 | sample_format => panic!("Unsupported sample format '{}'", sample_format), 94 | } 95 | } 96 | 97 | fn run(device: &cpal::Device, config: &cpal::StreamConfig, synth: Synth) -> Result<(), Error> 98 | where 99 | T: SizedSample + FromSample, 100 | { 101 | // Create a channel so the audio thread can request samples 102 | let (audio_tx, audio_rx) = mpsc::sync_channel(10); 103 | 104 | // Create the audio thread 105 | let stream = device.build_output_stream( 106 | config, 107 | move |buffer: &mut [T], _: &cpal::OutputCallbackInfo| { 108 | let (tx, rx) = mpsc::sync_channel(1); 109 | 110 | // Request samples from the main thread 111 | audio_tx.send((buffer.len(), tx)).unwrap(); 112 | let samples = rx.recv().unwrap(); 113 | 114 | for (elem, sample) in buffer.iter_mut().zip(samples) { 115 | *elem = T::from_sample(sample); 116 | } 117 | }, 118 | |err| eprintln!("an error occurred on stream: {err}"), 119 | None, 120 | )?; 121 | stream.play()?; 122 | 123 | let mut synth = synth.flat_map(ArrayVec::from); 124 | 125 | // Send samples requested by the audio thread. 126 | while let Ok((len, tx)) = audio_rx.recv() { 127 | let samples = synth.by_ref().take(len).collect::>(); 128 | let done = samples.is_empty(); 129 | tx.send(samples).unwrap(); 130 | if done { 131 | break; 132 | } 133 | } 134 | 135 | Ok(()) 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sonant-rs 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/sonant)](https://crates.io/crates/sonant "Crates.io version") 4 | [![Documentation](https://img.shields.io/docsrs/sonant)](https://docs.rs/sonant "Documentation") 5 | [![GitHub actions](https://img.shields.io/github/actions/workflow/status/parasyte/sonant-rs/ci.yml?branch=main)](https://github.com/parasyte/sonant-rs/actions "CI") 6 | [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) 7 | 8 | A Rust port of the [Sonant 4K synth](http://www.pouet.net/prod.php?which=53615) with streaming support. 9 | 10 | Sonant [(C) 2008-2009 Jake Taylor](https://creativecommons.org/licenses/by-nc-sa/2.5/) [ Ferris / Youth Uprising ] 11 | 12 | ## What is it? 13 | 14 | A tiny synthesizer written for 4K intros. It is capable of producing high quality audio with very little code and instrument data. Song files are just over 3KB, but can also be customized to reduce the number of instrument tracks or patterns if you have a tighter size budget. 15 | 16 | The `sonant::Synth` type is implemented as an iterator, which makes it ideal for producing realtime audio streams with very little memory overhead; about 6.2 KB for the song data, and another 2.5 KB for buffering note frequencies. It was originally written to target Nintendo 64, which has a baseline of 4 MB of system memory! 17 | 18 | Unfortunately, it's too slow to run on the N64's 93 MHz CPU. It would probably work on the RCP, e.g. by computing 8 samples at a time on the vector unit. But that would require porting the sample generators to use 16-bit fixed point numbers. Then there's also the problem that rustc cannot target RCP. Oh well! 19 | 20 | ## How does it work? 21 | 22 | Flippin' maths and magics! I have no idea. Synthesizers are weird and alien to me, but they make really pretty ear-candy. 23 | 24 | Each song has eight instrument tracks, and each instrument has two oscillators. The oscillators work together (or adversarially canceling each other, if you like) to vary the instrument frequencies. The "personality" of the instrument is provided by one of four waveforms: `Sine`, `Square`, `Saw`, or `Triangle`. The oscillators' frequencies modulate these basic waveforms to produce the final sounds. 25 | 26 | In addition to the primary oscillators, each instrument also has its own [LFO](https://en.wikipedia.org/wiki/Low-frequency_oscillation), which is what makes that slow pitch-bending that you hear all the time in electronic music. 27 | 28 | Finally, each instrument also has it own effects channel, which can do `HighPass`, `LowPass`, `BandPass`, and `Notch` filtering. The effects also provide simple resonance, delay (echo), and panning. 29 | 30 | The rest of the song structure is pretty standard for tracked tunes; Each instrument can have up to 10 patterns. And any pattern can be referenced from a 48-element sequence. Each pattern itself contains 32 notes. 31 | 32 | Delay effects are implemented as extra notes, which greatly reduces the memory footprint. The original implementation uses over 42 MB of memory to maintain the delay buffers. I made the tradeoff to pay for better memory efficiency by recomputing all of the delayed samples as they are needed. 33 | 34 | See the Sonant manual (bundled with the original release archive on Pouët) if you would like to learn more about the synth, tracker, or song format. 35 | 36 | ## How to use it? 37 | 38 | See the [`player` example](./examples/player.rs) for some code that loads and plays a `.snt` file. 39 | 40 | ```bash 41 | cargo run --release --example player -- ./examples/poseidon.snt 42 | ``` 43 | 44 | You can create `.snt` files using [sonant-tool](http://www.pouet.net/prod.php?which=53615) from the original release. You can also use the "Save" button (NOT the "Save JavaScript" button!) on [Sonant Live](http://sonantlive.bitsnbites.eu/tool/), but don't forget to check [its manual](http://sonantlive.bitsnbites.eu/)! 45 | 46 | ## Limitations 47 | 48 | The original synthesizer doesn't have many limitations beyond what the `.snt` format is capable of storing. The iterator-based implementation of this port does come with a few restrictions, though. For example, only up to 8 overlapping notes are able to be played simultaneously for each instrument track. `sonant-tool` is capable of producing `.snt` files which require up to 100 overlapping notes per instrument track, but this is only true in the most extreme possible case. *The `.snt` format itself is theoretically able to require up to 1,536 overlapping notes!* 49 | 50 | Songs which use a lot of delay effects on the instruments will more quickly hit the overlapping note limits. If you need to support more overlapping notes, you can simply increase the value in `consts.rs`; any value up to 32 will work without any other changes. 51 | 52 | Due to the way the delayed notes work, the length of quarter notes cannot be an odd number of samples. This would cause the length of eighth notes to be a fractional number, and would complicate the process of "finding notes in the past". To resolve the conflict, the length of quarter notes is adjusted to an even number by "rounding down" to the nearest even number. This has a small impact on playback duration; a four-minute song will be about 1 second shorter than it would as rendered by other players. 53 | 54 | Sonant generates samples in reverse order. We have to generate samples chronologically. This shifts the phase of the waveform for individual notes arbitrarily (it depends on note length, envelope, and the nondeterministic LFO). The differences are too subtle for humans to distinguish, but it is worth mentioning. 55 | -------------------------------------------------------------------------------- /src/song.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::{HEADER_LENGTH, INSTRUMENT_LENGTH, NUM_INSTRUMENTS, NUM_PATTERNS}; 2 | use crate::consts::{OSCILLATOR_LENGTH, PATTERN_LENGTH, SEQUENCE_LENGTH, SONG_LENGTH}; 3 | use arrayvec::ArrayVec; 4 | use byteorder::{ByteOrder as _, LittleEndian}; 5 | use core::num::Wrapping as w; 6 | #[cfg(feature = "std")] 7 | use thiserror::Error; 8 | 9 | /// Possible errors. 10 | #[derive(Debug)] 11 | #[cfg_attr(feature = "std", derive(Error))] 12 | pub enum Error { 13 | /// Incorrect file length 14 | #[cfg_attr(feature = "std", error("Incorrect file length"))] 15 | FileLength, 16 | 17 | /// Invalid waveform 18 | #[cfg_attr(feature = "std", error("Invalid waveform"))] 19 | InvalidWaveform, 20 | 21 | /// Invalid filter 22 | #[cfg_attr(feature = "std", error("Invalid filter"))] 23 | InvalidFilter, 24 | 25 | /// Invalid instruments 26 | #[cfg_attr(feature = "std", error("Invalid instruments"))] 27 | InvalidInstruments, 28 | } 29 | 30 | /// A `Song` contains a list of up to 8 `Instruments` and defines the sample 31 | /// length for each row (in the tracker). 32 | #[derive(Debug)] 33 | pub struct Song { 34 | pub(crate) instruments: [Instrument; NUM_INSTRUMENTS], 35 | pub(crate) seq_length: usize, // Total number of patterns to play 36 | pub(crate) quarter_note_length: u32, // In samples 37 | } 38 | 39 | /// Contains two `Oscillator`s, a simple `Envelope`, `Effects` and `LFO`. The 40 | /// tracker `Sequence` (up to 48) is defined here, as well as the tracker 41 | /// `Patterns` (up to 10). 42 | #[derive(Debug)] 43 | pub(crate) struct Instrument { 44 | pub(crate) osc: [Oscillator; 2], // Oscillators 0 and 1 45 | pub(crate) noise_fader: f32, // Noise Oscillator 46 | pub(crate) env: Envelope, // Envelope 47 | pub(crate) fx: Effects, // Effects 48 | pub(crate) lfo: Lfo, // Low-Frequency Oscillator 49 | pub(crate) seq: [usize; SEQUENCE_LENGTH], // Sequence of patterns 50 | pub(crate) pat: [Pattern; NUM_PATTERNS], // List of available patterns 51 | } 52 | 53 | /// The `Oscillator` defines the `Instrument` sound. 54 | #[derive(Debug)] 55 | pub(crate) struct Oscillator { 56 | pub(crate) octave: u8, // Octave knob 57 | pub(crate) detune_freq: u8, // Detune frequency 58 | pub(crate) detune: f32, // Detune knob 59 | pub(crate) envelope: bool, // Envelope toggle 60 | pub(crate) volume: f32, // Volume knob 61 | pub(crate) waveform: Waveform, // Wave form 62 | } 63 | 64 | /// `Envelope` is for compressing the sample amplitude over time. 65 | /// (E.g. raising and lowering volume.) 66 | #[derive(Debug)] 67 | pub(crate) struct Envelope { 68 | pub(crate) attack: u32, // Attack 69 | pub(crate) sustain: u32, // Sustain 70 | pub(crate) release: u32, // Release 71 | pub(crate) master: f32, // Master volume knob 72 | } 73 | 74 | /// The `Effects` provide filtering, resonance, and panning. 75 | #[derive(Debug)] 76 | pub(crate) struct Effects { 77 | pub(crate) filter: Filter, // Hi, lo, bandpass, or notch toggle 78 | pub(crate) freq: f32, // FX Frequency 79 | pub(crate) resonance: f32, // FX Resonance 80 | pub(crate) delay_time: u8, // Delay time 81 | pub(crate) delay_amount: f32, // Delay amount 82 | pub(crate) pan_freq: u8, // Panning frequency 83 | pub(crate) pan_amount: f32, // Panning amount 84 | } 85 | 86 | /// `LFO` is a Low-Frequency Oscillator. It can be used to adjust the frequency 87 | /// of `Oscillator` 0 and `Effects` over time. 88 | #[derive(Debug)] 89 | pub(crate) struct Lfo { 90 | pub(crate) osc0_freq: bool, // Modify Oscillator 0 frequency (FM) toggle 91 | pub(crate) fx_freq: bool, // Modify FX frequency toggle 92 | pub(crate) freq: u8, // LFO frequency 93 | pub(crate) amount: f32, // LFO amount 94 | pub(crate) waveform: Waveform, // LFO waveform 95 | } 96 | 97 | /// Contains the tracker notes (up to 32). 98 | #[derive(Debug)] 99 | pub(crate) struct Pattern { 100 | pub(crate) notes: [u8; PATTERN_LENGTH], 101 | } 102 | 103 | /// Available filters. 104 | #[derive(Debug)] 105 | pub(crate) enum Filter { 106 | None, 107 | HighPass, 108 | LowPass, 109 | BandPass, 110 | Notch, 111 | } 112 | 113 | /// Available wave forms. 114 | #[derive(Debug)] 115 | pub(crate) enum Waveform { 116 | Sine, 117 | Square, 118 | Saw, 119 | Triangle, 120 | } 121 | 122 | impl Song { 123 | /// Create a new `Song` from a byte slice. 124 | /// 125 | /// ``` 126 | /// use sonant::Song; 127 | /// 128 | /// let song = Song::from_slice(include_bytes!("../examples/poseidon.snt"))?; 129 | /// # Ok::<(), sonant::Error>(()) 130 | /// ``` 131 | /// 132 | /// # Errors 133 | /// 134 | /// An error is returned when the song data cannot be parsed. 135 | pub fn from_slice(slice: &[u8]) -> Result { 136 | if slice.len() != SONG_LENGTH { 137 | return Err(Error::FileLength); 138 | } 139 | 140 | // Get quarter note length and eighth note length (in samples) 141 | // This properly handles odd quarter note lengths 142 | let quarter_note_length = LittleEndian::read_u32(&slice[..HEADER_LENGTH]); 143 | let quarter_note_length = quarter_note_length - (quarter_note_length % 2); 144 | 145 | let seq_length = slice[HEADER_LENGTH + INSTRUMENT_LENGTH * 8] as usize; 146 | let mut instruments = ArrayVec::new(); 147 | for i in 0..NUM_INSTRUMENTS { 148 | instruments.push(load_instrument(slice, i)?); 149 | } 150 | let instruments = instruments 151 | .into_inner() 152 | .map_err(|_| Error::InvalidInstruments)?; 153 | 154 | Ok(Self { 155 | instruments, 156 | seq_length, 157 | quarter_note_length, 158 | }) 159 | } 160 | } 161 | 162 | fn parse_waveform(waveform: u8) -> Result { 163 | Ok(match waveform { 164 | 0 => Waveform::Sine, 165 | 1 => Waveform::Square, 166 | 2 => Waveform::Saw, 167 | 3 => Waveform::Triangle, 168 | _ => return Err(Error::InvalidWaveform), 169 | }) 170 | } 171 | 172 | fn load_oscillator(slice: &[u8], i: usize, o: usize) -> Result { 173 | let i = i + o * OSCILLATOR_LENGTH; 174 | let octave = ((w(slice[i]) - w(8)) * w(12)).0; 175 | let detune_freq = slice[i + 1]; 176 | let detune = libm::fmaf(f32::from(slice[i + 2]), 0.2 / 255.0, 1.0); 177 | let envelope = slice[i + 3] != 0; 178 | let volume = f32::from(slice[i + 4]) / 255.0; 179 | let waveform = parse_waveform(slice[i + 5])?; 180 | 181 | Ok(Oscillator { 182 | octave, 183 | detune_freq, 184 | detune, 185 | envelope, 186 | volume, 187 | waveform, 188 | }) 189 | } 190 | 191 | fn load_envelope(slice: &[u8], i: usize) -> Envelope { 192 | let attack = LittleEndian::read_u32(&slice[i..i + 4]); 193 | let sustain = LittleEndian::read_u32(&slice[i + 4..i + 8]); 194 | let release = LittleEndian::read_u32(&slice[i + 8..i + 12]); 195 | let master = f32::from(slice[i + 12]) * 156.0; 196 | 197 | Envelope { 198 | attack, 199 | sustain, 200 | release, 201 | master, 202 | } 203 | } 204 | 205 | fn load_effects(slice: &[u8], i: usize) -> Result { 206 | let filter = match slice[i] { 207 | 0 => Filter::None, 208 | 1 => Filter::HighPass, 209 | 2 => Filter::LowPass, 210 | 3 => Filter::BandPass, 211 | 4 => Filter::Notch, 212 | _ => return Err(Error::InvalidFilter), 213 | }; 214 | let i = i + 3; 215 | let freq = f32::from_bits(LittleEndian::read_u32(&slice[i..i + 4])); 216 | let resonance = f32::from(slice[i + 4]) / 255.0; 217 | let delay_time = slice[i + 5]; 218 | let delay_amount = f32::from(slice[i + 6]) / 255.0; 219 | let pan_freq = slice[i + 7]; 220 | let pan_amount = f32::from(slice[i + 8]) / 512.0; 221 | 222 | Ok(Effects { 223 | filter, 224 | freq, 225 | resonance, 226 | delay_time, 227 | delay_amount, 228 | pan_freq, 229 | pan_amount, 230 | }) 231 | } 232 | 233 | fn load_lfo(slice: &[u8], i: usize) -> Result { 234 | let osc0_freq = slice[i] != 0; 235 | let fx_freq = slice[i + 1] != 0; 236 | let freq = slice[i + 2]; 237 | let amount = f32::from(slice[i + 3]) / 512.0; 238 | let waveform = parse_waveform(slice[i + 4])?; 239 | 240 | Ok(Lfo { 241 | osc0_freq, 242 | fx_freq, 243 | freq, 244 | amount, 245 | waveform, 246 | }) 247 | } 248 | 249 | fn load_sequence(slice: &[u8], i: usize) -> [usize; SEQUENCE_LENGTH] { 250 | let mut seq = [0; SEQUENCE_LENGTH]; 251 | 252 | slice[i..i + SEQUENCE_LENGTH] 253 | .iter() 254 | .enumerate() 255 | .for_each(|(i, &x)| { 256 | seq[i] = x as usize; 257 | }); 258 | 259 | seq 260 | } 261 | 262 | fn load_pattern(slice: &[u8], i: usize, p: usize) -> Pattern { 263 | let i = i + p * PATTERN_LENGTH; 264 | let mut notes = [0; PATTERN_LENGTH]; 265 | notes.copy_from_slice(&slice[i..i + PATTERN_LENGTH]); 266 | 267 | Pattern { notes } 268 | } 269 | 270 | fn load_instrument(slice: &[u8], i: usize) -> Result { 271 | let i = HEADER_LENGTH + i * INSTRUMENT_LENGTH; 272 | let osc = [load_oscillator(slice, i, 0)?, load_oscillator(slice, i, 1)?]; 273 | 274 | let i = i + OSCILLATOR_LENGTH * 2; 275 | let noise_fader = f32::from(slice[i]) / 255.0; 276 | 277 | let i = i + 4; 278 | let env = load_envelope(slice, i); 279 | 280 | let i = i + 13; 281 | let fx = load_effects(slice, i)?; 282 | 283 | let i = i + 12; 284 | let lfo = load_lfo(slice, i)?; 285 | 286 | let i = i + 5; 287 | let seq = load_sequence(slice, i); 288 | 289 | let i = i + SEQUENCE_LENGTH; 290 | let mut pat = ArrayVec::new(); 291 | for j in 0..NUM_PATTERNS { 292 | pat.push(load_pattern(slice, i, j)); 293 | } 294 | let pat = pat.into_inner().unwrap(); 295 | 296 | Ok(Instrument { 297 | osc, 298 | noise_fader, 299 | env, 300 | fx, 301 | lfo, 302 | seq, 303 | pat, 304 | }) 305 | } 306 | -------------------------------------------------------------------------------- /src/synth.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::{MAX_OVERLAPPING_NOTES, NUM_CHANNELS, NUM_INSTRUMENTS, PATTERN_LENGTH}; 2 | use crate::song::{Envelope, Filter, Instrument, Song, Waveform}; 3 | use arrayvec::ArrayVec; 4 | use core::{f32::consts::PI, num::Wrapping as w}; 5 | use randomize::{Gen32 as _, PCG32}; 6 | 7 | /// The main struct for audio synthesis. 8 | /// 9 | /// `Synth` implements `Iterator`, so calling the `next` method on it will generate the next 10 | /// sample. 11 | /// 12 | /// Currently only generates 2-channel f32 samples at the given `sample_rate`. 13 | #[derive(Debug)] 14 | pub struct Synth<'a> { 15 | song: &'a Song, 16 | random: PCG32, 17 | sample_rate: f32, 18 | sample_ratio: f32, 19 | quarter_note_length: u32, 20 | eighth_note_length: u32, 21 | 22 | // TODO: Support seamless loops 23 | 24 | // Iterator state 25 | seq_count: usize, 26 | note_count: usize, 27 | sample_count: u32, 28 | tracks: [TrackState; NUM_INSTRUMENTS], 29 | } 30 | 31 | /// Iterator state for a single instrument track. 32 | #[derive(Debug)] 33 | struct TrackState { 34 | env: Envelope, 35 | 36 | // Max simultaneous notes per track 37 | notes: [Note; MAX_OVERLAPPING_NOTES], 38 | 39 | delay_samples: u32, 40 | delay_count: u32, 41 | 42 | // Static frequencies 43 | pan_freq: f32, 44 | lfo_freq: f32, 45 | } 46 | 47 | /// Data structure for quarter notes, which includes the pitch and sample 48 | /// counter reference for waveform modulation. It also contains state for sample 49 | /// synthesis and filtering. 50 | #[derive(Debug)] 51 | struct Note { 52 | pitch: u8, 53 | sample_count: u32, 54 | volume: f32, 55 | swap_stereo: bool, 56 | 57 | // Iterator state 58 | osc_freq: [f32; 2], 59 | osc_time: [f32; 2], 60 | low: f32, 61 | band: f32, 62 | } 63 | 64 | /// Sine wave generator 65 | fn osc_sin(value: f32) -> f32 { 66 | libm::sinf((value + 0.5) * PI * 2.0) 67 | } 68 | 69 | /// Square wave generator 70 | fn osc_square(value: f32) -> f32 { 71 | if osc_sin(value) < 0.0 { 72 | -1.0 73 | } else { 74 | 1.0 75 | } 76 | } 77 | 78 | /// Saw wave generator 79 | fn osc_saw(value: f32) -> f32 { 80 | let fract = value - libm::truncf(value); 81 | (1.0 - fract) - 0.5 82 | } 83 | 84 | /// Triangle wave generator 85 | fn osc_tri(value: f32) -> f32 { 86 | let fract = value - libm::truncf(value); 87 | let v2 = fract * 4.0; 88 | 89 | if v2 < 2.0 { 90 | v2 - 1.0 91 | } else { 92 | 3.0 - v2 93 | } 94 | } 95 | 96 | /// Get a `note` frequency on the exponential scale defined by reference 97 | /// frequency `ref_freq` and reference pitch `ref_pitch`, using the interval 98 | /// `semitone`. 99 | fn get_frequency(ref_freq: f32, semitone: f32, note: u8, ref_pitch: u8) -> f32 { 100 | ref_freq * libm::powf(semitone, f32::from(note) - f32::from(ref_pitch)) 101 | } 102 | 103 | /// Get the absolute frequency for a note value on the 12-TET scale. 104 | fn get_note_frequency(note: u8) -> f32 { 105 | const SEMITONE: f32 = 1.059_463_1; // Twelfth root of 2 106 | get_frequency(1.0 / 256.0, SEMITONE, note, 128) 107 | } 108 | 109 | /// Get a sample from the waveform generator at time `t` 110 | fn get_osc_output(waveform: &Waveform, t: f32) -> f32 { 111 | match waveform { 112 | Waveform::Sine => osc_sin(t), 113 | Waveform::Square => osc_square(t), 114 | Waveform::Saw => osc_saw(t), 115 | Waveform::Triangle => osc_tri(t), 116 | } 117 | } 118 | 119 | impl TrackState { 120 | fn new() -> Self { 121 | let mut notes = ArrayVec::new(); 122 | for _ in 0..MAX_OVERLAPPING_NOTES { 123 | notes.push(Note::new(0, 0, 0.0, false)); 124 | } 125 | let notes = notes.into_inner().unwrap(); 126 | 127 | Self { 128 | env: Envelope { 129 | attack: 0, 130 | sustain: 0, 131 | release: 0, 132 | master: 0.0, 133 | }, 134 | notes, 135 | delay_samples: 0, 136 | delay_count: 0, 137 | pan_freq: 0.0, 138 | lfo_freq: 0.0, 139 | } 140 | } 141 | } 142 | 143 | impl Note { 144 | fn new(pitch: u8, sample_count: u32, volume: f32, swap_stereo: bool) -> Self { 145 | Self { 146 | pitch, 147 | sample_count, 148 | volume, 149 | swap_stereo, 150 | osc_freq: [0.0; 2], 151 | osc_time: [0.0; 2], 152 | low: 0.0, 153 | band: 0.0, 154 | } 155 | } 156 | } 157 | 158 | impl<'a> Synth<'a> { 159 | /// Create a `Synth` that will play the provided `Song`. 160 | /// The optional seed will be used for the noise generator. 161 | /// `Synth` implements `Iterator` and generates two stereo samples at a time. 162 | /// 163 | /// ```no_run 164 | /// use byteorder::{ByteOrder, NativeEndian}; 165 | /// use getrandom::getrandom; 166 | /// use sonant::{Song, Synth}; 167 | /// 168 | /// let song = Song::from_slice(include_bytes!("../examples/poseidon.snt"))?; 169 | /// 170 | /// // Create a seed for the PRNG 171 | /// let mut seed = [0_u8; 16]; 172 | /// getrandom(&mut seed).expect("failed to getrandom"); 173 | /// let seed = ( 174 | /// NativeEndian::read_u64(&seed[0..8]), 175 | /// NativeEndian::read_u64(&seed[8..16]), 176 | /// ); 177 | /// 178 | /// let synth = Synth::new(&song, seed, 44100.0); 179 | /// for [sample_l, sample_r] in synth { 180 | /// // Do something with the samples 181 | /// } 182 | /// # Ok::<(), sonant::Error>(()) 183 | /// ``` 184 | #[must_use] 185 | pub fn new(song: &'a Song, seed: (u64, u64), sample_rate: f32) -> Self { 186 | let random = PCG32::new(seed.0, seed.1); 187 | let sample_ratio = sample_rate / 44100.0; 188 | let quarter_note_length = (sample_ratio * song.quarter_note_length as f32) as u32; 189 | let eighth_note_length = quarter_note_length / 2; 190 | 191 | let mut synth = Synth { 192 | song, 193 | random, 194 | sample_rate, 195 | sample_ratio, 196 | quarter_note_length, 197 | eighth_note_length, 198 | seq_count: 0, 199 | sample_count: 0, 200 | note_count: 0, 201 | tracks: Self::load_tracks( 202 | song, 203 | sample_ratio, 204 | quarter_note_length as f32, 205 | eighth_note_length as f32, 206 | ), 207 | }; 208 | synth.load_notes(); 209 | 210 | synth 211 | } 212 | 213 | /// Load the static state for each track. 214 | fn load_tracks( 215 | song: &Song, 216 | sample_ratio: f32, 217 | quarter_note_length: f32, 218 | eighth_note_length: f32, 219 | ) -> [TrackState; NUM_INSTRUMENTS] { 220 | let mut tracks = ArrayVec::<_, NUM_INSTRUMENTS>::new(); 221 | for _ in 0..NUM_INSTRUMENTS { 222 | tracks.push(TrackState::new()); 223 | } 224 | let mut tracks = tracks.into_inner().unwrap(); 225 | 226 | for (i, inst) in song.instruments.iter().enumerate() { 227 | // Configure attack, sustain, and release 228 | tracks[i].env.attack = (inst.env.attack as f32 * sample_ratio) as u32; 229 | tracks[i].env.sustain = (inst.env.sustain as f32 * sample_ratio) as u32; 230 | tracks[i].env.release = (inst.env.release as f32 * sample_ratio) as u32; 231 | 232 | // Configure delay 233 | tracks[i].delay_samples = (f32::from(inst.fx.delay_time) * eighth_note_length) as u32; 234 | tracks[i].delay_count = if inst.fx.delay_amount == 0.0 { 235 | // Special case for zero repeats 236 | 0 237 | } else if libm::fabsf(inst.fx.delay_amount - 1.0) < f32::EPSILON { 238 | // Special case for infinite repeats 239 | u32::MAX 240 | } else if tracks[i].delay_samples == 0 { 241 | // Special case for zero-delay time: only repeat once 242 | 1 243 | } else { 244 | // This gets the number of iterations required for the note 245 | // volume to drop below the audible threshold. 246 | let base = libm::logf(1.0 / inst.fx.delay_amount); 247 | (libm::logf(256.0) / base) as u32 248 | }; 249 | 250 | // Set LFO and panning frequencies 251 | tracks[i].lfo_freq = get_frequency(1.0, 2.0, inst.lfo.freq, 8) / quarter_note_length; 252 | tracks[i].pan_freq = get_frequency(1.0, 2.0, inst.fx.pan_freq, 8) / quarter_note_length; 253 | } 254 | 255 | tracks 256 | } 257 | 258 | /// Load the next set of notes into the iterator state. 259 | fn load_notes(&mut self) { 260 | let seq_count = self.seq_count; 261 | if seq_count > self.song.seq_length { 262 | return; 263 | } 264 | 265 | for i in 0..self.song.instruments.len() { 266 | // Add the note 267 | let note_count = self.note_count; 268 | self.add_note(i, seq_count, note_count, 1.0, false); 269 | } 270 | } 271 | 272 | /// Load delayed notes into the iterator state. 273 | fn load_delayed_notes(&mut self) { 274 | for (i, inst) in self.song.instruments.iter().enumerate() { 275 | for round in 1..=self.tracks[i].delay_count { 276 | // Compute the delay position 277 | let delay = self.tracks[i].delay_samples * round; 278 | if delay > self.sample_count { 279 | continue; 280 | } 281 | 282 | // Seek to the delayed note, and ensure it's aligned to the quarter note 283 | let position = self.sample_count - delay; 284 | if position % self.quarter_note_length != 0 { 285 | continue; 286 | } 287 | 288 | // Convert position into seq_count and note_count 289 | let pattern_length = self.quarter_note_length * PATTERN_LENGTH as u32; 290 | let seq_count = (position / pattern_length) as usize; 291 | if seq_count > self.song.seq_length { 292 | continue; 293 | } 294 | let note_count = ((position % pattern_length) / self.quarter_note_length) as usize; 295 | 296 | // Add the note 297 | let volume = libm::powf(inst.fx.delay_amount, round as f32); 298 | self.add_note(i, seq_count, note_count, volume, round % 2 == 1); 299 | } 300 | } 301 | } 302 | 303 | /// Get the index of the first empty note in the given `notes` slice. 304 | fn get_note_slot(notes: &[Note]) -> usize { 305 | // Find the first empty note 306 | if let Some((i, _)) = notes.iter().enumerate().find(|(_, x)| x.pitch == 0) { 307 | i 308 | } else { 309 | let iter = notes.iter().enumerate(); 310 | iter.min_by_key(|(_, x)| x.sample_count).unwrap().0 311 | } 312 | } 313 | 314 | /// Add a note to track `i`. 315 | fn add_note( 316 | &mut self, 317 | i: usize, 318 | seq_count: usize, 319 | note_count: usize, 320 | volume: f32, 321 | swap_stereo: bool, 322 | ) { 323 | let inst = &self.song.instruments[i]; 324 | 325 | // Get the pattern index 326 | let p = inst.seq[seq_count]; 327 | if p == 0 { 328 | return; 329 | } 330 | 331 | // Get the pattern 332 | let pattern = &inst.pat[p - 1]; 333 | 334 | // Get the note pitch 335 | let pitch = pattern.notes[note_count]; 336 | if pitch == 0 { 337 | return; 338 | } 339 | 340 | // Create a new note 341 | let j = Self::get_note_slot(&self.tracks[i].notes); 342 | self.tracks[i].notes[j] = Note::new(pitch, self.sample_count, volume, swap_stereo); 343 | 344 | // Set oscillator frequencies 345 | let pitch = w(self.tracks[i].notes[j].pitch); 346 | for o in 0..2 { 347 | let pitch = (pitch + w(inst.osc[o].octave) + w(inst.osc[o].detune_freq)).0; 348 | self.tracks[i].notes[j].osc_freq[o] = 349 | get_note_frequency(pitch) * inst.osc[o].detune / self.sample_ratio; 350 | } 351 | } 352 | 353 | /// Envelope 354 | fn env(position: u32, inst_env: &Envelope) -> Option<(f32, f32)> { 355 | let attack = inst_env.attack; 356 | let sustain = inst_env.sustain; 357 | let release = inst_env.release; 358 | 359 | let mut env = 1.0; 360 | 361 | if position < attack { 362 | env = position as f32 / attack as f32; 363 | } else if position >= attack + sustain + release { 364 | return None; 365 | } else if position >= attack + sustain { 366 | let pos = (position - attack - sustain) as f32; 367 | env -= pos / release as f32; 368 | } 369 | 370 | Some((env, env * env)) 371 | } 372 | 373 | /// Oscillator 0 374 | fn osc0(&mut self, inst: &Instrument, i: usize, j: usize, lfo: f32, env_sq: f32) -> f32 { 375 | let r = get_osc_output(&inst.osc[0].waveform, self.tracks[i].notes[j].osc_time[0]); 376 | let mut t = self.tracks[i].notes[j].osc_freq[0]; 377 | 378 | if inst.lfo.osc0_freq { 379 | t += lfo; 380 | } 381 | if inst.osc[0].envelope { 382 | t *= env_sq; 383 | } 384 | self.tracks[i].notes[j].osc_time[0] += t; 385 | 386 | r * inst.osc[0].volume 387 | } 388 | 389 | /// Oscillator 1 390 | fn osc1(&mut self, inst: &Instrument, i: usize, j: usize, env_sq: f32) -> f32 { 391 | let r = get_osc_output(&inst.osc[1].waveform, self.tracks[i].notes[j].osc_time[1]); 392 | let mut t = self.tracks[i].notes[j].osc_freq[1]; 393 | 394 | if inst.osc[1].envelope { 395 | t *= env_sq; 396 | } 397 | self.tracks[i].notes[j].osc_time[1] += t; 398 | 399 | r * inst.osc[1].volume 400 | } 401 | 402 | /// Filters 403 | fn filters(&mut self, inst: &Instrument, i: usize, j: usize, lfo: f32, sample: f32) -> f32 { 404 | let mut f = inst.fx.freq * self.sample_ratio; 405 | 406 | if inst.lfo.fx_freq { 407 | f *= lfo; 408 | } 409 | f = libm::sinf(f * PI / self.sample_rate) * 1.5; 410 | 411 | let low = libm::fmaf(f, self.tracks[i].notes[j].band, self.tracks[i].notes[j].low); 412 | let high = inst.fx.resonance * (sample - self.tracks[i].notes[j].band) - low; 413 | let band = libm::fmaf(f, high, self.tracks[i].notes[j].band); 414 | 415 | self.tracks[i].notes[j].low = low; 416 | self.tracks[i].notes[j].band = band; 417 | 418 | let sample = match inst.fx.filter { 419 | Filter::None => sample, 420 | Filter::HighPass => high, 421 | Filter::LowPass => low, 422 | Filter::BandPass => band, 423 | Filter::Notch => low + high, 424 | }; 425 | 426 | sample * inst.env.master 427 | } 428 | 429 | /// Generate samples for 2 channels using the given instrument. 430 | fn generate_samples( 431 | &mut self, 432 | inst: &Instrument, 433 | i: usize, 434 | j: usize, 435 | position: f32, 436 | ) -> Option<[f32; NUM_CHANNELS]> { 437 | // Envelope 438 | let note_sample_count = self.tracks[i].notes[j].sample_count; 439 | let (env, env_sq) = Self::env(self.sample_count - note_sample_count, &self.tracks[i].env)?; 440 | 441 | // LFO 442 | let lfo_freq = self.tracks[i].lfo_freq; 443 | let lfo = libm::fmaf( 444 | get_osc_output(&inst.lfo.waveform, lfo_freq * position), 445 | inst.lfo.amount * self.sample_ratio, 446 | 0.5, 447 | ); 448 | 449 | // Oscillator 0 450 | let mut sample = self.osc0(inst, i, j, lfo, env_sq); 451 | 452 | // Oscillator 1 453 | sample += self.osc1(inst, i, j, env_sq); 454 | 455 | // Noise oscillator 456 | sample += osc_sin(self.random.next_f32_unit()) * inst.noise_fader * env; 457 | 458 | // Envelope 459 | sample *= env * self.tracks[i].notes[j].volume; 460 | 461 | // Filters 462 | sample += self.filters(inst, i, j, lfo, sample); 463 | 464 | let pan_freq = self.tracks[i].pan_freq; 465 | let pan_t = libm::fmaf( 466 | osc_sin(pan_freq * position), 467 | inst.fx.pan_amount * self.sample_ratio, 468 | 0.5, 469 | ); 470 | 471 | if self.tracks[i].notes[j].swap_stereo { 472 | Some([sample * (1.0 - pan_t), sample * pan_t]) 473 | } else { 474 | Some([sample * pan_t, sample * (1.0 - pan_t)]) 475 | } 476 | } 477 | 478 | /// Update the sample generator. This is the main workhorse of the 479 | /// synthesizer. 480 | fn update(&mut self) -> [f32; NUM_CHANNELS] { 481 | let amplitude = f32::from(i16::MAX); 482 | let position = self.sample_count as f32; 483 | 484 | // Output samples 485 | let mut samples = [0.0; NUM_CHANNELS]; 486 | 487 | for (i, inst) in self.song.instruments.iter().enumerate() { 488 | for j in 0..self.tracks[i].notes.len() { 489 | if self.tracks[i].notes[j].pitch == 0 { 490 | continue; 491 | } 492 | 493 | if let Some(note_samples) = self.generate_samples(inst, i, j, position) { 494 | // Mix the samples 495 | for (i, sample) in samples.iter_mut().enumerate() { 496 | *sample += note_samples[i]; 497 | } 498 | } else { 499 | // Remove notes that have ended 500 | self.tracks[i].notes[j] = Note::new(0, 0, 0.0, false); 501 | } 502 | } 503 | } 504 | 505 | // Clip samples to [-1.0, 1.0] 506 | for sample in &mut samples { 507 | *sample = (*sample / amplitude).clamp(-1.0, 1.0); 508 | } 509 | 510 | samples 511 | } 512 | } 513 | 514 | impl<'a> Iterator for Synth<'a> { 515 | type Item = [f32; NUM_CHANNELS]; 516 | 517 | fn next(&mut self) -> Option { 518 | // Check for end of song 519 | if self.seq_count > self.song.seq_length 520 | && !self 521 | .tracks 522 | .iter() 523 | .flat_map(|x| x.notes.iter()) 524 | .any(|x| x.pitch != 0) 525 | { 526 | return None; 527 | } 528 | 529 | // Generate the next sample 530 | let samples = self.update(); 531 | 532 | // Advance to next sample 533 | self.sample_count += 1; 534 | let sample_in_quarter_note = self.sample_count % self.quarter_note_length; 535 | if sample_in_quarter_note == 0 { 536 | // Advance to next note 537 | self.note_count += 1; 538 | if self.note_count >= PATTERN_LENGTH { 539 | self.note_count = 0; 540 | 541 | // Advance to next pattern 542 | self.seq_count += 1; 543 | } 544 | 545 | // Fetch the next set of notes 546 | self.load_delayed_notes(); 547 | self.load_notes(); 548 | } else if sample_in_quarter_note == self.eighth_note_length { 549 | // Fetch the next set of notes 550 | self.load_delayed_notes(); 551 | } 552 | 553 | Some(samples) 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "alsa" 16 | version = "0.9.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" 19 | dependencies = [ 20 | "alsa-sys", 21 | "bitflags 2.5.0", 22 | "libc", 23 | ] 24 | 25 | [[package]] 26 | name = "alsa-sys" 27 | version = "0.3.1" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 30 | dependencies = [ 31 | "libc", 32 | "pkg-config", 33 | ] 34 | 35 | [[package]] 36 | name = "arrayvec" 37 | version = "0.7.4" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 40 | 41 | [[package]] 42 | name = "autocfg" 43 | version = "1.2.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 46 | 47 | [[package]] 48 | name = "bindgen" 49 | version = "0.69.4" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" 52 | dependencies = [ 53 | "bitflags 2.5.0", 54 | "cexpr", 55 | "clang-sys", 56 | "itertools", 57 | "lazy_static", 58 | "lazycell", 59 | "proc-macro2", 60 | "quote", 61 | "regex", 62 | "rustc-hash", 63 | "shlex", 64 | "syn", 65 | ] 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "1.3.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 72 | 73 | [[package]] 74 | name = "bitflags" 75 | version = "2.5.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 78 | 79 | [[package]] 80 | name = "bumpalo" 81 | version = "3.16.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 84 | 85 | [[package]] 86 | name = "bytemuck" 87 | version = "1.15.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" 90 | 91 | [[package]] 92 | name = "byteorder" 93 | version = "0.5.3" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" 96 | 97 | [[package]] 98 | name = "byteorder" 99 | version = "1.5.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 102 | 103 | [[package]] 104 | name = "bytes" 105 | version = "1.6.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 108 | 109 | [[package]] 110 | name = "cc" 111 | version = "1.0.95" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" 114 | dependencies = [ 115 | "jobserver", 116 | "libc", 117 | "once_cell", 118 | ] 119 | 120 | [[package]] 121 | name = "cesu8" 122 | version = "1.1.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 125 | 126 | [[package]] 127 | name = "cexpr" 128 | version = "0.6.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 131 | dependencies = [ 132 | "nom", 133 | ] 134 | 135 | [[package]] 136 | name = "cfg-if" 137 | version = "1.0.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 140 | 141 | [[package]] 142 | name = "clang-sys" 143 | version = "1.7.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" 146 | dependencies = [ 147 | "glob", 148 | "libc", 149 | "libloading", 150 | ] 151 | 152 | [[package]] 153 | name = "colored" 154 | version = "2.1.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 157 | dependencies = [ 158 | "lazy_static", 159 | "windows-sys 0.48.0", 160 | ] 161 | 162 | [[package]] 163 | name = "combine" 164 | version = "4.6.7" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 167 | dependencies = [ 168 | "bytes", 169 | "memchr", 170 | ] 171 | 172 | [[package]] 173 | name = "core-foundation-sys" 174 | version = "0.8.6" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 177 | 178 | [[package]] 179 | name = "coreaudio-rs" 180 | version = "0.11.3" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" 183 | dependencies = [ 184 | "bitflags 1.3.2", 185 | "core-foundation-sys", 186 | "coreaudio-sys", 187 | ] 188 | 189 | [[package]] 190 | name = "coreaudio-sys" 191 | version = "0.2.15" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" 194 | dependencies = [ 195 | "bindgen", 196 | ] 197 | 198 | [[package]] 199 | name = "cpal" 200 | version = "0.15.3" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" 203 | dependencies = [ 204 | "alsa", 205 | "core-foundation-sys", 206 | "coreaudio-rs", 207 | "dasp_sample", 208 | "jni", 209 | "js-sys", 210 | "libc", 211 | "mach2", 212 | "ndk", 213 | "ndk-context", 214 | "oboe", 215 | "wasm-bindgen", 216 | "wasm-bindgen-futures", 217 | "web-sys", 218 | "windows", 219 | ] 220 | 221 | [[package]] 222 | name = "dasp_sample" 223 | version = "0.11.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 226 | 227 | [[package]] 228 | name = "either" 229 | version = "1.11.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" 232 | 233 | [[package]] 234 | name = "equivalent" 235 | version = "1.0.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 238 | 239 | [[package]] 240 | name = "error-iter" 241 | version = "0.4.1" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "8070547d90d1b98debb6626421d742c897942bbb78f047694a5eb769495eccd6" 244 | 245 | [[package]] 246 | name = "getrandom" 247 | version = "0.2.14" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" 250 | dependencies = [ 251 | "cfg-if", 252 | "libc", 253 | "wasi", 254 | ] 255 | 256 | [[package]] 257 | name = "glob" 258 | version = "0.3.1" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 261 | 262 | [[package]] 263 | name = "hashbrown" 264 | version = "0.14.3" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 267 | 268 | [[package]] 269 | name = "indexmap" 270 | version = "2.2.6" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 273 | dependencies = [ 274 | "equivalent", 275 | "hashbrown", 276 | ] 277 | 278 | [[package]] 279 | name = "itertools" 280 | version = "0.12.1" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 283 | dependencies = [ 284 | "either", 285 | ] 286 | 287 | [[package]] 288 | name = "jni" 289 | version = "0.21.1" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 292 | dependencies = [ 293 | "cesu8", 294 | "cfg-if", 295 | "combine", 296 | "jni-sys", 297 | "log", 298 | "thiserror", 299 | "walkdir", 300 | "windows-sys 0.45.0", 301 | ] 302 | 303 | [[package]] 304 | name = "jni-sys" 305 | version = "0.3.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 308 | 309 | [[package]] 310 | name = "jobserver" 311 | version = "0.1.31" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" 314 | dependencies = [ 315 | "libc", 316 | ] 317 | 318 | [[package]] 319 | name = "js-sys" 320 | version = "0.3.69" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 323 | dependencies = [ 324 | "wasm-bindgen", 325 | ] 326 | 327 | [[package]] 328 | name = "lazy_static" 329 | version = "1.4.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 332 | 333 | [[package]] 334 | name = "lazycell" 335 | version = "1.3.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 338 | 339 | [[package]] 340 | name = "libc" 341 | version = "0.2.153" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 344 | 345 | [[package]] 346 | name = "libloading" 347 | version = "0.8.3" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" 350 | dependencies = [ 351 | "cfg-if", 352 | "windows-targets 0.52.5", 353 | ] 354 | 355 | [[package]] 356 | name = "libm" 357 | version = "0.2.8" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 360 | 361 | [[package]] 362 | name = "log" 363 | version = "0.4.21" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 366 | 367 | [[package]] 368 | name = "mach2" 369 | version = "0.4.2" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 372 | dependencies = [ 373 | "libc", 374 | ] 375 | 376 | [[package]] 377 | name = "memchr" 378 | version = "2.7.2" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 381 | 382 | [[package]] 383 | name = "minimal-lexical" 384 | version = "0.2.1" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 387 | 388 | [[package]] 389 | name = "ndk" 390 | version = "0.8.0" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" 393 | dependencies = [ 394 | "bitflags 2.5.0", 395 | "jni-sys", 396 | "log", 397 | "ndk-sys", 398 | "num_enum", 399 | "thiserror", 400 | ] 401 | 402 | [[package]] 403 | name = "ndk-context" 404 | version = "0.1.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 407 | 408 | [[package]] 409 | name = "ndk-sys" 410 | version = "0.5.0+25.2.9519653" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" 413 | dependencies = [ 414 | "jni-sys", 415 | ] 416 | 417 | [[package]] 418 | name = "nom" 419 | version = "7.1.3" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 422 | dependencies = [ 423 | "memchr", 424 | "minimal-lexical", 425 | ] 426 | 427 | [[package]] 428 | name = "num-derive" 429 | version = "0.4.2" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 432 | dependencies = [ 433 | "proc-macro2", 434 | "quote", 435 | "syn", 436 | ] 437 | 438 | [[package]] 439 | name = "num-traits" 440 | version = "0.2.18" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 443 | dependencies = [ 444 | "autocfg", 445 | ] 446 | 447 | [[package]] 448 | name = "num_enum" 449 | version = "0.7.2" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" 452 | dependencies = [ 453 | "num_enum_derive", 454 | ] 455 | 456 | [[package]] 457 | name = "num_enum_derive" 458 | version = "0.7.2" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" 461 | dependencies = [ 462 | "proc-macro-crate", 463 | "proc-macro2", 464 | "quote", 465 | "syn", 466 | ] 467 | 468 | [[package]] 469 | name = "oboe" 470 | version = "0.6.1" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" 473 | dependencies = [ 474 | "jni", 475 | "ndk", 476 | "ndk-context", 477 | "num-derive", 478 | "num-traits", 479 | "oboe-sys", 480 | ] 481 | 482 | [[package]] 483 | name = "oboe-sys" 484 | version = "0.6.1" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" 487 | dependencies = [ 488 | "cc", 489 | ] 490 | 491 | [[package]] 492 | name = "once_cell" 493 | version = "1.19.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 496 | 497 | [[package]] 498 | name = "pkg-config" 499 | version = "0.3.30" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 502 | 503 | [[package]] 504 | name = "proc-macro-crate" 505 | version = "3.1.0" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" 508 | dependencies = [ 509 | "toml_edit", 510 | ] 511 | 512 | [[package]] 513 | name = "proc-macro2" 514 | version = "1.0.81" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 517 | dependencies = [ 518 | "unicode-ident", 519 | ] 520 | 521 | [[package]] 522 | name = "quote" 523 | version = "1.0.36" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 526 | dependencies = [ 527 | "proc-macro2", 528 | ] 529 | 530 | [[package]] 531 | name = "randomize" 532 | version = "5.0.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "850866582058fb6528103ec08b9fc9739bbb8ac4b62181ea03292bc3a64348b3" 535 | dependencies = [ 536 | "bytemuck", 537 | ] 538 | 539 | [[package]] 540 | name = "regex" 541 | version = "1.10.4" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 544 | dependencies = [ 545 | "aho-corasick", 546 | "memchr", 547 | "regex-automata", 548 | "regex-syntax", 549 | ] 550 | 551 | [[package]] 552 | name = "regex-automata" 553 | version = "0.4.6" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 556 | dependencies = [ 557 | "aho-corasick", 558 | "memchr", 559 | "regex-syntax", 560 | ] 561 | 562 | [[package]] 563 | name = "regex-syntax" 564 | version = "0.8.3" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 567 | 568 | [[package]] 569 | name = "riff-wave" 570 | version = "0.1.3" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "2a749a2a6b5d4e3659a095accc05fdb5b2439fc409c4a00d049a2e72e8352df1" 573 | dependencies = [ 574 | "byteorder 0.5.3", 575 | ] 576 | 577 | [[package]] 578 | name = "rustc-hash" 579 | version = "1.1.0" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 582 | 583 | [[package]] 584 | name = "same-file" 585 | version = "1.0.6" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 588 | dependencies = [ 589 | "winapi-util", 590 | ] 591 | 592 | [[package]] 593 | name = "shlex" 594 | version = "1.3.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 597 | 598 | [[package]] 599 | name = "sonant" 600 | version = "0.2.0" 601 | dependencies = [ 602 | "arrayvec", 603 | "byteorder 1.5.0", 604 | "colored", 605 | "cpal", 606 | "error-iter", 607 | "getrandom", 608 | "libm", 609 | "randomize", 610 | "riff-wave", 611 | "thiserror", 612 | ] 613 | 614 | [[package]] 615 | name = "syn" 616 | version = "2.0.60" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 619 | dependencies = [ 620 | "proc-macro2", 621 | "quote", 622 | "unicode-ident", 623 | ] 624 | 625 | [[package]] 626 | name = "thiserror" 627 | version = "1.0.59" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" 630 | dependencies = [ 631 | "thiserror-impl", 632 | ] 633 | 634 | [[package]] 635 | name = "thiserror-impl" 636 | version = "1.0.59" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" 639 | dependencies = [ 640 | "proc-macro2", 641 | "quote", 642 | "syn", 643 | ] 644 | 645 | [[package]] 646 | name = "toml_datetime" 647 | version = "0.6.5" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 650 | 651 | [[package]] 652 | name = "toml_edit" 653 | version = "0.21.1" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" 656 | dependencies = [ 657 | "indexmap", 658 | "toml_datetime", 659 | "winnow", 660 | ] 661 | 662 | [[package]] 663 | name = "unicode-ident" 664 | version = "1.0.12" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 667 | 668 | [[package]] 669 | name = "walkdir" 670 | version = "2.5.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 673 | dependencies = [ 674 | "same-file", 675 | "winapi-util", 676 | ] 677 | 678 | [[package]] 679 | name = "wasi" 680 | version = "0.11.0+wasi-snapshot-preview1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 683 | 684 | [[package]] 685 | name = "wasm-bindgen" 686 | version = "0.2.92" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 689 | dependencies = [ 690 | "cfg-if", 691 | "wasm-bindgen-macro", 692 | ] 693 | 694 | [[package]] 695 | name = "wasm-bindgen-backend" 696 | version = "0.2.92" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 699 | dependencies = [ 700 | "bumpalo", 701 | "log", 702 | "once_cell", 703 | "proc-macro2", 704 | "quote", 705 | "syn", 706 | "wasm-bindgen-shared", 707 | ] 708 | 709 | [[package]] 710 | name = "wasm-bindgen-futures" 711 | version = "0.4.42" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" 714 | dependencies = [ 715 | "cfg-if", 716 | "js-sys", 717 | "wasm-bindgen", 718 | "web-sys", 719 | ] 720 | 721 | [[package]] 722 | name = "wasm-bindgen-macro" 723 | version = "0.2.92" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 726 | dependencies = [ 727 | "quote", 728 | "wasm-bindgen-macro-support", 729 | ] 730 | 731 | [[package]] 732 | name = "wasm-bindgen-macro-support" 733 | version = "0.2.92" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 736 | dependencies = [ 737 | "proc-macro2", 738 | "quote", 739 | "syn", 740 | "wasm-bindgen-backend", 741 | "wasm-bindgen-shared", 742 | ] 743 | 744 | [[package]] 745 | name = "wasm-bindgen-shared" 746 | version = "0.2.92" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 749 | 750 | [[package]] 751 | name = "web-sys" 752 | version = "0.3.69" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 755 | dependencies = [ 756 | "js-sys", 757 | "wasm-bindgen", 758 | ] 759 | 760 | [[package]] 761 | name = "winapi" 762 | version = "0.3.9" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 765 | dependencies = [ 766 | "winapi-i686-pc-windows-gnu", 767 | "winapi-x86_64-pc-windows-gnu", 768 | ] 769 | 770 | [[package]] 771 | name = "winapi-i686-pc-windows-gnu" 772 | version = "0.4.0" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 775 | 776 | [[package]] 777 | name = "winapi-util" 778 | version = "0.1.6" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 781 | dependencies = [ 782 | "winapi", 783 | ] 784 | 785 | [[package]] 786 | name = "winapi-x86_64-pc-windows-gnu" 787 | version = "0.4.0" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 790 | 791 | [[package]] 792 | name = "windows" 793 | version = "0.54.0" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" 796 | dependencies = [ 797 | "windows-core", 798 | "windows-targets 0.52.5", 799 | ] 800 | 801 | [[package]] 802 | name = "windows-core" 803 | version = "0.54.0" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" 806 | dependencies = [ 807 | "windows-result", 808 | "windows-targets 0.52.5", 809 | ] 810 | 811 | [[package]] 812 | name = "windows-result" 813 | version = "0.1.1" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" 816 | dependencies = [ 817 | "windows-targets 0.52.5", 818 | ] 819 | 820 | [[package]] 821 | name = "windows-sys" 822 | version = "0.45.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 825 | dependencies = [ 826 | "windows-targets 0.42.2", 827 | ] 828 | 829 | [[package]] 830 | name = "windows-sys" 831 | version = "0.48.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 834 | dependencies = [ 835 | "windows-targets 0.48.5", 836 | ] 837 | 838 | [[package]] 839 | name = "windows-targets" 840 | version = "0.42.2" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 843 | dependencies = [ 844 | "windows_aarch64_gnullvm 0.42.2", 845 | "windows_aarch64_msvc 0.42.2", 846 | "windows_i686_gnu 0.42.2", 847 | "windows_i686_msvc 0.42.2", 848 | "windows_x86_64_gnu 0.42.2", 849 | "windows_x86_64_gnullvm 0.42.2", 850 | "windows_x86_64_msvc 0.42.2", 851 | ] 852 | 853 | [[package]] 854 | name = "windows-targets" 855 | version = "0.48.5" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 858 | dependencies = [ 859 | "windows_aarch64_gnullvm 0.48.5", 860 | "windows_aarch64_msvc 0.48.5", 861 | "windows_i686_gnu 0.48.5", 862 | "windows_i686_msvc 0.48.5", 863 | "windows_x86_64_gnu 0.48.5", 864 | "windows_x86_64_gnullvm 0.48.5", 865 | "windows_x86_64_msvc 0.48.5", 866 | ] 867 | 868 | [[package]] 869 | name = "windows-targets" 870 | version = "0.52.5" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 873 | dependencies = [ 874 | "windows_aarch64_gnullvm 0.52.5", 875 | "windows_aarch64_msvc 0.52.5", 876 | "windows_i686_gnu 0.52.5", 877 | "windows_i686_gnullvm", 878 | "windows_i686_msvc 0.52.5", 879 | "windows_x86_64_gnu 0.52.5", 880 | "windows_x86_64_gnullvm 0.52.5", 881 | "windows_x86_64_msvc 0.52.5", 882 | ] 883 | 884 | [[package]] 885 | name = "windows_aarch64_gnullvm" 886 | version = "0.42.2" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 889 | 890 | [[package]] 891 | name = "windows_aarch64_gnullvm" 892 | version = "0.48.5" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 895 | 896 | [[package]] 897 | name = "windows_aarch64_gnullvm" 898 | version = "0.52.5" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 901 | 902 | [[package]] 903 | name = "windows_aarch64_msvc" 904 | version = "0.42.2" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 907 | 908 | [[package]] 909 | name = "windows_aarch64_msvc" 910 | version = "0.48.5" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 913 | 914 | [[package]] 915 | name = "windows_aarch64_msvc" 916 | version = "0.52.5" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 919 | 920 | [[package]] 921 | name = "windows_i686_gnu" 922 | version = "0.42.2" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 925 | 926 | [[package]] 927 | name = "windows_i686_gnu" 928 | version = "0.48.5" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 931 | 932 | [[package]] 933 | name = "windows_i686_gnu" 934 | version = "0.52.5" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 937 | 938 | [[package]] 939 | name = "windows_i686_gnullvm" 940 | version = "0.52.5" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 943 | 944 | [[package]] 945 | name = "windows_i686_msvc" 946 | version = "0.42.2" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 949 | 950 | [[package]] 951 | name = "windows_i686_msvc" 952 | version = "0.48.5" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 955 | 956 | [[package]] 957 | name = "windows_i686_msvc" 958 | version = "0.52.5" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 961 | 962 | [[package]] 963 | name = "windows_x86_64_gnu" 964 | version = "0.42.2" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 967 | 968 | [[package]] 969 | name = "windows_x86_64_gnu" 970 | version = "0.48.5" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 973 | 974 | [[package]] 975 | name = "windows_x86_64_gnu" 976 | version = "0.52.5" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 979 | 980 | [[package]] 981 | name = "windows_x86_64_gnullvm" 982 | version = "0.42.2" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 985 | 986 | [[package]] 987 | name = "windows_x86_64_gnullvm" 988 | version = "0.48.5" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 991 | 992 | [[package]] 993 | name = "windows_x86_64_gnullvm" 994 | version = "0.52.5" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 997 | 998 | [[package]] 999 | name = "windows_x86_64_msvc" 1000 | version = "0.42.2" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1003 | 1004 | [[package]] 1005 | name = "windows_x86_64_msvc" 1006 | version = "0.48.5" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1009 | 1010 | [[package]] 1011 | name = "windows_x86_64_msvc" 1012 | version = "0.52.5" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1015 | 1016 | [[package]] 1017 | name = "winnow" 1018 | version = "0.5.40" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 1021 | dependencies = [ 1022 | "memchr", 1023 | ] 1024 | --------------------------------------------------------------------------------