mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
160 lines
4.2 KiB
Rust
160 lines
4.2 KiB
Rust
// author: Rob Saunders <hello@robsaunders.io>
|
|
|
|
#[macro_use]
|
|
extern crate vst;
|
|
|
|
use vst::prelude::*;
|
|
|
|
use std::f64::consts::PI;
|
|
|
|
/// Convert the midi note's pitch into the equivalent frequency.
|
|
///
|
|
/// This function assumes A4 is 440hz.
|
|
fn midi_pitch_to_freq(pitch: u8) -> f64 {
|
|
const A4_PITCH: i8 = 69;
|
|
const A4_FREQ: f64 = 440.0;
|
|
|
|
// Midi notes can be 0-127
|
|
((f64::from(pitch as i8 - A4_PITCH)) / 12.).exp2() * A4_FREQ
|
|
}
|
|
|
|
struct SineSynth {
|
|
sample_rate: f64,
|
|
time: f64,
|
|
note_duration: f64,
|
|
note: Option<u8>,
|
|
}
|
|
|
|
impl SineSynth {
|
|
fn time_per_sample(&self) -> f64 {
|
|
1.0 / self.sample_rate
|
|
}
|
|
|
|
/// Process an incoming midi event.
|
|
///
|
|
/// The midi data is split up like so:
|
|
///
|
|
/// `data[0]`: Contains the status and the channel. Source: [source]
|
|
/// `data[1]`: Contains the supplemental data for the message - so, if this was a NoteOn then
|
|
/// this would contain the note.
|
|
/// `data[2]`: Further supplemental data. Would be velocity in the case of a NoteOn message.
|
|
///
|
|
/// [source]: http://www.midimountain.com/midi/midi_status.htm
|
|
fn process_midi_event(&mut self, data: [u8; 3]) {
|
|
match data[0] {
|
|
128 => self.note_off(data[1]),
|
|
144 => self.note_on(data[1]),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
fn note_on(&mut self, note: u8) {
|
|
self.note_duration = 0.0;
|
|
self.note = Some(note)
|
|
}
|
|
|
|
fn note_off(&mut self, note: u8) {
|
|
if self.note == Some(note) {
|
|
self.note = None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const TAU: f64 = PI * 2.0;
|
|
|
|
impl Plugin for SineSynth {
|
|
fn new(_host: HostCallback) -> Self {
|
|
SineSynth {
|
|
sample_rate: 44100.0,
|
|
note_duration: 0.0,
|
|
time: 0.0,
|
|
note: None,
|
|
}
|
|
}
|
|
|
|
fn get_info(&self) -> Info {
|
|
Info {
|
|
name: "SineSynth".to_string(),
|
|
vendor: "DeathDisco".to_string(),
|
|
unique_id: 6667,
|
|
category: Category::Synth,
|
|
inputs: 2,
|
|
outputs: 2,
|
|
parameters: 0,
|
|
initial_delay: 0,
|
|
..Info::default()
|
|
}
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
#[allow(clippy::single_match)]
|
|
fn process_events(&mut self, events: &Events) {
|
|
for event in events.events() {
|
|
match event {
|
|
Event::Midi(ev) => self.process_midi_event(ev.data),
|
|
// More events can be handled here.
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_sample_rate(&mut self, rate: f32) {
|
|
self.sample_rate = f64::from(rate);
|
|
}
|
|
|
|
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
|
let samples = buffer.samples();
|
|
let (_, mut outputs) = buffer.split();
|
|
let output_count = outputs.len();
|
|
let per_sample = self.time_per_sample();
|
|
let mut output_sample;
|
|
for sample_idx in 0..samples {
|
|
let time = self.time;
|
|
let note_duration = self.note_duration;
|
|
if let Some(current_note) = self.note {
|
|
let signal = (time * midi_pitch_to_freq(current_note) * TAU).sin();
|
|
|
|
// Apply a quick envelope to the attack of the signal to avoid popping.
|
|
let attack = 0.5;
|
|
let alpha = if note_duration < attack {
|
|
note_duration / attack
|
|
} else {
|
|
1.0
|
|
};
|
|
|
|
output_sample = (signal * alpha) as f32;
|
|
|
|
self.time += per_sample;
|
|
self.note_duration += per_sample;
|
|
} else {
|
|
output_sample = 0.0;
|
|
}
|
|
for buf_idx in 0..output_count {
|
|
let buff = outputs.get_mut(buf_idx);
|
|
buff[sample_idx] = output_sample;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn can_do(&self, can_do: CanDo) -> Supported {
|
|
match can_do {
|
|
CanDo::ReceiveMidiEvent => Supported::Yes,
|
|
_ => Supported::Maybe,
|
|
}
|
|
}
|
|
}
|
|
|
|
plugin_main!(SineSynth);
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use midi_pitch_to_freq;
|
|
|
|
#[test]
|
|
fn test_midi_pitch_to_freq() {
|
|
for i in 0..127 {
|
|
// expect no panics
|
|
midi_pitch_to_freq(i);
|
|
}
|
|
}
|
|
}
|