Skip to content

Commit

Permalink
Low and medium-level API: Support file-in-project callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Jun 7, 2024
1 parent fb39338 commit 8d25fed
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 21 deletions.
11 changes: 5 additions & 6 deletions main/high/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ use camino::{Utf8Path, Utf8PathBuf};
use either::Either;
use reaper_medium::ProjectContext::{CurrentProject, Proj};
use reaper_medium::{
AutoSeekBehavior, BeatAttachMode, BookmarkId, BookmarkRef, CountProjectMarkersResult,
DurationInSeconds, GetLastMarkerAndCurRegionResult, GetLoopTimeRange2Result,
MasterTrackBehavior, PanMode, PlayState, PositionInSeconds, ProjectContext, ProjectRef,
ReaProject, ReaperString, ReaperStringArg, SetEditCurPosOptions, TimeMap2TimeToBeatsResult,
TimeMode, TimeModeOverride, TimeRangeType, TimeSignature, TrackDefaultsBehavior, TrackLocation,
UndoBehavior,
AutoSeekBehavior, BookmarkId, BookmarkRef, CountProjectMarkersResult, DurationInSeconds,
GetLastMarkerAndCurRegionResult, GetLoopTimeRange2Result, MasterTrackBehavior, PanMode,
PlayState, PositionInSeconds, ProjectContext, ProjectRef, ReaProject, ReaperString,
ReaperStringArg, SetEditCurPosOptions, TimeMap2TimeToBeatsResult, TimeMode, TimeModeOverride,
TimeRangeType, TimeSignature, TrackDefaultsBehavior, TrackLocation, UndoBehavior,
};
use std::path::PathBuf;

Expand Down
5 changes: 2 additions & 3 deletions main/high/src/take.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use crate::{FxChain, OwnedSource, Reaper, ReaperSource, Track};
use reaper_medium::{
DurationInSeconds, FullPitchShiftMode, ItemAttributeKey, MediaItemTake, NativeColorValue,
PlaybackSpeedFactor, PositionInSeconds, ReaperFunctionError, ReaperStringArg,
ReaperVolumeValue, RgbColor, TakeAttributeKey,
DurationInSeconds, FullPitchShiftMode, MediaItemTake, NativeColorValue, PlaybackSpeedFactor,
ReaperFunctionError, ReaperStringArg, ReaperVolumeValue, RgbColor, TakeAttributeKey,
};

#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
Expand Down
19 changes: 19 additions & 0 deletions main/low/src/file_in_project_callback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::raw::{ReaProject, INT_PTR};
use std::ffi::{c_char, c_int, c_void};

/// This structure is only documented, in https://github.com/justinfrankel/reaper-sdk/blob/main/sdk/reaper_plugin.h

Check warning on line 4 in main/low/src/file_in_project_callback.rs

View workflow job for this annotation

GitHub Actions / Doc

this URL is not a hyperlink

Check warning on line 4 in main/low/src/file_in_project_callback.rs

View workflow job for this annotation

GitHub Actions / Doc

this URL is not a hyperlink
/// (see "file_in_project_ex2").
///
/// It's documented as array but in accordance with all the other types we express it as struct with named fields.
/// The important thing is that the memory layout is the same, which it is because each field in the struct
/// is a pointer (with the same size).
#[repr(C)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub struct file_in_project_ex2_t {
pub file_name: *mut c_char,
pub proj_ptr: *mut ReaProject,
pub user_data_context: *mut c_void,
pub file_in_project_callback: Option<
unsafe extern "C" fn(user_data: *mut c_void, msg: c_int, param: *mut c_void) -> INT_PTR,
>,
}
2 changes: 2 additions & 0 deletions main/low/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,6 @@ pub use pcm_sink::*;
mod project_state_context;
pub use project_state_context::*;

mod file_in_project_callback;

mod gaccel_register;
2 changes: 2 additions & 0 deletions main/low/src/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub use super::bindings::root::{
UNDO_STATE_FREEZE, UNDO_STATE_FX, UNDO_STATE_ITEMS, UNDO_STATE_MISCCFG, UNDO_STATE_TRACKCFG,
};

