helgoboss / reaper-rs

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

What is the minimum + simplest skeleton for an extension that receives all possible events/observable data (Project changes, actions, MIDI, etc)? #49

Open GavinRay97 opened 3 years ago

GavinRay97 commented 3 years ago

So I have pulled this out from referencing the below files, but I don't know enough about the project internals to tell whether this is the simplest and most-minimal implementation.

https://github.com/helgoboss/reaper-rs/blob/master/test/test/src/api.rs https://github.com/helgoboss/reaper-rs/blob/master/test/test/src/lib.rs

Can the example here be made any simpler? https://gist.github.com/GavinRay97/3195db1606d7e19344b23e3401583dc9

Also, few quick questions:

  1. Is the MidiRx stuff currently inaccessible as a WIP? Seems to be private:

image

  1. In the test, there's also this: static TEST: once_cell::sync::Lazy<Test> = once_cell::sync::Lazy::new(Default::default);

Is this necessary, or does it work with just &Test::get() like below?

impl ReaperRx {
  pub fn get() -> &'static ReaperRx {
      Reaper::get().require_main_thread();
      &ReaperRx::get()
      // TODO: Is this necessary?
      // static REAPER_RX: once_cell::sync::Lazy<ReaperRx> = once_cell::sync::Lazy::new(Default::default);
  }
}
  1. And the last bit -- when doing .subscribe() the type definition for some reason is empty: image

Thank you =)

helgoboss commented 3 years ago

I would recommend not relying at all on reaper-high and reaper-rx. It's going to be very different in future. If you want I can show you how to achieve this using reaper-medium. Okay?

GavinRay97 commented 3 years ago

Sounds good, you know the inner workings best 👍

Boscop commented 3 years ago

I'm also interested in how to do this with reaper-medium.

helgoboss commented 3 years ago

I'll try to create a minimal medium-level-API-only example project soon. Until then, check below references. The only thing that you should use from the high-level API is the entry point mechanism:

#[reaper_extension_plugin(
    name = "reaper-rs test extension plug-in",
    support_email_address = "info@helgoboss.org"
)]
fn main() -> Result<(), Box<dyn Error>> {
    // Just obtain the high-level REAPER instance and immediately go down to medium level.
    let reaper_session = Reaper::get().medium_session();
    reaper_session.reaper().show_console_msg("Hello world!");
    Ok(())
}

Important: Don't look on crates.io or docs.rs, look here on the master branch.

Incoming MIDI events

Control surface callbacks

Action invocations

GavinRay97 commented 3 years ago

Ahh okay got it, so it's just a regular csurf implementation, and then calling the plugin_register('hook_post_command', fn_ptr) Rust wrappers same as you'd normally do?

Thank you!

helgoboss commented 3 years ago

Yes. The goal of the medium-level API is to be 1:1 with the original API, just more safe and with idiomatic Rust. It's the hard part. Building something more convenient and more Rusty on top of that is easy ... but it's also very opinionated, that's why I have not settled on one way of doing things yet. Some of my abstractions are based on Rx (reaper-rx), some on enum variants (the "middleware" things in reaper-high).

Schroedingers-Cat commented 2 years ago

Control surface callbacks

* This is the main way to get notified about most things. Just implement the desired trait methods.
* See `reaper_medium::ReaperSession::plugin_register_add_csurf_inst` Rustdoc example.

I'm trying to make a basic control surface plugin. Following the tips from this issue and the generated docs, I ended up with a plugin that REAPER loads including running the plugin_main function. Also registering the control surface seems to work as I could use the debugger to verify the result from the plugin_register_add_csurf_inst call.

However, none of the implemented trait methods of the ControlSurface seem to be called as neither the debugger stops at those breakpoints and the println!() doesn't show up in the output during a debug session.

Is there anything more I have to do to get the csurf traits working?

use std::error::Error;
use reaper_macros::reaper_extension_plugin;
use reaper_low::PluginContext;
use reaper_medium::{ReaperSession, ControlSurface, ReaperStr };
use std::ffi::CString;

#[derive(Debug)]
struct MyControlSurface;

impl ControlSurface for MyControlSurface {
    fn set_track_list_change(&self) {
        println!("Tracks changed");
    }
    fn get_type_string(&self) -> Option<&ReaperStr> {
        let type_string = CString::new("ControlType").expect("Didn't work!");
        let type_string_ptr = type_string.as_ptr();

        unsafe{
            let return_string = ReaperStr::from_ptr(type_string_ptr);
            Some(return_string)
        }
    }
    fn get_desc_string(&self) -> Option<&ReaperStr> {
        let desc_string = CString::new("Control Description").expect("Didin't work!");
        let desc_string_ptr = desc_string.as_ptr();

        unsafe{
            let return_string = ReaperStr::from_ptr(desc_string_ptr);
            Some(return_string)
        }
    }
    fn run(&mut self) {
        println!("Running!");
    }
}

