[rea-rs]: ReaScript API для rust.

PianoIst

Well-Known Member
19 Май 2010
4.145
4.092
113
30
Kirchberg, kreis Zwickau
soundcloud.com
rea-rs

У меня выдалось относительно свободное время. И я решил потратить его на open-source :)

Необходимость сменить любимые Python + reapy на что-то другое назрела настолько, что последние полтора месяца я вбухал именно в эту задачу. Мне захотелось найти лучший способ писать скрипты, чтобы в итоге:
  • Была быстрая обратная связь: либо моментальный отклик Reaper, как во встроенном редакторе скриптов; либо быстрый запуск снаружи, как это сделано в reapy. Либо что-то другое (спойлер, это оно и есть).
  • Современная поддержка IDE, хотя бы на уровне JS\Python. Чего, к сожалению, нельзя сказать о линтерах и сниппетах для EEL и Lua.
  • Установка "в один клик", как для пользователя, так и для разработчика. С первым реальные проблемы были у reapy. Настолько реальные, что большей части своего кода пользовался я сам. Есть у меня ребята на поддержке (за дружбу), которым периодически приходится помогать устанавливать Python в reaper. Со вторым огромные проблемы у C++: У меня так и не получилось разобраться во всех этим makefile и структуре проектов SWS и WDL.
  • Безопасность. Надоело посреди работы получать портянки ошибок вида «не нашёл ни одного выделенного трека». Такого рода безопасность обеспечивается в первую голову, хорошей системой типов, во вторую, тестами. С типами у lua и eel всё совсем печально. В Python было уже заметно получше, но всё равно не так как хотелось бы. С тестами тоже невесело, по крайней мере, я так и не собрал себе ни одну CI среду, которая бы прогоняла тесты на живом Reaper.
Так вот, переезд на rust решает все вышеперечисленные проблемы, кроме одной, о которой я пока умолчал)

Обратная связь тут почти моментальная — поскольку, в любой IDE с поддержкой rust непрерывно работает стандартный линтер cargo check, а компилятор не даёт скомпилироваться, пока программа выглядит нехорошо на уровне типов — можно по два часа писать код, вообще не запуская Reaper. Более того, не всегда расширение в каждой функции контактирует с API. У меня, допустим, в нотном редакторе, есть всего пара модулей, отвечающих за сбор данных с Reaper, а всё остальное я делаю «внутри» пакета почти что на чистых функциях. Их можно и тестировать в один клик без запуска Reaper, и вообще язык очень помогает писать хорошо.

Кроме того, Бенджамин — автор биндингов к C++, собрал рабочее тестовое окружение, которое само запускает Reaper и прогоняет тесты. Для библиотеки я из него выкинул всё лишнее (почти всё), мигрировал на последнюю версию Reaper (6.71, т.к. занимался обёрткой современного API), и сейчас он также по кнопке прогоняет все тесты. Надеюсь, следующим шагом, я адаптирую этот модуль в отдельный crate, и можно будет легко запускать тесты на любом проекте.

С Установкой тоже проблем нет, т.к. итоговый результат компилируется в динамическую библиотеку (*dll, *so, .dylib), и пользователю достаточно положить её в папку UserPlugins. (Или использовать ReaPack).

Про безопасность я отдельную статью на хабр накатал. В общем и целом — мне очень нравится.

Что ещё классно — что для любого crate в rust генерируется потрясающая документация, с авто-тестами, поиском и скинами.


API

Ну а самое привлекательное — что библиотека получилась выразительной и интуитивной. Мне хотелось того же уюта и ясности, что был в reapy, поэтому я начал с попытки портировать его. Потом начал немного отходить в сторону более растасеанского API. Вот пара примеров из документации.

Минимальный пример плагина с регистрацией экшна и лисенером событий (что-то вроде defer): Приплыли, подсветку для rust на форум не завезли...
C-like:
use rea_rs::{
ActionKind, ControlSurface, PluginContext, Reaper, RegisteredAction,
};
use reaper_macros::reaper_extension_plugin;
use std::error::Error;

#[derive(Debug)]
struct Listener {
    action: RegisteredAction,
}

// Full list of function larger.
impl ControlSurface for Listener {
    fn run(&mut self) {
        Reaper::get().perform_action(self.action.command_id, 0, None);
    }
}

