diff --git a/main/medium/src/lib.rs b/main/medium/src/lib.rs index ffd7e125..ac429bae 100644 --- a/main/medium/src/lib.rs +++ b/main/medium/src/lib.rs @@ -371,6 +371,9 @@ pub use control_surface::*; mod midi; pub use midi::*; +mod source_midi; +pub use source_midi::*; + mod pcm_source; pub use pcm_source::*; diff --git a/main/medium/src/misc_enums.rs b/main/medium/src/misc_enums.rs index 7323f259..1b175f4a 100644 --- a/main/medium/src/misc_enums.rs +++ b/main/medium/src/misc_enums.rs @@ -5,6 +5,7 @@ use crate::{ }; use crate::util::concat_reaper_strs; +use derive_more::Display; use enumflags2::BitFlags; use helgoboss_midi::{U14, U7}; use reaper_low::raw; @@ -1586,3 +1587,52 @@ impl InsertMediaMode { bits as i32 } } + +/// Represents MediaItemTake midi CC shape kind. +/// +/// # Note +/// +/// If CcShapeKind::Beizer is given to CC event, additional midi event +/// should be put at the same position: +/// 0xF followed by 'CCBZ ' and 5 more bytes represents +/// bezier curve data for the previous MIDI event: +/// - 1 byte for the bezier type (usually 0) +/// - 4 bytes for the bezier tension as a float. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Display)] +pub enum CcShapeKind { + #[default] + Square, + Linear, + SlowStartEnd, + FastStart, + FastEnd, + Beizer, +} +impl CcShapeKind { + /// CcShapeKind from u8. + /// + /// Returns Err if can not find proper variant. + pub fn from_raw(value: u8) -> Result { + match value { + v if v == 0 => Ok(Self::Square), + v if v == 16 => Ok(Self::Linear), + v if v == 32 => Ok(Self::SlowStartEnd), + v if v == 16 | 32 => Ok(Self::FastStart), + v if v == 64 => Ok(Self::FastEnd), + v if v == 16 | 64 => Ok(Self::Beizer), + _ => Err(format!("not a cc shape: {:?}", value)), + } + } + + /// u8 representation of CcShapeKind + pub fn to_raw(&self) -> u8 { + match self { + Self::Square => 0, + Self::Linear => 16, + Self::SlowStartEnd => 32, + Self::FastStart => 16 | 32, + Self::FastEnd => 64, + Self::Beizer => 16 | 64, + } + } +} diff --git a/main/medium/src/misc_newtypes.rs b/main/medium/src/misc_newtypes.rs index 0b2ab6ef..a6901270 100644 --- a/main/medium/src/misc_newtypes.rs +++ b/main/medium/src/misc_newtypes.rs @@ -1118,6 +1118,90 @@ impl TryFrom for PositionInQuarterNotes { } } +impl std::ops::Add for PositionInQuarterNotes { + fn add(self, rhs: Self) -> Self { + PositionInQuarterNotes::new(self.get() + rhs.get()) + } + type Output = Self; +} +impl std::ops::Sub for PositionInQuarterNotes { + fn sub(self, rhs: Self) -> Self { + PositionInQuarterNotes::new(self.get() - rhs.get()) + } + type Output = Self; +} +impl std::ops::Div for PositionInQuarterNotes { + fn div(self, rhs: Self) -> Self { + PositionInQuarterNotes::new(self.get() / rhs.get()) + } + type Output = Self; +} +impl std::ops::Mul for PositionInQuarterNotes { + fn mul(self, rhs: Self) -> Self { + PositionInQuarterNotes::new(self.get() * rhs.get()) + } + type Output = Self; +} + +/// This represents a position expressed as an amount of midi ticks (PPQ). +/// +/// Can be negative, see [`PositionInSeconds`](struct.PositionInSeconds.html). +#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Default, Display)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(try_from = "f64") +)] +pub struct PositionInPpq(pub(crate) f64); + +impl PositionInPpq { + /// Position at 0.0 quarter notes. + pub const ZERO: PositionInPpq = PositionInPpq(0.0); + + fn is_valid(value: f64) -> bool { + !value.is_infinite() && !value.is_nan() + } + + /// Creates a value. + /// + /// # Panics + /// + /// This function panics if the given value is a special number. + pub fn new(value: f64) -> PositionInPpq { + assert!( + Self::is_valid(value), + "{} is not a valid PositionInQn value", + value + ); + PositionInPpq(value) + } + + /// Creates a PositionInQn value without bound checking. + /// + /// # Safety + /// + /// You must ensure that the given value is not a special number. + pub unsafe fn new_unchecked(value: f64) -> PositionInPpq { + PositionInPpq(value) + } + + /// Returns the wrapped value. + pub const fn get(self) -> f64 { + self.0 + } +} + +impl TryFrom for PositionInPpq { + type Error = TryFromGreaterError; + + fn try_from(value: f64) -> Result { + if !Self::is_valid(value) { + return Err(TryFromGreaterError::new("value must be non-special", value)); + } + Ok(PositionInPpq(value)) + } +} + /// This represents a volume measured in decibel. #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Default, Display)] #[cfg_attr( diff --git a/main/medium/src/reaper.rs b/main/medium/src/reaper.rs index 5a2beeb9..7e19c635 100644 --- a/main/medium/src/reaper.rs +++ b/main/medium/src/reaper.rs @@ -7,29 +7,33 @@ use reaper_low::{raw, register_plugin_destroy_hook}; use crate::ProjectContext::CurrentProject; use crate::{ require_non_null_panic, Accel, ActionValueChange, AddFxBehavior, AudioDeviceAttributeKey, - AutoSeekBehavior, AutomationMode, BookmarkId, BookmarkRef, Bpm, ChunkCacheHint, CommandId, Db, - DurationInSeconds, EditMode, EnvChunkName, FxAddByNameBehavior, FxChainVisibility, FxPresetRef, - FxShowInstruction, GangBehavior, GlobalAutomationModeOverride, HelpMode, Hidden, Hwnd, - InitialAction, InputMonitoringMode, InsertMediaFlag, InsertMediaMode, KbdSectionInfo, - MasterTrackBehavior, MeasureMode, MediaItem, MediaItemTake, MediaTrack, MessageBoxResult, - MessageBoxType, MidiImportBehavior, MidiInput, MidiInputDeviceId, MidiOutput, + AutoSeekBehavior, AutomationMode, BookmarkId, BookmarkRef, Bpm, CcShapeKind, ChunkCacheHint, + CommandId, Db, DurationInSeconds, EditMode, EnvChunkName, FxAddByNameBehavior, + FxChainVisibility, FxPresetRef, FxShowInstruction, GangBehavior, GlobalAutomationModeOverride, + HelpMode, Hidden, Hwnd, InitialAction, InputMonitoringMode, InsertMediaFlag, InsertMediaMode, + KbdSectionInfo, MasterTrackBehavior, MeasureMode, MediaItem, MediaItemTake, MediaTrack, + MessageBoxResult, MessageBoxType, MidiImportBehavior, MidiInput, MidiInputDeviceId, MidiOutput, MidiOutputDeviceId, NativeColor, NormalizedPlayRate, NotificationBehavior, OwnedPcmSource, OwnedReaperPitchShift, OwnedReaperResample, PanMode, ParamId, PcmSource, PitchShiftMode, - PitchShiftSubMode, PlaybackSpeedFactor, PluginContext, PositionInBeats, PositionInQuarterNotes, - PositionInSeconds, Progress, ProjectContext, ProjectRef, PromptForActionResult, ReaProject, - ReaperFunctionError, ReaperFunctionResult, ReaperNormalizedFxParamValue, ReaperPanLikeValue, - ReaperPanValue, ReaperPointer, ReaperStr, ReaperString, ReaperStringArg, ReaperVersion, - ReaperVolumeValue, ReaperWidthValue, RecordArmMode, RecordingInput, RequiredViewMode, - ResampleMode, SectionContext, SectionId, SendTarget, SetTrackUiFlags, SoloMode, - StuffMidiMessageTarget, TakeAttributeKey, TimeModeOverride, TimeRangeType, TrackArea, - TrackAttributeKey, TrackDefaultsBehavior, TrackEnvelope, TrackFxChainType, TrackFxLocation, - TrackLocation, TrackMuteOperation, TrackPolarityOperation, TrackRecArmOperation, - TrackSendAttributeKey, TrackSendCategory, TrackSendDirection, TrackSendRef, TrackSoloOperation, - TransferBehavior, UiRefreshBehavior, UndoBehavior, UndoScope, ValueChange, VolumeSliderValue, - WindowContext, + PitchShiftSubMode, PlaybackSpeedFactor, PluginContext, PositionInBeats, PositionInPpq, + PositionInQuarterNotes, PositionInSeconds, Progress, ProjectContext, ProjectRef, + PromptForActionResult, ReaProject, ReaperFunctionError, ReaperFunctionResult, + ReaperNormalizedFxParamValue, ReaperPanLikeValue, ReaperPanValue, ReaperPointer, ReaperStr, + ReaperString, ReaperStringArg, ReaperVersion, ReaperVolumeValue, ReaperWidthValue, + RecordArmMode, RecordingInput, RequiredViewMode, ResampleMode, SectionContext, SectionId, + SendTarget, SetTrackUiFlags, SoloMode, SourceMidiEvent, SourceMidiEventBuilder, + SourceMidiEventConsumer, SourceMidiMessage, StuffMidiMessageTarget, TakeAttributeKey, + TimeModeOverride, TimeRangeType, TrackArea, TrackAttributeKey, TrackDefaultsBehavior, + TrackEnvelope, TrackFxChainType, TrackFxLocation, TrackLocation, TrackMuteOperation, + TrackPolarityOperation, TrackRecArmOperation, TrackSendAttributeKey, TrackSendCategory, + TrackSendDirection, TrackSendRef, TrackSoloOperation, TransferBehavior, UiRefreshBehavior, + UndoBehavior, UndoScope, ValueChange, VolumeSliderValue, WindowContext, }; -use helgoboss_midi::ShortMessage; +use helgoboss_midi::{ + Channel, ControllerNumber, RawShortMessage, ShortMessage, ShortMessageFactory, + ShortMessageType, U7, +}; use reaper_low::raw::GUID; use crate::util::{ @@ -1548,6 +1552,54 @@ impl Reaper { PositionInSeconds::new(tpos) } + /// Converts the given quarter-note position to measure index + /// and returnes also measure bounds in quarter notes. + /// + /// # Panics + /// + /// Panics if the given project is not valid anymore. + pub fn time_map_qn_to_measure( + &self, + project: ProjectContext, + qn: PositionInQuarterNotes, + ) -> TimeMapQNToMeasuresResult + where + UsageScope: AnyThread, + { + self.require_valid_project(project); + unsafe { self.time_map_qn_to_measure_unchecked(project, qn) } + } + + /// Like [`time_map_qn_to_measure()`] but doesn't check if project is valid. + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid project. + /// + /// [`time_map_qn_to_measure()`]: #method.time_map_qn_to_measure + pub unsafe fn time_map_qn_to_measure_unchecked( + &self, + project: ProjectContext, + qn: PositionInQuarterNotes, + ) -> TimeMapQNToMeasuresResult + where + UsageScope: AnyThread, + { + let mut start_qn = MaybeUninit::zeroed(); + let mut end_qn = MaybeUninit::zeroed(); + let measure = self.low.TimeMap_QNToMeasures( + project.to_raw(), + qn.0, + start_qn.as_mut_ptr(), + end_qn.as_mut_ptr(), + ); + TimeMapQNToMeasuresResult { + measure_index: measure, + start_qn: PositionInQuarterNotes::new(start_qn.assume_init()), + end_qn: PositionInQuarterNotes::new(end_qn.assume_init()), + } + } + /// Converts the given quarter-note position to time. /// /// # Panics @@ -1696,6 +1748,116 @@ impl Reaper { PositionInQuarterNotes::new(qn) } + /// Returns the MIDI tick (PPQ) position corresponding to the start of the measure. + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid take. + pub unsafe fn midi_get_ppq_pos_start_of_measure( + &self, + take: MediaItemTake, + ppq: PositionInPpq, + ) -> PositionInPpq + where + UsageScope: AnyThread, + { + let ppq = self.low.MIDI_GetPPQPos_StartOfMeasure(take.as_ptr(), ppq.0); + PositionInPpq::new(ppq) + } + + /// Returns the MIDI tick (ppq) position corresponding to the start of the measure. + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid take. + pub unsafe fn midi_get_ppq_pos_end_of_measure( + &self, + take: MediaItemTake, + ppq: PositionInPpq, + ) -> PositionInPpq + where + UsageScope: AnyThread, + { + let ppq = self.low.MIDI_GetPPQPos_EndOfMeasure(take.as_ptr(), ppq.0); + PositionInPpq::new(ppq) + } + + /// Converts the given ppq in take to a project quarter-note position. + /// + /// Quarter notes are counted from the start of the project, regardless of any partial measures. + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid take. + pub unsafe fn midi_get_proj_qn_from_ppq_pos( + &self, + take: MediaItemTake, + ppqpos: PositionInPpq, + ) -> PositionInQuarterNotes + where + UsageScope: AnyThread, + { + let qn = self.low.MIDI_GetProjQNFromPPQPos(take.as_ptr(), ppqpos.0); + PositionInQuarterNotes::new(qn) + } + + /// Converts the given project quarter-note position to take PPQ. + /// + /// Quarter notes are counted from the start of the project, regardless of any partial measures. + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid take. + pub unsafe fn midi_get_ppq_pos_from_proj_qn( + &self, + take: MediaItemTake, + qn: PositionInQuarterNotes, + ) -> PositionInPpq + where + UsageScope: AnyThread, + { + let ppq = self.low.MIDI_GetPPQPosFromProjQN(take.as_ptr(), qn.0); + PositionInPpq::new(ppq) + } + + /// Converts the given ppq in take to a seconds from the project start. + /// + /// Time is counted from the start of the project, regardless of any partial measures. + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid take. + pub unsafe fn midi_get_proj_time_from_ppq_pos( + &self, + take: MediaItemTake, + ppqpos: PositionInPpq, + ) -> PositionInSeconds + where + UsageScope: AnyThread, + { + let seconds = self.low.MIDI_GetProjTimeFromPPQPos(take.as_ptr(), ppqpos.0); + PositionInSeconds::new(seconds) + } + + /// Converts the given project time in seconds to the take PPQ + /// + /// Time is counted from the start of the project, regardless of any partial measures. + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid take. + pub unsafe fn midi_get_ppq_pos_from_proj_time( + &self, + take: MediaItemTake, + time: PositionInSeconds, + ) -> PositionInPpq + where + UsageScope: AnyThread, + { + let ppq = self.low.MIDI_GetPPQPosFromProjTime(take.as_ptr(), time.0); + PositionInPpq::new(ppq) + } + /// Gets the arrange view start/end time for the given screen coordinates. /// /// Set both `screen_x_start` and `screen_x_end` to 0 to get the full arrange view's start/end @@ -5542,6 +5704,19 @@ impl Reaper { NonNull::new(ptr) } + /// # Safety + /// + /// REAPER can crash if passed item is invalid. + pub unsafe fn get_media_item_track(&self, item: MediaItem) -> ReaperFunctionResult + where + UsageScope: MainThreadOnly, + { + let ptr = self.low.GetMediaItemTrack(item.as_ptr()); + MediaTrack::new(ptr).ok_or(ReaperFunctionError::new( + "Can not find item track. Probably, item is invalid.", + )) + } + /// Returns the active take in this item. /// /// # Safety @@ -5573,6 +5748,461 @@ impl Reaper { NonNull::new(ptr).ok_or(ReaperFunctionError::new("couldn't get MIDI editor take")) } + /// Create a new MIDI media item, containing no MIDI events. + /// + /// Time is in seconds unless time_in_qn is true. + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid track. + pub unsafe fn create_midi_item_in_proj( + &self, + track: MediaTrack, + starttime: f64, + endtime: f64, + time_in_qn: bool, + ) -> ReaperFunctionResult + where + UsageScope: MainThreadOnly, + { + self.require_main_thread(); + let ptr = self + .low + .CreateNewMIDIItemInProj(track.as_ptr(), starttime, endtime, &time_in_qn); + NonNull::new(ptr).ok_or_else(|| { + ReaperFunctionError::new("couldn't create MediaItem (maybe track is invalid)") + }) + } + + /// returns false if there are no plugins on the track that support MIDI programs, + /// or if all programs have been enumerated + pub fn enum_track_midi_program_names( + &self, + track: i32, + program_number: i32, + buffer_size: u32, + ) -> Option + where + UsageScope: MainThreadOnly, + { + unsafe { + let (program_name, status) = with_string_buffer(buffer_size, |buffer, max_size| { + self.low + .EnumTrackMIDIProgramNames(track, program_number, buffer, max_size) + }); + match status { + true => Some(program_name), + false => None, + } + } + } + + /// returns false if there are no plugins on the track that support MIDI programs, + /// or if all programs have been enumerated + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid project or track. + pub unsafe fn enum_track_midi_program_names_ex( + &self, + project: ProjectContext, + track: MediaTrack, + program_number: i32, + buffer_size: u32, + ) -> Option + where + UsageScope: MainThreadOnly, + { + self.require_valid_project(project); + self.enum_track_midi_program_names_ex_unchecked(project, track, program_number, buffer_size) + } + + /// returns false if there are no plugins on the track that support MIDI programs, + /// or if all programs have been enumerated + /// + /// # Safety + /// + /// REAPER can crash if you pass an invalid project or track. + pub unsafe fn enum_track_midi_program_names_ex_unchecked( + &self, + project: ProjectContext, + track: MediaTrack, + program_number: i32, + buffer_size: u32, + ) -> Option + where + UsageScope: MainThreadOnly, + { + let (program_name, status) = with_string_buffer(buffer_size, |buffer, max_size| { + self.low.EnumTrackMIDIProgramNamesEx( + project.to_raw(), + track.as_ptr(), + program_number, + buffer, + max_size, + ) + }); + match status { + true => Some(program_name), + false => None, + } + } + + /// Gets note name for the note, if set on track. + pub fn get_track_midi_note_name<'a>( + &self, + track_index: u32, + pitch: u32, + channel: u32, + ) -> Option<&'a ReaperStr> + where + UsageScope: MainThreadOnly, + { + let ptr = self + .low + .GetTrackMIDINoteName(track_index as i32, pitch as i32, channel as i32); + unsafe { create_passing_c_str(ptr) } + } + + /// Gets note name for the note, if set on track. + /// + /// # Safety + /// + /// REAPER can crash, if you pass an invalid track. + pub unsafe fn get_track_midi_note_name_ex<'a>( + &self, + project: ProjectContext, + track: MediaTrack, + pitch: u32, + channel: u32, + ) -> Option<&'a ReaperStr> + where + UsageScope: MainThreadOnly, + { + self.require_valid_project(project); + self.get_track_midi_note_name_ex_unchecked(project, track, pitch, channel) + } + + /// Gets note name for the note, if set on track. + /// + /// # Safety + /// + /// REAPER can crash, if you pass an invalid track. + pub unsafe fn get_track_midi_note_name_ex_unchecked<'a>( + &self, + project: ProjectContext, + track: MediaTrack, + pitch: u32, + channel: u32, + ) -> Option<&'a ReaperStr> + where + UsageScope: MainThreadOnly, + { + let ptr = self.low.GetTrackMIDINoteNameEx( + project.to_raw(), + track.as_ptr(), + pitch as i32, + channel as i32, + ); + create_passing_c_str(ptr) + } + + /// Assignes name to the midi note or CC on the entire track. + /// + /// channel < 0 assigns these note names to all channels. + pub fn set_track_midi_note_name<'a>( + &self, + track: u32, + pitch: u32, + channel: u32, + name: impl Into>, + ) -> bool + where + UsageScope: MainThreadOnly, + { + unsafe { + self.low.SetTrackMIDINoteName( + track as i32, + pitch as i32, + channel as i32, + name.into().as_ptr(), + ) + } + } + + // Assignes name to the midi note or CC on the entire track. + /// + /// channel < 0 assigns these note names to all channels. + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid track. + pub unsafe fn set_track_midi_note_name_ex<'a>( + &self, + project: ProjectContext, + track: MediaTrack, + pitch: u32, + channel: i32, + name: impl Into>, + ) -> bool + where + UsageScope: MainThreadOnly, + { + self.project_is_valid(project); + self.set_track_midi_note_name_ex_unchecked(project, track, pitch, channel, name) + } + + // Assignes name to the midi note or CC on the entire track. + /// + /// channel < 0 assigns these note names to all channels. + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid track. + pub unsafe fn set_track_midi_note_name_ex_unchecked<'a>( + &self, + project: ProjectContext, + mut track: MediaTrack, + pitch: u32, + channel: i32, + name: impl Into>, + ) -> bool + where + UsageScope: MainThreadOnly, + { + self.low.SetTrackMIDINoteNameEx( + project.to_raw(), + track.as_mut(), + pitch as i32, + channel, + name.into().as_ptr(), + ) + } + + /// Returns Iterator over all midi events from the given take + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid take. + pub unsafe fn midi_get_all_evts( + &self, + take: MediaItemTake, + max_size: u32, + ) -> Option + where + UsageScope: MainThreadOnly, + { + match self.midi_get_all_evts_raw(take, max_size) { + None => None, + Some(buf) => Some(SourceMidiEventBuilder::new(buf)), + } + } + + /// Returns all midi events from the given take + /// + /// These events represented as Vec as they are returned from + /// REAPER take. + /// + /// Each event consist of: + /// - offset in ppq from the previous event: 4 bytes — little-endian i32 (u32) + /// - flag: 1 bit + /// - 0b1000_0000 — selected + /// - 0b0100_0000 — muted + /// - 0b0000_1000 — CcShapeKind::Linear + /// - 0b0000_0100 — CcShapeKind::SlowStartEnd + /// - 0b0000_1100 — CcShapeKind::FastStart + /// - 0b0000_0010 — CcShapeKind::FastEnd + /// - 0b0000_1010 — CcShapeKind::Beizer + /// - length in bytes: 4 bytes — little-endinan i32 (u32) + /// - message: bytes + /// + /// A meta-event of type 0xF followed by 'CCBZ ' and 5 more bytes represents + /// bezier curve data for the previous MIDI event: + /// - 1 byte for the bezier type (usually 0) + /// - 4 bytes for the bezier tension as a float. + /// + /// The rest of the vector is filled by zeroes. + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid take. + pub unsafe fn midi_get_all_evts_raw( + &self, + take: MediaItemTake, + max_size: u32, + ) -> Option> + where + UsageScope: MainThreadOnly, + { + let (events, status) = with_buffer(max_size, |buffer, size| { + let mut size = MaybeUninit::new(size); + self.low + .MIDI_GetAllEvts(take.as_ptr(), buffer, size.as_mut_ptr()) + }); + match status { + true => Some(events), + false => None, + } + } + + /// Replace all events in the given take. + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid take. + pub unsafe fn midi_set_all_evts( + &self, + take: MediaItemTake, + events: Vec>, + sort: bool, + ) -> bool + where + UsageScope: MainThreadOnly, + { + self.midi_set_all_evts_raw( + take, + SourceMidiEventConsumer::new(events, sort).collect::>(), + ) + } + + /// Returns all midi events from the given take + /// + /// These events represented as Vec as they are returned from + /// REAPER take. + /// + /// For the raw event representation of events see `Reaper::midi_get_all_evts_raw()` + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid take. + pub unsafe fn midi_set_all_evts_raw(&self, take: MediaItemTake, buffer: Vec) -> bool + where + UsageScope: MainThreadOnly, + { + self.low + .MIDI_SetAllEvts(take.as_ptr(), buffer.as_ptr(), buffer.len() as i32) + } + + /// Get CC event from given take. + /// + /// index is 0-based + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid take. + pub unsafe fn midi_get_cc( + &self, + take: MediaItemTake, + cc_index: u32, + ) -> Option> + where + UsageScope: MainThreadOnly, + { + let (mut selected, mut muted) = (MaybeUninit::new(false), MaybeUninit::new(false)); + let mut ppqpos = MaybeUninit::new(0.0); + let (mut chanmsg, mut chan, mut msg2, mut msg3) = ( + MaybeUninit::new(0), + MaybeUninit::new(0), + MaybeUninit::new(0), + MaybeUninit::new(0), + ); + let result = self.low.MIDI_GetCC( + take.as_ptr(), + cc_index as i32, + selected.as_mut_ptr(), + muted.as_mut_ptr(), + ppqpos.as_mut_ptr(), + chanmsg.as_mut_ptr(), + chan.as_mut_ptr(), + msg2.as_mut_ptr(), + msg3.as_mut_ptr(), + ); + match result { + false => None, + true => Some(SourceMidiEvent::new( + PositionInPpq(ppqpos.assume_init()), + selected.assume_init(), + muted.assume_init(), + CcShapeKind::Square, + RawShortMessage::control_change( + Channel::new(chan.assume_init() as u8), + ControllerNumber::new(msg2.assume_init() as u8), + U7::new(msg3.assume_init() as u8), + ), + )), + } + } + + /// Change ControlChange event at the given index. + /// + /// Returns error if: + /// - is not CC message + /// - faced problems with unpacking the message + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid take. + pub unsafe fn midi_set_cc( + &self, + take: MediaItemTake, + cc_index: u32, + event: SourceMidiEvent, + sort_after: bool, + ) -> Result + where + UsageScope: MainThreadOnly, + { + let msg = event.get_message(); + if msg.r#type() != ShortMessageType::ControlChange { + return Err(String::from("should be ControlChange message")); + } + Ok(self.low().MIDI_SetCC( + take.as_ptr(), + cc_index as i32, + &event.get_selected(), + &event.get_muted(), + &event.get_position().get(), + &(msg.status_byte() as i32), + &(msg.channel().ok_or("should have channel")?.get() as i32), + &i32::from(msg.controller_number().ok_or("should have cc_num")?), + &i32::from(msg.control_value().ok_or("should have control value")?), + &sort_after, + )) + } + + /// Insert ControlChange event. + /// + /// Returns error if: + /// - is not CC message + /// - faced problems with unpacking the message + /// + /// # Safety + /// + /// REAPER can crash, it you pass an invalid take. + pub unsafe fn midi_insert_cc( + &self, + take: MediaItemTake, + event: SourceMidiEvent, + ) -> Result + where + UsageScope: MainThreadOnly, + { + let msg = event.get_message(); + if msg.r#type() != ShortMessageType::ControlChange { + return Err(String::from("should be ControlChange message")); + } + Ok(self.low().MIDI_InsertCC( + take.as_ptr(), + event.get_selected(), + event.get_muted(), + event.get_position().get(), + msg.status_byte() as i32, + msg.channel().ok_or("should have channel")?.get() as i32, + i32::from(msg.controller_number().ok_or("should have cc_num")?), + i32::from(msg.control_value().ok_or("should have control value")?), + )) + } + /// Selects exactly one track and deselects all others. /// /// If `None` is passed, deselects all tracks. @@ -6276,6 +6906,16 @@ impl Reaper { use_name(passing_c_str.ok_or_else(|| ReaperFunctionError::new("invalid take"))) } + /// #Safety + /// + /// REAPER can crash if invalid track is passed. + pub fn take_is_midi(&self, take: &MediaItemTake) -> bool + where + UsageScope: MainThreadOnly, + { + unsafe { self.low.TakeIsMIDI(take.as_ptr()) } + } + /// Returns the current on/off state of a toggleable action. /// /// Returns `None` if the action doesn't support on/off states (or if the action doesn't exist). @@ -7187,6 +7827,16 @@ pub struct TimeMapGetMeasureInfoResult { pub tempo: Bpm, } +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct TimeMapQNToMeasuresResult { + /// Measue index in project. + pub measure_index: i32, + /// Start position of the measure in quarter notes. + pub start_qn: PositionInQuarterNotes, + /// End position of the measure in quarter notes. + pub end_qn: PositionInQuarterNotes, +} + /// Time signature. #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub struct TimeSignature { diff --git a/main/medium/src/source_midi.rs b/main/medium/src/source_midi.rs new file mode 100644 index 00000000..a119f139 --- /dev/null +++ b/main/medium/src/source_midi.rs @@ -0,0 +1,232 @@ +use std::vec::IntoIter; + +use helgoboss_midi::{RawShortMessage, ShortMessage, ShortMessageFactory, U7}; + +use crate::{CcShapeKind, PositionInPpq}; + +pub trait SourceMidiMessage { + fn from_raw(buf: Vec) -> Option + where + Self: Sized; + fn get_raw(&self) -> Vec; +} + +#[derive(Clone, PartialEq, PartialOrd, Debug, Default)] +pub struct RawMidiMessage { + buf: Vec, +} +impl SourceMidiMessage for RawMidiMessage { + fn from_raw(buf: Vec) -> Option { + Some(Self { buf }) + } + fn get_raw(&self) -> Vec { + self.buf.clone() + } +} + +impl SourceMidiMessage for RawShortMessage { + fn from_raw(buf: Vec) -> Option + where + Self: Sized, + { + if buf.len() > 3 { + return None; + } + match RawShortMessage::from_bytes((buf[0], U7::new(buf[1]), U7::new(buf[2]))) { + Err(_) => None, + Ok(msg) => Some(msg), + } + } + + fn get_raw(&self) -> Vec { + vec![ + self.status_byte(), + u8::from(self.data_byte_1()), + u8::from(self.data_byte_2()), + ] + } +} + +#[derive(Clone, PartialEq, PartialOrd, Debug, Default)] +pub struct SourceMidiEvent { + position_in_ppq: PositionInPpq, + is_selected: bool, + is_muted: bool, + cc_shape_kind: CcShapeKind, + /// Message can be as ordinary 3-bytes midi-message, + /// as well as SysEx and custom messages, including lyrics and text. + message: T, +} +impl SourceMidiEvent { + pub fn new( + position_in_ppq: PositionInPpq, + is_selected: bool, + is_muted: bool, + cc_shape_kind: CcShapeKind, + message: T, + ) -> Self { + Self { + position_in_ppq, + is_selected, + is_muted, + cc_shape_kind, + message, + } + } + pub fn get_position(&self) -> PositionInPpq { + self.position_in_ppq + } + pub fn set_position(&mut self, position: PositionInPpq) { + self.position_in_ppq = position; + } + pub fn get_selected(&self) -> bool { + self.is_selected + } + pub fn set_selected(&mut self, selected: bool) { + self.is_selected = selected; + } + pub fn get_muted(&self) -> bool { + self.is_muted + } + pub fn set_muted(&mut self, muted: bool) { + self.is_muted = muted; + } + pub fn get_cc_shape_kind(&self) -> CcShapeKind { + self.cc_shape_kind + } + pub fn set_cc_shape_kind(&mut self, cc_shape_kind: CcShapeKind) { + self.cc_shape_kind = cc_shape_kind; + } + pub fn get_message(&self) -> &T { + &self.message + } + pub fn get_message_mut(&mut self) -> &mut T { + &mut self.message + } + pub fn set_message(&mut self, message: T) { + self.message = message; + } +} + +/// Iterates over raw take midi data and builds SourceMediaEvent objects. +#[derive(Debug)] +pub struct SourceMidiEventBuilder { + buf: IntoIter, + current_ppq: u32, +} +impl SourceMidiEventBuilder { + pub(crate) fn new(buf: Vec) -> Self { + Self { + buf: buf.into_iter(), + current_ppq: 0, + } + } + + fn next_4(&mut self) -> Option<[u8; 4]> { + match ( + self.buf.next(), + self.buf.next(), + self.buf.next(), + self.buf.next(), + ) { + (Some(a), Some(b), Some(c), Some(d)) => Some([a, b, c, d]), + _ => None, + } + } +} +impl Iterator for SourceMidiEventBuilder { + type Item = SourceMidiEvent; + + fn next(&mut self) -> Option { + let result = match self.next_4() { + Some(value) => value, + None => return None, + }; + let offset = u32::from_le_bytes(result); + let flag = self + .buf + .next() + .expect("unexpectetly ended. Should be flag."); + let length = u32::from_le_bytes(self.next_4().expect("should take length")); + if length == 0 { + return None; + } + self.current_ppq += offset; + let buf = self.buf.by_ref().take(length as usize); + Some(SourceMidiEvent { + position_in_ppq: PositionInPpq::new(self.current_ppq as f64), + cc_shape_kind: CcShapeKind::from_raw(flag & 0b11110000) + .expect("Can not infer CcShapeKind, received from take."), + is_selected: (flag & 1) != 0, + is_muted: (flag & 2) != 0, + message: RawMidiMessage { + buf: Vec::from_iter(buf), + }, + }) + } +} + +/// Iterates through SourceMediaEvent objects and builds raw midi data +/// to be passed to take. +#[derive(Debug)] +pub struct SourceMidiEventConsumer { + events: IntoIter>, + last_ppq: u32, + buf: Option>, +} +impl SourceMidiEventConsumer { + /// Build iterator. + /// + /// If sort is true — vector would be sorted by ppq_position. + /// Be careful, this costs additional O(log n) operation in the worst case. + pub fn new(mut events: Vec>, sort: bool) -> Self { + if sort == true { + events.sort_by_key(|ev| ev.get_position().get() as u32); + } + Self { + events: events.into_iter(), + last_ppq: 0, + buf: None, + } + } + + /// Checks if some events are left and builds new buf for iteration. + fn next_buf(&mut self) -> Option { + match self.events.next() { + None => None, + Some(mut event) => { + let size = event.get_message().get_raw().len() + 9; + let pos = event.get_position().get() as u32; + let mut offset = (pos - self.last_ppq).to_le_bytes().to_vec(); + self.last_ppq = pos; + let flag = (event.get_selected() as u8) + | ((event.get_muted() as u8) << 1) + | event.get_cc_shape_kind().to_raw(); + let mut length = event.get_message().get_raw().len().to_le_bytes().to_vec(); + // + let mut buf = Vec::with_capacity(size); + buf.append(&mut offset); + buf.push(flag); + buf.append(&mut length); + buf.append(&mut event.get_message_mut().get_raw()); + // + self.buf = Some(buf.into_iter()); + // Some(i8) + Some(self.buf.as_mut().unwrap().next().unwrap() as i8) + } + } + } +} + +impl Iterator for SourceMidiEventConsumer { + type Item = i8; + fn next(&mut self) -> Option { + match self.buf.as_mut() { + Some(buf) => match buf.next() { + Some(next) => Some(next as i8), + None => self.next_buf(), + }, + None => self.next_buf(), + } + } +} diff --git a/test/test/src/tests.rs b/test/test/src/tests.rs index 7523966c..3c3471d2 100644 --- a/test/test/src/tests.rs +++ b/test/test/src/tests.rs @@ -120,6 +120,7 @@ pub fn create_test_steps() -> impl Iterator { unsolo_track(), generate_guid(), main_section_functions(), + midi_functions(), register_and_unregister_action(), register_and_unregister_toggle_action(), ] @@ -3577,6 +3578,60 @@ fn add_track_fx_by_original_name(get_fx_chain: GetFxChain) -> TestStep { ) } +fn midi_functions() -> TestStep { + step(AllVersions, "Test MIDI functions on track.", |_, _| { + let reaper = Reaper::get(); + let project = reaper.current_project(); + + let track = project.add_track()?; + + let medium = reaper.medium_reaper(); + + unsafe { + let item = medium.create_midi_item_in_proj(track.raw(), 1.0, 2.0, true)?; + let take = medium + .get_active_take(item) + .ok_or("No tke in created item!")?; + + assert!(medium.take_is_midi(&take)); + let track_index = track.index().ok_or("Can not take Track index.")?; + match medium.set_track_midi_note_name(track_index, 60, 0, "test name") { + true => Ok(true), + false => Err("returned false"), + }?; + match medium.set_track_midi_note_name_ex( + project.context(), + track.raw(), + 61, + -1, + "test name_ex", + ) { + true => Ok(true), + false => Err("returned false"), + }?; + assert_eq!( + medium + .get_track_midi_note_name(track_index, 61, 3) + .ok_or("should be set")? + .to_str(), + "test name_ex" + ); + assert_eq!( + medium + .get_track_midi_note_name_ex(project.context(), track.raw(), 60, 0) + .ok_or("should be set")? + .to_str(), + "test name" + ); + } + + project.remove_track(&track); + + Ok(()) + }) +} + + fn get_track(index: u32) -> Result { Reaper::get() .current_project()