pub use super::file_in_project_callback::file_in_project_ex2_t;

/// Structs, types and constants defined by `swell.h` (on Linux and Mac OS X) and
/// `windows.h` (on Windows).
///
Expand Down
11 changes: 5 additions & 6 deletions main/medium/src/accelerator_register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,11 @@ extern "C" fn delegating_translate_accel<T: TranslateAccel>(
let ctx = unsafe { NonNull::new_unchecked(ctx) };
let callback_struct: &mut T = decode_user_data(unsafe { ctx.as_ref() }.user);
let msg = AccelMsg::from_raw(unsafe { *msg });
callback_struct
.call(TranslateAccelArgs {
msg,
ctx: &AcceleratorRegister::new(ctx),
})
.to_raw()
let args = TranslateAccelArgs {
msg,
ctx: &AcceleratorRegister::new(ctx),
};
callback_struct.call(args).to_raw()
})
.unwrap_or(0)
}
Expand Down
107 changes: 107 additions & 0 deletions main/medium/src/file_in_project_hook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use crate::{decode_user_data, encode_user_data, ReaProject, ReaperStr};
use reaper_low::raw::INT_PTR;
use reaper_low::{firewall, raw};
use std::ffi::c_void;
use std::fmt::{Debug, Formatter};
use std::os::raw::c_int;

/// Consumers need to implement this trait in order to be called back if REAPER does something with a registered project
/// file.
///
/// See [`ReaperSession::plugin_register_add_file_in_project_ex2`].

Check failure on line 11 in main/medium/src/file_in_project_hook.rs

View workflow job for this annotation

GitHub Actions / Doc

unresolved link to `ReaperSession::plugin_register_add_file_in_project_ex2`

Check failure on line 11 in main/medium/src/file_in_project_hook.rs

View workflow job for this annotation

GitHub Actions / Doc

unresolved link to `ReaperSession::plugin_register_add_file_in_project_ex2`
pub trait FileInProjectCallback {
/// This is called before REAPER renames files and allows you to determine in which subdirectory the
/// file should go. Return `None` if you don't need the file to be in a subdirectory.
fn get_directory_name(&mut self) -> Option<&'static ReaperStr> {
None
}

/// File has been renamed.
fn renamed(&mut self, new_name: &ReaperStr) {
let _ = new_name;
}

/// *reaper-rs* calls this for unknown msg types. It's the fallback handler, so to say.
fn ext(&mut self, args: FileInProjectCallbackExtArgs) -> INT_PTR {
let _ = args;
0
}
}

pub struct FileInProjectCallbackExtArgs {
pub msg: c_int,
pub parm: *mut c_void,
}

pub(crate) struct OwnedFileInProjectHook {
inner: raw::file_in_project_ex2_t,
callback: Box<dyn FileInProjectCallback>,
}

impl Debug for OwnedFileInProjectHook {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// FileInProjectCallback doesn't generally implement Debug.
f.debug_struct("OwnedFileInProjectHook")
.field("inner", &self.inner)
.field("callback", &"<omitted>")
.finish()
}
}

impl OwnedFileInProjectHook {
pub fn new<T>(file_name: &ReaperStr, project: ReaProject, callback: Box<T>) -> Self
where
T: FileInProjectCallback + 'static,
{
Self {
inner: raw::file_in_project_ex2_t {
// It's okay that this file name goes out of memory. It's explicitly documented that it
// only needs to be accessible at the time of registering the callback.
file_name: file_name.as_ptr() as *mut _,
proj_ptr: project.as_ptr(),
user_data_context: encode_user_data(&callback),
file_in_project_callback: Some(delegating_callback::<T>),
},
callback,
}
}

pub fn into_callback(self) -> Box<dyn FileInProjectCallback> {
self.callback
}
}

impl AsRef<raw::file_in_project_ex2_t> for OwnedFileInProjectHook {
fn as_ref(&self) -> &raw::file_in_project_ex2_t {
&self.inner
}
}