fn my_action_func(_flag: i32) -> Result<(), Box<dyn Error>> {
    Reaper::get().show_console_msg("running");
    Ok(())
}

#[reaper_extension_plugin]
fn plugin_main(context: PluginContext) -> Result<(), Box<dyn Error>> {
    Reaper::load(context);
    let reaper = Reaper::get_mut();

    let action = reaper.register_action(
        // This will be capitalized and used as action ID in action window
        "command_name",
        // This is the line user searches action for
        "description",
        my_action_func,
        // Only type currently supported
        ActionKind::NotToggleable,
    )?;

    reaper
        .medium_session_mut()
        .plugin_register_add_csurf_inst(Box::new(Listener { action })).unwrap();
    Ok(())
}

Для ExtState я завёл унифицированный интерфейс, который работает одинаково (и одинаково хорошо) на хосте, проекте, треке, итеме, тейке, сенде (кажется, всё). Кроме того, сохраняет не голые строки, а может сохранять целые структуры оптимально, и без лишнего бойлер-плейта:

C-like:
use rea_rs::{ExtState, HasExtState, Reaper, Project};
let rpr = Reaper::get();
let mut state =
    ExtState::new("test section", "first", Some(10), true, rpr);
assert_eq!(state.get().expect("can not get value"), 10);
state.set(56);
assert_eq!(state.get().expect("can not get value"), 56);
state.delete();
assert!(state.get().is_none());

let mut pr = rpr.current_project();
let mut state: ExtState<u32, Project> =
    ExtState::new("test section", "first", None, true, &pr);
assert_eq!(state.get().expect("can not get value"), 10);
state.set(56);
assert_eq!(state.get().expect("can not get value"), 56);
state.delete();
assert!(state.get().is_none());

let tr = pr.get_track_mut(0).unwrap();
let mut state = ExtState::new("testsection", "first", 45, false, &tr);
assert_eq!(state.get().expect("can not get value"), 45);
state.set(15);
assert_eq!(state.get().expect("can not get value"), 15);
state.delete();
assert_eq!(state.get(), None);

Выяснилось, что для того, чтобы прочитать один midi-event из тейка (MIDI_GetEvt и т.п.), Reaper распаковывает всю сырую миди-дорожку , пока не доберётся до этого эвента. То есть, для того, чтобы поменять пару нот — надо два раза распаковать\запаковать всё миди на тейке. Поэтому, библиотека принципиально не использует эти функции, а итерирует сырой миди. Но делает это удобно)

Блин! А для lua подсветки тоже нет! На форуме же, в основном на lua общаются.
C-like:
-- Notation Events Chords to Midi Marker Text Events or Regions
-- juliansader https://forum.cockos.com/member.php?u=14710

