conformal_vst_wrapper 0.6.5

Implements a VST3-compatible plug-in for audio processors implemented with the conformal audio plug-in framework.
use vst3::{
    ComRef,
    Steinberg::Vst::{IEventList, IEventListTrait},
};

use conformal_component::{
    events::{Data, Event, NoteData, NoteID},
    synth::NumericPerNoteExpression,
};

use crate::{mpe, u32_to_enum};

unsafe fn get_event(
    event_list: ComRef<'_, IEventList>,
    index: i32,
) -> Option<vst3::Steinberg::Vst::Event> {
    unsafe {
        let mut event = vst3::Steinberg::Vst::Event {
            busIndex: 0,
            sampleOffset: 0,
            ppqPosition: 0.0,
            flags: 0,
            r#type: 0,
            __field0: vst3::Steinberg::Vst::Event__type0 {
                noteOn: vst3::Steinberg::Vst::NoteOnEvent {
                    channel: 0,
                    pitch: 0,
                    tuning: 0.0,
                    velocity: 0.0,
                    length: 0,
                    noteId: 0,
                },
            },
        };
        let result = event_list.getEvent(index, &raw mut event);
        if result != vst3::Steinberg::kResultOk {
            return None;
        }
        Some(event)
    }
}

fn get_note_id(pitch: u8, channel: i16, note_id: i32) -> NoteID {
    if note_id != -1 {
        return NoteID::from_id(note_id);
    }
    if channel != 0 {
        NoteID::from_channel_id(channel)
    } else {
        NoteID::from_pitch(pitch)
    }
}

fn get_mpe_note_id(note_id: i32) -> Option<i32> {
    if note_id == -1 { None } else { Some(note_id) }
}

