Add a way to output note events
This supports all note events supported by NIH-plug, and both CLAP and VST3.
This commit is contained in:
parent
1f8db9d9c2
commit
65d87f87ed
9 changed files with 584 additions and 15 deletions
|
@ -94,8 +94,9 @@ for download links.
|
||||||
- A simple and safe API for state saving and restoring from the editor is
|
- A simple and safe API for state saving and restoring from the editor is
|
||||||
provided by the framework if you want to do your own internal preset
|
provided by the framework if you want to do your own internal preset
|
||||||
management.
|
management.
|
||||||
- Full support for both modern polyphonic note expressions as well as MIDI CCs,
|
- Full support for receiving and outputting both modern polyphonic note
|
||||||
channel pressure, and pitch bend for CLAP and VST3.
|
expression events as well as MIDI CCs, channel pressure, and pitch bend for
|
||||||
|
CLAP and VST3.
|
||||||
- A plugin bundler accessible through the
|
- A plugin bundler accessible through the
|
||||||
`cargo xtask bundle <package> <build_arguments>` command that automatically
|
`cargo xtask bundle <package> <build_arguments>` command that automatically
|
||||||
detects which plugin targets your plugin exposes and creates the correct
|
detects which plugin targets your plugin exposes and creates the correct
|
||||||
|
|
|
@ -53,6 +53,11 @@ pub trait ProcessContext {
|
||||||
/// ```
|
/// ```
|
||||||
fn next_event(&mut self) -> Option<NoteEvent>;
|
fn next_event(&mut self) -> Option<NoteEvent>;
|
||||||
|
|
||||||
|
/// Send an event to the host. Only available when
|
||||||
|
/// [`Plugin::MIDI_OUTPUT`][crate::prelude::Plugin::MIDI_INPUT] is set. Will not do anything
|
||||||
|
/// otherwise.
|
||||||
|
fn send_event(&mut self, event: NoteEvent);
|
||||||
|
|
||||||
/// Update the current latency of the plugin. If the plugin is currently processing audio, then
|
/// Update the current latency of the plugin. If the plugin is currently processing audio, then
|
||||||
/// this may cause audio playback to be restarted.
|
/// this may cause audio playback to be restarted.
|
||||||
fn set_latency_samples(&self, samples: u32);
|
fn set_latency_samples(&self, samples: u32);
|
||||||
|
|
|
@ -40,9 +40,14 @@ pub trait Plugin: Default + Send + Sync + 'static {
|
||||||
/// instead of setting up the busses properly.
|
/// instead of setting up the busses properly.
|
||||||
const DEFAULT_NUM_OUTPUTS: u32 = 2;
|
const DEFAULT_NUM_OUTPUTS: u32 = 2;
|
||||||
|
|
||||||
/// Whether the plugin accepts note events, and which level of . If this is set to
|
/// Whether the plugin accepts note events, and what which events it wants to receive. If this
|
||||||
/// [`MidiConfig::None`], then the plugin won't receive any note events.
|
/// is set to [`MidiConfig::None`], then the plugin won't receive any note events.
|
||||||
const MIDI_INPUT: MidiConfig = MidiConfig::None;
|
const MIDI_INPUT: MidiConfig = MidiConfig::None;
|
||||||
|
/// Whether the plugin can output note events. If this is set to [`MidiConfig::None`], then the
|
||||||
|
/// plugin won't have a note output port. When this is set to another value, then in most hsots
|
||||||
|
/// the plugin will consume all note and MIDI CC input. If you don't want that, then you will
|
||||||
|
/// need to forward those events yourself.
|
||||||
|
const MIDI_OUTPUT: MidiConfig = MidiConfig::None;
|
||||||
/// If enabled, the audio processing cycle may be split up into multiple smaller chunks if
|
/// If enabled, the audio processing cycle may be split up into multiple smaller chunks if
|
||||||
/// parameter values change occur in the middle of the buffer. Depending on the host these
|
/// parameter values change occur in the middle of the buffer. Depending on the host these
|
||||||
/// blocks may be as small as a single sample. Bitwig Studio sends at most one parameter change
|
/// blocks may be as small as a single sample. Bitwig Studio sends at most one parameter change
|
||||||
|
|
|
@ -23,6 +23,7 @@ pub(crate) struct WrapperGuiContext<P: ClapPlugin> {
|
||||||
pub(crate) struct WrapperProcessContext<'a, P: ClapPlugin> {
|
pub(crate) struct WrapperProcessContext<'a, P: ClapPlugin> {
|
||||||
pub(super) wrapper: &'a Wrapper<P>,
|
pub(super) wrapper: &'a Wrapper<P>,
|
||||||
pub(super) input_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
|
pub(super) input_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
|
||||||
|
pub(super) output_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
|
||||||
pub(super) transport: Transport,
|
pub(super) transport: Transport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +100,10 @@ impl<P: ClapPlugin> ProcessContext for WrapperProcessContext<'_, P> {
|
||||||
self.input_events_guard.pop_front()
|
self.input_events_guard.pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_event(&mut self, event: NoteEvent) {
|
||||||
|
self.output_events_guard.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
fn set_latency_samples(&self, samples: u32) {
|
fn set_latency_samples(&self, samples: u32) {
|
||||||
// Only make a callback if it's actually needed
|
// Only make a callback if it's actually needed
|
||||||
// XXX: For CLAP we could move this handling to the Plugin struct, but it may be worthwhile
|
// XXX: For CLAP we could move this handling to the Plugin struct, but it may be worthwhile
|
||||||
|
|
|
@ -32,6 +32,7 @@ use clap_sys::ext::gui::{
|
||||||
use clap_sys::ext::latency::{clap_host_latency, clap_plugin_latency, CLAP_EXT_LATENCY};
|
use clap_sys::ext::latency::{clap_host_latency, clap_plugin_latency, CLAP_EXT_LATENCY};
|
||||||
use clap_sys::ext::note_ports::{
|
use clap_sys::ext::note_ports::{
|
||||||
clap_note_port_info, clap_plugin_note_ports, CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP,
|
clap_note_port_info, clap_plugin_note_ports, CLAP_EXT_NOTE_PORTS, CLAP_NOTE_DIALECT_CLAP,
|
||||||
|
CLAP_NOTE_DIALECT_MIDI,
|
||||||
};
|
};
|
||||||
use clap_sys::ext::params::{
|
use clap_sys::ext::params::{
|
||||||
clap_host_params, clap_param_info, clap_plugin_params, CLAP_EXT_PARAMS,
|
clap_host_params, clap_param_info, clap_plugin_params, CLAP_EXT_PARAMS,
|
||||||
|
@ -128,6 +129,9 @@ pub struct Wrapper<P: ClapPlugin> {
|
||||||
/// TODO: Maybe load these lazily at some point instead of needing to spool them all to this
|
/// TODO: Maybe load these lazily at some point instead of needing to spool them all to this
|
||||||
/// queue first
|
/// queue first
|
||||||
input_events: AtomicRefCell<VecDeque<NoteEvent>>,
|
input_events: AtomicRefCell<VecDeque<NoteEvent>>,
|
||||||
|
/// Stores any events the plugin has output during the current processing cycle, analogous to
|
||||||
|
/// `input_events`.
|
||||||
|
output_events: AtomicRefCell<VecDeque<NoteEvent>>,
|
||||||
/// The last process status returned by the plugin. This is used for tail handling.
|
/// The last process status returned by the plugin. This is used for tail handling.
|
||||||
last_process_status: AtomicCell<ProcessStatus>,
|
last_process_status: AtomicCell<ProcessStatus>,
|
||||||
/// The current latency in samples, as set by the plugin through the [`ProcessContext`]. uses
|
/// The current latency in samples, as set by the plugin through the [`ProcessContext`]. uses
|
||||||
|
@ -478,6 +482,7 @@ impl<P: ClapPlugin> Wrapper<P> {
|
||||||
}),
|
}),
|
||||||
current_buffer_config: AtomicCell::new(None),
|
current_buffer_config: AtomicCell::new(None),
|
||||||
input_events: AtomicRefCell::new(VecDeque::with_capacity(512)),
|
input_events: AtomicRefCell::new(VecDeque::with_capacity(512)),
|
||||||
|
output_events: AtomicRefCell::new(VecDeque::with_capacity(512)),
|
||||||
last_process_status: AtomicCell::new(ProcessStatus::Normal),
|
last_process_status: AtomicCell::new(ProcessStatus::Normal),
|
||||||
current_latency: AtomicU32::new(0),
|
current_latency: AtomicU32::new(0),
|
||||||
output_buffer: AtomicRefCell::new(Buffer::default()),
|
output_buffer: AtomicRefCell::new(Buffer::default()),
|
||||||
|
@ -580,6 +585,7 @@ impl<P: ClapPlugin> Wrapper<P> {
|
||||||
WrapperProcessContext {
|
WrapperProcessContext {
|
||||||
wrapper: self,
|
wrapper: self,
|
||||||
input_events_guard: self.input_events.borrow_mut(),
|
input_events_guard: self.input_events.borrow_mut(),
|
||||||
|
output_events_guard: self.output_events.borrow_mut(),
|
||||||
transport,
|
transport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -831,6 +837,301 @@ impl<P: ClapPlugin> Wrapper<P> {
|
||||||
if parameter_values_changed {
|
if parameter_values_changed {
|
||||||
self.notify_param_values_changed();
|
self.notify_param_values_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also send all note events generated by the plugin
|
||||||
|
let mut output_events = self.output_events.borrow_mut();
|
||||||
|
while let Some(event) = output_events.pop_front() {
|
||||||
|
let time = event.timing() + current_sample_idx as u32;
|
||||||
|
|
||||||
|
let push_successful = match event {
|
||||||
|
NoteEvent::NoteOn {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
velocity,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_ON,
|
||||||
|
// We don't have a way to denote live events
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
velocity: velocity as f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::NoteOff {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
velocity,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_OFF,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
velocity: velocity as f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::PolyPressure {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
pressure,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note_expression {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_EXPRESSION,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
expression_id: CLAP_NOTE_EXPRESSION_PRESSURE,
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
value: pressure as f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::PolyVolume {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
gain,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note_expression {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_EXPRESSION,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
expression_id: CLAP_NOTE_EXPRESSION_VOLUME,
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
value: gain as f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::PolyPan {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
pan,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note_expression {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_EXPRESSION,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
expression_id: CLAP_NOTE_EXPRESSION_PAN,
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
value: (pan as f64 + 1.0) / 2.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::PolyTuning {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
tuning,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note_expression {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_EXPRESSION,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
expression_id: CLAP_NOTE_EXPRESSION_TUNING,
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
value: tuning as f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::PolyVibrato {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
vibrato,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note_expression {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_EXPRESSION,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
expression_id: CLAP_NOTE_EXPRESSION_VIBRATO,
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
value: vibrato as f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::PolyExpression {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
expression,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note_expression {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_EXPRESSION,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
expression_id: CLAP_NOTE_EXPRESSION_EXPRESSION,
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
value: expression as f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::PolyBrightness {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
brightness,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
let event = clap_event_note_expression {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_NOTE_EXPRESSION,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
expression_id: CLAP_NOTE_EXPRESSION_BRIGHTNESS,
|
||||||
|
port_index: 0,
|
||||||
|
key: note as i16,
|
||||||
|
channel: channel as i16,
|
||||||
|
value: brightness as f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::MidiChannelPressure {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
pressure,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::MidiCCs => {
|
||||||
|
let event = clap_event_midi {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_MIDI,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
port_index: 0,
|
||||||
|
data: [
|
||||||
|
midi::CHANNEL_KEY_PRESSURE | channel as u8,
|
||||||
|
(pressure * 127.0).round() as u8,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::MidiPitchBend {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
value,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::MidiCCs => {
|
||||||
|
let scaled = (value * ((1 << 14) - 1) as f32).round() as i32;
|
||||||
|
|
||||||
|
let event = clap_event_midi {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_MIDI,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
port_index: 0,
|
||||||
|
data: [
|
||||||
|
midi::PITCH_BEND_CHANGE | channel as u8,
|
||||||
|
(scaled & 0b01111111) as u8,
|
||||||
|
((scaled >> 7) & 0b01111111) as u8,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
NoteEvent::MidiCC {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
cc,
|
||||||
|
value,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::MidiCCs => {
|
||||||
|
let event = clap_event_midi {
|
||||||
|
header: clap_event_header {
|
||||||
|
size: mem::size_of::<clap_event_param_gesture>() as u32,
|
||||||
|
time,
|
||||||
|
space_id: CLAP_CORE_EVENT_SPACE_ID,
|
||||||
|
type_: CLAP_EVENT_MIDI,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
port_index: 0,
|
||||||
|
data: [
|
||||||
|
midi::CONTROL_CHANGE | channel as u8,
|
||||||
|
cc,
|
||||||
|
(value * 127.0).round() as u8,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
(out.try_push)(out, &event.header)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
nih_debug_assert_failure!(
|
||||||
|
"Invalid output event for the current MIDI_OUTPUT setting"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nih_debug_assert!(push_successful, "Could not send note event");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle an incoming CLAP event. The sample index is provided to support block splitting for
|
/// Handle an incoming CLAP event. The sample index is provided to support block splitting for
|
||||||
|
@ -1462,7 +1763,8 @@ impl<P: ClapPlugin> Wrapper<P> {
|
||||||
ProcessStatus::KeepAlive => CLAP_PROCESS_CONTINUE,
|
ProcessStatus::KeepAlive => CLAP_PROCESS_CONTINUE,
|
||||||
};
|
};
|
||||||
|
|
||||||
// After processing audio, send all spooled events to the host
|
// After processing audio, send all spooled events to the host. This include note
|
||||||
|
// events.
|
||||||
if !process.out_events.is_null() {
|
if !process.out_events.is_null() {
|
||||||
wrapper.handle_out_events(&*process.out_events, block_start);
|
wrapper.handle_out_events(&*process.out_events, block_start);
|
||||||
}
|
}
|
||||||
|
@ -2031,6 +2333,7 @@ impl<P: ClapPlugin> Wrapper<P> {
|
||||||
// TODO: Outputting notes
|
// TODO: Outputting notes
|
||||||
match is_input {
|
match is_input {
|
||||||
true if P::MIDI_INPUT >= MidiConfig::Basic => 1,
|
true if P::MIDI_INPUT >= MidiConfig::Basic => 1,
|
||||||
|
false if P::MIDI_OUTPUT >= MidiConfig::Basic => 1,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2047,14 +2350,30 @@ impl<P: ClapPlugin> Wrapper<P> {
|
||||||
|
|
||||||
let info = &mut *info;
|
let info = &mut *info;
|
||||||
info.id = 0;
|
info.id = 0;
|
||||||
// TODO: Implement MIDI CC handling
|
// TODO: Implement MPE (would just be a toggle for the plugin to expose it) and MIDI2
|
||||||
// TODO: Implement MPE and MIDI2
|
|
||||||
info.supported_dialects = CLAP_NOTE_DIALECT_CLAP;
|
info.supported_dialects = CLAP_NOTE_DIALECT_CLAP;
|
||||||
|
if P::MIDI_INPUT >= MidiConfig::MidiCCs {
|
||||||
|
info.supported_dialects |= CLAP_NOTE_DIALECT_MIDI;
|
||||||
|
}
|
||||||
info.preferred_dialect = CLAP_NOTE_DIALECT_CLAP;
|
info.preferred_dialect = CLAP_NOTE_DIALECT_CLAP;
|
||||||
strlcpy(&mut info.name, "Note Input");
|
strlcpy(&mut info.name, "Note Input");
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
(0, false) if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
*info = std::mem::zeroed();
|
||||||
|
|
||||||
|
let info = &mut *info;
|
||||||
|
info.id = 0;
|
||||||
|
info.supported_dialects = CLAP_NOTE_DIALECT_CLAP;
|
||||||
|
if P::MIDI_OUTPUT >= MidiConfig::MidiCCs {
|
||||||
|
info.supported_dialects |= CLAP_NOTE_DIALECT_MIDI;
|
||||||
|
}
|
||||||
|
info.preferred_dialect = CLAP_NOTE_DIALECT_CLAP;
|
||||||
|
strlcpy(&mut info.name, "Note Output");
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ pub(crate) struct WrapperGuiContext<P: Vst3Plugin> {
|
||||||
pub(crate) struct WrapperProcessContext<'a, P: Vst3Plugin> {
|
pub(crate) struct WrapperProcessContext<'a, P: Vst3Plugin> {
|
||||||
pub(super) inner: &'a WrapperInner<P>,
|
pub(super) inner: &'a WrapperInner<P>,
|
||||||
pub(super) input_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
|
pub(super) input_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
|
||||||
|
pub(super) output_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
|
||||||
pub(super) transport: Transport,
|
pub(super) transport: Transport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +109,10 @@ impl<P: Vst3Plugin> ProcessContext for WrapperProcessContext<'_, P> {
|
||||||
self.input_events_guard.pop_front()
|
self.input_events_guard.pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_event(&mut self, event: NoteEvent) {
|
||||||
|
self.output_events_guard.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
fn set_latency_samples(&self, samples: u32) {
|
fn set_latency_samples(&self, samples: u32) {
|
||||||
// Only trigger a restart if it's actually needed
|
// Only trigger a restart if it's actually needed
|
||||||
let old_latency = self.inner.current_latency.swap(samples, Ordering::SeqCst);
|
let old_latency = self.inner.current_latency.swap(samples, Ordering::SeqCst);
|
||||||
|
|
|
@ -82,6 +82,9 @@ pub(crate) struct WrapperInner<P: Vst3Plugin> {
|
||||||
/// interleave parameter changes and note events, this queue has to be sorted when
|
/// interleave parameter changes and note events, this queue has to be sorted when
|
||||||
/// creating the process context
|
/// creating the process context
|
||||||
pub input_events: AtomicRefCell<VecDeque<NoteEvent>>,
|
pub input_events: AtomicRefCell<VecDeque<NoteEvent>>,
|
||||||
|
/// Stores any events the plugin has output during the current processing cycle, analogous to
|
||||||
|
/// `input_events`.
|
||||||
|
pub output_events: AtomicRefCell<VecDeque<NoteEvent>>,
|
||||||
/// VST3 has several useful predefined note expressions, but for some reason they are the only
|
/// VST3 has several useful predefined note expressions, but for some reason they are the only
|
||||||
/// note event type that don't have MIDI note ID and channel fields. So we need to keep track of
|
/// note event type that don't have MIDI note ID and channel fields. So we need to keep track of
|
||||||
/// the msot recent VST3 note IDs we've seen, and then map those back to MIDI note IDs and
|
/// the msot recent VST3 note IDs we've seen, and then map those back to MIDI note IDs and
|
||||||
|
@ -273,6 +276,7 @@ impl<P: Vst3Plugin> WrapperInner<P> {
|
||||||
current_latency: AtomicU32::new(0),
|
current_latency: AtomicU32::new(0),
|
||||||
output_buffer: AtomicRefCell::new(Buffer::default()),
|
output_buffer: AtomicRefCell::new(Buffer::default()),
|
||||||
input_events: AtomicRefCell::new(VecDeque::with_capacity(1024)),
|
input_events: AtomicRefCell::new(VecDeque::with_capacity(1024)),
|
||||||
|
output_events: AtomicRefCell::new(VecDeque::with_capacity(1024)),
|
||||||
note_expression_controller: AtomicRefCell::new(NoteExpressionController::default()),
|
note_expression_controller: AtomicRefCell::new(NoteExpressionController::default()),
|
||||||
process_events: AtomicRefCell::new(Vec::with_capacity(4096)),
|
process_events: AtomicRefCell::new(Vec::with_capacity(4096)),
|
||||||
updated_state_sender,
|
updated_state_sender,
|
||||||
|
@ -303,6 +307,7 @@ impl<P: Vst3Plugin> WrapperInner<P> {
|
||||||
WrapperProcessContext {
|
WrapperProcessContext {
|
||||||
inner: self,
|
inner: self,
|
||||||
input_events_guard: self.input_events.borrow_mut(),
|
input_events_guard: self.input_events.borrow_mut(),
|
||||||
|
output_events_guard: self.output_events.borrow_mut(),
|
||||||
transport,
|
transport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,4 +98,46 @@ impl NoteExpressionController {
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Translate a NIH-plug note expression event a VST3 `NoteExpressionValueEvent`. Will return
|
||||||
|
/// `None` if the event is not a polyphonic expression event, i.e. one of the events handled by
|
||||||
|
/// `translate_event()`.
|
||||||
|
pub fn translate_event_reverse(
|
||||||
|
note_id: i32,
|
||||||
|
event: &NoteEvent,
|
||||||
|
) -> Option<NoteExpressionValueEvent> {
|
||||||
|
match &event {
|
||||||
|
NoteEvent::PolyVolume { gain, .. } => Some(NoteExpressionValueEvent {
|
||||||
|
type_id: 0, // kVolumeTypeID
|
||||||
|
note_id,
|
||||||
|
value: *gain as f64 / 4.0,
|
||||||
|
}),
|
||||||
|
NoteEvent::PolyPan { pan, .. } => Some(NoteExpressionValueEvent {
|
||||||
|
type_id: 1, // kPanTypeId
|
||||||
|
note_id,
|
||||||
|
value: (*pan as f64 + 1.0) / 2.0,
|
||||||
|
}),
|
||||||
|
NoteEvent::PolyTuning { tuning, .. } => Some(NoteExpressionValueEvent {
|
||||||
|
type_id: 2, // kTuningTypeID
|
||||||
|
note_id,
|
||||||
|
value: (*tuning as f64 / 240.0) + 0.5,
|
||||||
|
}),
|
||||||
|
NoteEvent::PolyVibrato { vibrato, .. } => Some(NoteExpressionValueEvent {
|
||||||
|
type_id: 3, // kVibratoTypeID
|
||||||
|
note_id,
|
||||||
|
value: *vibrato as f64,
|
||||||
|
}),
|
||||||
|
NoteEvent::PolyExpression { expression, .. } => Some(NoteExpressionValueEvent {
|
||||||
|
type_id: 4, // kExpressionTypeID
|
||||||
|
note_id,
|
||||||
|
value: *expression as f64,
|
||||||
|
}),
|
||||||
|
NoteEvent::PolyBrightness { brightness, .. } => Some(NoteExpressionValueEvent {
|
||||||
|
type_id: 5, // kBrightnessTypeID
|
||||||
|
note_id,
|
||||||
|
value: *brightness as f64,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,9 @@ use vst3_sys::base::{kInvalidArgument, kNoInterface, kResultFalse, kResultOk, tr
|
||||||
use vst3_sys::base::{IBStream, IPluginBase};
|
use vst3_sys::base::{IBStream, IPluginBase};
|
||||||
use vst3_sys::utils::SharedVstPtr;
|
use vst3_sys::utils::SharedVstPtr;
|
||||||
use vst3_sys::vst::{
|
use vst3_sys::vst::{
|
||||||
kNoProgramListId, kRootUnitId, EventTypes, IAudioProcessor, IComponent, IEditController,
|
kNoProgramListId, kRootUnitId, Event, EventTypes, IAudioProcessor, IComponent, IEditController,
|
||||||
IEventList, IMidiMapping, IParamValueQueue, IParameterChanges, IUnitInfo, ParameterFlags,
|
IEventList, IMidiMapping, IParamValueQueue, IParameterChanges, IUnitInfo, LegacyMidiCCOutEvent,
|
||||||
ProgramListInfo, TChar, UnitInfo,
|
NoteOffEvent, NoteOnEvent, ParameterFlags, PolyPressureEvent, ProgramListInfo, TChar, UnitInfo,
|
||||||
};
|
};
|
||||||
use vst3_sys::VST3;
|
use vst3_sys::VST3;
|
||||||
use widestring::U16CStr;
|
use widestring::U16CStr;
|
||||||
|
@ -26,6 +26,7 @@ use crate::util::permit_alloc;
|
||||||
use crate::wrapper::state;
|
use crate::wrapper::state;
|
||||||
use crate::wrapper::util::{process_wrapper, u16strlcpy};
|
use crate::wrapper::util::{process_wrapper, u16strlcpy};
|
||||||
use crate::wrapper::vst3::inner::ProcessEvent;
|
use crate::wrapper::vst3::inner::ProcessEvent;
|
||||||
|
use crate::wrapper::vst3::note_expressions::NoteExpressionController;
|
||||||
use crate::wrapper::vst3::util::{VST3_MIDI_CHANNELS, VST3_MIDI_PARAMS_END};
|
use crate::wrapper::vst3::util::{VST3_MIDI_CHANNELS, VST3_MIDI_PARAMS_END};
|
||||||
|
|
||||||
// Alias needed for the VST3 attribute macro
|
// Alias needed for the VST3 attribute macro
|
||||||
|
@ -78,6 +79,12 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
|
||||||
{
|
{
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
x if x == vst3_sys::vst::MediaTypes::kEvent as i32
|
||||||
|
&& dir == vst3_sys::vst::BusDirections::kOutput as i32
|
||||||
|
&& P::MIDI_OUTPUT >= MidiConfig::Basic =>
|
||||||
|
{
|
||||||
|
1
|
||||||
|
}
|
||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,7 +137,23 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
|
||||||
info.media_type = vst3_sys::vst::MediaTypes::kEvent as i32;
|
info.media_type = vst3_sys::vst::MediaTypes::kEvent as i32;
|
||||||
info.direction = vst3_sys::vst::BusDirections::kInput as i32;
|
info.direction = vst3_sys::vst::BusDirections::kInput as i32;
|
||||||
info.channel_count = 16;
|
info.channel_count = 16;
|
||||||
u16strlcpy(&mut info.name, "MIDI");
|
u16strlcpy(&mut info.name, "Note Input");
|
||||||
|
info.bus_type = vst3_sys::vst::BusTypes::kMain as i32;
|
||||||
|
info.flags = vst3_sys::vst::BusFlags::kDefaultActive as u32;
|
||||||
|
kResultOk
|
||||||
|
}
|
||||||
|
(t, d, 0)
|
||||||
|
if t == vst3_sys::vst::MediaTypes::kEvent as i32
|
||||||
|
&& d == vst3_sys::vst::BusDirections::kOutput as i32
|
||||||
|
&& P::MIDI_OUTPUT >= MidiConfig::Basic =>
|
||||||
|
{
|
||||||
|
*info = mem::zeroed();
|
||||||
|
|
||||||
|
let info = &mut *info;
|
||||||
|
info.media_type = vst3_sys::vst::MediaTypes::kEvent as i32;
|
||||||
|
info.direction = vst3_sys::vst::BusDirections::kOutput as i32;
|
||||||
|
info.channel_count = 16;
|
||||||
|
u16strlcpy(&mut info.name, "Note Output");
|
||||||
info.bus_type = vst3_sys::vst::BusTypes::kMain as i32;
|
info.bus_type = vst3_sys::vst::BusTypes::kMain as i32;
|
||||||
info.flags = vst3_sys::vst::BusFlags::kDefaultActive as u32;
|
info.flags = vst3_sys::vst::BusFlags::kDefaultActive as u32;
|
||||||
kResultOk
|
kResultOk
|
||||||
|
@ -158,6 +181,17 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
|
||||||
|
|
||||||
kResultOk
|
kResultOk
|
||||||
}
|
}
|
||||||
|
(t, 0)
|
||||||
|
if t == vst3_sys::vst::MediaTypes::kEvent as i32
|
||||||
|
&& P::MIDI_INPUT >= MidiConfig::Basic
|
||||||
|
&& P::MIDI_OUTPUT >= MidiConfig::Basic =>
|
||||||
|
{
|
||||||
|
out_info.media_type = vst3_sys::vst::MediaTypes::kEvent as i32;
|
||||||
|
out_info.bus_index = in_info.bus_index;
|
||||||
|
out_info.channel = in_info.channel;
|
||||||
|
|
||||||
|
kResultOk
|
||||||
|
}
|
||||||
_ => kInvalidArgument,
|
_ => kInvalidArgument,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +213,13 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
|
||||||
{
|
{
|
||||||
kResultOk
|
kResultOk
|
||||||
}
|
}
|
||||||
|
(t, d, 0)
|
||||||
|
if t == vst3_sys::vst::MediaTypes::kEvent as i32
|
||||||
|
&& d == vst3_sys::vst::BusDirections::kOutput as i32
|
||||||
|
&& P::MIDI_OUTPUT >= MidiConfig::Basic =>
|
||||||
|
{
|
||||||
|
kResultOk
|
||||||
|
}
|
||||||
_ => kInvalidArgument,
|
_ => kInvalidArgument,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1029,11 +1070,152 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut plugin = self.inner.plugin.write();
|
let result = {
|
||||||
let mut context = self.inner.make_process_context(transport);
|
let mut plugin = self.inner.plugin.write();
|
||||||
|
let mut context = self.inner.make_process_context(transport);
|
||||||
let result = plugin.process(&mut output_buffer, &mut context);
|
plugin.process(&mut output_buffer, &mut context)
|
||||||
|
};
|
||||||
self.inner.last_process_status.store(result);
|
self.inner.last_process_status.store(result);
|
||||||
|
|
||||||
|
// Send any events output by the plugin during the process cycle
|
||||||
|
if let Some(events) = data.output_events.upgrade() {
|
||||||
|
let mut output_events = self.inner.output_events.borrow_mut();
|
||||||
|
while let Some(event) = output_events.pop_front() {
|
||||||
|
// We'll set the correct variant on this struct, or skip to the next
|
||||||
|
// loop iteration if we don't handle the event type
|
||||||
|
let mut vst3_event: Event = mem::zeroed();
|
||||||
|
vst3_event.bus_index = 0;
|
||||||
|
// There's also a ppqPos field, but uh how about no
|
||||||
|
vst3_event.sample_offset = event.timing() as i32 + block_start as i32;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
NoteEvent::NoteOn {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
velocity,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
vst3_event.type_ = EventTypes::kNoteOnEvent as u16;
|
||||||
|
vst3_event.event.note_on = NoteOnEvent {
|
||||||
|
channel: channel as i16,
|
||||||
|
pitch: note as i16,
|
||||||
|
tuning: 0.0,
|
||||||
|
velocity,
|
||||||
|
length: 0, // What?
|
||||||
|
// We'll use this for our note IDs, that way we don't have
|
||||||
|
// to do anything complicated here
|
||||||
|
note_id: ((channel as i32) << 8) | note as i32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
NoteEvent::NoteOff {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
velocity,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
vst3_event.type_ = EventTypes::kNoteOffEvent as u16;
|
||||||
|
vst3_event.event.note_off = NoteOffEvent {
|
||||||
|
channel: channel as i16,
|
||||||
|
pitch: note as i16,
|
||||||
|
velocity,
|
||||||
|
note_id: ((channel as i32) << 8) | note as i32,
|
||||||
|
tuning: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
NoteEvent::PolyPressure {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
pressure,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::Basic => {
|
||||||
|
vst3_event.type_ = EventTypes::kPolyPressureEvent as u16;
|
||||||
|
vst3_event.event.poly_pressure = PolyPressureEvent {
|
||||||
|
channel: channel as i16,
|
||||||
|
pitch: note as i16,
|
||||||
|
note_id: ((channel as i32) << 8) | note as i32,
|
||||||
|
pressure,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
event @ (NoteEvent::PolyVolume { channel, note, .. }
|
||||||
|
| NoteEvent::PolyPan { channel, note, .. }
|
||||||
|
| NoteEvent::PolyTuning { channel, note, .. }
|
||||||
|
| NoteEvent::PolyVibrato { channel, note, .. }
|
||||||
|
| NoteEvent::PolyExpression { channel, note, .. }
|
||||||
|
| NoteEvent::PolyBrightness { channel, note, .. })
|
||||||
|
if P::MIDI_OUTPUT >= MidiConfig::Basic =>
|
||||||
|
{
|
||||||
|
match NoteExpressionController::translate_event_reverse(
|
||||||
|
((channel as i32) << 8) | note as i32,
|
||||||
|
&event,
|
||||||
|
) {
|
||||||
|
Some(translated_event) => {
|
||||||
|
vst3_event.type_ =
|
||||||
|
EventTypes::kNoteExpressionValueEvent as u16;
|
||||||
|
vst3_event.event.note_expression_value =
|
||||||
|
translated_event;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
nih_debug_assert_failure!(
|
||||||
|
"Mishandled note expression value event"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NoteEvent::MidiChannelPressure {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
pressure,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::MidiCCs => {
|
||||||
|
vst3_event.type_ = EventTypes::kLegacyMIDICCOutEvent as u16;
|
||||||
|
vst3_event.event.legacy_midi_cc_out = LegacyMidiCCOutEvent {
|
||||||
|
control_number: 128, // kAfterTouch
|
||||||
|
channel: channel as i8,
|
||||||
|
value: (pressure * 127.0).round() as i8,
|
||||||
|
value2: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
NoteEvent::MidiPitchBend {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
value,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::MidiCCs => {
|
||||||
|
let scaled = (value * ((1 << 14) - 1) as f32).round() as i32;
|
||||||
|
|
||||||
|
vst3_event.type_ = EventTypes::kLegacyMIDICCOutEvent as u16;
|
||||||
|
vst3_event.event.legacy_midi_cc_out = LegacyMidiCCOutEvent {
|
||||||
|
control_number: 129, // kPitchBend
|
||||||
|
channel: channel as i8,
|
||||||
|
value: (scaled & 0b01111111) as i8,
|
||||||
|
value2: ((scaled >> 7) & 0b01111111) as i8,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
NoteEvent::MidiCC {
|
||||||
|
timing: _,
|
||||||
|
channel,
|
||||||
|
cc,
|
||||||
|
value,
|
||||||
|
} if P::MIDI_OUTPUT >= MidiConfig::MidiCCs => {
|
||||||
|
vst3_event.type_ = EventTypes::kLegacyMIDICCOutEvent as u16;
|
||||||
|
vst3_event.event.legacy_midi_cc_out = LegacyMidiCCOutEvent {
|
||||||
|
control_number: cc,
|
||||||
|
channel: channel as i8,
|
||||||
|
value: (value * 127.0).round() as i8,
|
||||||
|
value2: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
nih_debug_assert_failure!(
|
||||||
|
"Invalid output event for the current MIDI_OUTPUT setting"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = events.add_event(&mut vst3_event);
|
||||||
|
nih_debug_assert_eq!(result, kResultOk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
ProcessStatus::Error(err) => {
|
ProcessStatus::Error(err) => {
|
||||||
nih_debug_assert_failure!("Process error: {}", err);
|
nih_debug_assert_failure!("Process error: {}", err);
|
||||||
|
|
Loading…
Add table
Reference in a new issue