reaper.Undo_BeginBlock2(0)
--reaper.Main_OnCommand(40421,0) --Item: Select all items in track 40421
reaper.Main_OnCommand(40153,0) --Item: Open in built-in MIDI editor (set default behavior in preferences) 40153
take = reaper.MIDIEditor_GetTake(reaper.MIDIEditor_GetActive())
reaper.MIDI_Sort(take)
MIDIOK, MIDI = reaper.MIDI_GetAllEvts(take, "")
tChords = {}
stringPos, ticks = 1, 0
while stringPos < MIDI:len() do
    offset, flags, msg, stringPos = string.unpack("i4Bs4", MIDI, stringPos)
    ticks = ticks + offset
    if msg:byte(1) == 0xFF then
        chord = msg:match("text (.+)")
        if chord then
            tChords[#tChords+1] = {chord = chord, ticks = ticks}
        end
    end
end

tChords[#tChords+1] = {ticks = ticks}
for i = 1, #tChords do
    tChords[i].time = reaper.MIDI_GetProjTimeFromPPQPos(take, tChords[i].ticks)
end
for i = 1, #tChords-1 do
    reaper.MIDI_InsertTextSysexEvt( take, true, false, tChords[i].ticks, 6, tChords[i].chord ) -- Insert Midi Marker Text Event
    -- Set Region Color RGB > reaper.ColorToNative(55,118,235)
    --reaper.AddProjectMarker2(0, true, tChords[i].time, tChords[i+1].time, tChords[i].chord, 0, reaper.ColorToNative(55,118,235)|0x1000000) -- Insert Region
end

reaper.MIDIEditor_OnCommand( reaper.MIDIEditor_GetActive(), 2 ) -- File Close wimdow
reaper.Undo_EndBlock2(0, "Convert notation chords to midi markers", -1)

С rea-rs это выглядит примерно так:
C-like:
let pr = Reaper::get().current_project();
let mut tr = pr.get_track_mut(0).unwrap();
let mut item = tr.get_item(0).unwrap();
let mut take = item.active_take();
let events = take.iter_midi();

println!("\n----CC EVENTS----");
let mut cc_events: Vec<MidiEvent<CCMessage>> =
    events.clone().filter_cc().collect();
for mut event in cc_events.iter() {
    println!("{}", event);
    event.set_cc_num(2);
    event.set_channel(4);
}

println!("\n\n----NOTE EVENTS----");
println!("======================");
let mut notes: Vec<MidiNoteEvent> =
    events.clone().filter_notes().collect();
for mut event in notes.iter_mut() {
    println!("{:#?}, note start: {:?}, note end: {:?}",
        event, Position::from_ppq(event.start_in_ppq, take),
        Position::from_ppq(event.end_in_ppq, take));
    event.off_velocity = 45;
}

println!("\n\n----Back to RAW EVENTS----");
println!("======================");

// Теперь запаковываем их обратно в буфер.
// Надо распаковать ноты, т.к. внутри они представляют собой два отдельный сообщения..
let raw_events = flatten_midi_notes(notes.into_iter())
    // В CC может содержаться напряжение кривой Безье.
    // Поэтому, их тоже надо распаковать перед тем, как перегонять в raw
    // The same with CC events.
    .chain(to_raw_midi_events(flatten_events_with_beizer_curve(
        cc_events.into_iter(),
    )));

// получившийся вектор можно слать обратно в тейк.
let raw_buf: Vec<u8> =
    MidiEventConsumer::new(sorted_by_ppq(raw_events)).collect();
take.set_midi(raw_buf).unwrap();
Кстати: никаких тебе TiemToQN, есть одна сущность Position — время с начала проекта. Внутри оно представлено, как секунды и наносекунды (std::time::Duration). Можно в нужных ситуациях брать как PPQ, четверти, или даже сэмплы (главное, чтобы не было переполнения).

Кроме того, теоретически, можно использовать API и библиотеку внутри VST-плагина, и это может вывести проекты вроде reaticulate на новый уровень. Собственно, оригинальная библиотека reaper-rs и появилась потому что Бенджамин писал свой плагин rea-learn. Так что никаких принципиальный препятствий к этому нет. Я просто ещё не тестировал возможности. Кстати говоря, писать плагины на rust тоже достаточно приятно. Есть хорошие библиотеки, которые собираются с первого раза и выглядят выразительнее камрадес из C++.

Теперь о минусах.

  • Нет GUI. Не то, чтобы его нет вообще, есть нативная open-gl библиотека egui, которую используют создатели VST и CLAP. Есть ещё, как минимум, 5 достойных фреймворков, которые можно запускать в отдельном потоке. Но пока нет никаких внятных механисзмов коммуникации потока GUI с основным потоком, из которого можно вызывать функции Reaper. В принципе, на борту лежит полная копия WDL, так что чисто технически можно написать «совсем родной» GUI, как это сделано в SWS. Но я не представляю, сколько надо на это потратить сил. Пока что у меня не получилось даже окно создать.
  • На настоящий момент библиотека потоко-небезопасна. То есть, она безопасна почти в любом аспекте, кроме того, когда мы передвигаем тейки из потока с GUI, или из аудио-потока (для VST). Бенджамин об это сильно обжёгся, поэтому стал делать свою систему защиты от дурака. Но его подход — написать обёртку трижды: сначала автоматический генератор из звголовков C++, потом безопасную обёртку над ним без всяких вмешательств в API (по сути, поднять rust до уровня eel), а потом только делать что-то человеко-читаемое. Я попробовал, мне показалось, что это невыполнимо. Поэтому написал поверх сырых биндингов. В принципе, есть места, в которые я потом смогу впихнуть потоко-безопасность без изменения API.
  • Альфа-релиз, нестабильный API. Вполне может быть, что в угоду красоте и удобству некоторые функции будут пропадать, некоторые появляться. Зато, как только проект выйдет на crates.io, можно будет привязываться к конкретной версии, и не беспокоиться, что с очередным пулл-реквестом на мойм мастере, в коде что-то сломается. Опубликую на crates.io сразу, как только Бенджамин примет мой патч в reaper-rs, и сам обновит свой пакет.
  • Экосистема rust не такая богатая, как у Python или C++. Поэтому можно встрять с какой-то задачей, которую ещё никто до тебя не решал. И месяц писать какой-нибудь анализатор. Но вообще сообщество rust audio очень живое, и разрабатывают активно. Даже пишут свою открытую DAW на rust.
Надеюсь, что у проекта, как и у ReaScript на rust большое будущее :) Wellcome!
 
Последнее редактирование:
Статья, навеянная выпуском.
 
  • Like
Реакции: Antonio и BAYANBAYAN
Выделил в отдельный репозиторий инструмент для тестирования: https://github.com/Levitanus/reaper-test

Он независим от rea-rs, и можно на нём тестировать плагины, написанные и на голом reaper-rs. Но на windows пока автоматическую установку портабельного reaper и тестового плагина не настроил. Только mac + linux, и, пока что только версия 6.71.
 
Недавно сделал станочек для намотки фортепианных басов, всё никак не соберусь отписать в теме про настройку фортепиано. На ардуино, с примитивным кодом с использованием сторонних библиотек. Пока что это мой максимум, до вашего, Тимофей, уровня, к сожалению, далеко, а двое юных гениев вдохновляют тратить кучу времени на них, а не на освоение чего-то нового.
Пишу здесь, чтобы выразить своё восхищение и поддержать тему. :)

Очень понравилась статейка. Жду продолжения. Прошу в нём упомянуть, как вам удалось иммигрировать в Германию. Насколько я знаю, там всё не просто с документами. Меня 20 лет назад из Берлина депортировали. :)
 
  • Like
Реакции: PianoIst
двое юных гениев
ох, да... Мы твёрдо решили, что одной — за глаза :)

А мне наоборот — ардуино и хочется и колется пощупать, это ж целая бездна расходов открывается! Очень интересно про станок, я своим так и не обзавёлся.

С Германией мы всё ещё в процессе, но пока, кажется, оформляемся. Надеюсь...
 
  • Like
Реакции: Antonio
Очень интересно про станок, я своим так и не обзавёлся.
Обойдется в пару дней и в около $200 если по моему минималистичному рецепту делать. Сам 2 недели строил, самостоятельно вникая, просматривая кучу видео.

Мы твёрдо решили, что одной — за глаза

Это лотерея, можно играть, а можно не играть. У меня вообще четверо и не жалею. Последний в самом деле гениальный, каждый день радуюсь возможности быть рядом. Но могло и не повезти. )
 
  • Like
Реакции: PianoIst
Потихоньку дифференцирую Sys ивенты. Вот, например, полностью код плагина, который выводит в консоль нотацию:
C-like:
use rea_rs::{
    ActionKind, MidiMessage, NotationMessage, PluginContext, Reaper,
};
use reaper_macros::reaper_extension_plugin;
use std::error::Error;

pub fn print_midi() {
    let pr = Reaper::get().current_project();
    let it = pr.get_selected_item(0).unwrap();
    let take = it.active_take();
    take.iter_midi(None)
        .unwrap()
        .map(|event| {
            let msg = event.message();
            match NotationMessage::from_raw(msg.get_raw()) {
                None => (),
                Some(msg) => {
                    println!("notation: {}", msg);
                }
            }
        })
        .count();
}

#[reaper_extension_plugin]
fn plugin_main(context: PluginContext) -> Result<(), Box<dyn Error>> {
    Reaper::load(context);
    let rpr = Reaper::get_mut();
    let _id = rpr.register_action(
        "test_ext_print_midi",
        "TestExtension: print_midi",
        |_| Ok(print_midi()),
        ActionKind::NotToggleable,
    );
    Ok(())
}

