samiperttu / fundsp Goto Github PK
View Code? Open in Web Editor NEWLibrary for audio processing and synthesis
License: Apache License 2.0
Library for audio processing and synthesis
License: Apache License 2.0
I was playing around with the display()
function on peaking bell filters and got these results:
20 dB ------------------------------------------------ 20 dB
...***..
10 dB -----------------..************..--------------- 10 dB
...********************..
0 dB .........********************************....... 0 dB
************************************************
-10 dB ************************************************ -10 dB
************************************************
-20 dB ************************************************ -20 dB
************************************************
-30 dB ************************************************ -30 dB
************************************************
-40 dB ************************************************ -40 dB
| | | | | | | | | |
10 50 100 200 500 1k 2k 5k 10k 20k Hz
20 dB ------------------------------------------------ 20 dB
..**.
10 dB ----------------------------------..********.--- 10 dB
..**************
0 dB ..........................*********************. 0 dB
************************************************
-10 dB ************************************************ -10 dB
************************************************
-20 dB ************************************************ -20 dB
************************************************
-30 dB ************************************************ -30 dB
************************************************
-40 dB ************************************************ -40 dB
| | | | | | | | | |
10 50 100 200 500 1k 2k 5k 10k 20k Hz
The first curve is symmetrical as expected, the high frequency one is asymmetrical as expected because of cramping at Nyquist. Also oversampling works as expected to get a symmetrical curve at 10 kHz:
20 dB ------------------------------------------------ 20 dB
...***..
10 dB ---------------------------------..************. 10 dB
..*****************
0 dB .........................*********************** 0 dB
************************************************
-10 dB ************************************************ -10 dB
************************************************
-20 dB ************************************************ -20 dB
************************************************
-30 dB ************************************************ -30 dB
************************************************
-40 dB ************************************************ -40 dB
| | | | | | | | | |
10 50 100 200 500 1k 2k 5k 10k 20k Hz
However, I did not expect a filter at 60 Hz to be asymmetrical:
20 dB ------------------------------------------------ 20 dB
.***...
10 dB -.**********..---------------------------------- 10 dB
.***************..
0 dB **********************.......................... 0 dB
************************************************
-10 dB ************************************************ -10 dB
************************************************
-20 dB ************************************************ -20 dB
************************************************
-30 dB ************************************************ -30 dB
************************************************
-40 dB ************************************************ -40 dB
| | | | | | | | | |
10 50 100 200 500 1k 2k 5k 10k 20k Hz
My knowledge about signal processing is still pretty rudimentary. I haven't found a theoretical explanation on why that happens, so I tried to recreate these settings with some EQ plugins. Some showed cramping at high frequencies, but none shared the behavior at lower center frequencies. Is this a bug or is it correct and I'm missing something? If this is indeed the intended behavior, could you point out how I can obtain a symmetrical curve at low center frequencies?
Hi congrats on the crate release!
Would it be possible to have an example made of how to use Tagged constants? I have read the readme.md but can't seem to figure out what its used for or how its used. Would this allow the changing of, say, a frequency for a pulse() at runtime?
Thanks!
Looks like I've figured it out:
const FREQUENCY_TAG: i64 = 0;
const DUTY_TAG: i64 = 1;
const VOLUME_TAG: i64 = 2;
let func =
(tag(FREQUENCY_TAG, 0.0) | tag(DUTY_TAG, 0.0)) >> (tag(VOLUME_TAG, 0.0)) * pulse();
// And setting the values later with...
func.set(FREQUENCY_TAG, new_frequency);
func.set(VOLUME_TAG, 0.0);
Great project!
It would be cool if you could use this library to do time stretching (while preserving pitch), perhaps using Rubberband, or one of the algorithms that browsers use to implement the playbackRate property.
This is a really fantastic crate and I have enjoyed using it thus far.
It was pure luck that I found it. I searched crates.io
on input
looking for a console-keyboard input crate (ultimately settling on the excellent read_input
crate) and fundsp
was the fifth on the list of results.
I had been looking in vain for a satisfactory synthesizer crate. This one has met my needs very well thus far!
I suggest adding the following keywords to help more people find it:
#synthesizer
#synth
#music
#sound
#wave
Hi,
I couldn't make the sequence exemple work, that was right after running cargo build
without any error
$ cargo run --example sequence
Compiling fundsp v0.11.0 (/Users/Pitcairn/Documents/code/rust/fundsp)
error[E0618]: expected function, found `funutd::Rnd`
--> examples/sequence.rs:31:40
|
11 | let mut rnd = Rnd::new();
| ------- `rnd` has type `funutd::Rnd`
...
31 | let f = xerp(50.0, 2000.0, rnd(i ^ seed));
| ^^^----------
| |
| call expression requires function
For more information about this error, try `rustc --explain E0618`.
error: could not compile `fundsp` due to previous error
I tried $ rustc --explain E0618
but that didn't help me much.
$ rustc --version
rustc 1.66.0 (69f9c33d7 2022-12-12)
Thanks
Hi there!
First of all: great work on this library! It's a real joy to use and it definitely pushes DSP development in Rust forward.
I'm sorry to open an issue for this, but I couldn't find an answer in the examples. How could I patch one signal to different inputs of a node (and, if possible, of different nodes)?
let modulator = (sine_hz(1.0) + 1.0) * 0.5;
let synth = (saw_hz(100.0) | modulator * 3000.0 | modulator * 0.2) >> moog();
Of course Rust comes in by not allowing to use a moved value (modulator
) on the second occurrence. I wonder: how would I express this using a more fundsp
syntax?
System:
OS: Windows 10 10.0.19041
CPU: (12) x64 AMD Ryzen 5 2600 Six-Core Processor
Memory: 8.90 GB / 15.93 GB
compile is successful, but the audio is completely distorted
I appended a full example. The network plays a sine wave at 440 Hz. After replacing signal
with the same sine wave, the pitch is higher. I don't think the need to set the sample rate after a replace
is intended behavior.
On a side note, I understand that declick
fades in the signal. When I replace the signal node, a click happens even when the pitch is the same. Is it possible to do a fade in on every replace
? Thanks a lot in advance.
//! Make real-time changes to a network while it is playing.
#![allow(clippy::precedence)]
use assert_no_alloc::*;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{FromSample, SizedSample};
use fundsp::hacker::*;
use std::time::Duration;
#[cfg(debug_assertions)] // required when disable_release is set (default)
#[global_allocator]
static A: AllocDisabler = AllocDisabler;
fn main() {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("Failed to find a default output device");
let config = device.default_output_config().unwrap();
match config.sample_format() {
cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()).unwrap(),
cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()).unwrap(),
cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()).unwrap(),
_ => panic!("Unsupported format"),
}
}
fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error>
where
T: SizedSample + FromSample<f64>,
{
let sample_rate = config.sample_rate.0 as f64;
let channels = config.channels as usize;
// prints 48000
println!("{sample_rate}");
let a = Box::new(sine_hz(440.0));
let mut net = Net64::new(0, 2);
let signal = net.chain(a.clone());
net.chain(Box::new(declick() >> pan(0.0)));
net.set_sample_rate(sample_rate);
let mut backend = net.backend();
let mut next_value = move || assert_no_alloc(|| backend.get_stereo());
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
write_data(data, channels, &mut next_value)
},
err_fn,
None,
)?;
stream.play()?;
std::thread::sleep(Duration::from_secs(3));
println!("replace signal with the same function, but now it outputs a higher pitch");
net.replace(signal, a.clone());
// uncomment to set sample rate again and the pitch stays at 440 Hz
// net.set_sample_rate(sample_rate);
net.commit();
std::thread::sleep(Duration::from_secs(3));
Ok(())
}
fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> (f64, f64))
where
T: SizedSample + FromSample<f64>,
{
for frame in output.chunks_mut(channels) {
let sample = next_sample();
let left = T::from_sample(sample.0);
let right: T = T::from_sample(sample.1);
for (channel, sample) in frame.iter_mut().enumerate() {
if channel & 1 == 0 {
*sample = left;
} else {
*sample = right;
}
}
}
}
Hello,
I am very inexperienced with DSP for sound synthesis, so if this issue resulted from me overlooking something obvious, please accept my apologies.
I'm experimenting with a system architecture for a polyphonic synthesizer. I am creating a multi-channel synth sound as a Net64
. I am embedding Var
objects within it. Whenever the player presses a key on the keyboard, it alters the pitch of one of the Var
objects to correspond to the key's pitch.
In general, this works pretty well. Problems arise when I use more than five or six Var
objects to store dynamic pitches. At that point, the sound output collapses into a pile of noise. I experimented with a high-pass filter but it was not effective in eliminating the noise.
To demonstrate the problem, I created the midi_var
repository. The main.rs
program simplifies the problem by removing the MIDI device. It activates one note every second; with 7 or more Var
objects, even the first note activation causes the noise problems. Fewer than 7, and it generally sounds fine, with maybe a hint of noise when you get to 5 or 6.
Here is the code for embedding the Var
objects, in this case pitch
and velocity
(corresponding to the MIDI messages coming from the keyboard). Placing them inside an envelope()
was the only way I could figure out to make them responsive to changes in the Var
, even though it just ignores the time
parameter of the envelope()
:
envelope(move |_| midi_hz(pitch.value()))
>> triangle() * (envelope(move |_| velocity.value() / 127.0))
Here is the code from the aforementioned main.rs
, in case you can point out an error somewhere at a glance. Thank you in advance for any insight you may be able to offer:
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{Device, Sample, SampleFormat, StreamConfig};
use fundsp::hacker::*;
use fundsp::prelude::AudioUnit64;
use bare_metal_modulo::*;
use std::collections::BTreeMap;
use std::{thread, time};
const NUM_TO_USE: usize = 7;
const NOTES: [(u8, u8); 10] = [
(60, 127),
(62, 100),
(64, 127),
(65, 50),
(67, 80),
(69, 100),
(71, 60),
(72, 127),
(74, 100),
(76, 127),
];
fn main() -> anyhow::Result<()> {
let mut vars: Vars<NUM_TO_USE> = Vars::new();
run_output(vars.clone());
let rest = time::Duration::from_secs(1);
thread::sleep(rest);
for i in 0..NOTES.len() {
println!("{i}");
vars.on(NOTES[i].0, NOTES[i].1);
thread::sleep(rest);
}
Ok(())
}
fn run_output<const N: usize>(vars: Vars<N>) {
thread::spawn(move || {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("failed to find a default output device");
let config = device.default_output_config().unwrap();
match config.sample_format() {
SampleFormat::F32 => run_synth::<N, f32>(vars, device, config.into()),
SampleFormat::I16 => run_synth::<N, i16>(vars, device, config.into()),
SampleFormat::U16 => run_synth::<N, u16>(vars, device, config.into()),
};
});
}
#[derive(Clone)]
struct Vars<const N: usize> {
pitches: [An<Var<f64>>; N],
velocities: [An<Var<f64>>; N],
next: ModNumC<usize, N>,
pitch2var: BTreeMap<u8, usize>,
recent_pitches: [Option<u8>; N],
}
impl<const N: usize> Vars<N> {
pub fn new() -> Self {
Self {
pitches: [(); N].map(|_| var(0, 0.0)),
velocities: [(); N].map(|_| var(1, 0.0)),
next: ModNumC::new(0),
pitch2var: BTreeMap::new(),
recent_pitches: [None; N],
}
}
pub fn sound_at(&self, i: usize) -> Box<dyn AudioUnit64> {
let pitch = self.pitches[i].clone();
let velocity = self.velocities[i].clone();
Box::new(
envelope(move |_| midi_hz(pitch.value()))
>> triangle() * (envelope(move |_| velocity.value() / 127.0)),
)
}
pub fn sound(&self) -> Net64 {
let mut sound = Net64::wrap(self.sound_at(0));
for i in 1..N {
sound = Net64::bin_op(sound, Net64::wrap(self.sound_at(i)), FrameAdd::new());
}
sound
}
pub fn on(&mut self, pitch: u8, velocity: u8) {
self.pitches[self.next.a()].clone().set_value(pitch as f64);
self.velocities[self.next.a()]
.clone()
.set_value(velocity as f64);
self.pitch2var.insert(pitch, self.next.a());
self.recent_pitches[self.next.a()] = Some(pitch);
self.next += 1;
}
}
fn run_synth<const N: usize, T: Sample>(vars: Vars<N>, device: Device, config: StreamConfig) {
let sample_rate = config.sample_rate.0 as f64;
let mut sound = vars.sound();
sound.reset(Some(sample_rate));
let mut next_value = move || sound.get_stereo();
let channels = config.channels as usize;
let err_fn = |err| eprintln!("an error occurred on stream: {err}");
let stream = device
.build_output_stream(
&config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
write_data(data, channels, &mut next_value)
},
err_fn,
)
.unwrap();
stream.play().unwrap();
loop {}
}
fn write_data<T: Sample>(
output: &mut [T],
channels: usize,
next_sample: &mut dyn FnMut() -> (f64, f64),
) {
for frame in output.chunks_mut(channels) {
let sample = next_sample();
let left: T = Sample::from::<f32>(&(sample.0 as f32));
let right: T = Sample::from::<f32>(&(sample.1 as f32));
for (channel, sample) in frame.iter_mut().enumerate() {
*sample = if channel & 1 == 0 { left } else { right };
}
}
}
Hi, I recently discovered this library, I haven't really used a synthesizer before so it was exciting to write some code and have sounds generated from my speakers :) I'm also impressed by the "DSL" you created while avoiding macros, nice work!
I'm working on a small project at the moment where among other audio generators, I want to be able to load up and play samples from WAV files at arbitrary times, having them "overlap" and mix together, up to some max amount of concurrent plays.
I think this is similar to the issue of playing sound effects in a video game where a particular action generates the sound, and if you repeat that action quickly you hear the sound multiple times, perhaps getting louder or higher pitched as the many similar samples overlap.
Here's an example of what I'm going for.
At first I thought I could use the WavePlayer
audio node and create them on the fly as new sound effect "requests" come in, but I wasn't sure about how to clean up the node when the sound sample is done so I figured that would eventually blow up memory usage. As far as I can tell, there isn't a way to determine when a WavePlayer
is done producing samples if you don't loop it.
My next thought was to create and destroy Net
s as these requests come and go, but that also doesn't seem like the right path.
Finally, I'm thinking of creating my own struct and implementing AudioNode
for it. The struct could hold however many Wave
s I want in an arbitrary sized vector, and the tick
function would iterate through each one and grab the next sample. I could then also detect when the sounds are done because I'd have access to the state that controls where in the sound we are.
To request a new sound to be played, I would (ab)use the Setting
associated type and the listen()
function to send it new sound requests over time. My main hesitation here is that AudioNode
doesn't feel like something I should be implementing myself, especially with the const ID: u64
I have to specify.
Am I on the right path here, or is this straying outside the intended usage of fundsp
?
Thanks!
Could functions be added to load a Wave32
/Wave64
from a slice of bytes? In my application I'd be receiving ogg data from an external source (no filesystem).
From what I can tell Symphonia is able to create a MediaSource
from a Cursor
, so hopefully most of the implementation can be shared with the existing load
/load_track
functions.
Would it be possible to expose another hacker
module but for 32-bit mode? Potentially a hacker32
module behind a feature flag.
I tried doing this myself, but I'm not very successful as some items make assumptions about the sample size/type and wrongly(?) sets some type bounds, making it hard to progress without good knowledge of how everything fits together.
How would one implement an ADSR envelope with fundsp. I was going through this tutorial which implemented a simple instrument which responded to note-on and -off events. It uses a simple envelope mostly to make sure the note ends. It isn't able to sustain.
I'm thinking it could be implemented with two envelopes multiplied together:
envelope = envelope_on * envelope_off.
The 'on' envelope takes care of going through the attack, decay, and settling on the sustain value and the 'off' envelope goes from 1.0 to 0.0 over the release period.
A note on event would look like:
A note off event would look like:
Example code:
let offset_on = || tag(Tag::NoteOn as i64, 0.0);
let env_on = || offset_on() >> envelope2(|t, offset| {
let attack = 0.2;
let decay = 0.2;
let sustain = 0.5;
let position = t - offset;
if position < attack {
position / attack
} else if position < decay + attack{
let decay_position = (position - attack) / decay;
(1.0 - decay_position) * (1.0 - sustain) + sustain
} else {
sustain
}
});
let offset_off = || tag(Tag::NoteOff as i64, 0.0);
let env_off = || offset_off() >> envelope2(|t, offset| {
// Somewhat hacky: using 0.0 as a sentinel value indicating that the 'off'
// envelope should be disabled when a note is playing.
if offset <= 0.0 {
return 1.0;
}
let release = 0.2;
let position = t - offset;
if position < release {
1.0 - position / release
} else {
0.0
}
});
let env = env_on() * env_off();
Some questions:
On note-off, release starts from the sustain value rather than current. This is especially noticeable with a long attack, as shown below:
Is there a reason for having the max buffer size set at 64?
My use case is using block processing to interface with a live input/output with >64 samples (128 to 2048) per "frame".
A buffer of 64 items is not enough for this.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.