[rea-rs]: ReaScript API для rust. (1 онлайн

PianoIst

Well-Known Member
19 Май 2010
4.248
4.310
113
32
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, gregobal и 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
Я начал летом опять писать. То, чего пишу показывать пока рано, и даже пока примерных планов нет.

Но зато в процессе я понял, что со старым подходом к интерфейсу жить дальше невозможно, и, не без помощи копилота, добавил в библиотеку почти полную поддержку SWELL и LICE: теперь можно рисовать самые типичные окна Reaper, со всеми доступными виджетами и собственной системой разметки с адаптивным дизайном.
Также добавлена поддержка toggle-actions, любых меню и расширение может создавать свою страницу в настройках reaper.

Попутно, я вылизал API библиотеки, и считаю, что теперь она стала достаточно стабильной, чтобы назвать это 1.0.0 и не бояться ей пользоваться в долгосрочной перспективе.

В процессе допилил систему интеграционных тестов: в репозитории лежат настройки для VSCode и SublimeText, чтобы собирать интеграционный тест и запускать портативный Reaper для тестирования и разработки расширения.

В качестве побочного эффекта, эта версия ломает очень много из старого API. Заодно все предыдущие мои окна расширений надо переписать на swell, так что процесс переезда на 1.0 займёт много времени.
1791464440851.png



1791472138369.png
 
Приветствую, Тимофей! В двадцать шестом году эта работа уже не кажется хорошей идей. Еще немного (год другой) и нейросети смогут новый рипер с одного промпта написать... Ну а сегодня за недельку можно сварганить минирипер под свою узкую задачу...
 
@Antonio, неправда)

Во-первых, чтобы нейросети могли что-то писать, им нужны библиотеки с нормальным API. Без этого спагетти получается на самых маленьких задачах. А хороший API из ниоткуда не берётся.

Во-вторых, за неделю мини-рипер не написать. Я делал два сносных размеров проекта, преимущественно на вайбкодинге. Оба стоили 2-недельного спринта и ~100€ токенов. И поддерживать их по сути невозможно, потому что там, где ты отдаёшь контроль аишке ‒ разрастается гора кода, в которой человек не разберётся, а аишка тратит всё больше и больше токенов. Я вот подумывал, миди-редактор с аишкой написать, но понял, что это ‒ слишком амбициозно. Куда легче встроить всё что мне надо ‒ в оригинальный Midi-редактор Reaper.

В-третьих, у меня есть несколько идей касательно архитектуры оркестровой DAW или сэмплирования инструментов. Мысли эти основаны на концепциях, никак не связанных с технологическими достижениями последних лет 40, просто на самой обычной логике, DSP и немного статистики. Так вот, я думал, что за десять лет эти концепции протухнут, устареют и их десять раз реализуют. Но мне они кажутся всё ещё очень современными и своевременными)
 

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