rea-rs
У меня выдалось относительно свободное время. И я решил потратить его на open-source
Необходимость сменить любимые
Обратная связь тут почти моментальная — поскольку, в любой 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 на форум не завезли...
Для ExtState я завёл унифицированный интерфейс, который работает одинаково (и одинаково хорошо) на хосте, проекте, треке, итеме, тейке, сенде (кажется, всё). Кроме того, сохраняет не голые строки, а может сохранять целые структуры оптимально, и без лишнего бойлер-плейта:
Выяснилось, что для того, чтобы прочитать один midi-event из тейка (
С rea-rs это выглядит примерно так:
Кстати: никаких тебе TiemToQN, есть одна сущность Position — время с начала проекта. Внутри оно представлено, как секунды и наносекунды (
Кроме того, теоретически, можно использовать API и библиотеку внутри VST-плагина, и это может вывести проекты вроде reaticulate на новый уровень. Собственно, оригинальная библиотека reaper-rs и появилась потому что Бенджамин писал свой плагин rea-learn. Так что никаких принципиальный препятствий к этому нет. Я просто ещё не тестировал возможности. Кстати говоря, писать плагины на rust тоже достаточно приятно. Есть хорошие библиотеки, которые собираются с первого раза и выглядят выразительнее камрадес из C++.
Теперь о минусах.
У меня выдалось относительно свободное время. И я решил потратить его на open-source
Необходимость сменить любимые
Python + reapy
на что-то другое назрела настолько, что последние полтора месяца я вбухал именно в эту задачу. Мне захотелось найти лучший способ писать скрипты, чтобы в итоге:- Была быстрая обратная связь: либо моментальный отклик Reaper, как во встроенном редакторе скриптов; либо быстрый запуск снаружи, как это сделано в reapy. Либо что-то другое (спойлер, это оно и есть).
- Современная поддержка IDE, хотя бы на уровне JS\Python. Чего, к сожалению, нельзя сказать о линтерах и сниппетах для EEL и Lua.
- Установка "в один клик", как для пользователя, так и для разработчика. С первым реальные проблемы были у reapy. Настолько реальные, что большей части своего кода пользовался я сам. Есть у меня ребята на поддержке (за дружбу), которым периодически приходится помогать устанавливать Python в reaper. Со вторым огромные проблемы у C++: У меня так и не получилось разобраться во всех этим makefile и структуре проектов SWS и WDL.
- Безопасность. Надоело посреди работы получать портянки ошибок вида «не нашёл ни одного выделенного трека». Такого рода безопасность обеспечивается в первую голову, хорошей системой типов, во вторую, тестами. С типами у lua и eel всё совсем печально. В Python было уже заметно получше, но всё равно не так как хотелось бы. С тестами тоже невесело, по крайней мере, я так и не собрал себе ни одну CI среду, которая бы прогоняла тесты на живом Reaper.
Обратная связь тут почти моментальная — поскольку, в любой 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();
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.
Последнее редактирование: