helgoboss / reaper-rs

Rust bindings for the REAPER C++ API
MIT License
78 stars 8 forks source link

Medium-level API: insert_media() #32

Closed helgoboss closed 1 year ago

helgoboss commented 3 years ago

@boscop This works already but the mode/flags parameters are not yet evaluated because I'm unsure about the structure. There are clearly some flags that can be combined (and therefore should be represented via bitflags) but also some that totally exclude each other and therefore would make more sense as an enum. So far I tried to model it a bit by looking at the REAPER doc for this function:

// mode: 0=add to current track, 1=add new track, 3=add to selected items as takes, &4=stretch/loop to fit time sel, &8=try to match tempo 1x, &16=try to match tempo 0.5x, &32=try to match tempo 2x, &64=don't preserve pitch when matching tempo, &128=no loop/section if startpct/endpct set, &256=force loop regardless of global preference for looping imported items, &512=use high word as absolute track index if mode&3==0, &1024=insert into reasamplomatic on a new track, &2048=insert into open reasamplomatic instance, &4096=move to source preferred position (BWF start offset)

I put the modes which I think are exclusive into InsertMediaMode and the flags which I think can be combined into InsertMediaFlag - but I've no idea if this is completely correct. Did you experiment with the function a bit and can maybe improve it?

helgoboss commented 3 years ago

The test actually succeeds (there's some non-deterministic issue with the integration test execution on Linux).

Boscop commented 3 years ago

Sorry that it took so long, I finally got around to trying this branch in my VST. I'm calling it like this:

if let TypeSpecificPluginContext::Vst(ctx) =
    reaper.plugin_context().type_specific()
{
    unsafe {
        let mut my_track: NonNull<MediaTrack> = ctx
            .request_containing_track(
                NonNull::new(host.raw_effect()).expect("NonNull"),
            )
            .expect("track fx");

        // clear track before inserting midi file
        for i in 0 .. reaper
            .low()
            .CountTrackMediaItems(my_track.as_mut())
        {
            let media_item = reaper
                .low()
                .GetTrackMediaItem(my_track.as_mut(), i);
            let res = reaper.low().DeleteTrackMediaItem(
                my_track.as_mut(),
                media_item,
            );
            info!("DeleteTrackMediaItem: {:?}", res);
        }

        let track_idx = (0 .. reaper
            .count_tracks(ProjectContext::CurrentProject))
            .find(|&track_idx| {
                reaper
                    .get_track(
                        ProjectContext::CurrentProject,
                        track_idx,
                    )
                    .expect("get_track") == my_track
            })
            .expect("track_idx");

        // insert midi file
        let _ = reaper
            .insert_media(
                &midi_path,
                InsertMediaMode::AddToTrackAtIndex(track_idx),
                InsertMediaFlag::TryToMatchTempo1X
                    | InsertMediaFlag::StretchLoopToFitTimeSelection
                    | InsertMediaFlag::ForceLoopRegardlessOfGlobalPreference,
            )
            .map_err(|e| error!("insert_media: {}", e));
    }
}

But I ran into some issues: Even though I pass the right track_idx and file path, it's not inserting the file as midi item on the track: Instead, this call creates a new track but the midi item does not appear on it. What's noteworthy though: The new track is named after the midi file, just like when dragging a midi file from explorer into Reaper. (The Reaper track name is taken from the name of the first named track inside the multitrack midi file.) So it is reading the midi file! And when I try to rename the midi file while this Reaper session is open, Explorer complains that the file is locked in Reaper. So it's also locking it. (Which is bad for my use case of continuously re-generating this midi file from my livecoding application. Any idea how to make it not lock it? Btw, if I drop a midi file from Explorer into Reaper, it doesn't lock it!) And any idea why the midi file doesn't appear as a media item on the track? (The track is empty afterwards.) (Btw, track_idx is correct, it's removing the media items of the correct track, and DeleteTrackMediaItem returns true.)


Another issue I encountered: I use the hotwatch crate to watch the midi file to auto-reload it whenever it changes and I get this panic:

thread 'unnamed' panicked at 'called main-thread-only function from wrong thread': C:\Users\me.cargo\git\checkouts\reaper-rs-d32fed07113e6874\5eee308\main\medium\src\reaper.rs:4020

https://github.com/helgoboss/reaper-rs/blob/5eee308bad13d1e6cc8abdb0f53c266e5892e9db/main/medium/src/reaper.rs#L4020

But it's not being called from the audio thread, but from the newly spawned background watcher thread. (hotwatch also has a blocking API but I can't use that because I need to be able to terminate the watcher thread in the Drop impl of my plugin, which isn't possible with hotwatch's blocking API.) When I was using my own Reaper API bindings, I never had problems calling them from a new thread. So with reaper-rs, shouldn't it also be allowed to call these functions from any non-audio thread? :)

helgoboss commented 3 years ago