#[reaper_extension_plugin]
fn plugin_main(context: PluginContext) -> Result<(), Box<dyn Error>> {
    let mut session = ReaperSession::load(context);
    let result = session.plugin_register_add_csurf_inst(Box::new(MyControlSurface));
    match result {
        Ok(_v) => session.reaper().show_console_msg("Csurf created successfully!\n"),
        Err(_e) => session.reaper().show_console_msg("Creating csurf failed!"),
    };

    Ok(())
}
Schroedingers-Cat commented 2 years ago

Is the problem that at the end of plugin_main the registered control surface gets dropped? If so, how should we ensure that this doesn't happen?

helgoboss commented 2 years ago

@Schroedingers-Cat Sorry for the delay. Yes, the drop is the issue. Actually the complete ReaperSession gets dropped and the control surface along with it. (Note how this doesn't crash REAPER though ... that's a safety/cleanup feature added by reaper-medium :-) The only solution is to store the ReaperSession in a static (which is what a C++ extension would do as well). Either you do that manually or you use a small part of reaper-high to bootstrap the extension in a way that makes everything stay in memory. I recommend the latter because this is hopefully a part of the high-level API that's not going to change too much in future. It lets you access the high-level API from anywhere via static function reaper_high::Reaper::get(). Since the high-level API is still very much in flux, I recommend to immediately go down to the medium-level API by calling medium_reaper() on the high-level REAPER instance. That lets you call REAPER functions. If you need access to the medium-level session, you call medium_session() instead. There you can register/unregister stuff and reaper-rs will do the memory management for you.

Schroedingers-Cat commented 2 years ago

Thanks a lot for the explanations! I tried it via reaper-high as you suggested and the implemented control surface callbacks seem to be working. Maybe it'd be helpful to others to add a minimal example to the docs like the following?

use std::error::Error;
use reaper_macros::reaper_extension_plugin;
use reaper_medium::ControlSurface;

#[derive(Debug)]
struct MyControlSurface;

impl ControlSurface for MyControlSurface {
    // Implement Reaper's CSurf functions here
    fn set_track_list_change(&self) {
        reaper_high::Reaper::get().medium_reaper().show_console_msg("CSurf: Track list changed!\n");
    }
}

#[reaper_extension_plugin(
    name = "NAME_OF_YOUR_PLUGIN",
    support_email_address = "SUPPORT_MAIL_ADDRESS"
)]
fn plugin_main() -> Result<(), Box<dyn Error>> {
    let session = reaper_high::Reaper::get();
    let result = session.medium_session().plugin_register_add_csurf_inst(Box::new(MyControlSurface));
    match result {
        Ok(_v) => session.medium_reaper().show_console_msg("Csurf created successfully!\n"),
        Err(_e) => session.medium_reaper().show_console_msg("Creating csurf failed!"),
    };

    Ok(())
}
Levitanus commented 1 year ago

I'm Sorry, for several hours can't figure the way of registering the action (command).

I've tried as from high-level with, as I think, a static closure:

#[reaper_extension_plugin(
    name = "NAME_OF_YOUR_PLUGIN",
    support_email_address = "SUPPORT_MAIL_ADDRESS"
)]
fn plugin_main() -> Result<(), Box<dyn Error>> {
    let session = reaper_high::Reaper::get();

    session.register_action("test_reaper_ext", "description", ||reaper_high::Reaper::get().show_console_msg("action"), ActionKind::NotToggleable);

    Ok(())
}

As well, as with medium-level by moving around previously noticed reaper_medium::ReaperSession::plugin_register_add_hook_post_command reaper_medium::ReaperSession::plugin_register_add_hook_post_command_2

Anyway, still I can not reach for any verbose error or panic, not to see the action in actions list.

P.S. I want to start with the usual "scripted" way of making the extension, as I want to continue the development of my python GUI plugin in rust for taking benefits of type system and speed.

Levitanus commented 1 year ago

Well, for now I've figured this with using also of reaper_high::Action struct

use reaper_high;
use reaper_macros::reaper_extension_plugin;
use reaper_medium::{CommandId, HookCommand, OwnedGaccelRegister};
use std::error::Error;

const NAME: &str = "TEST_COMMAND";

fn id(name: &str) -> CommandId {
    let session = reaper_high::Reaper::get();
    let action = session.action_by_command_name(name);
    action.command_id().unwrap()
}

struct MyHook {}

impl HookCommand for MyHook {
    fn call(command_id: CommandId, _flag: i32) -> bool {
        let id = id(NAME);
        if command_id !=  id{
            reaper_high::Reaper::get().show_console_msg(format!(
                "wrong command, original is {}, got {}",
                id, command_id
            ));
            return false;
        }
        reaper_high::Reaper::get().show_console_msg("test");
        true
    }
}

#[reaper_extension_plugin(
    name = "NAME_OF_YOUR_PLUGIN",
    support_email_address = "SUPPORT_MAIL_ADDRESS"
)]
fn plugin_main() -> Result<(), Box<dyn Error>> {
    let mut session = reaper_high::Reaper::get().medium_session();
    let id = session.plugin_register_add_command_id(NAME)?;
    session.plugin_register_add_gaccel(OwnedGaccelRegister::without_key_binding(
        id,
        "test_rust_ext",
    ))?;
    session
        .plugin_register_add_hook_command::<MyHook>()
        .unwrap();

    Ok(())
}