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