extern "C" fn delegating_callback<T: FileInProjectCallback>(
user_data: *mut c_void,
msg: c_int,
parm: *mut c_void,
) -> INT_PTR {
firewall(|| {
let callback_struct: &mut T = decode_user_data(user_data);
match msg {
0x000 => {
if parm.is_null() {
return 0;
}
let new_name = unsafe { ReaperStr::from_ptr(parm as _) };
callback_struct.renamed(new_name);
0
}
0x100 => callback_struct
.get_directory_name()
.map(|name| name.as_ptr() as _)
.unwrap_or(0),
_ => {
let args = FileInProjectCallbackExtArgs { msg, parm };
callback_struct.ext(args)
}
}
})
.unwrap_or(0)
}
1 change: 0 additions & 1 deletion main/medium/src/key_enums.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::{concat_reaper_strs, ReaperStr, ReaperStringArg};

use crate::TrackAttributeKey::MidiInputChanMap;
use std::borrow::Cow;

// TODO-medium Consider migrating to newtypes around Cow<str> for this kind of enums.
Expand Down
3 changes: 3 additions & 0 deletions main/medium/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ pub use gaccel_register::*;
mod accelerator_register;
pub use accelerator_register::*;

mod file_in_project_hook;
pub use file_in_project_hook::*;

mod preview_register;
pub use preview_register::*;