unsafe fn convert_event(event: &vst3::Steinberg::Vst::Event) -> Option<Event> {
    unsafe {
        if event.sampleOffset < 0 {
            return None;
        }
        match u32_to_enum(u32::from(event.r#type)) {
            Ok(vst3::Steinberg::Vst::Event_::EventTypes_::kNoteOnEvent) => {
                let pitch = u8::try_from(event.__field0.noteOn.pitch).ok()?;
                let channel = event.__field0.noteOn.channel;
                Some(Event {
                    sample_offset: event.sampleOffset as usize,
                    data: Data::NoteOn {
                        data: NoteData {
                            pitch,
                            tuning: event.__field0.noteOn.tuning,
                            velocity: event.__field0.noteOn.velocity,
                            id: get_note_id(pitch, channel, event.__field0.noteOn.noteId),
                        },
                    },
                })
            }
            Ok(vst3::Steinberg::Vst::Event_::EventTypes_::kNoteOffEvent) => {
                let pitch = u8::try_from(event.__field0.noteOff.pitch).ok()?;
                let channel = event.__field0.noteOff.channel;
                Some(Event {
                    sample_offset: event.sampleOffset as usize,
                    data: Data::NoteOff {
                        data: NoteData {
                            pitch,
                            tuning: event.__field0.noteOff.tuning,
                            velocity: event.__field0.noteOff.velocity,
                            id: get_note_id(pitch, channel, event.__field0.noteOff.noteId),
                        },
                    },
                })
            }
            _ => None,
        }
    }
}

unsafe fn convert_mpe_event(event: &vst3::Steinberg::Vst::Event) -> Option<mpe::NoteEvent> {
    unsafe {
        if event.sampleOffset < 0 {
            return None;
        }
        match u32_to_enum(u32::from(event.r#type)) {
            Ok(vst3::Steinberg::Vst::Event_::EventTypes_::kNoteOnEvent) => {
                let note_id = event.__field0.noteOn.noteId;
                let note_id = get_mpe_note_id(note_id)?;
                Some(mpe::NoteEvent {
                    sample_offset: event.sampleOffset as usize,
                    data: mpe::NoteEventData::On { note_id },
                })
            }
            Ok(vst3::Steinberg::Vst::Event_::EventTypes_::kNoteOffEvent) => {
                let note_id = event.__field0.noteOff.noteId;
                let note_id = get_mpe_note_id(note_id)?;
                Some(mpe::NoteEvent {
                    sample_offset: event.sampleOffset as usize,
                    data: mpe::NoteEventData::Off { note_id },
                })
            }
            Ok(vst3::Steinberg::Vst::Event_::EventTypes_::kPolyPressureEvent) => {
                let note_id = get_mpe_note_id(event.__field0.polyPressure.noteId)?;
                let pressure = event.__field0.polyPressure.pressure;
                Some(mpe::NoteEvent {
                    sample_offset: event.sampleOffset as usize,
                    data: mpe::NoteEventData::ExpressionChange {
                        note_id,
                        expression: NumericPerNoteExpression::Aftertouch,
                        value: pressure,
                    },
                })
            }
            Ok(vst3::Steinberg::Vst::Event_::EventTypes_::kNoteExpressionValueEvent) => {
                let note_id = event.__field0.noteExpressionValue.noteId;
                let note_id = get_mpe_note_id(note_id)?;
                let expression = match event.__field0.noteExpressionValue.typeId {
                    vst3::Steinberg::Vst::NoteExpressionTypeIDs_::kTuningTypeID => {
                        NumericPerNoteExpression::PitchBend
                    }
                    super::NOTE_EXPRESSION_TIMBRE_TYPE_ID => NumericPerNoteExpression::Timbre,
                    _ => return None,
                };
                #[allow(clippy::cast_possible_truncation)]
                let value = match expression {
                    NumericPerNoteExpression::PitchBend => {
                        (event.__field0.noteExpressionValue.value as f32 - 0.5) * 240.0
                    }
                    NumericPerNoteExpression::Timbre | NumericPerNoteExpression::Aftertouch => {
                        event.__field0.noteExpressionValue.value as f32
                    }
                };
                Some(mpe::NoteEvent {
                    sample_offset: event.sampleOffset as usize,
                    data: mpe::NoteEventData::ExpressionChange {
                        note_id,
                        expression,
                        value,
                    },
                })
            }
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use conformal_component::events::{Data, NoteData, NoteIDInternals};

    use super::*;

    fn note_on_event(channel: i16, note_id: i32, pitch: i16) -> vst3::Steinberg::Vst::Event {
        vst3::Steinberg::Vst::Event {
            busIndex: 0,
            sampleOffset: 0,
            ppqPosition: 0.0,
            flags: 0,
            r#type: vst3::Steinberg::Vst::Event_::EventTypes_::kNoteOnEvent as u16,
            __field0: vst3::Steinberg::Vst::Event__type0 {
                noteOn: vst3::Steinberg::Vst::NoteOnEvent {
                    channel,
                    pitch,
                    tuning: 0.0,
                    velocity: 0.5,
                    length: 0,
                    noteId: note_id,
                },
            },
        }
    }

    fn note_off_event(channel: i16, note_id: i32, pitch: i16) -> vst3::Steinberg::Vst::Event {
        vst3::Steinberg::Vst::Event {
            busIndex: 0,
            sampleOffset: 0,
            ppqPosition: 0.0,
            flags: 0,
            r#type: vst3::Steinberg::Vst::Event_::EventTypes_::kNoteOffEvent as u16,
            __field0: vst3::Steinberg::Vst::Event__type0 {
                noteOff: vst3::Steinberg::Vst::NoteOffEvent {
                    channel,
                    pitch,
                    tuning: 0.0,
                    velocity: 0.5,
                    noteId: note_id,
                },
            },
        }
    }

    #[test]
    fn note_ids_prefer_vst_note_id_over_channel() {
        let event = note_on_event(3, 42, 64);
        let converted = unsafe { convert_event(&event) }.unwrap();
        assert_eq!(
            converted,
            Event {
                sample_offset: 0,
                data: Data::NoteOn {
                    data: NoteData {
                        id: NoteID::from_id(42),
                        pitch: 64,
                        velocity: 0.5,
                        tuning: 0.0,
                    },
                },
            }
        );
    }

    #[test]
    fn official_mpe_note_on_accepts_nonzero_channel_when_note_id_present() {
        let event = note_on_event(7, 42, 64);
        let converted = unsafe { convert_mpe_event(&event) }.unwrap();
        match converted.data {
            mpe::NoteEventData::On { note_id } => assert_eq!(note_id, 42),
            _ => panic!("expected note on"),
        }
    }

    #[test]
    fn official_mpe_note_off_accepts_nonzero_channel_when_note_id_present() {
        let event = note_off_event(7, 42, 64);
        let converted = unsafe { convert_mpe_event(&event) }.unwrap();
        match converted.data {
            mpe::NoteEventData::Off { note_id } => assert_eq!(note_id, 42),
            _ => panic!("expected note off"),
        }
    }

    #[test]
    fn quirks_note_without_note_id_still_uses_channel_id() {
        let event = note_on_event(7, -1, 64);
        let converted = unsafe { convert_event(&event) }.unwrap();
        match converted.data {
            Data::NoteOn { data } => {
                assert_eq!(data.id.internals, NoteIDInternals::NoteIDFromChannelID(7));
            }
            _ => panic!("expected note on"),
        }
        assert!(unsafe { convert_mpe_event(&event) }.is_none());
    }
}

pub unsafe fn event_iterator(
    event_list: ComRef<'_, IEventList>,
) -> impl Iterator<Item = Event> + Clone {
    unsafe {
        (0..event_list.getEventCount()).filter_map(move |i| -> Option<Event> {
            get_event(event_list, i)
                .as_ref()
                .and_then(|x| convert_event(x))
        })
    }
}

pub unsafe fn all_zero_event_iterator(
    event_list: ComRef<'_, IEventList>,
) -> Option<impl Iterator<Item = Data> + Clone> {
    unsafe {
        let i = (0..event_list.getEventCount()).filter_map(move |i| -> Option<Event> {
            get_event(event_list, i)
                .as_ref()
                .and_then(|x| convert_event(x))
        });
        if i.clone().any(|x| x.sample_offset != 0) {
            None
        } else {
            Some(i.map(|x| x.data))
        }
    }
}

pub unsafe fn mpe_event_iterator(
    event_list: ComRef<'_, IEventList>,
) -> impl Iterator<Item = mpe::NoteEvent> + Clone {
    unsafe {
        (0..event_list.getEventCount()).filter_map(move |i| -> Option<mpe::NoteEvent> {
            get_event(event_list, i)
                .as_ref()
                .and_then(|x| convert_mpe_event(x))
        })
    }
}

pub unsafe fn all_zero_mpe_event_iterator(
    event_list: ComRef<'_, IEventList>,
) -> Option<impl Iterator<Item = mpe::NoteEventData> + Clone> {
    unsafe {
        let i = (0..event_list.getEventCount()).filter_map(move |i| -> Option<mpe::NoteEvent> {
            get_event(event_list, i)
                .as_ref()
                .and_then(|x| convert_mpe_event(x))
        });
        if i.clone().any(|x| x.sample_offset != 0) {
            None
        } else {
            Some(i.map(|x| x.data))
        }
    }
}