| @ -0,0 +1,4 @@ | |||||
| [root] | |||||
| name = "music_cli" | |||||
| version = "0.1.0" | |||||
| @ -0,0 +1,11 @@ | |||||
| [package] | |||||
| name = "music_cli" | |||||
| version = "0.1.0" | |||||
| authors = ["Erin <erin@hashbang.sh>"] | |||||
| [dependencies] | |||||
| log = "0.3" | |||||
| env_logger = "0.4" | |||||
| portaudio = "0.7" | |||||
| soundchip = {path = "../soundchip"} | |||||
| @ -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<Error>> { | |||||
| 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); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,4 @@ | |||||
| [root] | |||||
| name = "soundchip" | |||||
| version = "0.1.0" | |||||
| @ -0,0 +1,6 @@ | |||||
| [package] | |||||
| name = "soundchip" | |||||
| version = "0.1.0" | |||||
| authors = ["Erin <erin@hashbang.sh>"] | |||||
| [dependencies] | |||||
| @ -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<f32> 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<I: Iterator<Item=Frame>>(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)); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,50 @@ | |||||
| use ::waveform::Waveform; | |||||
| use ::audio::{Frame, Renderable, from_bitamp, from_bitpan}; | |||||
| #[derive(Default)] | |||||
| pub struct Channel<W> 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<W> Channel<W> where W: Waveform + Default { | |||||
| pub fn new(wavf: W, f0: f64, a0: f32, p0: f32) -> Channel<W> { | |||||
| 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<W> Renderable for Channel<W> 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 | |||||
| } | |||||
| } | |||||
| @ -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<Message>, | |||||
| renderables: Vec<Box<Renderable>>, | |||||
| } | |||||
| pub enum Message { | |||||
| NoteOn(usize, u8), | |||||
| NoteOff(usize, u8), | |||||
| } | |||||
| impl Chip { | |||||
| pub fn new(mpi_rx: Receiver<Message>) -> 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1 @@ | |||||
| 6a6ccedd67c95a31 | |||||
| @ -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":[]} | |||||
| @ -0,0 +1 @@ | |||||
| /home/liam/git/star/soundchip/target/debug/libsoundchip.rlib: /home/liam/git/star/soundchip/src/lib.rs | |||||