Вывод:
Bash:
[levitanus@levitanus-main rea-score-rs]$ './target/reaper/reaper_linux_x86_64/REAPER/reaper'
LC_NUMERIC / LANG is set to 'ru_RU.utf8', overriding LC_NUMERIC environment to POSIX
notation: NotationMessage{notation: Notation: Track(tokens: ["dynamic", "ppp"])}
notation: NotationMessage{notation: Notation: Note(channel:1, note: 62, tokens: ["articulation", "staccato"])}
notation: NotationMessage{notation: Notation: Note(channel:3, note: 62, tokens: ["articulation", "staccato"])}
 

Вложения

  • Like
Реакции: Antonio

Обернул ReaImGui, можно использовать. Правда, на данный момент очень неудобно, т.к. приходится писать кучу unsafe и ffi типов. Про удобство буду думать попозже)
 
  • Like
Реакции: Antonio
Начал прикручивать ImGui. Попутно отвязался от зависимости reaper-medium, включил в пакет *low и *macros, так что получилось наконец опубликовать всё это дело на crates.io!

ImGui обернул сначала в совсем страшном варианте, где сплошной unsafe и С-интерфейсы наружу. Но сейчас получается вполне так себе симпатичная обёртка.



C-like:
use rea_rs::{PluginContext, Reaper, Timer};
use rea_rs_macros::reaper_extension_plugin;
use reaper_imgui::{Context, ImGui};
use std::error::Error;


#[reaper_extension_plugin]
fn test_main(context: PluginContext) -> Result<(), Box<dyn Error>> {
    println!("test plugin main");
    let rpr = Reaper::init_global(context);
    rpr.register_action(
        "test_imgui_window",
        "reaper-imgui preview window",
        |flag: i32| run_imgui(flag),
        None,
    )?;
    println!("Registered action");
    Ok(())
}

fn run_imgui(_: i32) -> Result<(), Box<dyn Error>> {
    let imgui = ImGui::load(Reaper::get().plugin_context());
    Reaper::get_mut().register_timer(Box::new(ImGuiRunner::new(imgui)));
    Ok(())
}

struct ImGuiRunner {
    imgui: ImGui,
    ctx: Context,
    count: i32,
}

impl ImGuiRunner {
    fn new(imgui: ImGui) -> Self {
        let ctx = imgui.create_context("main context");
        Self {
            imgui,
            ctx,
            count: 0,
        }
    }
}

impl Timer for ImGuiRunner {
    fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        if !self.ctx.window("main").open(|ctx| {
            ctx.text("Hello World!").show();
            {
                if ctx.button("dec").clicked() {
                    self.count -= 1;
                }
                ctx.sameline(50, None);
                if ctx.button("inc").clicked() {
                    self.count += 1;
                }
            }
            ctx.text(format!("current count: {}", self.count)).show();
        }) {
            self.stop();
        }
        Ok(())
    }
    fn id_string(&self) -> String {
        "imgui runner".to_string()
    }
}
 
  • Like
Реакции: Antonio, Greev и Slick
Почти допилил превью для нотного редактора, попутно сделал много всякой мелочёвки: накидал виджетов и функций в imgui, поменял алгоритм сериализации в ExtState (всё-таки, всех надёжней и красивей оказался JSON). Научился конвертировать пиксели в миллиметры и асинхронно рендерить lilypond)



Полоска для ввода кода — временная заплатка, надеюсь, завтра наконец избавлюсь, и будет честный рендер, пусть и минималистичный

Надеюсь в январе выкатить альфа-релиз rea-score, а вместе с ним, как раз созреют относительно стабильные версии всех библиотек, и можно будет ими пользоваться почти без оглядки.
 
Ну вот, выглядит как что-то относительно рабочее) Дальше уже в принципе остались технические моменты, надеюсь, не буду больше тут флудить)
 
  • Like
Реакции: Antonio
Здравствуйте! Не соображу, как установить reaper_levitanus_plugin.dll?
 
252353

Пробовал в C:\Program Files\REAPER\Plugins и в C:\Users\Basta\AppData\Roaming\REAPER\UserPlugins
 
хм, интересно. Если есть время ‒ было бы интересно созвониться и попытаться разобраться. Потому что на GitHub всё собирается и запускается
 
  • Like
Реакции: variator

Сейчас просматривают