Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use fundsp network to play up to 20 samples at once #35

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Cargo.lock
**/*.rs.bk

.vscode
.DS_Store
2 changes: 2 additions & 0 deletions crates/composer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ edition = "2021"
bincode = "1"
clap = { version = "4.2", features = ["derive"] }
color-eyre = "0.6"
cpal = "0.15"
composer_api = { path = "../composer_api" }
eyre = "0.6"
fundsp = "0.13"
rodio = { version = "0.17", features = ["symphonia-wav"] }
29 changes: 8 additions & 21 deletions crates/composer/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
#![warn(clippy::all, clippy::clone_on_ref_ptr)]

use crate::jukebox::{Jukebox, Sample};
use crate::sound::SoundController;
use clap::Parser;
use composer_api::{Event, DEFAULT_SERVER_ADDRESS};
use eyre::{Context, Result};
use rodio::{OutputStream, OutputStreamHandle};
use eyre::Result;
use std::{
net::UdpSocket,
time::{Duration, Instant},
};

mod jukebox;
mod sound;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
Expand All @@ -27,37 +26,25 @@ fn main() -> Result<()> {
let socket = UdpSocket::bind(args.address.as_deref().unwrap_or(DEFAULT_SERVER_ADDRESS))?;
println!("Listening on {}", socket.local_addr()?);

let (_stream, stream_handle) = OutputStream::try_default()?;
let mut sound_controller = SoundController::new()?;

let jukebox = Jukebox::new().context("creating jukebox")?;
let mut stats = Stats { since: Instant::now(), events: 0, total_bytes: 0 };
loop {
match handle_datagram(&socket, &stream_handle, &jukebox) {
match handle_datagram(&socket, &mut sound_controller) {
Ok(bytes_received) => stats.record_event(bytes_received),
Err(err) => eprintln!("Could not process datagram. Ignoring and continuing. {:?}", err),
}
}
}

/// Block until next datagram is received and handle it. Returns its size in bytes.
fn handle_datagram(
socket: &UdpSocket,
output_stream: &OutputStreamHandle,
jukebox: &Jukebox,
) -> Result<usize> {
fn handle_datagram(socket: &UdpSocket, sound_controller: &mut SoundController) -> Result<usize> {
// Size up to max normal network packet size
let mut buf = [0; 1500];
let (number_of_bytes, _) = socket.recv_from(&mut buf)?;

let event: Event = bincode::deserialize(&buf[..number_of_bytes])?;
let _event: Event = bincode::deserialize(&buf[..number_of_bytes])?;

let sample = match event {
Event::TestTick => Sample::Click,

// TODO(Matej): add different sounds for these, and vary some their quality based on length.
Event::StderrWrite { length: _ } | Event::StdoutWrite { length: _ } => Sample::Click,
};
jukebox.play(output_stream, sample)?;
sound_controller.play_click();

Ok(number_of_bytes)
}
Expand Down
107 changes: 107 additions & 0 deletions crates/composer/src/sound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
BufferSize, SampleRate, StreamConfig,
};
use eyre::Result;
use fundsp::hacker::*;
use std::sync::Arc;

const NUM_SLOTS: usize = 20;

pub struct SoundController {
_stream: cpal::Stream,
frontend_net: Net64,
slots: Vec<NodeId>,
slot_index: usize,
click: Arc<Wave64>,
}

impl SoundController {
pub fn new() -> Result<Self> {
let click = Wave64::load("src/sound_samples/click.wav")?;

let host = cpal::default_host();

let device = host.default_output_device().expect("Failed to find a default output device");

// TODO(bschwind) - Hardcode this for now, but let's extract these param
// from device.default_output_config later.
let stream_config = StreamConfig {
channels: 1,
sample_rate: SampleRate(48_000),
buffer_size: BufferSize::Default,
};

let err_fn = |err| eprintln!("an error occurred on stream: {}", err);

let (frontend_net, mut backend, slots) = Self::build_dsp_graph();

let stream = device.build_output_stream(
&stream_config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
for sample in data {
*sample = backend.get_mono() as f32;
}
},
err_fn,
None,
)?;

stream.play()?;

Ok(Self { _stream: stream, frontend_net, slots, slot_index: 0, click: Arc::new(click) })
}

fn build_dsp_graph() -> (Net64, Net64Backend, Vec<NodeId>) {
let mut net = Net64::new(0, 1);

// Create a node with 20 inputs that are mixed into one output
let mixer = pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass()
+ pass();
Comment on lines +58 to +78
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I guess this cannot use a for loop because this uses some kind of "incremental" generic type? Might be a candidate for a (declarative) macro if we choose to go this way and polish the code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this definitely would benefit from a macro. Not sure it's possible to do with declarative macros alone, though (as in; not sure it's possible to expand a number into a variable number of lines, though I may be forgetting how! the closest I've done is matrix expansion).

Pulling proc macros into this seems too heavyweight, but then I start to wonder if a crate that lets you write code as crazy as this is a good idea 🤔.


// add the mixer to the network and connect its output to the network's output
let mixer_id = net.push(Box::new(mixer));
net.connect_output(mixer_id, 0, 0);

// Add 20 silent nodes to the network and connect each to one of the inputs of the mixer.
let slots = (0..NUM_SLOTS)
.map(|i| {
let node_id = net.push(Box::new(zero()));
net.connect(node_id, 0, mixer_id, i);
node_id
})
.collect::<Vec<_>>();

let backend = net.backend();

(net, backend, slots)
}

pub fn play_click(&mut self) {
let player = Wave64Player::new(&self.click, 0, 0, self.click.length(), None);
let node_id = self.slots.get(self.slot_index).expect("programmer made a mistake");

self.frontend_net.replace(*node_id, Box::new(An(player)));
self.frontend_net.commit();

self.slot_index = if self.slot_index == NUM_SLOTS - 1 { 0 } else { self.slot_index + 1 };
}
}