| @ -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 | |||