But I ran into some issues: Even though I pass the right track_idx and file path, it's not inserting the file as midi item on the track: Instead, this call creates a new track but the midi item does not appear on it. What's noteworthy though: The new track is named after the midi file, just like when dragging a midi file from explorer into Reaper. (The Reaper track name is taken from the name of the first named track inside the multitrack midi file.) So it is reading the midi file! And when I try to rename the midi file while this Reaper session is open, Explorer complains that the file is locked in Reaper. So it's also locking it. (Which is bad for my use case of continuously re-generating this midi file from my livecoding application. Any idea how to make it not lock it? Btw, if I drop a midi file from Explorer into Reaper, it doesn't lock it!) And any idea why the midi file doesn't appear as a media item on the track? (The track is empty afterwards.) (Btw, track_idx is correct, it's removing the media items of the correct track, and DeleteTrackMediaItem returns true.)

This phenomena is probably more related to the way this particular REAPER function works, not reaper-rs. I would like to give you more hints but I'm not familiar with this function. I think it's best if you experiment a bit with this in ReaScript/Lua first to get a feeling how the function works. That's what I always do if I want to get to know a REAPER function (since most of them are not well documented). If this doesn't get you further, writing Justin an email is usually the way to go.

And if you found the answer, adding some reaper-rs method documentation would be awesome :)

Another issue I encountered: I use the hotwatch crate to watch the midi file to auto-reload it whenever it changes and I get this panic:

thread 'unnamed' panicked at 'called main-thread-only function from wrong thread': C:\Users\me.cargo\git\checkouts\reaper-rs-d32fed07113e6874\5eee308\main\medium\src\reaper.rs:4020

https://github.com/helgoboss/reaper-rs/blob/5eee308bad13d1e6cc8abdb0f53c266e5892e9db/main/medium/src/reaper.rs#L4020

But it's not being called from the audio thread, but from the newly spawned background watcher thread. (hotwatch also has a blocking API but I can't use that because I need to be able to terminate the watcher thread in the Drop impl of my plugin, which isn't possible with hotwatch's blocking API.) When I was using my own Reaper API bindings, I never had problems calling them from a new thread. So with reaper-rs, shouldn't it also be allowed to call these functions from any non-audio thread? :)

Most REAPER functions must only be called from the main thread, otherwise it's undefined behavior and can even lead to crashes. There are some functions which may be called from other threads as well but it's the exception (and inserting a media item is most likely not one of these exceptions). reaper-rs embraces safety and therefore checks proactively if it's the correct thread - in order to spare you from situations in which everything works on your machine, but only most of the time and maybe not on other machines. Happened to me a lot!

The usual way I approach this is to schedule something for execution on the main thread (by sending closures via channel to a ControlSurface and executing them in the run method). This is something you need for sure when writing multi-threaded REAPER plug-ins. In ReaLearn I have channels all over the place.

Boscop commented 3 years ago

This phenomena is probably more related to the way this particular REAPER function works, not reaper-rs. I would like to give you more hints but I'm not familiar with this function. I think it's best if you experiment a bit with this in ReaScript/Lua first to get a feeling how the function works. That's what I always do if I want to get to know a REAPER function (since most of them are not well documented).

I tested InsertMedia from lua and it worked, but it always opens this popup:

image

I didn't find a way to suppress this popup.. But in the manual of this tool it says:

image

Which I thought implies that there is a way to suppress this popup when calling InsertMedia (used for "MIDI CH mode"). But it seems to always call it with 1 as flag arg (insert on new track), without any other flags: https://github.com/daniellumertz/DanielLumertz-Scripts/blob/f3bffa2bfd07a075ea0c4c7a678acb9d19b7975f/MIDI%20Transfer/main.lua#L208

But I found something useful in that manual:

image and image

So it seems that's the only solution, to not permanently lock the inserted midi file. Then, when Reaper's window is not focused and it's not playing, it will unlock the midi file, which allows my livecoding application to replace the file without this popup showing up. But it's still not optimal, because the midi file can't be replaced while Reaper is playing when it's not focused..

Maybe I can get InsertMedia to work such that it behaves like drag&drop (so that it doesn't lock the file, with the above setting set to "Import midi files as midi items (in project)"). But then I'd have to find a way to suppress that popup. Do you know if there's a way (maybe through a hidden setting)? :)

If this doesn't get you further, writing Justin an email is usually the way to go.

You mean sending to support@cockos.com? I tried that a while ago but never got a response. Or do you mean a different email address?

The usual way I approach this is to schedule something for execution on the main thread (by sending closures via channel to a ControlSurface and executing them in the run method). This is something you need for sure when writing multi-threaded REAPER plug-ins. In ReaLearn I have channels all over the place.

Thanks, I'll try that. Btw, is there any way to run things periodically on the main thread without having a control surface? (We used to have Plugin::idle() in vst-rs but removed it because the opcode was deprecated and wasn't being called by all hosts. And Editor::idle() depends on having a GUI.)

helgoboss commented 3 years ago

