Last active
September 1, 2024 23:00
-
-
Save iitalics/101870795709b1460c07e77b52cc5958 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
[package] | |
name = "basic-midi-synth-alsa" | |
version = "0.1.0" | |
edition = "2021" | |
[dependencies] | |
anyhow = {version = "1.0"} | |
tracing = {version = "0.1"} | |
tracing-subscriber = {version = "0.3", features = ["env-filter"]} | |
alsa = {version = "0.9.1"} | |
*/ | |
#[macro_use] | |
extern crate tracing; | |
use anyhow::{Context as _, Result}; | |
fn main() -> Result<()> { | |
tracing_subscriber::fmt() | |
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) | |
.with_timer(tracing_subscriber::fmt::time::uptime()) | |
.init(); | |
// init MIDI sequencer | |
let seq = alsa::Seq::open(None, Some(alsa::Direction::Capture), true) | |
.context("open sequencer failed")?; | |
seq.set_client_name(c"meowv MIDI")?; | |
fn is_valid_midi_port(pnfo: &alsa::seq::PortInfo) -> bool { | |
pnfo.get_type().contains(alsa::seq::PortType::MIDI_GENERIC) | |
&& pnfo.get_capability().contains(alsa::seq::PortCap::READ) | |
&& pnfo.get_midi_channels() > 0 | |
} | |
fn find_midi_port(seq: &alsa::Seq) -> Option<alsa::seq::Addr> { | |
for cnfo in alsa::seq::ClientIter::new(&seq) { | |
let client = cnfo.get_client(); | |
'enum_ports: for port in 0.. { | |
let addr = alsa::seq::Addr { client, port }; | |
let Ok(pnfo) = seq.get_any_port_info(addr) else { | |
break 'enum_ports; | |
}; | |
debug!( | |
id = format!("{}:{}", client, port), | |
client = cnfo.get_name().unwrap_or("(?)"), | |
port = pnfo.get_name().unwrap_or("(?)"), | |
"port" | |
); | |
if is_valid_midi_port(&pnfo) { | |
return Some(addr); | |
} | |
} | |
} | |
None | |
} | |
let midi_addr = find_midi_port(&seq).context("did not find suitable midi port")?; | |
{ | |
let mut port = alsa::seq::PortInfo::empty()?; | |
port.set_name(c"meowv MIDI port rx"); | |
port.set_capability(alsa::seq::PortCap::WRITE); | |
port.set_type(alsa::seq::PortType::MIDI_GENERIC | alsa::seq::PortType::SYNTHESIZER); | |
seq.create_port(&port)?; | |
let port_addr = alsa::seq::Addr { | |
client: seq.client_id()?, | |
port: port.get_port(), | |
}; | |
let sub = alsa::seq::PortSubscribe::empty()?; | |
sub.set_sender(midi_addr); | |
sub.set_dest(port_addr); | |
seq.subscribe_port(&sub)?; | |
} | |
info!( | |
"MIDI initialized: {:?} ({}:{})", | |
seq.get_any_port_info(midi_addr)?.get_name()?, | |
midi_addr.client, | |
midi_addr.port, | |
); | |
let mut input = seq.input(); | |
// init PCM playback | |
const SAMPLE_RATE: u32 = 44100; | |
const PERIOD_SIZE: i64 = 256; | |
let pcm = | |
alsa::PCM::new("default", alsa::Direction::Playback, true).context("open pcm failed")?; | |
fn setup_pcm(pcm: &alsa::PCM) -> alsa::Result<(u32, usize)> { | |
let hw = alsa::pcm::HwParams::any(&pcm)?; | |
hw.set_format(alsa::pcm::Format::FloatLE)?; | |
hw.set_channels(1)?; | |
hw.set_access(alsa::pcm::Access::RWInterleaved)?; | |
hw.set_rate(SAMPLE_RATE, alsa::ValueOr::Nearest)?; | |
hw.set_period_size(PERIOD_SIZE, alsa::ValueOr::Nearest)?; | |
hw.set_buffer_size_near(0)?; | |
pcm.hw_params(&hw)?; | |
drop(hw); | |
let sample_rate = pcm.hw_params_current()?.get_rate()?; | |
let (_buf_size, per_size) = pcm.get_params()?; | |
Ok((sample_rate, per_size as usize)) | |
} | |
let (sr, frames) = setup_pcm(&pcm).context("setup pcm failed")?; | |
let mut buf = Vec::with_capacity(frames); | |
let io = pcm.io_f32()?; | |
info!("PCM initialized: {sr}, {frames}"); | |
// synthesizer | |
// parameters | |
let vol = 0.15; | |
let lpf = (200.0, 4.0); // cutoff, env amplitude (octaves) | |
let a_env = (0.02, f32::INFINITY, 0.5); // A,D,R | |
let f_env = (0.5, 5.0, 1.0); // A,D,R | |
// constants depending on sample rate | |
let a4_dt = 440.0 / sr as f32; | |
let dr = 6.907755 / sr as f32; | |
let g0 = 3.14159 / sr as f32; | |
let note = std::cell::Cell::new(0u8); | |
let trigger = std::cell::Cell::new(false); | |
let mut t = 0.0f32; // osc phase | |
let mut s = 0.0f32; // filter state | |
let mut ae = (0.0, false); // amp envelope | |
let mut fe = (0.0, false); // filter enveloper | |
fn envelope(dr: f32, env: (f32, f32, f32), s: &mut (f32, bool), on: bool) -> f32 { | |
let (amp, decay) = s; | |
if on { | |
if *decay { | |
*amp -= *amp * dr / env.1; | |
} else { | |
*amp += (1.001 - *amp) * dr / env.0; | |
if *amp >= 1.0 { | |
*amp = 1.0; | |
*decay = !env.1.is_infinite(); | |
} | |
} | |
} else { | |
*amp -= *amp * dr / env.2; | |
*decay = false; | |
} | |
*amp | |
} | |
let mut next_sample = || { | |
let pitch = note.get() as f32 - 69.0; | |
let dt = a4_dt * f32::exp2(pitch / 12.0); | |
// saw + BELP approx | |
let one_minus_t = 1.0 - t; | |
let d0 = t.min(dt) / dt; | |
let d1 = one_minus_t.min(dt) / dt; | |
// let (d0, d1) = (1.0, 1.0); /* disable BLEP (sounds bad!) */ | |
let x = t - one_minus_t + (d0 + d1 - 2.0) * (d0 - d1); | |
t += dt; | |
t -= t.trunc(); | |
let on = trigger.get(); | |
let ae = envelope(dr, a_env, &mut ae, on); | |
let fe = envelope(dr, f_env, &mut fe, on); | |
let amp = vol * ae; | |
let cut = lpf.0 * f32::exp2(fe * lpf.1); | |
// 1-pole lowpass filter | |
let g = cut * g0; | |
let v = (x - s) * g / (g + 1.0); | |
let y = v + s; | |
s = y + v; | |
y * amp | |
}; | |
// event loop | |
let seq_cap = (&seq, Some(alsa::Direction::Capture)); | |
let polls: [&dyn alsa::poll::Descriptors; 2] = [&seq_cap, &pcm]; | |
loop { | |
let t = std::time::Instant::now(); | |
let p = alsa::poll::poll_all(&polls, -1)?; | |
let dt = t.elapsed(); | |
let n = p.len(); | |
trace!(?dt, n, "poll"); | |
// read MIDI events | |
let mut evt_pending = input.event_input_pending(true)?; | |
while evt_pending > 0 { | |
let evt = input.event_input()?; | |
evt_pending -= 1; | |
trace!(evt = ?evt.get_type()); | |
if let Some(evt) = evt.get_data::<alsa::seq::EvNote>() { | |
if evt.velocity > 0 { | |
note.set(evt.note); | |
trigger.set(true); | |
} else if note.get() == evt.note { | |
trigger.set(false); | |
} | |
} | |
} | |
// write PCM samples | |
let avail = pcm.avail_update()? as usize; | |
trace!(avail); | |
if avail >= frames { | |
buf.resize_with(frames, &mut next_sample); | |
let n = io.writei(&buf)?; | |
assert_eq!(n, frames); | |
buf.clear(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment