Play MIDI controllers through instruments
Imported MIDI keeps its controller events: sustain pedal and other CCs, pitch bend, channel and poly pressure, program changes. Playback sent instruments only note-ons and note-offs, so a piano part lost its pedal and bends went flat. - Plugin::midi takes any other MIDI 1.0 channel message at a sample offset. The mix engine sends each clip's controllers at their sample, after the Real-Time Properties delay (and transpose, for poly pressure), on channel 1 like the notes. - After a seek, a stop or a loop wrap, an instrument first gets Reset All Controllers (CC 121), then the last value each controller had on its track before the playhead. A pedal held before the start point is still down. - CLAP: raw MIDI events when the note port speaks the MIDI dialect. - VST3: CCs, aftertouch and pitch bend become parameter changes at their offset, through the plugin's IMidiMapping. It is asked once in prepare, on the main thread, and there is one point per parameter per block. Poly pressure is a PolyPressureEvent. Program changes have no VST3 mapping and are dropped. - AU: MusicDeviceMIDIEvent for instruments and music effects. - The built-in synth follows the sustain pedal, pitch bend (2 semitones each way), Reset All Controllers and All Notes Off. - The sampler ignores the pedal, pitch bend and Reset All Controllers for now. The AU check of MIDI volume lives inside the existing DLS test. Creating two DLSMusicDevice instances at once on two threads crashes inside the unit, with or without this change. Two instances created one after another render in parallel fine.
This commit is contained in:
parent
33ad8264c8
commit
acd9e7d54e
|
|
@ -231,6 +231,13 @@ impl Plugin for AuPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
fn midi(&mut self, offset: usize, msg: [u8; 3]) {
|
||||
let [status, data1, data2] = msg;
|
||||
if status & 0xF0 >= 0xA0 && status & 0xF0 < 0xF0 {
|
||||
self.push_event(MidiEvent { time: u32::try_from(offset).unwrap_or(u32::MAX), status, data1: data1 & 0x7F, data2: data2 & 0x7F });
|
||||
}
|
||||
}
|
||||
|
||||
fn all_notes_off(&mut self) {
|
||||
for k in 0..128u8 {
|
||||
if self.held.get(k as usize).copied().unwrap_or(false) {
|
||||
|
|
|
|||
|
|
@ -167,4 +167,23 @@ fn dls_synth_plays_a_note() {
|
|||
assert!(e > 1e-3, "{e}");
|
||||
p.note_off(0, 64);
|
||||
p.reset();
|
||||
// Controllers reach the synth: MIDI volume 0 silences a note. (In this test because creating
|
||||
// two DLS instances at once on two threads crashes inside the unit.)
|
||||
p.prepare(48_000.0, 512, 2);
|
||||
let loudest = |p: &mut Box<dyn Plugin>, volume: u8| {
|
||||
p.midi(0, [0xB0, 7, volume]);
|
||||
p.note_on(0, 60, 110);
|
||||
let mut energy = 0.0f32;
|
||||
for _ in 0..20 {
|
||||
let mut io = vec![vec![0.0f32; 512]; 2];
|
||||
p.process(&mut io, 512);
|
||||
energy = energy.max(rms(&io[0]));
|
||||
}
|
||||
p.note_off(0, 60);
|
||||
p.reset();
|
||||
energy
|
||||
};
|
||||
let full = loudest(&mut p, 127);
|
||||
let muted = loudest(&mut p, 0);
|
||||
assert!(full > 1e-3 && muted < full * 0.01, "volume 127: {full}, volume 0: {muted}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ pub struct ClapPlugin {
|
|||
in_main: Option<usize>,
|
||||
out_main: Option<usize>,
|
||||
notes: NoteMode,
|
||||
/// The note port takes raw MIDI (controllers, pitch bend, program changes).
|
||||
midi_in: bool,
|
||||
held: [bool; 128],
|
||||
channels: usize,
|
||||
max_block: usize,
|
||||
|
|
@ -56,6 +58,7 @@ impl ClapPlugin {
|
|||
in_main: None,
|
||||
out_main: None,
|
||||
notes: NoteMode::None,
|
||||
midi_in: false,
|
||||
held: [false; 128],
|
||||
channels: 2,
|
||||
max_block: 0,
|
||||
|
|
@ -116,7 +119,9 @@ impl Plugin for ClapPlugin {
|
|||
self.out_main = pick_main(&outs);
|
||||
self.inputs = PortBuffers::new(&ins.iter().map(|p| p.channels).collect::<Vec<_>>(), mb);
|
||||
self.outputs = PortBuffers::new(&outs.iter().map(|p| p.channels).collect::<Vec<_>>(), mb);
|
||||
self.notes = note_mode(self.inst.note_port());
|
||||
let port = self.inst.note_port();
|
||||
self.notes = note_mode(port);
|
||||
self.midi_in = port.is_some_and(|p| p.supported & CLAP_NOTE_DIALECT_MIDI != 0);
|
||||
self.max_block = mb;
|
||||
self.inst.state().restart.store(false, Ordering::Relaxed);
|
||||
match self.inst.activate(sr, 1, u32::try_from(mb).unwrap_or(u32::MAX)) {
|
||||
|
|
@ -254,6 +259,12 @@ impl Plugin for ClapPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
fn midi(&mut self, offset: usize, msg: [u8; 3]) {
|
||||
if self.midi_in {
|
||||
self.push(Event::Midi { time: u32::try_from(offset).unwrap_or(u32::MAX), data: msg });
|
||||
}
|
||||
}
|
||||
|
||||
fn all_notes_off(&mut self) {
|
||||
for k in 0..128u8 {
|
||||
if self.held.get(k as usize).copied().unwrap_or(false) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
//! - `org.soundcraft.test.gain`: stereo in/out, param 7 "Gain" (0..2, default 1), param 3 "Mode"
|
||||
//! (stepped enum A/B/C), hidden param 9, latency 3 samples.
|
||||
//! - `org.soundcraft.test.synth`: instrument, stereo out, CLAP note port; outputs a constant equal
|
||||
//! to the velocity of the held note (0 when none), switching at the event's sample offset.
|
||||
//! to the velocity of the held note (0 when none), switching at the event's sample offset. A
|
||||
//! MIDI CC 7 (volume) sets the constant to its value / 127.
|
||||
//!
|
||||
//! The gain plugin also implements `clap.state` (gain and mode as two little-endian f64s) and a
|
||||
//! floating-only `clap.gui` without a real window: showing it "edits" the gain to 0.25 in the GUI,
|
||||
|
|
@ -150,6 +151,12 @@ fn apply(f: &mut Fx, h: &clap_event_header) {
|
|||
f.level = e.velocity;
|
||||
}
|
||||
CLAP_EVENT_NOTE_OFF => f.level = 0.0,
|
||||
CLAP_EVENT_MIDI => {
|
||||
let e = unsafe { &*(h as *const clap_event_header as *const clap_event_midi) };
|
||||
if e.data[0] & 0xF0 == 0xB0 && e.data[1] == 7 {
|
||||
f.level = f64::from(e.data[2]) / 127.0;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ fn synth_plays_notes_at_their_offsets() {
|
|||
p.all_notes_off();
|
||||
p.process(&mut io, 64);
|
||||
assert!(io[0].iter().all(|x| x.abs() < 1e-6));
|
||||
p.midi(20, [0xB0, 7, 64]);
|
||||
p.process(&mut io, 64);
|
||||
assert!(io[0][19].abs() < 1e-6 && (io[0][20] - 64.0 / 127.0).abs() < 1e-6, "controllers arrive as MIDI at their offset");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -218,6 +218,9 @@ pub trait Plugin: Send {
|
|||
/// Instruments: note events at sample offsets within the next block.
|
||||
fn note_on(&mut self, _offset: usize, _note: u8, _velocity: u8) {}
|
||||
fn note_off(&mut self, _offset: usize, _note: u8) {}
|
||||
/// Instruments: any other MIDI 1.0 channel message (control change, pitch bend, pressure,
|
||||
/// program change) at a sample offset within the next block.
|
||||
fn midi(&mut self, _offset: usize, _msg: [u8; 3]) {}
|
||||
fn all_notes_off(&mut self) {}
|
||||
/// Sample-playing instruments (the Sampler): the recorded audio to play, `None` to clear it.
|
||||
/// `false` when the plugin does not play samples. Does not allocate; sounding voices stop.
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@ impl DrumSynth {
|
|||
fn apply(&mut self, kind: EventKind) {
|
||||
match kind {
|
||||
EventKind::On(n, v) => self.trigger(n, v),
|
||||
EventKind::Off(_) => {}
|
||||
EventKind::Off(_) | EventKind::Sustain(_) | EventKind::Bend(_) | EventKind::ResetControllers => {}
|
||||
EventKind::AllOff => {
|
||||
let fast = decay60(30.0, self.sr);
|
||||
for v in &mut self.voices {
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ impl Sampler {
|
|||
v.env.gate_off();
|
||||
}
|
||||
}
|
||||
EventKind::Sustain(_) | EventKind::Bend(_) | EventKind::ResetControllers => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,15 @@ pub(crate) enum EventKind {
|
|||
On(u8, u8),
|
||||
Off(u8),
|
||||
AllOff,
|
||||
Sustain(bool),
|
||||
/// Pitch bend as a frequency ratio.
|
||||
Bend(f32),
|
||||
ResetControllers,
|
||||
}
|
||||
|
||||
/// Pitch bend range, semitones each way.
|
||||
const BEND_RANGE: f32 = 2.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Event {
|
||||
pub(crate) offset: usize,
|
||||
|
|
@ -212,6 +219,8 @@ struct Voice {
|
|||
svf: Svf,
|
||||
age: u64,
|
||||
held: bool,
|
||||
/// Released while the sustain pedal was down.
|
||||
sustained: bool,
|
||||
}
|
||||
|
||||
impl Voice {
|
||||
|
|
@ -228,6 +237,7 @@ impl Voice {
|
|||
svf: Svf::default(),
|
||||
age: 0,
|
||||
held: false,
|
||||
sustained: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -250,6 +260,8 @@ pub struct SubtractiveSynth {
|
|||
counter: u64,
|
||||
level: Smoother,
|
||||
cutoff: Smoother,
|
||||
sustain: bool,
|
||||
bend: f32,
|
||||
}
|
||||
|
||||
impl Default for SubtractiveSynth {
|
||||
|
|
@ -277,6 +289,8 @@ impl SubtractiveSynth {
|
|||
counter: 0,
|
||||
level: Smoother::new(0.5),
|
||||
cutoff: Smoother::new(2000.0),
|
||||
sustain: false,
|
||||
bend: 1.0,
|
||||
};
|
||||
s.update();
|
||||
s
|
||||
|
|
@ -337,15 +351,32 @@ impl SubtractiveSynth {
|
|||
v.filt.gate_on();
|
||||
v.age = counter;
|
||||
v.held = true;
|
||||
v.sustained = false;
|
||||
}
|
||||
self.last_freq = Some(freq);
|
||||
}
|
||||
|
||||
fn stop_note(&mut self, note: u8) {
|
||||
let sustain = self.sustain;
|
||||
for v in self.voices.iter_mut().filter(|v| v.held && v.note == note) {
|
||||
v.held = false;
|
||||
v.amp.gate_off();
|
||||
v.filt.gate_off();
|
||||
if sustain {
|
||||
v.sustained = true;
|
||||
} else {
|
||||
v.amp.gate_off();
|
||||
v.filt.gate_off();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sustain(&mut self, on: bool) {
|
||||
self.sustain = on;
|
||||
if !on {
|
||||
for v in self.voices.iter_mut().filter(|v| v.sustained) {
|
||||
v.sustained = false;
|
||||
v.amp.gate_off();
|
||||
v.filt.gate_off();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -356,10 +387,17 @@ impl SubtractiveSynth {
|
|||
EventKind::AllOff => {
|
||||
for v in &mut self.voices {
|
||||
v.held = false;
|
||||
v.sustained = false;
|
||||
v.amp.gate_off();
|
||||
v.filt.gate_off();
|
||||
}
|
||||
}
|
||||
EventKind::Sustain(on) => self.set_sustain(on),
|
||||
EventKind::Bend(ratio) => self.bend = ratio,
|
||||
EventKind::ResetControllers => {
|
||||
self.set_sustain(false);
|
||||
self.bend = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,6 +412,7 @@ impl SubtractiveSynth {
|
|||
let env_oct = self.p.v(7) / 100.0 * 6.0;
|
||||
let cutoff = self.cutoff.next();
|
||||
let nyq = sr * 0.45;
|
||||
let bend = self.bend;
|
||||
let mut out = 0.0;
|
||||
for v in &mut self.voices {
|
||||
if !v.active() {
|
||||
|
|
@ -386,8 +425,8 @@ impl SubtractiveSynth {
|
|||
}
|
||||
let a = v.amp.next(&self.amp_rates);
|
||||
let fe = v.filt.next(&self.filt_rates);
|
||||
let s1 = v.osc1.next(w1, v.freq / sr);
|
||||
let s2 = v.osc2.next(w2, v.freq * ratio2 / sr);
|
||||
let s1 = v.osc1.next(w1, v.freq * bend / sr);
|
||||
let s2 = v.osc2.next(w2, v.freq * bend * ratio2 / sr);
|
||||
let raw = s1 * (1.0 - mix) + s2 * mix;
|
||||
let fc = (cutoff * 2f32.powf(env_oct * fe)).clamp(20.0, nyq);
|
||||
let g = (std::f32::consts::PI * fc / sr).tan();
|
||||
|
|
@ -418,6 +457,8 @@ impl Plugin for SubtractiveSynth {
|
|||
self.last_freq = None;
|
||||
self.level.snap();
|
||||
self.cutoff.snap();
|
||||
self.sustain = false;
|
||||
self.bend = 1.0;
|
||||
}
|
||||
|
||||
fn process(&mut self, io: &mut [Vec<f32>], frames: usize) {
|
||||
|
|
@ -467,6 +508,21 @@ impl Plugin for SubtractiveSynth {
|
|||
self.events.push(offset, EventKind::Off(note));
|
||||
}
|
||||
|
||||
fn midi(&mut self, offset: usize, msg: [u8; 3]) {
|
||||
let [status, d1, d2] = msg;
|
||||
let kind = match (status & 0xF0, d1) {
|
||||
(0xB0, 64) => EventKind::Sustain(d2 >= 64),
|
||||
(0xB0, 120 | 123) => EventKind::AllOff,
|
||||
(0xB0, 121) => EventKind::ResetControllers,
|
||||
(0xE0, _) => {
|
||||
let v = (i32::from(d2 & 0x7F) << 7 | i32::from(d1 & 0x7F)) - 8192;
|
||||
EventKind::Bend(2f32.powf(v as f32 / 8192.0 * BEND_RANGE / 12.0))
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
self.events.push(offset, kind);
|
||||
}
|
||||
|
||||
fn all_notes_off(&mut self) {
|
||||
self.events.push(0, EventKind::AllOff);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -394,6 +394,34 @@ fn synth_note_on_sounds_and_note_off_decays() {
|
|||
assert!(peak(&out[0][72_000..]) < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synth_follows_the_sustain_pedal_and_pitch_bend() {
|
||||
let mut p = prepared("subtractive_synth");
|
||||
p.midi(0, [0xB0, 64, 127]);
|
||||
p.note_on(0, 60, 100);
|
||||
run(p.as_mut(), &vec![vec![0.0; 4800]; 2]);
|
||||
p.note_off(0, 60);
|
||||
let out = run(p.as_mut(), &vec![vec![0.0; 96_000]; 2]);
|
||||
assert!(rms(&out[0][72_000..]) > 0.01, "the pedal holds the note");
|
||||
p.midi(0, [0xB0, 64, 0]);
|
||||
let out = run(p.as_mut(), &vec![vec![0.0; 96_000]; 2]);
|
||||
assert!(peak(&out[0][72_000..]) < 1e-6, "lifting the pedal releases it");
|
||||
|
||||
let rises = |x: &[f32]| x.windows(2).filter(|w| w[0] < 0.0 && w[1] >= 0.0).count() as f64;
|
||||
let mut p = prepared("subtractive_synth");
|
||||
p.set_param("osc1_wave", 2.0);
|
||||
p.set_param("osc_mix", 0.0);
|
||||
p.note_on(0, 69, 100);
|
||||
let plain = run(p.as_mut(), &[vec![0.0; 48_000]]);
|
||||
p.midi(0, [0xE0, 0x7F, 0x7F]);
|
||||
let bent = run(p.as_mut(), &[vec![0.0; 48_000]]);
|
||||
let ratio = rises(&bent[0][4800..]) / rises(&plain[0][4800..]);
|
||||
assert!((ratio - 2f64.powf(2.0 / 12.0)).abs() < 0.01, "{ratio}");
|
||||
p.midi(0, [0xB0, 121, 0]);
|
||||
let reset = run(p.as_mut(), &[vec![0.0; 48_000]]);
|
||||
assert!((rises(&reset[0][4800..]) / rises(&plain[0][4800..]) - 1.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synth_event_offsets_are_sample_accurate() {
|
||||
let mut p = prepared("subtractive_synth");
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
use soundcraft_dsp::pan::{PanLaw, SpeakerPos, SurroundParams, gains};
|
||||
use soundcraft_dsp::{Plugin, SampleSource, Transport, db_to_gain};
|
||||
use soundcraft_midi::CtrlKind;
|
||||
use soundcraft_model::{
|
||||
AutoParam, BusId, ChannelFormat, Clip, ClipContent, Route, Session, SourceAudio, Speaker, SurroundPan, Track, TrackId, TrackKind,
|
||||
};
|
||||
|
|
@ -199,6 +200,8 @@ struct Strip {
|
|||
sample: Option<Arc<SourceAudio>>,
|
||||
/// MIDI notes currently sounding on the instrument (pitch), for note-offs at stops/seek.
|
||||
held: Vec<u8>,
|
||||
/// The instrument's next block starts somewhere new: send it the controllers in effect there.
|
||||
chase: bool,
|
||||
muted: bool,
|
||||
gr: f32,
|
||||
/// Per-clip effect chains (Clip Effects) and a scratch buffer to render clips into.
|
||||
|
|
@ -273,6 +276,7 @@ impl Strip {
|
|||
instrument: None,
|
||||
sample: None,
|
||||
held: Vec::new(),
|
||||
chase: true,
|
||||
muted: false,
|
||||
gr: 0.0,
|
||||
clip_fx: HashMap::new(),
|
||||
|
|
@ -474,6 +478,7 @@ impl MixEngine {
|
|||
i.plugin.reset();
|
||||
}
|
||||
s.held.clear();
|
||||
s.chase = true;
|
||||
}
|
||||
self.meters.clear();
|
||||
self.main_meter = StripMeter::default();
|
||||
|
|
@ -576,6 +581,7 @@ impl MixEngine {
|
|||
i.plugin.all_notes_off();
|
||||
}
|
||||
st.held.clear();
|
||||
st.chase = true;
|
||||
}
|
||||
}
|
||||
self.last_end = pos.saturating_add(frames as i64);
|
||||
|
|
@ -898,6 +904,9 @@ fn process_strip(
|
|||
apply_params(inst, ins, t, INSTRUMENT_SLOT, pos);
|
||||
}
|
||||
inst.plugin.set_transport(&transport);
|
||||
if std::mem::take(&mut strip.chase) {
|
||||
chase_controllers(s, t, pos, inst);
|
||||
}
|
||||
schedule_notes(s, t, pos, frames, inst, &mut strip.held);
|
||||
if strip.buf.len() >= 2 {
|
||||
inst.plugin.process(&mut strip.buf, frames);
|
||||
|
|
@ -1310,7 +1319,7 @@ fn render_clip(s: &Session, clip: &Clip, pos: Samples, frames: usize, buf: &mut
|
|||
}
|
||||
}
|
||||
|
||||
/// Send note on/offs for MIDI clips overlapping the block.
|
||||
/// Send note on/offs and controllers for MIDI clips overlapping the block.
|
||||
fn schedule_notes(s: &Session, t: &Track, pos: Samples, frames: usize, inst: &mut PluginSlot, held: &mut Vec<u8>) {
|
||||
let sr = s.sample_rate;
|
||||
let end = pos.saturating_add(frames as i64);
|
||||
|
|
@ -1357,6 +1366,80 @@ fn schedule_notes(s: &Session, t: &Track, pos: Samples, frames: usize, inst: &mu
|
|||
held.retain(|p| *p != n.pitch);
|
||||
}
|
||||
}
|
||||
let near = s.tempo.samples_to_ticks(pos, sr).saturating_sub(base).saturating_sub(1)
|
||||
..=s.tempo.samples_to_ticks(end, sr).saturating_sub(base).saturating_add(1);
|
||||
for c in &sequence.ctrls {
|
||||
let tick = c.tick.saturating_add(delay).max(0);
|
||||
if !near.contains(&tick) {
|
||||
continue;
|
||||
}
|
||||
let at = s.tempo.tick_to_samples(base.saturating_add(tick), sr);
|
||||
if at >= pos && at < end && at < clip.end() {
|
||||
inst.plugin.midi(usize::try_from(at - pos).unwrap_or(0), ctrl_message(c.kind, c.value, dp));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A controller event as a MIDI 1.0 message on channel 1 (where the notes go), with `transpose`
|
||||
/// applied to a poly-pressure key.
|
||||
fn ctrl_message(kind: CtrlKind, value: i32, transpose: i64) -> [u8; 3] {
|
||||
let v7 = value.clamp(0, 127) as u8;
|
||||
match kind {
|
||||
CtrlKind::Cc(n) => [0xB0, n & 0x7F, v7],
|
||||
CtrlKind::PitchBend => {
|
||||
let v = value.saturating_add(8192).clamp(0, 16383);
|
||||
[0xE0, (v & 0x7F) as u8, (v >> 7) as u8]
|
||||
}
|
||||
CtrlKind::ChannelPressure => [0xD0, v7, 0],
|
||||
CtrlKind::PolyPressure(k) => [0xA0, (i64::from(k) + transpose).clamp(0, 127) as u8, v7],
|
||||
CtrlKind::Program => [0xC0, v7, 0],
|
||||
}
|
||||
}
|
||||
|
||||
/// Before an instrument plays from `pos`: reset its controllers, then send each one the last value
|
||||
/// it had on this track before `pos`.
|
||||
fn chase_controllers(s: &Session, t: &Track, pos: Samples, inst: &mut PluginSlot) {
|
||||
inst.plugin.midi(0, [0xB0, 121, 0]);
|
||||
const BEND: usize = 128;
|
||||
const PRESSURE: usize = 129;
|
||||
const PROGRAM: usize = 130;
|
||||
let mut last = [(i64::MIN, 0i32); 131];
|
||||
let sr = s.sample_rate;
|
||||
for clip in t.clips() {
|
||||
let ClipContent::Midi { sequence } = &clip.content else { continue };
|
||||
if clip.muted || clip.start >= pos {
|
||||
continue;
|
||||
}
|
||||
let base = s.tempo.samples_to_ticks(clip.start, sr);
|
||||
let until = pos.min(clip.end());
|
||||
for c in &sequence.ctrls {
|
||||
let slot = match c.kind {
|
||||
CtrlKind::Cc(n) => usize::from(n & 0x7F),
|
||||
CtrlKind::PitchBend => BEND,
|
||||
CtrlKind::ChannelPressure => PRESSURE,
|
||||
CtrlKind::Program => PROGRAM,
|
||||
CtrlKind::PolyPressure(_) => continue,
|
||||
};
|
||||
let at = s.tempo.tick_to_samples(base.saturating_add(c.tick), sr);
|
||||
if at < until
|
||||
&& let Some(l) = last.get_mut(slot)
|
||||
&& at >= l.0
|
||||
{
|
||||
*l = (at, c.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (slot, &(at, value)) in last.iter().enumerate() {
|
||||
let kind = match slot {
|
||||
BEND => CtrlKind::PitchBend,
|
||||
PRESSURE => CtrlKind::ChannelPressure,
|
||||
PROGRAM => CtrlKind::Program,
|
||||
n => CtrlKind::Cc(u8::try_from(n).unwrap_or(0)),
|
||||
};
|
||||
if at != i64::MIN {
|
||||
inst.plugin.midi(0, ctrl_message(kind, value, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2245,6 +2328,58 @@ mod tests {
|
|||
assert!(!seen[1].playing);
|
||||
}
|
||||
|
||||
struct Ears {
|
||||
inner: Box<dyn Plugin>,
|
||||
heard: Arc<std::sync::Mutex<Vec<(usize, [u8; 3])>>>,
|
||||
}
|
||||
|
||||
impl Plugin for Ears {
|
||||
fn info(&self) -> &'static soundcraft_dsp::PluginInfo {
|
||||
self.inner.info()
|
||||
}
|
||||
fn prepare(&mut self, sr: f32, mb: usize, ch: usize) {
|
||||
self.inner.prepare(sr, mb, ch);
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn set_param(&mut self, id: &str, v: f32) -> bool {
|
||||
self.inner.set_param(id, v)
|
||||
}
|
||||
fn param(&self, id: &str) -> Option<f32> {
|
||||
self.inner.param(id)
|
||||
}
|
||||
fn process(&mut self, io: &mut [Vec<f32>], frames: usize) {
|
||||
self.inner.process(io, frames);
|
||||
}
|
||||
fn midi(&mut self, offset: usize, msg: [u8; 3]) {
|
||||
self.heard.lock().unwrap().push((offset, msg));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controllers_play_at_their_offsets_and_are_chased_after_a_seek() {
|
||||
let mut s = Session::default();
|
||||
let t = s.add_track(TrackKind::Instrument, ChannelFormat::Stereo, None);
|
||||
s.track_mut(t).unwrap().instrument = Some(Insert::new("clap:test.ears"));
|
||||
let cc = |tick, kind, value| soundcraft_midi::CtrlEvent { tick, channel: 0, kind, value };
|
||||
let ctrls = vec![cc(4, CtrlKind::Cc(64), 127), cc(960, CtrlKind::PitchBend, -8192), cc(1920, CtrlKind::Cc(64), 0)];
|
||||
let seq = soundcraft_midi::Sequence { ctrls, ..Default::default() };
|
||||
let cid = s.new_clip_id();
|
||||
s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(Clip::midi(cid, "m", 0, 48_000 * 4, seq));
|
||||
let heard = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let ears = Ears { inner: soundcraft_dsp::create("gain").unwrap(), heard: Arc::clone(&heard) };
|
||||
let mut eng = MixEngine::new(48_000.0, 512);
|
||||
eng.set_external_instances(true);
|
||||
eng.adopt(vec![PreparedInstance { track: t, slot: INSTRUMENT_SLOT, id: "clap:test.ears".into(), ch: 2, plugin: Box::new(ears) }]);
|
||||
render_block(&mut eng, &s, 0);
|
||||
assert_eq!(std::mem::take(&mut *heard.lock().unwrap()), vec![(0, [0xB0, 121, 0]), (100, [0xB0, 64, 127])]);
|
||||
render_block(&mut eng, &s, 36_000);
|
||||
assert_eq!(std::mem::take(&mut *heard.lock().unwrap()), vec![(0, [0xB0, 121, 0]), (0, [0xB0, 64, 127]), (0, [0xE0, 0, 0])]);
|
||||
render_block(&mut eng, &s, 36_512);
|
||||
assert!(heard.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
fn render_block(eng: &mut MixEngine, s: &Session, pos: i64) -> f32 {
|
||||
let mut out = vec![vec![0.0f32; 512]; 2];
|
||||
eng.render(s, pos, 512, &mut out);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use std::time::Duration;
|
|||
use vst3::Steinberg::Vst::{self as sv};
|
||||
use vst3::Steinberg::Vst::{
|
||||
IAudioProcessorTrait, IComponentHandlerTrait, IComponentTrait, IConnectionPointTrait, IEditControllerTrait, IEventListTrait,
|
||||
IHostApplicationTrait, IParamValueQueueTrait, IParameterChangesTrait,
|
||||
IHostApplicationTrait, IMidiMappingTrait, IParamValueQueueTrait, IParameterChangesTrait,
|
||||
};
|
||||
use vst3::Steinberg::{self as sb};
|
||||
use vst3::Steinberg::{FUnknown, IBStreamTrait, IPlugFrameTrait, IPlugViewTrait, IPluginBaseTrait, IPluginFactory2Trait, IPluginFactoryTrait};
|
||||
|
|
@ -82,6 +82,10 @@ fn ok(r: sb::tresult) -> bool {
|
|||
r == sb::kResultOk
|
||||
}
|
||||
|
||||
/// VST3's numbers for the MIDI controllers that aren't CCs.
|
||||
pub(crate) const AFTERTOUCH: usize = sv::ControllerNumbers_::kAfterTouch as usize;
|
||||
pub(crate) const PITCH_BEND: usize = sv::ControllerNumbers_::kPitchBend as usize;
|
||||
|
||||
// ---- loaded binaries -----------------------------------------------------------------------
|
||||
|
||||
/// One class as reported by the factory.
|
||||
|
|
@ -959,17 +963,18 @@ impl IBStreamTrait for MemoryStream {
|
|||
pub(crate) enum NoteEvent {
|
||||
On { time: u32, key: u8, velocity: f32 },
|
||||
Off { time: u32, key: u8 },
|
||||
Pressure { time: u32, key: u8, pressure: f32 },
|
||||
}
|
||||
|
||||
impl NoteEvent {
|
||||
pub fn time(&self) -> u32 {
|
||||
match *self {
|
||||
NoteEvent::On { time, .. } | NoteEvent::Off { time, .. } => time,
|
||||
NoteEvent::On { time, .. } | NoteEvent::Off { time, .. } | NoteEvent::Pressure { time, .. } => time,
|
||||
}
|
||||
}
|
||||
pub fn set_time(&mut self, t: u32) {
|
||||
match self {
|
||||
NoteEvent::On { time, .. } | NoteEvent::Off { time, .. } => *time = t,
|
||||
NoteEvent::On { time, .. } | NoteEvent::Off { time, .. } | NoteEvent::Pressure { time, .. } => *time = t,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -988,6 +993,10 @@ impl NoteEvent {
|
|||
e.r#type = sv::Event_::EventTypes_::kNoteOffEvent as u16;
|
||||
e.__field0.noteOff = sv::NoteOffEvent { channel: 0, pitch: i16::from(key), velocity: 0.0, noteId: -1, tuning: 0.0 };
|
||||
}
|
||||
NoteEvent::Pressure { key, pressure, .. } => {
|
||||
e.r#type = sv::Event_::EventTypes_::kPolyPressureEvent as u16;
|
||||
e.__field0.polyPressure = sv::PolyPressureEvent { channel: 0, pitch: i16::from(key), pressure, noteId: -1 };
|
||||
}
|
||||
}
|
||||
e
|
||||
}
|
||||
|
|
@ -1261,6 +1270,20 @@ impl Instance {
|
|||
v.is_finite().then(|| v.clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// The parameter each MIDI controller drives on channel 1 (`IMidiMapping`), indexed by VST3's
|
||||
/// controller numbers: CCs 0–127, then [`AFTERTOUCH`] and [`PITCH_BEND`]. Main thread only.
|
||||
pub fn midi_mapping(&self) -> Vec<Option<u32>> {
|
||||
let Some(m) = self.controller.as_ref().and_then(|c| c.cast::<sv::IMidiMapping>()) else { return Vec::new() };
|
||||
(0..=PITCH_BEND)
|
||||
.map(|n| {
|
||||
let mut id: sv::ParamID = 0;
|
||||
let n = i16::try_from(n).unwrap_or(0);
|
||||
// SAFETY: valid mapping interface on the main thread; `id` is a local.
|
||||
ok(unsafe { m.getMidiControllerAssignment(0, 0, n, &mut id) }).then_some(id)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn value_text(&self, id: u32, normalized: f64) -> Option<String> {
|
||||
let c = self.controller.as_ref()?;
|
||||
let mut s: sv::String128 = [0; 128];
|
||||
|
|
@ -1414,13 +1437,16 @@ impl Instance {
|
|||
}
|
||||
|
||||
/// Runs one `process` call over `frames` frames: `params` become one point each at offset 0,
|
||||
/// `notes` (sorted by time) have their times made relative to `offset`.
|
||||
/// `controls` (time, id, value) one point at their time when it falls in this call (the last
|
||||
/// one wins per parameter), and `notes` (sorted by time) have their times made relative to
|
||||
/// `offset`.
|
||||
pub fn process(
|
||||
&mut self,
|
||||
frames: usize,
|
||||
inputs: &mut BusBuffers,
|
||||
outputs: &mut BusBuffers,
|
||||
params: &[(u32, f64)],
|
||||
controls: &[(u32, u32, f64)],
|
||||
notes: &[NoteEvent],
|
||||
offset: u32,
|
||||
project_time: i64,
|
||||
|
|
@ -1443,6 +1469,11 @@ impl Instance {
|
|||
for &(id, v) in params.iter().take(PARAM_QUEUE_CAP) {
|
||||
self.in_params.set(id, 0, v.clamp(0.0, 1.0));
|
||||
}
|
||||
for &(time, id, v) in controls {
|
||||
if let Some(at) = time.checked_sub(offset).filter(|&t| (t as usize) < frames) {
|
||||
self.in_params.set(id, i32::try_from(at).unwrap_or(0), v.clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
self.out_params.clear();
|
||||
let event_in = self.event_in;
|
||||
self.in_events.with(|v| {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! `Vst3Plugin`: a loaded VST3 instance behind the safe `soundcraft_dsp::Plugin` trait.
|
||||
|
||||
use crate::ffi::{BusBuffers, EVENT_CAP, EditorLink, HostContext, Instance, NoteEvent, PARAM_QUEUE_CAP};
|
||||
use crate::ffi::{AFTERTOUCH, BusBuffers, EVENT_CAP, EditorLink, HostContext, Instance, NoteEvent, PARAM_QUEUE_CAP, PITCH_BEND};
|
||||
use soundcraft_dsp::{Plugin, PluginEditor, PluginInfo, Transport, Unit};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
|
@ -35,6 +35,10 @@ pub struct Vst3Plugin {
|
|||
pending: Vec<(u32, f64)>,
|
||||
/// Pending notes, kept sorted by time. Never grows past `EVENT_CAP`.
|
||||
notes: Vec<NoteEvent>,
|
||||
/// The parameter each MIDI controller drives (see `Instance::midi_mapping`).
|
||||
midi_map: Vec<Option<u32>>,
|
||||
/// Controller points for the next block: (time, param id, normalized value). Bounded.
|
||||
controls: Vec<(u32, u32, f64)>,
|
||||
inputs: BusBuffers,
|
||||
outputs: BusBuffers,
|
||||
has_input: bool,
|
||||
|
|
@ -78,6 +82,8 @@ impl Vst3Plugin {
|
|||
values: Vec::with_capacity(ids.len()),
|
||||
pending: Vec::with_capacity(ids.len().min(PARAM_QUEUE_CAP)),
|
||||
notes: Vec::with_capacity(EVENT_CAP),
|
||||
midi_map: Vec::new(),
|
||||
controls: Vec::with_capacity(EVENT_CAP),
|
||||
inputs: BusBuffers::new(&[], 0),
|
||||
outputs: BusBuffers::new(&[], 0),
|
||||
has_input: false,
|
||||
|
|
@ -180,6 +186,7 @@ impl Plugin for Vst3Plugin {
|
|||
self.max_block = mb;
|
||||
self.latency = self.inst.latency() as usize;
|
||||
self.tail = self.inst.tail() as usize;
|
||||
self.midi_map = self.inst.midi_mapping();
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("{}: {e}", self.info.id);
|
||||
|
|
@ -197,6 +204,7 @@ impl Plugin for Vst3Plugin {
|
|||
|
||||
fn reset(&mut self) {
|
||||
self.notes.clear();
|
||||
self.controls.clear();
|
||||
self.held = [false; 128];
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +247,9 @@ impl Plugin for Vst3Plugin {
|
|||
e.set_time(last);
|
||||
}
|
||||
}
|
||||
for c in &mut self.controls {
|
||||
c.0 = c.0.min(last);
|
||||
}
|
||||
let mut start = 0usize;
|
||||
while start < frames {
|
||||
let n = (frames - start).min(self.max_block);
|
||||
|
|
@ -255,7 +266,17 @@ impl Plugin for Vst3Plugin {
|
|||
let k = self.notes.iter().position(|e| e.time() >= end).unwrap_or(self.notes.len());
|
||||
let offset = u32::try_from(start).unwrap_or(u32::MAX);
|
||||
let notes = self.notes.get(..k).unwrap_or(&[]);
|
||||
let r = self.inst.process(n, &mut self.inputs, &mut self.outputs, &self.pending, notes, offset, self.position, self.transport.as_ref());
|
||||
let r = self.inst.process(
|
||||
n,
|
||||
&mut self.inputs,
|
||||
&mut self.outputs,
|
||||
&self.pending,
|
||||
&self.controls,
|
||||
notes,
|
||||
offset,
|
||||
self.position,
|
||||
self.transport.as_ref(),
|
||||
);
|
||||
self.pending.clear();
|
||||
self.notes.drain(..k);
|
||||
self.position = self.position.saturating_add(i64::try_from(n).unwrap_or(0));
|
||||
|
|
@ -280,6 +301,7 @@ impl Plugin for Vst3Plugin {
|
|||
}
|
||||
start += n;
|
||||
}
|
||||
self.controls.clear();
|
||||
}
|
||||
|
||||
fn latency(&self) -> usize {
|
||||
|
|
@ -316,6 +338,26 @@ impl Plugin for Vst3Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
fn midi(&mut self, offset: usize, msg: [u8; 3]) {
|
||||
let time = u32::try_from(offset).unwrap_or(u32::MAX);
|
||||
let [_, d1, d2] = msg.map(|b| b & 0x7F);
|
||||
let (ctrl, value) = match msg[0] & 0xF0 {
|
||||
0xB0 => (usize::from(d1), f64::from(d2) / 127.0),
|
||||
0xD0 => (AFTERTOUCH, f64::from(d1) / 127.0),
|
||||
0xE0 => (PITCH_BEND, f64::from(u16::from(d2) << 7 | u16::from(d1)) / 16383.0),
|
||||
0xA0 if self.event_input => {
|
||||
self.push_note(NoteEvent::Pressure { time, key: d1, pressure: f32::from(d2) / 127.0 });
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
if let Some(&Some(id)) = self.midi_map.get(ctrl)
|
||||
&& self.controls.len() < self.controls.capacity()
|
||||
{
|
||||
self.controls.push((time, id, value));
|
||||
}
|
||||
}
|
||||
|
||||
fn all_notes_off(&mut self) {
|
||||
for k in 0..128u8 {
|
||||
if self.held.get(k as usize).copied().unwrap_or(false) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
//! 320×240, no real drawing) supporting NSView/HWND/X11.
|
||||
//! - "Fixture Synth" (`Instrument|Synth`): a single-component instrument (the component is also
|
||||
//! its edit controller). No audio input, one stereo output, one event input; every output
|
||||
//! sample is the velocity of the held note (sample-accurate), 0 when none.
|
||||
//! sample is the velocity of the held note (sample-accurate), 0 when none. Poly pressure, and
|
||||
//! MIDI CC 7 through `IMidiMapping` (hidden parameter 1), set it to their value too.
|
||||
//!
|
||||
//! The module checks the host's module lifecycle the way real plugins depend on it (see "module
|
||||
//! lifecycle" below) and aborts the process when it is broken.
|
||||
|
|
@ -417,7 +418,21 @@ struct Synth {
|
|||
}
|
||||
|
||||
impl Class for Synth {
|
||||
type Interfaces = (IComponent, IAudioProcessor, IEditController);
|
||||
type Interfaces = (IComponent, IAudioProcessor, IEditController, IMidiMapping);
|
||||
}
|
||||
|
||||
/// The synth's hidden parameter that MIDI CC 7 drives.
|
||||
const VOLUME_PARAM: ParamID = 1;
|
||||
|
||||
impl IMidiMappingTrait for Synth {
|
||||
unsafe fn getMidiControllerAssignment(&self, bus: i32, channel: i16, ctrl: CtrlNumber, id: *mut ParamID) -> tresult {
|
||||
if bus == 0 && channel == 0 && ctrl == 7 {
|
||||
unsafe { *id = VOLUME_PARAM };
|
||||
kResultOk
|
||||
} else {
|
||||
kResultFalse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IPluginBaseTrait for Synth {
|
||||
|
|
@ -521,6 +536,16 @@ impl IAudioProcessorTrait for Synth {
|
|||
}
|
||||
}
|
||||
}
|
||||
let mut volume = None;
|
||||
if let Some(changes) = unsafe { ComRef::from_raw(d.inputParameterChanges) } {
|
||||
for i in 0..unsafe { changes.getParameterCount() } {
|
||||
let Some(q) = (unsafe { ComRef::from_raw(changes.getParameterData(i)) }) else { continue };
|
||||
let (mut at, mut v) = (0i32, 0f64);
|
||||
if unsafe { q.getParameterId() } == VOLUME_PARAM && unsafe { q.getPoint(0, &mut at, &mut v) } == kResultOk {
|
||||
volume = Some((at.max(0) as usize, v as f32));
|
||||
}
|
||||
}
|
||||
}
|
||||
let outs = unsafe { channels(&*d.outputs, frames) };
|
||||
let mut level = f32::from_bits(self.level.load(Ordering::Relaxed));
|
||||
let mut out = vec![0.0f32; frames];
|
||||
|
|
@ -530,8 +555,15 @@ impl IAudioProcessorTrait for Synth {
|
|||
level = unsafe { e.__field0.noteOn.velocity };
|
||||
} else if e.r#type == Event_::EventTypes_::kNoteOffEvent as u16 {
|
||||
level = 0.0;
|
||||
} else if e.r#type == Event_::EventTypes_::kPolyPressureEvent as u16 {
|
||||
level = unsafe { e.__field0.polyPressure.pressure };
|
||||
}
|
||||
}
|
||||
if let Some((at, v)) = volume
|
||||
&& at == i
|
||||
{
|
||||
level = v;
|
||||
}
|
||||
*o = level;
|
||||
}
|
||||
self.level.store(level.to_bits(), Ordering::Relaxed);
|
||||
|
|
|
|||
|
|
@ -121,6 +121,16 @@ fn synth_plays_notes_at_their_offsets() {
|
|||
p.all_notes_off();
|
||||
p.process(&mut io, 64);
|
||||
assert!(io[0].iter().all(|x| x.abs() < 1e-6));
|
||||
// MIDI CC 7 reaches the parameter the plugin maps it to, and poly pressure arrives as an event,
|
||||
// each at its offset; an unmapped controller is dropped.
|
||||
p.midi(10, [0xA0, 60, 32]);
|
||||
p.midi(20, [0xB0, 7, 64]);
|
||||
p.midi(30, [0xB0, 1, 127]);
|
||||
p.process(&mut io, 64);
|
||||
assert!(io[0][9].abs() < 1e-6 && (io[0][10] - 32.0 / 127.0).abs() < 1e-6);
|
||||
assert!((io[0][20] - 64.0 / 127.0).abs() < 1e-6 && (io[0][63] - 64.0 / 127.0).abs() < 1e-6);
|
||||
p.midi(0, [0xB0, 7, 0]);
|
||||
p.process(&mut io, 64);
|
||||
// Notes in a later sub-block of a chunked call land at the right frame.
|
||||
p.prepare(48_000.0, 32, 2);
|
||||
let mut io = vec![vec![0.0f32; 96], vec![0.0f32; 96]];
|
||||
|
|
|
|||
Loading…
Reference in New Issue