Expand Down
6 changes: 6 additions & 0 deletions main/medium/src/misc_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,8 @@ pub enum RegistrationObject<'a> {
BackAccelerator(Handle<raw::accelerator_register_t>),
/// A record which lets you get the first place in the keyboard processing queue.
FrontAccelerator(Handle<raw::accelerator_register_t>),
/// Registers a used project file and receives callbacks associated with that project file.
FileInProjectCallback(Handle<raw::file_in_project_ex2_t>),
/// A hidden control surface (useful for being notified by REAPER about events).
///
/// Extract from `reaper_plugin.h`:
Expand Down Expand Up @@ -916,6 +918,10 @@ impl<'a> RegistrationObject<'a> {
key: reaper_str!("<accelerator").into(),
value: reg.as_ptr() as _,
},
FileInProjectCallback(reg) => PluginRegistration {
key: reaper_str!("file_in_project_ex2").into(),
value: reg.as_ptr() as _,
},
CsurfInst(inst) => PluginRegistration {
key: reaper_str!("csurf_inst").into(),
value: inst.as_ptr() as _,
Expand Down
69 changes: 64 additions & 5 deletions main/medium/src/reaper_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ use crate::{
concat_reaper_strs, delegating_hook_command, delegating_hook_command_2,
delegating_hook_post_command, delegating_hook_post_command_2, delegating_toggle_action,
AcceleratorPosition, BufferingBehavior, CommandId, ControlSurface, ControlSurfaceAdapter,
Handle, HookCommand, HookCommand2, HookCustomMenu, HookPostCommand, HookPostCommand2,
MainThreadScope, MeasureAlignment, OnAudioBuffer, OwnedAcceleratorRegister,
FileInProjectCallback, Handle, HookCommand, HookCommand2, HookCustomMenu, HookPostCommand,
HookPostCommand2, MainThreadScope, MeasureAlignment, OnAudioBuffer, OwnedAcceleratorRegister,
OwnedAudioHookRegister, OwnedGaccelRegister, OwnedPreviewRegister, PluginRegistration,
ProjectContext, RealTimeAudioThreadScope, Reaper, ReaperFunctionError, ReaperFunctionResult,
ReaperMutex, ReaperString, ReaperStringArg, RegistrationHandle, RegistrationObject,
ToggleAction, ToolbarIconMap, TranslateAccel,
ProjectContext, ReaProject, RealTimeAudioThreadScope, Reaper, ReaperFunctionError,
ReaperFunctionResult, ReaperMutex, ReaperString, ReaperStringArg, RegistrationHandle,
RegistrationObject, ToggleAction, ToolbarIconMap, TranslateAccel,
};
use reaper_low::raw::audio_hook_register_t;

use crate::file_in_project_hook::OwnedFileInProjectHook;
use crate::fn_traits::{delegating_hook_custom_menu, delegating_toolbar_icon_map};
use enumflags2::BitFlags;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -68,6 +69,8 @@ pub struct ReaperSession {
gaccel_registers: Keeper<OwnedGaccelRegister, raw::gaccel_register_t>,
/// Provides a safe place in memory for accelerator registers.
accelerator_registers: Keeper<OwnedAcceleratorRegister, raw::accelerator_register_t>,
/// Provides a safe place in memory for file-in-project hooks.
file_in_project_hooks: Keeper<OwnedFileInProjectHook, raw::file_in_project_ex2_t>,
/// Provides a safe place in memory for currently playing preview registers.
preview_registers: SharedKeeper<ReaperMutex<OwnedPreviewRegister>, raw::preview_register_t>,
/// Provides a safe place in memory for command names used in command ID registrations.
Expand Down Expand Up @@ -113,6 +116,7 @@ impl ReaperSession {
reaper: Reaper::new(low),
gaccel_registers: Default::default(),
accelerator_registers: Default::default(),
file_in_project_hooks: Default::default(),
preview_registers: Default::default(),
command_names: Default::default(),
api_defs: Default::default(),
Expand Down Expand Up @@ -646,6 +650,32 @@ impl ReaperSession {
Ok(handle)
}

pub fn plugin_register_add_file_in_project_callback<'a, T>(
&mut self,
project: ReaProject,
file_name: impl Into<ReaperStringArg<'a>>,
callback: Box<T>,
) -> ReaperFunctionResult<RegistrationHandle<T>>
where
T: FileInProjectCallback + 'static,
{
// Create thin pointer of callback before making it a trait object (for being able to
// restore the original callback later).
let callback_thin_ptr: NonNull<T> = callback.as_ref().into();
// Create hook and make it own the callback (as user data)
let file_name = file_name.into();
let register = OwnedFileInProjectHook::new(file_name.as_reaper_str(), project, callback);
// Store it in memory. Although we keep it here, conceptually it's owned by REAPER, so we
// should not access it while being registered.
let reaper_ptr = self.file_in_project_hooks.keep(register);
// Register the low-level hook at REAPER
let reg = RegistrationObject::FileInProjectCallback(reaper_ptr);
unsafe { self.plugin_register_add(reg)? };
// Returns a handle which the consumer can use to unregister
let handle = RegistrationHandle::new(callback_thin_ptr, reaper_ptr.cast());
Ok(handle)
}

/// Plays a preview register.
///
/// # Errors
Expand Down Expand Up @@ -922,6 +952,35 @@ impl ReaperSession {
Some(callback)
}

pub fn plugin_register_remove_file_in_project_callback<T>(
&mut self,
handle: RegistrationHandle<T>,
) -> Option<Box<T>>
where
T: FileInProjectCallback,
{
// Unregister the low-level register from REAPER
let reaper_ptr = handle.reaper_handle().cast();
unsafe {
self.plugin_register_remove(RegistrationObject::FileInProjectCallback(reaper_ptr))
};
// Take the owned hook out of its storage
let owned_hook = self
.file_in_project_hooks
.release(handle.reaper_handle().cast())?;
// Reconstruct the initial value for handing ownership back to the consumer
let dyn_callback = owned_hook.into_callback();
// We are not interested in the fat pointer (Box<dyn TranslateAccel>) anymore.
// By calling leak(), we make the pointer go away but prevent Rust from
// dropping its content.
Box::leak(dyn_callback);
// Here we pick up the content again and treat it as a Box - but this
// time not a trait object box (Box<dyn TranslateAccel> = fat pointer) but a
// normal box (Box<T> = thin pointer) ... original type restored.
let callback = unsafe { handle.restore_original() };
Some(callback)
}

/// Registers a hidden control surface.
///
/// This is very useful for being notified by REAPER about all kinds of events in the main
Expand Down

0 comments on commit 8d25fed

Please sign in to comment.