beamer_core/
plugin.rs

1//! Core plugin trait definitions.
2
3use crate::buffer::{AuxiliaryBuffers, Buffer};
4use crate::error::PluginResult;
5use crate::midi::{
6    KeyswitchInfo, Midi2Controller, MidiBuffer, MidiEvent, MpeInputDeviceSettings,
7    NoteExpressionTypeInfo, PhysicalUIMap,
8};
9use crate::midi_params::MidiCcParams;
10use crate::params::Parameters;
11use crate::process_context::ProcessContext;
12
13// =============================================================================
14// Bus Configuration
15// =============================================================================
16
17/// Audio bus type.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum BusType {
20    /// Main audio bus (e.g., primary stereo input/output).
21    #[default]
22    Main,
23    /// Auxiliary bus (e.g., sidechain input).
24    Aux,
25}
26
27/// Information about an audio bus.
28#[derive(Debug, Clone)]
29pub struct BusInfo {
30    /// Display name for the bus (e.g., "Input", "Sidechain").
31    pub name: &'static str,
32    /// Bus type (main or auxiliary).
33    pub bus_type: BusType,
34    /// Number of channels in this bus.
35    pub channel_count: u32,
36    /// Whether the bus is active by default.
37    pub is_default_active: bool,
38}
39
40impl Default for BusInfo {
41    fn default() -> Self {
42        Self {
43            name: "Main",
44            bus_type: BusType::Main,
45            channel_count: 2,
46            is_default_active: true,
47        }
48    }
49}
50
51impl BusInfo {
52    /// Create a stereo main bus.
53    pub const fn stereo(name: &'static str) -> Self {
54        Self {
55            name,
56            bus_type: BusType::Main,
57            channel_count: 2,
58            is_default_active: true,
59        }
60    }
61
62    /// Create a mono main bus.
63    pub const fn mono(name: &'static str) -> Self {
64        Self {
65            name,
66            bus_type: BusType::Main,
67            channel_count: 1,
68            is_default_active: true,
69        }
70    }
71
72    /// Create an auxiliary bus (e.g., sidechain).
73    pub const fn aux(name: &'static str, channel_count: u32) -> Self {
74        Self {
75            name,
76            bus_type: BusType::Aux,
77            channel_count,
78            is_default_active: false,
79        }
80    }
81}
82
83// =============================================================================
84// AudioProcessor Trait
85// =============================================================================
86
87/// Core trait for audio processing logic.
88///
89/// This trait defines the DSP (Digital Signal Processing) interface that
90/// plugin implementations must provide. It is designed to be format-agnostic,
91/// meaning the same implementation can be wrapped for VST3, CLAP, or other
92/// plugin formats.
93///
94/// # Thread Safety
95///
96/// Implementors must be `Send` because the plugin may be moved between threads.
97/// The `process` method is called on the audio thread and must be real-time safe:
98/// - No allocations
99/// - No locks (use lock-free structures)
100/// - No syscalls
101/// - No unbounded loops
102pub trait AudioProcessor: Send {
103    /// Called when audio processing setup changes.
104    ///
105    /// This is called before audio processing begins, whenever the sample rate
106    /// or maximum block size changes. Use this to initialize buffers, filters,
107    /// or other sample-rate dependent state.
108    ///
109    /// # Arguments
110    /// * `sample_rate` - The sample rate in Hz (e.g., 44100.0, 48000.0)
111    /// * `max_buffer_size` - Maximum number of samples per process call
112    fn setup(&mut self, sample_rate: f64, max_buffer_size: usize);
113
114    /// Process an audio buffer with transport context.
115    ///
116    /// This is the main DSP entry point, called on the audio thread for each
117    /// block of audio. The buffer provides input samples and mutable output
118    /// buffers for the main bus.
119    ///
120    /// # Arguments
121    ///
122    /// * `buffer` - Main audio bus (stereo/surround input and output)
123    /// * `aux` - Auxiliary buses (sidechain, aux sends) - ignore if not needed
124    /// * `context` - Processing context with sample rate, buffer size, and transport info
125    ///
126    /// # Real-Time Safety
127    ///
128    /// This method must be real-time safe. Do not allocate, lock mutexes,
129    /// or perform any operation with unbounded execution time.
130    ///
131    /// # Example: Simple Gain
132    ///
133    /// ```ignore
134    /// fn process(&mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, _context: &ProcessContext) {
135    ///     let gain = self.params.gain();
136    ///     for (input, output) in buffer.zip_channels() {
137    ///         for (i, o) in input.iter().zip(output.iter_mut()) {
138    ///             *o = *i * gain;
139    ///         }
140    ///     }
141    /// }
142    /// ```
143    ///
144    /// # Example: Tempo-Synced LFO
145    ///
146    /// ```ignore
147    /// fn process(&mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, context: &ProcessContext) {
148    ///     // Calculate LFO rate synced to host tempo
149    ///     let lfo_hz = context.transport.tempo
150    ///         .map(|tempo| tempo / 60.0 / 4.0)  // 1 cycle per 4 beats
151    ///         .unwrap_or(2.0);                   // Fallback: 2 Hz
152    ///
153    ///     let increment = (lfo_hz * 2.0 * std::f32::consts::PI) / context.sample_rate as f32;
154    ///
155    ///     for (input, output) in buffer.zip_channels() {
156    ///         for (i, o) in input.iter().zip(output.iter_mut()) {
157    ///             let lfo = self.phase.sin();
158    ///             *o = *i * (1.0 + lfo * 0.5);
159    ///             self.phase += increment;
160    ///         }
161    ///     }
162    /// }
163    /// ```
164    ///
165    /// # Example: Sidechain Ducker
166    ///
167    /// ```ignore
168    /// fn process(&mut self, buffer: &mut Buffer, aux: &mut AuxiliaryBuffers, _context: &ProcessContext) {
169    ///     let duck = aux.sidechain()
170    ///         .map(|sc| (sc.rms(0) * 4.0).min(1.0))
171    ///         .unwrap_or(0.0);
172    ///
173    ///     buffer.copy_to_output();
174    ///     buffer.apply_output_gain(1.0 - duck * 0.8);
175    /// }
176    /// ```
177    fn process(&mut self, buffer: &mut Buffer, aux: &mut AuxiliaryBuffers, context: &ProcessContext);
178
179    /// Called when the plugin is activated or deactivated.
180    ///
181    /// Activation typically happens when the user inserts the plugin into a
182    /// track or opens a project. Deactivation happens when removed or project
183    /// is closed.
184    ///
185    /// Default implementation does nothing.
186    fn set_active(&mut self, _active: bool) {}
187
188    /// Get the tail length in samples.
189    ///
190    /// This indicates how many samples of audio "tail" the plugin produces
191    /// after input stops (e.g., reverb decay). Return 0 for no tail, or
192    /// `u32::MAX` for infinite tail.
193    ///
194    /// Default returns 0 (no tail).
195    fn tail_samples(&self) -> u32 {
196        0
197    }
198
199    /// Get the latency in samples.
200    ///
201    /// If the plugin introduces processing latency (e.g., lookahead limiters),
202    /// return the latency in samples here. The host can use this for delay
203    /// compensation.
204    ///
205    /// Default returns 0 (no latency).
206    fn latency_samples(&self) -> u32 {
207        0
208    }
209
210    /// Get the bypass ramp length in samples.
211    ///
212    /// When bypass is engaged or disengaged, this defines the crossfade
213    /// duration to avoid clicks. The host uses this value (combined with
214    /// `tail_samples()`) to determine how long to continue calling `process()`
215    /// after input stops.
216    ///
217    /// Return 0 for instant bypass (no crossfade), or a sample count for
218    /// smooth crossfading. Typical values:
219    /// - 64 samples (~1.3ms at 48kHz) - fast, suitable for most effects
220    /// - 256 samples (~5.3ms at 48kHz) - smoother, for sensitive material
221    /// - 512+ samples - very smooth, for reverbs/delays with long tails
222    ///
223    /// Default returns 64 samples.
224    ///
225    /// # Example
226    ///
227    /// ```ignore
228    /// fn bypass_ramp_samples(&self) -> u32 {
229    ///     // Use 10ms crossfade based on current sample rate
230    ///     (self.sample_rate * 0.01) as u32
231    /// }
232    /// ```
233    fn bypass_ramp_samples(&self) -> u32 {
234        64
235    }
236
237    // =========================================================================
238    // 64-bit Processing Support
239    // =========================================================================
240
241    /// Returns true if the plugin supports native 64-bit (double precision) processing.
242    ///
243    /// Override this to return `true` if your plugin implements `process_f64()` natively.
244    /// When false (default), the framework will automatically convert 64-bit host buffers
245    /// to 32-bit, call `process()`, and convert back.
246    ///
247    /// # Performance Considerations
248    ///
249    /// - For most plugins, f32 is sufficient and the default conversion is fine
250    /// - Implement native f64 only if your DSP algorithm benefits from double precision
251    ///   (e.g., IIR filters with long decay, precision-sensitive synthesis)
252    /// - The conversion overhead is minimal (~few microseconds per buffer)
253    ///
254    /// Default returns `false`.
255    fn supports_double_precision(&self) -> bool {
256        false
257    }
258
259    /// Process an audio buffer at 64-bit (double) precision.
260    ///
261    /// This is the f64 equivalent of `process()`. Override this method AND
262    /// return `true` from `supports_double_precision()` to enable native
263    /// 64-bit processing.
264    ///
265    /// If `supports_double_precision()` returns `false`, this method is never
266    /// called - the framework converts to f32 and calls `process()` instead.
267    ///
268    /// # Default Implementation
269    ///
270    /// The default implementation converts f64→f32, calls `process()`, then
271    /// converts f32→f64. This allows any plugin to work in a 64-bit host
272    /// without modification.
273    ///
274    /// # Example: Native f64 Plugin
275    ///
276    /// ```ignore
277    /// fn supports_double_precision(&self) -> bool {
278    ///     true
279    /// }
280    ///
281    /// fn process_f64(
282    ///     &mut self,
283    ///     buffer: &mut Buffer<f64>,
284    ///     aux: &mut AuxiliaryBuffers<f64>,
285    ///     context: &ProcessContext,
286    /// ) {
287    ///     let gain = self.params.gain_linear() as f64;
288    ///     for (input, output) in buffer.zip_channels() {
289    ///         for (i, o) in input.iter().zip(output.iter_mut()) {
290    ///             *o = *i * gain;
291    ///         }
292    ///     }
293    /// }
294    /// ```
295    fn process_f64(
296        &mut self,
297        buffer: &mut Buffer<f64>,
298        _aux: &mut AuxiliaryBuffers<f64>,
299        context: &ProcessContext,
300    ) {
301        // Default implementation: convert f64 → f32, process, convert back
302        //
303        // NOTE: This is a fallback implementation that allocates memory.
304        // In practice, this method is rarely called because:
305        // - The VST3 wrapper handles conversion with pre-allocated buffers
306        //   (see `process_audio_f64_converted` in beamer-vst3/src/processor.rs)
307        // - Future format wrappers (CLAP, etc.) should also pre-allocate
308        //
309        // If you're implementing a custom wrapper, ensure you handle
310        // f64→f32 conversion with pre-allocated buffers for real-time safety.
311
312        let num_samples = buffer.num_samples();
313        let num_input_channels = buffer.num_input_channels();
314        let num_output_channels = buffer.num_output_channels();
315
316        // Allocate conversion buffers (VST3 wrapper uses pre-allocated buffers,
317        // this is only for the fallback default implementation)
318        let input_f32: Vec<Vec<f32>> = (0..num_input_channels)
319            .map(|ch| buffer.input(ch).iter().map(|&s| s as f32).collect())
320            .collect();
321        let mut output_f32: Vec<Vec<f32>> = (0..num_output_channels)
322            .map(|_| vec![0.0f32; num_samples])
323            .collect();
324
325        // Build f32 buffer slices
326        let input_slices: Vec<&[f32]> = input_f32.iter().map(|v| v.as_slice()).collect();
327        let output_slices: Vec<&mut [f32]> = output_f32
328            .iter_mut()
329            .map(|v| v.as_mut_slice())
330            .collect();
331
332        let mut buffer_f32 = Buffer::new(input_slices, output_slices, num_samples);
333
334        // For aux buffers, we use empty for now (full aux conversion is complex)
335        // The VST3 wrapper handles proper aux conversion
336        let mut aux_f32: AuxiliaryBuffers<f32> = AuxiliaryBuffers::empty();
337
338        // Process at f32
339        self.process(&mut buffer_f32, &mut aux_f32, context);
340
341        // Convert output back to f64
342        for (ch, output_samples) in output_f32.iter().enumerate().take(num_output_channels) {
343            let output_ch = buffer.output(ch);
344            for (i, sample) in output_samples.iter().enumerate() {
345                output_ch[i] = *sample as f64;
346            }
347        }
348    }
349
350    /// Save the plugin state to bytes.
351    ///
352    /// This is called when the DAW saves a project or preset. The returned
353    /// bytes should contain all state needed to restore the plugin to its
354    /// current configuration.
355    ///
356    /// Default returns an empty vector.
357    fn save_state(&self) -> PluginResult<Vec<u8>> {
358        Ok(Vec::new())
359    }
360
361    /// Load the plugin state from bytes.
362    ///
363    /// This is called when the DAW loads a project or preset. The data is
364    /// the same bytes returned from a previous `save_state` call.
365    ///
366    /// Default does nothing.
367    fn load_state(&mut self, _data: &[u8]) -> PluginResult<()> {
368        Ok(())
369    }
370
371    // =========================================================================
372    // Bus Configuration
373    // =========================================================================
374
375    /// Returns the number of audio input buses.
376    ///
377    /// Default returns 1 (single stereo input).
378    fn input_bus_count(&self) -> usize {
379        1
380    }
381
382    /// Returns the number of audio output buses.
383    ///
384    /// Default returns 1 (single stereo output).
385    fn output_bus_count(&self) -> usize {
386        1
387    }
388
389    /// Returns information about an input bus.
390    ///
391    /// Default returns a stereo main bus for index 0.
392    fn input_bus_info(&self, index: usize) -> Option<BusInfo> {
393        if index == 0 {
394            Some(BusInfo::stereo("Input"))
395        } else {
396            None
397        }
398    }
399
400    /// Returns information about an output bus.
401    ///
402    /// Default returns a stereo main bus for index 0.
403    fn output_bus_info(&self, index: usize) -> Option<BusInfo> {
404        if index == 0 {
405            Some(BusInfo::stereo("Output"))
406        } else {
407            None
408        }
409    }
410
411    // =========================================================================
412    // MIDI Processing
413    // =========================================================================
414
415    /// Process MIDI events.
416    ///
417    /// Called during processing with any incoming MIDI events. Plugins can
418    /// transform events and add them to the output buffer, pass them through
419    /// unchanged, or consume them entirely.
420    ///
421    /// # Arguments
422    /// * `input` - Slice of incoming MIDI events (sorted by sample_offset)
423    /// * `output` - Buffer to write output MIDI events to
424    ///
425    /// # Real-Time Safety
426    ///
427    /// This method must be real-time safe. Do not allocate, lock mutexes,
428    /// or perform any operation with unbounded execution time.
429    ///
430    /// **Note:** Cloning a `SysEx` event allocates due to `Box<SysEx>`. SysEx
431    /// events are rare in typical use cases. If strict real-time safety is
432    /// required, override this method to handle SysEx specially.
433    ///
434    /// # Default Implementation
435    ///
436    /// The default implementation passes all events through unchanged.
437    fn process_midi(&mut self, input: &[MidiEvent], output: &mut MidiBuffer) {
438        for event in input {
439            output.push(event.clone());
440        }
441    }
442
443    /// Returns whether this plugin processes MIDI events.
444    ///
445    /// Override to return `true` if your plugin needs MIDI input/output.
446    /// This is used by the host to determine event bus configuration.
447    ///
448    /// Default returns `false`.
449    fn wants_midi(&self) -> bool {
450        false
451    }
452}
453
454// =============================================================================
455// Plugin Trait
456// =============================================================================
457
458/// Main plugin trait combining audio processing and parameters.
459///
460/// This is the primary trait that plugin authors implement to create a complete
461/// audio plugin. It combines [`AudioProcessor`] for DSP with a [`Parameters`]
462/// collection for host communication.
463///
464/// # Example
465///
466/// ```ignore
467/// use beamer_core::{Plugin, AudioProcessor, Buffer, AuxiliaryBuffers, Parameters, ProcessContext};
468///
469/// pub struct MyGain {
470///     params: MyGainParams,
471/// }
472///
473/// impl AudioProcessor for MyGain {
474///     fn setup(&mut self, _sample_rate: f64, _max_buffer_size: usize) {}
475///
476///     fn process(&mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, _context: &ProcessContext) {
477///         let gain = self.params.gain_linear();
478///         for (input, output) in buffer.zip_channels() {
479///             for (i, o) in input.iter().zip(output.iter_mut()) {
480///                 *o = *i * gain;
481///             }
482///         }
483///     }
484/// }
485///
486/// impl Plugin for MyGain {
487///     type Params = MyGainParams;
488///
489///     fn params(&self) -> &Self::Params {
490///         &self.params
491///     }
492///
493///     fn create() -> Self {
494///         Self { params: MyGainParams::new() }
495///     }
496/// }
497/// ```
498pub trait Plugin: AudioProcessor {
499    /// The parameter collection type for this plugin.
500    type Params: Parameters + crate::params::Units + crate::param_types::Params;
501
502    /// Returns a reference to the plugin's parameters.
503    ///
504    /// The VST3 wrapper uses this to communicate parameter values with the host.
505    fn params(&self) -> &Self::Params;
506
507    /// Returns a mutable reference to the plugin's parameters.
508    ///
509    /// Used by the framework for operations like resetting smoothers after
510    /// loading state. Most plugins can use the default implementation.
511    fn params_mut(&mut self) -> &mut Self::Params;
512
513    /// Creates a new instance of the plugin with default state.
514    ///
515    /// Called by the host when instantiating the plugin.
516    fn create() -> Self
517    where
518        Self: Sized;
519
520    // =========================================================================
521    // MIDI Mapping (IMidiMapping)
522    // =========================================================================
523
524    /// Get the parameter ID mapped to a MIDI CC.
525    ///
526    /// Override this to enable DAW MIDI learn for your parameters. When the
527    /// DAW queries which parameter is assigned to a MIDI CC, this method is
528    /// called.
529    ///
530    /// # Arguments
531    /// * `bus_index` - MIDI bus index (usually 0)
532    /// * `channel` - MIDI channel (0-15), or -1 to query all channels
533    /// * `cc` - MIDI CC number (0-127)
534    ///
535    /// # Returns
536    /// `Some(param_id)` if this CC is mapped to a parameter, `None` otherwise.
537    ///
538    /// # Example
539    /// ```ignore
540    /// fn midi_cc_to_param(&self, _bus: i32, _channel: i16, cc: u8) -> Option<u32> {
541    ///     match cc {
542    ///         cc::MOD_WHEEL => Some(PARAM_VIBRATO_DEPTH),
543    ///         cc::EXPRESSION => Some(PARAM_VOLUME),
544    ///         _ => None,
545    ///     }
546    /// }
547    /// ```
548    fn midi_cc_to_param(&self, bus_index: i32, channel: i16, cc: u8) -> Option<u32> {
549        let _ = (bus_index, channel, cc);
550        None
551    }
552
553    // =========================================================================
554    // MIDI CC Emulation (VST3 IMidiMapping hidden parameters)
555    // =========================================================================
556
557    /// Returns MIDI CC parameters for automatic host mapping.
558    ///
559    /// Override to enable MIDI CC/pitch bend/aftertouch reception via IMidiMapping.
560    /// The framework will create hidden parameters that receive CC values from
561    /// the host and convert them to MidiEvents before calling process_midi().
562    ///
563    /// This solves the VST3 MIDI input problem where most DAWs don't send
564    /// `kLegacyMIDICCOutEvent` for input. Instead, they use the `IMidiMapping`
565    /// interface to map MIDI controllers to parameters.
566    ///
567    /// # Example
568    ///
569    /// ```ignore
570    /// fn midi_cc_params(&self) -> Option<&MidiCcParams> {
571    ///     Some(&self.midi_cc_params)
572    /// }
573    ///
574    /// fn create() -> Self {
575    ///     Self {
576    ///         params: MyParams::default(),
577    ///         midi_cc_params: MidiCcParams::new()
578    ///             .with_pitch_bend()
579    ///             .with_mod_wheel()
580    ///             .with_ccs(&[7, 10, 11, 64]),
581    ///     }
582    /// }
583    /// ```
584    fn midi_cc_params(&self) -> Option<&MidiCcParams> {
585        None
586    }
587
588    // =========================================================================
589    // MIDI Learn (IMidiLearn)
590    // =========================================================================
591
592    /// Called by DAW when live MIDI CC input is received during learn mode.
593    ///
594    /// Override this to implement MIDI learn in your plugin UI. When the user
595    /// enables "MIDI Learn" mode and moves a MIDI CC knob, the DAW calls this
596    /// method so the plugin can map that CC to a parameter.
597    ///
598    /// # Arguments
599    /// * `bus_index` - MIDI bus index (usually 0)
600    /// * `channel` - MIDI channel (0-15)
601    /// * `cc` - MIDI CC number that was moved
602    ///
603    /// # Returns
604    /// `true` if the input was handled (learned), `false` otherwise.
605    fn on_midi_learn(&mut self, bus_index: i32, channel: i16, cc: u8) -> bool {
606        let _ = (bus_index, channel, cc);
607        false
608    }
609
610    // =========================================================================
611    // MIDI 2.0 Mapping (IMidiMapping2)
612    // =========================================================================
613
614    /// Get all MIDI 1.0 CC assignments for bulk query.
615    ///
616    /// Override to provide mappings for DAW queries. This is more efficient
617    /// than individual `midi_cc_to_param` queries when there are many mappings.
618    ///
619    /// Default returns empty slice (no mappings).
620    fn midi1_assignments(&self) -> &[Midi1Assignment] {
621        &[]
622    }
623
624    /// Get all MIDI 2.0 controller assignments for bulk query.
625    ///
626    /// Override to provide MIDI 2.0 Registered/Assignable controller mappings.
627    ///
628    /// Default returns empty slice (no mappings).
629    fn midi2_assignments(&self) -> &[Midi2Assignment] {
630        &[]
631    }
632
633    // =========================================================================
634    // MIDI 2.0 Learn (IMidiLearn2)
635    // =========================================================================
636
637    /// Called when MIDI 1.0 CC input is received during learn mode.
638    ///
639    /// This is the MIDI 2.0 version of `on_midi_learn` with separate methods
640    /// for MIDI 1.0 and MIDI 2.0 controllers.
641    ///
642    /// Default returns `false` (not handled).
643    fn on_midi1_learn(&mut self, bus_index: i32, channel: u8, cc: u8) -> bool {
644        let _ = (bus_index, channel, cc);
645        false
646    }
647
648    /// Called when MIDI 2.0 controller input is received during learn mode.
649    ///
650    /// Override to implement MIDI 2.0 controller learning.
651    ///
652    /// Default returns `false` (not handled).
653    fn on_midi2_learn(&mut self, bus_index: i32, channel: u8, controller: Midi2Controller) -> bool {
654        let _ = (bus_index, channel, controller);
655        false
656    }
657
658    // =========================================================================
659    // Note Expression Controller (INoteExpressionController - VST3 SDK 3.5.0)
660    // =========================================================================
661
662    /// Returns the number of supported note expression types.
663    ///
664    /// Override to advertise which note expressions your plugin supports
665    /// (e.g., volume, pan, tuning for MPE instruments).
666    ///
667    /// Default returns 0 (no note expressions).
668    fn note_expression_count(&self, bus_index: i32, channel: i16) -> usize {
669        let _ = (bus_index, channel);
670        0
671    }
672
673    /// Returns information about a note expression type by index.
674    ///
675    /// Override to provide details about each supported expression type.
676    ///
677    /// Default returns None.
678    fn note_expression_info(
679        &self,
680        bus_index: i32,
681        channel: i16,
682        index: usize,
683    ) -> Option<NoteExpressionTypeInfo> {
684        let _ = (bus_index, channel, index);
685        None
686    }
687
688    /// Converts a normalized note expression value to a display string.
689    ///
690    /// Override to provide custom formatting (e.g., "2.5 semitones" for tuning).
691    ///
692    /// Default returns the value as a percentage.
693    fn note_expression_value_to_string(
694        &self,
695        bus_index: i32,
696        channel: i16,
697        type_id: u32,
698        value: f64,
699    ) -> String {
700        let _ = (bus_index, channel, type_id);
701        format!("{:.1}%", value * 100.0)
702    }
703
704    /// Parses a string to a normalized note expression value.
705    ///
706    /// Override to support custom parsing.
707    ///
708    /// Default returns None (parsing not supported).
709    fn note_expression_string_to_value(
710        &self,
711        bus_index: i32,
712        channel: i16,
713        type_id: u32,
714        string: &str,
715    ) -> Option<f64> {
716        let _ = (bus_index, channel, type_id, string);
717        None
718    }
719
720    // =========================================================================
721    // Keyswitch Controller (IKeyswitchController - VST3 SDK 3.5.0)
722    // =========================================================================
723
724    /// Returns the number of keyswitches (articulations).
725    ///
726    /// Override for sample libraries and orchestral instruments that
727    /// support keyswitching between articulations.
728    ///
729    /// Default returns 0 (no keyswitches).
730    fn keyswitch_count(&self, bus_index: i32, channel: i16) -> usize {
731        let _ = (bus_index, channel);
732        0
733    }
734
735    /// Returns information about a keyswitch by index.
736    ///
737    /// Override to provide keyswitch details for DAW expression maps.
738    ///
739    /// Default returns None.
740    fn keyswitch_info(&self, bus_index: i32, channel: i16, index: usize) -> Option<KeyswitchInfo> {
741        let _ = (bus_index, channel, index);
742        None
743    }
744
745    // =========================================================================
746    // Physical UI Mapping (INoteExpressionPhysicalUIMapping - VST3 SDK 3.6.11)
747    // =========================================================================
748
749    /// Returns mappings from physical UI controllers to note expressions.
750    ///
751    /// Override to define how MPE controllers (X-axis, Y-axis, Pressure)
752    /// map to your plugin's note expression types.
753    ///
754    /// # Example
755    /// ```ignore
756    /// fn physical_ui_mappings(&self, _bus: i32, _channel: i16) -> &[PhysicalUIMap] {
757    ///     &[
758    ///         PhysicalUIMap::y_axis(note_expression::BRIGHTNESS),
759    ///         PhysicalUIMap::pressure(note_expression::EXPRESSION),
760    ///     ]
761    /// }
762    /// ```
763    ///
764    /// Default returns empty slice (no mappings).
765    fn physical_ui_mappings(&self, bus_index: i32, channel: i16) -> &[PhysicalUIMap] {
766        let _ = (bus_index, channel);
767        &[]
768    }
769
770    // =========================================================================
771    // MPE Wrapper Support (IVst3WrapperMPESupport - VST3 SDK 3.6.12)
772    // =========================================================================
773
774    /// Called to enable or disable MPE input processing.
775    ///
776    /// Override to handle MPE enable/disable notifications from wrappers.
777    ///
778    /// Default does nothing and returns true.
779    fn enable_mpe_input_processing(&mut self, enabled: bool) -> bool {
780        let _ = enabled;
781        true
782    }
783
784    /// Called when the MPE input device settings change.
785    ///
786    /// Override to receive MPE zone configuration from wrappers.
787    ///
788    /// Default does nothing and returns true.
789    fn set_mpe_input_device_settings(&mut self, settings: MpeInputDeviceSettings) -> bool {
790        let _ = settings;
791        true
792    }
793}
794
795// =============================================================================
796// MIDI Mapping Types
797// =============================================================================
798
799/// Base assignment info for MIDI controller → parameter mapping.
800#[derive(Debug, Clone, Copy)]
801pub struct MidiControllerAssignment {
802    /// Parameter ID this controller maps to.
803    pub param_id: u32,
804    /// MIDI bus index.
805    pub bus_index: i32,
806    /// MIDI channel (0-15).
807    pub channel: u8,
808}
809
810/// MIDI 1.0 CC assignment.
811///
812/// Maps a MIDI 1.0 Control Change to a parameter.
813#[derive(Debug, Clone, Copy)]
814pub struct Midi1Assignment {
815    /// Base assignment info (param_id, bus, channel).
816    pub assignment: MidiControllerAssignment,
817    /// CC number (0-127).
818    pub controller: u8,
819}
820
821impl Midi1Assignment {
822    /// Create a new MIDI 1.0 CC assignment.
823    pub const fn new(param_id: u32, bus_index: i32, channel: u8, controller: u8) -> Self {
824        Self {
825            assignment: MidiControllerAssignment {
826                param_id,
827                bus_index,
828                channel,
829            },
830            controller,
831        }
832    }
833
834    /// Create an assignment for the default bus and all channels.
835    pub const fn simple(param_id: u32, controller: u8) -> Self {
836        Self::new(param_id, 0, 0, controller)
837    }
838}
839
840/// MIDI 2.0 controller assignment.
841///
842/// Maps a MIDI 2.0 Registered/Assignable Controller to a parameter.
843#[derive(Debug, Clone, Copy)]
844pub struct Midi2Assignment {
845    /// Base assignment info (param_id, bus, channel).
846    pub assignment: MidiControllerAssignment,
847    /// MIDI 2.0 controller identifier.
848    pub controller: Midi2Controller,
849}
850
851impl Midi2Assignment {
852    /// Create a new MIDI 2.0 controller assignment.
853    pub const fn new(
854        param_id: u32,
855        bus_index: i32,
856        channel: u8,
857        controller: Midi2Controller,
858    ) -> Self {
859        Self {
860            assignment: MidiControllerAssignment {
861                param_id,
862                bus_index,
863                channel,
864            },
865            controller,
866        }
867    }
868
869    /// Create an assignment for the default bus and all channels.
870    pub const fn simple(param_id: u32, controller: Midi2Controller) -> Self {
871        Self::new(param_id, 0, 0, controller)
872    }
873}