Code Monkey home page Code Monkey logo

fundsp's Issues

Asymmetric Bell Filter at Lower Frequencies

I was playing around with the display() function on peaking bell filters and got these results:

bell_hz(1000.0, 0.3, db_amp(18.0))
 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
bell_hz(10000.0, 0.3, db_amp(18.0))
 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:

oversample(bell_hz(10000.0, 0.3, db_amp(18.0)))
 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:

bell_hz(60.0, 0.3, db_amp(18.0))
 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?

Tags and Tagged example request

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);

Tags on crates.io

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

cargo run --example sequence ... could not compile `fundsp` due to previous error

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

Patching a signal to multiple inputs

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?

Replacing a node in a network resets its sample rate

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;
            }
        }
    }
}

Unwanted noise when mixing

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 };
        }
    }
}

Architecture for mixing multiple Waves

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 Nets 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 Waves 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!

Load wave from bytes

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.

Provide `hacker` in 32-bit "mode"

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.

ADSR Envelope

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:

  • signal the 'on' envelope to begin (start at 0.0).
  • signal the 'off' envelope to go directly to 1.0.

A note off event would look like:

  • signal the 'off' envelope to begin (start at 1.0).

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:

  • Is there a simpler way to implement this?
  • Is signaling with the time a good way to signal note-on/off events?
  • Would an envelope like this be worth including in fundsp itself?

Unintuitive adsr behaviour

On note-off, release starts from the sustain value rather than current. This is especially noticeable with a long attack, as shown below:

out.mp4

option to increase buffer size

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.