Skip to content

Instantly share code, notes, and snippets.

@iitalics
Last active September 1, 2024 23:00
Show Gist options
  • Save iitalics/101870795709b1460c07e77b52cc5958 to your computer and use it in GitHub Desktop.
Save iitalics/101870795709b1460c07e77b52cc5958 to your computer and use it in GitHub Desktop.
/*
[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