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