This phenomena is probably more related to the way this particular REAPER function works, not reaper-rs. I would like to give you more hints but I'm not familiar with this function. I think it's best if you experiment a bit with this in ReaScript/Lua first to get a feeling how the function works. That's what I always do if I want to get to know a REAPER function (since most of them are not well documented).

I tested InsertMedia from lua and it worked, but it always opens this popup:

image

I didn't find a way to suppress this popup.. But in the manual of this tool it says:

image

Which I thought implies that there is a way to suppress this popup when calling InsertMedia (used for "MIDI CH mode"). But it seems to always call it with 1 as flag arg (insert on new track), without any other flags: https://github.com/daniellumertz/DanielLumertz-Scripts/blob/f3bffa2bfd07a075ea0c4c7a678acb9d19b7975f/MIDI%20Transfer/main.lua#L208

But I found something useful in that manual:

image and image

So it seems that's the only solution, to not permanently lock the inserted midi file. Then, when Reaper's window is not focused and it's not playing, it will unlock the midi file, which allows my livecoding application to replace the file without this popup showing up. But it's still not optimal, because the midi file can't be replaced while Reaper is playing when it's not focused..

Maybe I can get InsertMedia to work such that it behaves like drag&drop (so that it doesn't lock the file, with the above setting set to "Import midi files as midi items (in project)"). But then I'd have to find a way to suppress that popup. Do you know if there's a way (maybe through a hidden setting)? :)

Not aware of that. If this InsertMedia stuff is too restrictive, then it might be time for some of the more low-level functions (inserting single events directly into the MediaItem). But as we discussed before, it will need much thought to lift those to medium-level API level. Maybe you can gain some experience by trying the low-level methods - this is what REAPER C++ devs have to put up with all the time ;)

If this doesn't get you further, writing Justin an email is usually the way to go.

You mean sending to support@cockos.com? I tried that a while ago but never got a response. Or do you mean a different email address?

https://forums.cockos.com/showpost.php?p=134239&postcount=8

The usual way I approach this is to schedule something for execution on the main thread (by sending closures via channel to a ControlSurface and executing them in the run method). This is something you need for sure when writing multi-threaded REAPER plug-ins. In ReaLearn I have channels all over the place.

Thanks, I'll try that. Btw, is there any way to run things periodically on the main thread without having a control surface? (We used to have Plugin::idle() in vst-rs but removed it because the opcode was deprecated and wasn't being called by all hosts. And Editor::idle() depends on having a GUI.)

Control surface is the way to go. That's REAPER's way to let you participate in the main loop. And it works brilliantly, so I think there's no need for an alternative.

helgoboss commented 3 years ago

@Boscop Looks I was wrong with the last statement. There might be another way to run something periodically which I was not aware of: Search for "timer" in https://github.com/justinfrankel/reaper-sdk/blob/main/sdk/reaper_plugin.h. I guess that - apart from the fact that all it needs is a function pointer - it's not much different from the IReaperControlSurface Run() method.

Boscop commented 3 years ago

Thanks, I'm doing everything in ControlSurface::run now and it's not crashing on startup anymore, and everything works fine now :) Btw, does it run at a constant framerate?


It turns out that with InsertMedia there's no way to suppress this popup:

image

So I'm doing it this way instead:

pub unsafe fn reaper_insert_midi_file(
    reaper: &Reaper,
    track: MediaTrack,
    midi_path: impl AsRef<Path>,
) {
    let insert_pos_sec = 0.;
    let path_str_c = CString::new(midi_path.as_ref().to_string_lossy().as_bytes()).unwrap();

    let reaper = reaper.low();
    let item = reaper.AddMediaItemToTrack(track.as_ptr());
    let take = reaper.AddTakeToMediaItem(item);
    let source = reaper.PCM_Source_CreateFromFile(path_str_c.as_ptr());
    let mut is_qn: bool = false;
    let mut length = reaper.GetMediaSourceLength(source, &mut is_qn as *mut _);
    if is_qn {
        let pos_qn = reaper.TimeMap2_timeToQN(null_mut(), insert_pos_sec);
        length = reaper.TimeMap2_QNToTime(null_mut(), pos_qn + length) - insert_pos_sec;
    }
    reaper.SetMediaItemTake_Source(take, source);
    reaper.SetMediaItemInfo_Value(item, cstr_const!(D_POSITION), insert_pos_sec);
    reaper.SetMediaItemInfo_Value(item, cstr_const!(D_LENGTH), length);
    reaper.UpdateArrange();
}

So I guess you could merge this PR (and implement InsertMediaMode::to_raw). Even though I'm not using this function for this use case, the interpretation of flags seems correct. I played around with it in cfillion's iReaScript lua console that I installed from ReaPack.


My above function works well, the only disadvantage is that I'd have to manually construct the tempo map from the tempo meta events in the midi file and create the tempo markers using the extension API :/ There doesn't seem a way to import the midi file while also importing its tempo map and suppressing this popup.. (I have a lot of tempo micro-variations in my livecoded midi songs, to make them more "organic".)

helgoboss commented 1 year ago

Available on master.