diff --git a/music_cli/Cargo.lock b/music_cli/Cargo.lock new file mode 100644 index 0000000..e37a488 --- /dev/null +++ b/music_cli/Cargo.lock @@ -0,0 +1,4 @@ +[root] +name = "music_cli" +version = "0.1.0" + diff --git a/music_cli/Cargo.toml b/music_cli/Cargo.toml new file mode 100644 index 0000000..84bd733 --- /dev/null +++ b/music_cli/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "music_cli" +version = "0.1.0" +authors = ["Erin "] + +[dependencies] +log = "0.3" +env_logger = "0.4" +portaudio = "0.7" + +soundchip = {path = "../soundchip"} diff --git a/music_cli/src/main.rs b/music_cli/src/main.rs new file mode 100644 index 0000000..476bbd8 --- /dev/null +++ b/music_cli/src/main.rs @@ -0,0 +1,46 @@ +#[macro_use] extern crate log; +extern crate env_logger; +extern crate portaudio; +extern crate soundchip; + +use std::error::Error; +use std::sync::mpsc; +use portaudio as pa; + +// TODO: replace with config struct, loaded via Serde +const CHANNELS: u32 = 2; +const SAMPLE_RATE: f64 = 44_100.0; // 44.1kHz +const FRAMES_PER_BUFFER: u32 = 64; + +fn run() -> Result<(), Box> { + env_logger::init().unwrap(); + + info!("Portaudio launching!"); + let pa = pa::PortAudio::new()?; + info!("PortAudio:"); + info!("version: {}, {}", pa.version(), pa.version_text().unwrap()); + + let (ctx, crx) = mpsc::channel(); + let mut chip = soundchip::Chip::new(crx); + + let pa_settings = pa.default_output_stream_settings(CHANNELS as i32, SAMPLE_RATE, FRAMES_PER_BUFFER)?; + let pa_callback = move |pa::OutputStreamCallbackArgs {mut buffer, frames, ..}| { + chip.render_frames(&mut buffer, frames, SAMPLE_RATE, CHANNELS); + pa::Continue + }; + + let mut pa_stream = pa.open_non_blocking_stream(pa_settings, pa_callback)?; + pa_stream.start()?; + + loop { + pa.sleep(1_000); + } + + Ok(()) +} + +fn main() { + if let Err(err) = run() { + error!("{:?}", err); + } +} diff --git a/music_cli/target/debug/.cargo-lock b/music_cli/target/debug/.cargo-lock new file mode 100644 index 0000000..e69de29 diff --git a/soundchip/Cargo.lock b/soundchip/Cargo.lock new file mode 100644 index 0000000..7a24c9a --- /dev/null +++ b/soundchip/Cargo.lock @@ -0,0 +1,4 @@ +[root] +name = "soundchip" +version = "0.1.0" + diff --git a/soundchip/Cargo.toml b/soundchip/Cargo.toml new file mode 100644 index 0000000..74a028a --- /dev/null +++ b/soundchip/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "soundchip" +version = "0.1.0" +authors = ["Erin "] + +[dependencies] diff --git a/soundchip/src/audio.rs b/soundchip/src/audio.rs new file mode 100644 index 0000000..01e105c --- /dev/null +++ b/soundchip/src/audio.rs @@ -0,0 +1,94 @@ +use std::{u8, i8, f32}; +use std::ops::{Add, Mul}; +use std::iter::{Iterator, Sum}; + +pub type Sample = f32; +#[derive(Debug)] +pub struct Frame { + pub l: Sample, + pub r: Sample, +} + +fn mono_pan_amplitudes(angle: f32) -> (f32, f32) { + // L_amp = √2/2 (cosθ - sinθ) + // R_amp = √2/2 (cosθ + sinθ) + + let l = (2.0_f32).sqrt()/2.0 * (angle.cos() - angle.sin()); + let r = (2.0_f32).sqrt()/2.0 * (angle.cos() + angle.sin()); + (l, r) +} + +pub fn from_bitpan(angle: i8) -> f32 { + const MAX_ANGLE: f32 = 0.7853982; // +/- 45 deg + + (angle as f32) / (i8::max_value() as f32) * MAX_ANGLE +} + +pub fn from_bitamp(amp: u8) -> f32 { + (amp as f32) / (u8::max_value() as f32) +} + +impl Frame { + pub fn mono_to_stereo(x: Sample, panning: f32) -> Frame { + let (l_amp, r_amp) = mono_pan_amplitudes(panning); + + Frame {l: l_amp * x, r: r_amp * x} + } + + pub fn zero() -> Frame { + Frame {l: 0., r: 0.,} + } +} + +impl Add for Frame { + type Output = Self; + fn add(self, rhs: Frame) -> Frame { + Frame { + l: self.l + rhs.l, + r: self.r + rhs.r, + } + } +} + +impl Mul for Frame { + type Output = Self; + fn mul(self, rhs: f32) -> Frame { + Frame { + l: self.l * rhs, + r: self.r * rhs, + } + } +} + +impl Sum for Frame { + fn sum>(iter: I) -> Frame { + iter.fold(Frame::zero(), Add::add) + } +} + +pub trait Renderable { + fn render_sample(&mut self, sample_rate: f64) -> Frame; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn pan_amp() { + let pan_right = mono_pan_amplitudes(0.2); + assert!(pan_right.0 < pan_right.1); + let pan_left = mono_pan_amplitudes(-0.2); + assert!(pan_left.0 > pan_left.1); + + let pan_center = mono_pan_amplitudes(0.); + assert!(pan_center.0 == pan_center.1); + } + + #[test] + fn bitpan() { + assert_eq!(0., from_bitpan(0)); + assert_eq!((45.0_f32).to_radians(), from_bitpan(127)); + assert_eq!((-45.0_f32).to_radians(), from_bitpan(-127)); + } +} diff --git a/soundchip/src/channels.rs b/soundchip/src/channels.rs new file mode 100644 index 0000000..6640efd --- /dev/null +++ b/soundchip/src/channels.rs @@ -0,0 +1,50 @@ +use ::waveform::Waveform; +use ::audio::{Frame, Renderable, from_bitamp, from_bitpan}; + +#[derive(Default)] +pub struct Channel where W: Waveform { + waveform: W, + phase: f64, + + pub frequency: f64, + + pan: f32, + amp: f32, +} + +fn next_sample_phase(phase: f64, frequency: f64, sample_rate: f64) -> f64 { + phase + (frequency / sample_rate) +} + +impl Channel where W: Waveform + Default { + pub fn new(wavf: W, f0: f64, a0: f32, p0: f32) -> Channel { + Channel { + waveform: wavf, + phase: 0., + frequency: f0, + pan: p0, + amp: a0, + } + } + + pub fn set_pan(&mut self, angle: i8) { + self.pan = from_bitpan(angle); + } + + pub fn set_amp(&mut self, amp: u8) { + self.amp = from_bitamp(amp); + } + + pub fn note_on(&mut self, note: u8) {} + pub fn note_off(&mut self, note: u8) {} +} + +impl Renderable for Channel where W: Waveform { + fn render_sample(&mut self, sample_rate: f64) -> Frame { + let x = Frame::mono_to_stereo(self.waveform.x(self.phase), self.pan); + + self.phase = next_sample_phase(self.phase, self.frequency, sample_rate); + + x * self.amp + } +} diff --git a/soundchip/src/lib.rs b/soundchip/src/lib.rs new file mode 100644 index 0000000..6442ac0 --- /dev/null +++ b/soundchip/src/lib.rs @@ -0,0 +1,62 @@ +mod channels; +mod waveform; +mod audio; + +use audio::{Frame, Renderable}; +use channels::Channel; +use waveform::{Waveform, Saw, Square}; +use std::sync::mpsc::Receiver; + +pub struct Chip { + pub vol: f32, + rx: Receiver, + + renderables: Vec>, +} + +pub enum Message { + NoteOn(usize, u8), + NoteOff(usize, u8), +} + + +impl Chip { + pub fn new(mpi_rx: Receiver) -> Chip { + Chip { + vol: 0.25, + rx: mpi_rx, + renderables: Vec::with_capacity(8), + } + } + + fn process_messages(&mut self) { + match self.rx.try_recv() { + Ok(m) => match m { + Message::NoteOn(channel, note) => { + } + Message::NoteOff(channel, note) => { + } + }, + Err(_) => {} // nop if there's nothing pending in the channel + } + } + + pub fn render_sample(&mut self, sample_rate: f64) -> Frame { + self.renderables.iter_mut() + .map(|m| m.render_sample(sample_rate)) + .sum() + } + + pub fn render_frames(&mut self, buffer: &mut [f32], n_frames: usize, + sample_rate: f64, _channels: u32) { + let mut idx = 0; + for _ in 0..n_frames { + self.process_messages(); + + let sample = self.render_sample(sample_rate); + buffer[idx] = sample.l; + buffer[idx+1] = sample.r; + idx += 2; + } + } +} diff --git a/soundchip/src/waveform.rs b/soundchip/src/waveform.rs new file mode 100644 index 0000000..9e633a2 --- /dev/null +++ b/soundchip/src/waveform.rs @@ -0,0 +1,24 @@ +pub trait Waveform { + fn x(&self, t: f64) -> f32; +} + +#[derive(Default, Debug)] +pub struct Saw; +#[derive(Default, Debug)] +pub struct Square; + +impl Waveform for Saw { + fn x(&self, t: f64) -> f32 { + ((t % 1.0) * 2.0 - 1.0) as f32 + } +} + +impl Waveform for Square { + fn x(&self, t: f64) -> f32 { + if t % 1.0 < 0.5 { + -1.0 + } else { + 1.0 + } + } +} diff --git a/soundchip/target/debug/.cargo-lock b/soundchip/target/debug/.cargo-lock new file mode 100644 index 0000000..e69de29 diff --git a/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/dep-lib-soundchip-678087a299b215b4 b/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/dep-lib-soundchip-678087a299b215b4 new file mode 100644 index 0000000..1e1df69 Binary files /dev/null and b/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/dep-lib-soundchip-678087a299b215b4 differ diff --git a/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/lib-soundchip-678087a299b215b4 b/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/lib-soundchip-678087a299b215b4 new file mode 100644 index 0000000..2357090 --- /dev/null +++ b/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/lib-soundchip-678087a299b215b4 @@ -0,0 +1 @@ +6a6ccedd67c95a31 \ No newline at end of file diff --git a/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/lib-soundchip-678087a299b215b4.json b/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/lib-soundchip-678087a299b215b4.json new file mode 100644 index 0000000..5fbfeec --- /dev/null +++ b/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/lib-soundchip-678087a299b215b4.json @@ -0,0 +1 @@ +{"rustc":2995933453150367971,"features":"[]","target":7514913036628055629,"profile":4671609428991786967,"deps":[],"local":{"MtimeBased":[[1498694003,977093121],"/home/liam/git/star/soundchip/target/debug/.fingerprint/soundchip-678087a299b215b4/dep-lib-soundchip-678087a299b215b4"]},"rustflags":[]} \ No newline at end of file diff --git a/soundchip/target/debug/libsoundchip.d b/soundchip/target/debug/libsoundchip.d new file mode 100644 index 0000000..1ee68de --- /dev/null +++ b/soundchip/target/debug/libsoundchip.d @@ -0,0 +1 @@ +/home/liam/git/star/soundchip/target/debug/libsoundchip.rlib: /home/liam/git/star/soundchip/src/lib.rs