# Metro This plugin showcases several interesting topics: First of all, it shows how to synthesize notes using a sampled sine wave and an envelope. These notes are also synchronized to the host's transport via the "time" extension and lastly, it uses the [pipes library](https://github.com/Janonard/pipes) to express the internal processing pipeline. A pipe is similar to an iterator as it has a `next` method that produces the next item of the pipeline. However, it also takes an input item to create this output item. Therefore, individual pipes can be chained into larger pipes and even complete pipelines. Using pipes has multiple advantages over writing the processing algorithm "manually": First of all, it slices the pipeline into well-defined pieces that can easily be understood on their own. Then, they also provide a testable interface to the individual parts of the algorithm, which is very useful since you can't properly test your code online, and lastly, they also improve the reusability of your code. However, they also have some downsides: First of all, they require more code than a "manual" implementation, since every pipe is a type on its own. Also, since the algorithm is split into many small methods, there is an overhead from the function calls and it might be hard for the compiler to use [SIMD instructions](https://en.wikipedia.org/wiki/SIMD). We don't tell you which approach to use, but we would like to show you both so you can decide! ### metro/eg-metro-rs.lv2/manifest.ttl ```ttl @prefix lv2: . @prefix rdfs: . @prefix ui: . a lv2:Plugin ; lv2:binary ; rdfs:seeAlso . ``` ### metro/eg-metro-rs.lv2/metro.ttl ```ttl @prefix atom: . @prefix doap: . @prefix lv2: . @prefix time: . @prefix urid: . a lv2:Plugin ; doap:name "Example Metronome" ; doap:license ; lv2:project ; lv2:requiredFeature urid:map , lv2:inPlaceBroken ; lv2:optionalFeature lv2:hardRTCapable ; lv2:port [ ``` There are atom objects, which are semantically similar to Turtle files, but only use URIDs and atom types as properties. `time:Position` is a class of such objects and this input port accepts it as an event. Therefore, the host knows to deliver time and tempo information here. ```ttl a lv2:InputPort , atom:AtomPort ; atom:bufferType atom:Sequence ; atom:supports time:Position ; lv2:index 0 ; lv2:symbol "control" ; lv2:name "Control" ; ] , [ a lv2:AudioPort , lv2:OutputPort ; lv2:index 1 ; lv2:symbol "out" ; lv2:name "Out" ; ] . ``` ### metro/Cargo.toml ```toml [package] name = "metro" version = "0.1.0" authors = ["Jan-Oliver 'Janonard' Opdenhövel "] license = "ISC" edition = "2018" [lib] crate-type = ["cdylib"] ``` This is the first time we need a non-default LV2 feature. In this case, this is the `lv2-time` crate. ```toml [dependencies] lv2 = { version = "0.6.0", features = ["lv2-time"] } iterpipes = "0.2.0" ``` ### metro/src/pipes.rs We cover the individual pipes of the plugin before putting it all together: ```rs use iterpipes::*; use lv2::prelude::*; ``` `Sampler` is a simple sampler that plays back the contents of a pre-recorded sample. It simply returns a frame for every index it receives as an input, which means that it can also be played backward or at a different speed. The actual type of frames isn't important and therefore, this sampler is generic. ```rs pub struct Sampler { sample: Box<[T]>, } impl Sampler { pub fn new(sample: S) -> Self where S: Into>, { Self { sample: sample.into(), } } } impl Pipe for Sampler where T: Copy, { type InputItem = usize; type OutputItem = T; fn next(&mut self, index: usize) -> T { self.sample[index % self.sample.len()] } } impl ResetablePipe for Sampler where T: Copy, { fn reset(&mut self) {} } ``` We try to test as much of the individual parts as possible to reduce the error cases. ```rs #[test] fn test_sampler() { let sample: Vec = vec![1, 2, 3, 4]; let mut sampler = Sampler::new(sample); for i in (0..32).chain(32..0) { assert_eq!((i % 4 + 1) as u8, sampler.next(i)); } } ``` `Envelope` receives a pulse and an index and creates an envelope after every pulse. This envelope is multiplied with the sample output to generate the sound signal. ```rs pub struct Envelope { attack_len: usize, decay_len: usize, impulse_index: usize, } impl Envelope { pub fn new(attack_len: usize, decay_len: usize) -> Self { Self { attack_len, decay_len, impulse_index: std::usize::MAX, } } } impl Pipe for Envelope { type InputItem = (usize, bool); type OutputItem = f32; fn next(&mut self, (index, impulse): (usize, bool)) -> f32 { if impulse { self.impulse_index = index; } if index < self.impulse_index { 0.0 } else if index < self.impulse_index + self.attack_len { (index - self.impulse_index) as f32 / (self.attack_len) as f32 } else if index < self.impulse_index + self.attack_len + self.decay_len { 1.0 - ((index - self.impulse_index - self.attack_len) as f32 / (self.decay_len) as f32) } else { 0.0 } } } impl ResetablePipe for Envelope { fn reset(&mut self) { self.impulse_index = std::usize::MAX; } } #[test] fn test_envelope() { let mut pipe = Envelope::new(4, 4).compose() >> Lazy::new(|frame: f32| (frame * 4.0).round() as u8); for i in 0..32 { assert_eq!(0, pipe.next((i, false))); } assert_eq!(0, pipe.next((32, true))); assert_eq!(1, pipe.next((33, false))); assert_eq!(2, pipe.next((34, false))); assert_eq!(3, pipe.next((35, false))); assert_eq!(4, pipe.next((36, false))); assert_eq!(3, pipe.next((37, false))); assert_eq!(2, pipe.next((38, false))); assert_eq!(1, pipe.next((39, false))); assert_eq!(0, pipe.next((40, false))); for i in 41..64 { assert_eq!(0, pipe.next((i, false))); } } ``` The `PulseGenerator` interprets the settings of the host and creates a pulse every time a new note should be played. This pulse is a `bool` that flips from `false` to `true`. The host settings are updated via `PulseInput` objects, which contain new BPM and speed measures as well as the number of the current beat in the current bar for synchronization. Note that the `elapsed_frames` counter is only used internally to generate pulses. The index counters for the envelope and the samples are separate, which means that the audio won't stutter after a hard update. ```rs pub struct PulseGenerator { sample_rate: f32, beats_per_minute: f32, speed_coefficient: f32, frames_per_beat: usize, elapsed_frames: usize, } impl PulseGenerator { pub fn new(sample_rate: f32) -> Self { Self { sample_rate, beats_per_minute: 120.0, speed_coefficient: 0.0, frames_per_beat: 0, elapsed_frames: 0, } } } impl Pipe for PulseGenerator { type InputItem = PulseInput; type OutputItem = bool; fn next(&mut self, input: PulseInput) -> bool { self.elapsed_frames += 1; let mut parameters_changed = false; if let Some(new_bpm) = input.bpm_update { self.beats_per_minute = new_bpm; parameters_changed = true; } if let Some(new_speed) = input.speed_update { self.speed_coefficient = new_speed; parameters_changed = true; } if parameters_changed { self.frames_per_beat = (self.speed_coefficient * (60.0 / self.beats_per_minute) * self.sample_rate).abs() as usize; } if let Some(new_beat) = input.beat_update { self.elapsed_frames = (new_beat * self.frames_per_beat as f64) as usize; } self.frames_per_beat != 0 && self.elapsed_frames % self.frames_per_beat == 0 } } impl ResetablePipe for PulseGenerator { fn reset(&mut self) { self.beats_per_minute = 120.0; self.speed_coefficient = 0.0; self.frames_per_beat = 0; self.elapsed_frames = 0; } } #[test] fn test_pulse_generator() { let mut pipe = PulseGenerator::new(44100.0); assert!(pipe.next(PulseInput { beat_update: Some(0.0), bpm_update: Some(120.0), speed_update: Some(1.0) })); for i in 1..88100 { let input = PulseInput { beat_update: None, bpm_update: None, speed_update: None, }; if i % 22050 == 0 { assert!(pipe.next(input)); } else { assert!(!pipe.next(input)); } } } ``` This is the input type for the pulse generator. The `bpm_update` and `speed_update` fields tell the pulse generator of the new number of beats per second and playback speed. The `beat_update` contains the number of the current beat in the current bar and is used to synchronize the plugin with the host. ```rs #[derive(Clone, Copy, Debug)] pub struct PulseInput { pub beat_update: Option, pub bpm_update: Option, pub speed_update: Option, } ``` The `EventAtomizer` wraps an iterator over events and transforms them into frames, which either contain an event or don't. This iterator will be the atom event iterator later, but for now, it's good to be generic. Internally, it stores the next event of the event sequence. Every time `next` is called, this counter is increased and once it hits this next event, it is yielded and the next "next event" is retrieved. This is continued as long as the sequence contains events. Once it is depleted, this pipe only emits `None`s. Since every frame can only contain one event and frames must be emitted chronologically, it drops every event that has the same or an earlier timestamp than a previous event. ```rs pub struct EventAtomizer where I: Iterator, { sequence: I, next_event: Option<(usize, T)>, index: usize, } impl EventAtomizer where I: Iterator, { pub fn new(sequence: I) -> Self { let mut instance = Self { sequence, next_event: None, index: 0, }; instance.retrieve_next_event(); instance } fn retrieve_next_event(&mut self) { self.next_event = None; if let Some((index, item)) = self.sequence.next() { if index >= self.index { self.next_event = Some((index, item)); } } } } impl Pipe for EventAtomizer where I: Iterator, { type InputItem = (); type OutputItem = Option; fn next(&mut self, _: ()) -> Option { match self.next_event.take() { Some((event_index, event_atom)) => { let event_is_due = event_index == self.index; self.index += 1; if event_is_due { self.retrieve_next_event(); Some(event_atom) } else { self.next_event = Some((event_index, event_atom)); None } } None => None, } } } #[test] fn test_atomizer() { let events: Box<[(usize, u32)]> = Box::new([(4, 1), (10, 5)]); let mut pipe = EventAtomizer::new(events.iter().cloned()); for i in 0..15 { let output = pipe.next(()); match i { 4 => assert_eq!(Some(1), output), 10 => assert_eq!(Some(5), output), _ => assert_eq!(None, output), } } } ``` In the final plugin, the `EventAtomizer` emits `Option`s, which might be any atom at all, and the `PulseGenerator` consumes `PulseInput`s. The `EventReader` bridges the gap between these two pipes by identifying the atom, reading it and emitting an appropriate `PulseInput`. This is the only pipe that isn't tested since creating a testbed for it would require too much code for this book. ```rs pub struct EventReader<'a> { atom_urids: &'a AtomURIDCollection, time_urids: &'a TimeURIDCollection, } impl<'a> EventReader<'a> { pub fn new(atom_urids: &'a AtomURIDCollection, time_urids: &'a TimeURIDCollection) -> Self { Self { atom_urids, time_urids, } } } impl<'a> Pipe for EventReader<'a> { type InputItem = Option>; type OutputItem = PulseInput; fn next(&mut self, atom: Option) -> PulseInput { let mut updates = PulseInput { beat_update: None, bpm_update: None, speed_update: None, }; if let Some(atom) = atom { if let Some((object_header, object_reader)) = atom .read(self.atom_urids.object, ()) .or_else(|| atom.read(self.atom_urids.blank, ())) { if object_header.otype == self.time_urids.position_class { for (property_header, property) in object_reader { if property_header.key == self.time_urids.bar_beat { updates.beat_update = property .read(self.atom_urids.float, ()) .map(|float| float as f64); } if property_header.key == self.time_urids.beats_per_minute { updates.bpm_update = property.read(self.atom_urids.float, ()); } if property_header.key == self.time_urids.speed { updates.speed_update = property.read(self.atom_urids.float, ()); } } } } } updates } } ``` ### metro/src/lib.rs Now, we put it all together: ```rs use iterpipes::*; use lv2::prelude::*; mod pipes; use pipes::*; ``` In future iterations of the plugin, these values could be parameters, but for now, the're constants: ```rs const ATTACK_DURATION: f64 = 0.005; const DECAY_DURATION: f64 = 0.075; const NOTE_FREQUENCY: f64 = 440.0 * 2.0; #[derive(URIDCollection)] struct URIDs { atom: AtomURIDCollection, unit: UnitURIDCollection, time: TimeURIDCollection, } #[derive(PortCollection)] pub struct Ports { control: InputPort, output: OutputPort