От крышки рояля до фреймворка на rust: как системное программирование помогает творческой реализации

в 1:57, , рубрики: Без рубрики
Сижу за корпоративным маком и тоскую по этим нашим Линуксам
Сижу за корпоративным маком и тоскую по этим нашим Линуксам

Здравствуйте, меня зовут Тимофей, и я алкоголик программирую, чтобы писать музыку. Судя по всему, недуг мой прогрессирует:

  • Пять лет назад, когда я впервые услышал про LilyPond — язык программирования, на котором можно писать партитуры в текстовом редакторе — я посмеялся над гиком, что это придумал, и прошёл дальше.

  • Два года назад я прочитал код партитуры из примеров — удивился, насколько логично и музыкально он выглядит, подивился задротству автора, и пошёл набирать ноты в MuseScore.

  • В этом году я пишу экспортёр MIDI из Reaper в исходники LilyPond, а сегодня зарелизил rea-rs: фреймворк для написания расширений для Reaper на rust.

И, несмотря на то, что в этом альфа-релизе >16 300 строк, я всё ещё считаю, что занимаюсь «бытовым программированием». Имею в виду что-то вроде готовки: можно, конечно, сходить в ресторан, когда хочется чего-то эдакого, но в ресторан каждый день не набегаешься. А то, что погреб на даче потихоньку превращается в винодельню — так это побочный эффект. Зато всё своё, домашнее, натуральное.

В последнее время, подобные мысли я проговаривал не как здоровый сарказм, а как симптомы болезни: мол, «это не хумус, видео не передаёт запах»! Но, намедни, моя музыкальная школа подогнала мне мак с Finale и полным комплектом Adobe на борту, чтобы совместно работать над сборником рождественских песен. И… Holly crap, I was damn right! Но расскажу по порядку, как я дошёл до жизни такой.

Можно, конечно, написать байопик, начиная с того, как мне родители запрещали много играть в компьютер, но разрешали бесконечно долго заниматься на нём «чем-то полезным», поэтому в семь лет я три дня безвылазно тыкал мышкой в CakeWalk, «миди-программируя» (фу-фу-фу, нельзя так делать!) свою первую аранжировку. Но лет до 18-ти моя диджитал-музыкальная жизнь была вполне типичная для подростка. За исключением того, что я писал не «биты во фруктах», а оркестровки в Cubase.

Первый код

В 18 лет я получил заказ на… — целый балет для барнаульской филармонии. За баснословные тогда для меня деньги — 100 000 рублей! Спойлер — заплатили 30, и пришлось за ними ехать на автобусе в Барнаул. Балет был русской тематики: композитор писала фортепианные эскизы, я пытался сделать из них оркестр. Договорились писать для коллектива русских народных инструментов с десантом из симфонического оркестра. Дело было в 2012-м году, и тогда в VSTi не было почти никаких русских народных. И только посреди моей работы над проектом Илья Ефимов выпустил свой комплект пузочёсов.

Пузочёсы Ильи образца 2012-го года:

За первой эйфорией от появившихся инструментов пришло осознание, что труЪ русский народный саунд на этих инструментах не сделать: одинокие сольные инструменты звучат нежирно, незадорно, негодно. А писать для народников мне уже понравилось! Периодически, ныл об этом на форуме. Видимо, ныл настолько громко, что один из моих старших коллег по цеху дал пендель такой силы, что уже на следующий день мы вместе с однокурсником Артёмом сидели на студии и писали первые сэмплы домры.

Месяц я нарезал и выравнивал сэмплы, прочитал KontaktScriptProcessor</p>" data-abbr="KSP ">KSPKontaktScriptProcessor</p>" data-abbr="KSP "> Reference для сэмплера KONTAKT, и собрал первый инструмент. Назвать эмоции эйфорией — ничего не сказать! Папа у меня учился на кафедре АвтоматическиеСистемыУправления</p>" data-abbr="АСУ">АСУ, и меня всегда подмывало что-то закодить. Но никогда не получалось: я терялся в сущностях и аналогиях вроде: «машина это объект, у неё есть детали. Колёса — это потомки абстрактной детали, которые потом мы складываем в машину». Это всё заманчиво звучало, но не отвечало на вопрос: «а чё писать-то?» А здесь передо мной книжка, в которой прямым текстом написано, что, если хочешь сыграть ноту до — напиши: note_on(60, $EVENT_VELOCITY, 0, -1)! Ты пишешь — и оно играет ноту до, вот так просто!

В общем, в лучших традициях быдло-стартапа, в стандартном блокноте, процедурщиной, я сделал свой первый продукт, который потом заработал нам порядка $300.

Потом был крауд на оркестровые сэмплы, пришедшийся ровно на момент «крымнаша», так что я закладывал ребятам-исполнителям зарплаты по 500₽час, имея в виду $15, а в итоге платил 500₽ в час, в эквиваленте $7. Но они всё равно были довольные и мы-таки записали весь оркестр. И я даже частично их привёл в товарный вид. Этот долгострой я до сих пор не доделал. Сейчас в планах — закончить пакет для нарезания сэмплов, и выпустить инструменты на HISE. Параллельно мы с Даней, звукарём из харьковской консерватории, выпустили пару инструментов, заработав в общей сложности ~$500.

Повелитель рабочего окружения

Мои пузочёсы образца 2019-го:

Особый кайф получаешь от того, что в твоём музоне играют твои инструменты. И что без них музон бы не состоялся. И кажешься себе всемогущим: не хватает инструмента в работе — ты его делаешь. Или допиливаешь чужой. Кроме того, уже где-то ко второму или третьему инструменту я обнаружил, что та редактура сэмплов, которая в Cubase у меня занимает порядка рабочего дня, в Reaper выполняется за 1,5-2 часа. Потому что в Reaper есть матрица экспорта, цикл-экшны, ReaTune. А ещё в Reaper можно грабить корованы писать скрипты и аудио-эффекты. Через пару месяцев я полностью переехал на Reaper.

Показался себе достаточно крутым, чтобы зарабатывать кодингом, и устроился в стартап инструментов для киношной музыки. За полгода мой модуль эффектов в фирменной процедурщине разросся до ~40 000 строк, а когда макросы распаковывались, получалось уже около 250 000, что KONTAKT жевать не мог. Написал на Python первый плагин к SublimeText, чтобы он ставил всегда истинные условия  в особо больших функциях, чтобы не переполнять стек.

Кроме того, оказалось, что прогать то, к чему душа не лежит — не прикольно. Это в музыке я кайфую от процесса, вне зависимости от целеполагания. Да и уровень профессионализма позволяет фильтровать на входе всякое говно. А писать drag’n’drop на системе без поддержки DnD для окошка настроек, в которые 90% пользователей не заглянет никогда... Да ещё когда тебе становится противно смотреть на собственный код….

Мой модуль настроек для стартапа.
Мой модуль настроек для стартапа.

Эксперимент закончился тем, что я худо-бедно сдал свой модуль и уволился. Написал компилятор из Python в KSP. Чтобы он сам за меня определял типы, границы массивов, и рефакторил. И это был первый проект, на котором я начал что-то понимать в архитектуре, системе типов и программировании в целом. Обидно: компилятор получился шикарный! Я на нём написал несколько инструментов, в том числе один — на заказ, с  полифоничными модуляциями, которые распаковывались в красивый лаконичный и более-менее производительный KSP.

Компилятор не взлетел: сообщество и так неплохо пишет скрипты на KSP. И компилятор на незнакомом языке никому не нужен) А мне постепенно перестал нравиться KONTAKT: своей закрытостью, прожорливостью и накладными расходами для конечных пользователей инструментов, даже если они бесплатные.

Бытовое программирование

От крышки рояля до фреймворка на rust: как системное программирование помогает творческой реализации - 3

Иногда приятно себе сказать «ты никогда не…». В тот момент я себе сказал, что никогда не буду зарабатывать на программировании. Просто потому что с 5 лет играю на рояле, а с 7 лет аранжирую и пишу музыку. И этот вклад в профессию я вряд ли чем-то ещё перебью. И кодить стало кайфово. Потому что для себя. И потому что избавляю себя от боли ежедневной профессиональной рутины.

Бытовое программирование, как я его понимаю: есть проблема — реши её. В папке скриптов и плагинов Reaper лежит куча мелочи, которую я никогда даже на GitHub не заливал. Когда-то мне что-то мешало в проекте, я сделал удобно, и забыл об этом) Или интегрировал в инструмент-темплейт, а потом забыл.

Вот, три дня назад меня достал баг миди-клавиатуры. Когда пользуешься педалью — приходит 4-6 событий CC64 с разными значениями. Может это не баг, а фича, но все сэмплерные рояли на такое поведение отвечают тем, что несколько раз проигрывают сэмпл педали: такой глухой стук демпферов по струнам. Решение проблемы заняло 2 минуты, и запустилось с первого раза:

desc:filters annoying sustain messages
 
@sample
while (midirecv(ofst, msg1, msg2, msg3))(
 msg1 >= 0xb0 && msg1 < 0xc0?(
   msg2 != 64 || msg3 == 0 || msg3 == 127?(midisend(ofst, msg1, msg2, msg3);)
  
 ) : (midisend(ofst, msg1, msg2, msg3);)
)

Мелочь? Мелочь! Приятно? Приятно! А ведь на свете есть куча владельцев Roland RD-700, жующих кактус. И перед релизом они, может, руками вычищают все лишние ивенты, чтобы по ушам не било педалью.

Open-source

Что мне не очень нравится в Reaper — он ломает стандартные механизмы языков, которые используются для скриптов. Lua там какой-то свой, обрезанный. EEL — ЯП собственной разработки Джастина (автора), для которого лучшая IDE — встроенная в Reaper. Но писать в ней что-то длиннее примера выше больно. Потом, свой API Reaper экспортирует в момент запуска. Поэтому функции IDE с ними не работают: в лучшем случае, автокомплит, и то, только тот — что добавили сторонние пользователи.

А пара лет разработки на Python уже приучили к хорошей жизни. Оттого, на фоне московских протестов за мундепов 2019-го, я впрягся в reapy француза Ромео Деспре.

  • Добавил ему аннотаций типов.

  • Задизайнил модуль работы с миди.

  • Оптимизировал несколько бутылочных горлышек.

  • Хакнул функции, которые Reaper некорректно оборачивал в оригинальном модуле.

  • Добавил обёрток для популярных C++ расширений.

В ковид Ромео загрустил и почти перестал заниматься проектом. В конце концов мне надоело жить с 5-ю подвисшими pull-реквестами, и я официально отделил свой форк, вплоть до отдельного пакета на PyPi. Недавно, кстати, наши камрадес переехали с оригинала на форк, т.к. от меня есть хоть психологическая, но поддержка ;)

Большое решение для большой проблемы

Партитура.На 50% экспортирована из миди — на 50% допилена в исходниках LilyPond. Да, это не просто преобразование `0x903C` в `c`, тут настраивать надо :)
Партитура.
На 50% экспортирована из миди — на 50% допилена в исходниках LilyPond. Да, это не просто преобразование `0x903C` в `c`, тут настраивать надо :)

Иногда проблема большая. И для её решения нужны большие инструменты. Год назад у меня появилось много заказов на виртуальное исполнение, но с дубляжом аранжировки в нотах. Обычно, в таких ситуациях либо пишешь аранжировку сначала в нотах — потом играешь, и, если что-то не нравится — правишь ноты. Либо пишешь сразу в звук, а потом чистишь MIDI. Примерно как чистят Собственно, мы сейчас исполнение виртуальными инструментами тоже называем MoCap, потому что мы не пишем мышкой, мы играем и записываем своё живое исполнение.</p>" data-abbr="мокап в 3D">мокап в 3D*. Но это долго: приходится делать одну работу дважды. Что мы делаем в таких случаях? — автоматизируем.

Так что где-то в прошлом декабре я за 2-3 недели набросал на Python конвертер миди-партий в партии LilyPond, чтобы потом из них собирать партитуру. И с его помощью, потихоньку допиливая, я работаю над проектами, где нужны и ноты, и исполнение.

Оказалось, что, если писать музыку без промежуточного звена в виде нотного редактора, которому надо указывать, в каком ты размере пишешь, какой голос записываешь, и вообще — выполнять по 30-40 дополнительных действий для выражения одного такта музыкальной мысли — получается быстрее и удобнее! Примерно в то же время началась работа над альбомом вальсов для фортепиано. И за ноты в этом проекте будет отвечать новый пакет.

Виртуозная сольная музыка — это такая штука, которую нельзя писать умозрительно. Она должна быть физиологически прилажена к исполнителю: потому большая часть виртуозных произведений написана исполнителями. Для фортепиано — Рахманинов, Лист, Капустин; для скрипки — Паганини, Венявский, Вивальди; у альтистов есть Хоффмастер и т.п. Пианисты и скрипачи много играют и Чайковского, но лишний раз поругают за то, что не удобно. Поэтому я пишу из-под пальцев: играю, пусть медленно, коряво, с ошибками и остановками — но я уверен в исполнительском удобстве того, что пишу. А формат MIDI — просто идеальный для такого рода сочинительства. Потом ошибки исправляются, остановки вырезаются, темпы поднимаются — на выходе можно слушать.

Поскольку музыкальная нотация обширна, даже если ограничиться той, что можно представить в MIDI — потихоньку пакет оброс функционалом простенького нотного редактора: сейчас в нём уже порядка 30 хоткеев (2-го порядка, чтобы не засорять основной функционал Reaper), и я его использую для тех нот, которые сложно поддаются набору в MuseScore.

Снова переезд

От крышки рояля до фреймворка на rust: как системное программирование помогает творческой реализации - 5
50 оттенков Киршберга
50 оттенков Киршберга

Но вот делать очередной большой проект на Python мне уже боязно. Самое слабое место этого замечательного языка — дистрибуция. Я как-то писал простенькую программку для девочки с аутическим расстройством. Не знаю, сколько у меня заняла разработка на pygame: день, может, два. Но следующие пару дней я потратил на настройку виртуальной машины с Win10 и невиртуальными матюгами на то, что не получается собрать exe-шник. На удивление, проблема решилась тем, что у девочки на ноутбуке стояла Убунта.

С Reaper проблема усугубляется тем, что сначала надо установить сам Python. И, несмотря на упрощение установки reapy для пользователей, путём поиска работающей инстанции Reaper и правки конфига так, как нам нужно — всё равно 80% пользователей отпадают во время установки. А когда делаешь большой пакет — хочется, чтоб им пользовалось много народа.

Да и на таких объёмах кода стали проявляться недостатки системы типов Python: либо не получается красиво выразить то, что хочешь. Либо всё равно где-то вылезает None. А ещё я не замечаю, как начинаю пихать в продакшн-код всякие фишки, которые позволяют делать выразительные библиотеки, вроде мета-программирования, множественного наследования и т.п. Получается слишком implicity.

В августе мы завели трактор и, кажется, удачно. Выдался целый месяц почитать литературу. Сначала почитал Глуховского, потом начал «Прощай оружие» на английском, но, то ли язык мешает, то ли Хемингуэй не мой автор — так на середине открытым и остался. Потом взялся за Ремарка (уже на русском, хоть и пора бы что-то по-немецки читать), но он, как обычно, быстро закончился. В общем, одна неделя ушла и на Rust-book. Второй раз в жизни, кажется, прочитал мануал с начала до конца. Не зря: когда получилось достать из чемодана системник — rust показался очень дружелюбным.

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

rea-rs

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

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

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

«hello world»:

use rea_rs::{errors::ReaperResult, ActionKind, Reaper, PluginContext};
use reaper_macros::reaper_extension_plugin;
use std::error::Error;
#[reaper_extension_plugin]
fn plugin_main(context: PluginContext) -> Result<(), Box> {
    Reaper::load(context);
    let reaper = Reaper::get_mut();
    let message = "Hello from small extension";
    reaper.show_console_msg(message);
    Ok(())
}

Но, чтобы плагин и после запуска программы что-то делал — надо воспользоваться Action или ControlSurface:

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> {
    Reaper::get().show_console_msg("running");
    Ok(())
}
#[reaper_extension_plugin]
fn plugin_main(context: PluginContext) -> Result<(), Box> {
    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 — отличный пример очеловечивания API фреймфорком. В reaper есть много объектов, в которые можно сериализоваться. Для всех них немного разные интерфейсы и правила работы. Ну и, конечно, принимают они только String. ExtState предоставляет единый интерфейс и сериализует много разных типов через serde:

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 =
    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());

Убрал под капот всевозможные сущности, отображающие время от начала проекта (позицию): секунды, четверти, биты (видимо, тоже четверти), PPQ с начала тейка и т.п. Теперь есть одна структура Position — обёртка над std::time::Duration. Обеспечивает точность положения выше, чем один сэмпл (1/48000 секунды), конвертируется во все эти разные сущности внутри фреймворка. Или снаружи, если пользователю так удобнее. Можно задать позицию в сэмплах — API её передаст в секундах. Правда, есть у меня опасение, что f64 не даст sample-accuracy. Но, к сожалению, тут Джастин сам подложил свинью, принимая на вход запроса блока сэмплов позицию в секундах…

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

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

  • Нет GUI. Не то, чтобы его нет вообще, есть нативная open-gl библиотека egui, которую используют создатели VST и CLAP. Есть ещё, как минимум, 5 достойных фреймворков, которые можно запускать в отдельном потоке. Но пока нет никаких внятных механизмов коммуникации потока GUI с основным потоком, из которого можно вызывать функции Reaper. В принципе, на борту лежит полная копия Cocos WDL, так что чисто технически можно написать «совсем родной» GUI, как это сделано в SWS. Но я не представляю, сколько надо на это потратить сил. Пока что у меня не получилось даже окно создать.

  • Вызовы API потоко-небезопасны. Но не факт, что библиотека не попытается работать из другого потока. Бенджамин об это сильно обжёгся, поэтому стал делать свою систему защиты от дурака. Но его подход — написать обёртку трижды: сначала автоматический генератор из заголовков C++, потом безопасную обёртку над ним без всяких вмешательств в API (по сути, поднять rust до уровня eel), а потом только делать что-то человеко-читаемое. Я попробовал, мне показалось, что это невыполнимо. Поэтому написал поверх сырых биндингов.

  • Альфа-релиз, нестабильный API. Вполне может быть, что в угоду красоте и удобству некоторые функции будут пропадать, некоторые появляться. Зато, как только проект выйдет на crates.io, можно будет привязываться к конкретной версии, и не беспокоиться, что с очередным пулл-реквестом на моём мастере, в коде что-то сломается. Опубликую на crates.io сразу, как только Бенджамин примет мой патч в reaper-rs, и сам обновит свой пакет.

  • Экосистема rust не такая богатая, как у Python или C++. Поэтому можно встрять с какой-то задачей, которую ещё никто до тебя не решал. И месяц писать какой-нибудь анализатор. Но сообщество rust audio очень живое, и разрабатывают активно. Даже пишут свою открытую DAW на rust.

Я впервые за долгое время выбрался из собственной кухни и приготовил что-то для сообщества. И надеюсь, что сообществу пригодится :)

От крышки рояля до фреймворка на rust: как системное программирование помогает творческой реализации - 7

Казалось бы, достаточно распространённая задача, вроде написания фортепианных пьес в нотном редакторе. А ведь, на самопальном Python-скрипте решается на порядок быстрее, чем на iMac с кучей дорогого софта: он написан немного для других задач, и в него не залезешь. Да что уж, даже поменять немецкий интерфейс на английский — проблема. Зато теперь я знаю слова fenster, speichern, Einstellungen и много других)

Заключение

Всё-таки получился байопик… За кадром осталось много историй: о том, как я переехал на Linux, и почему здесь творится лучше. Ещё остались за кадром мысли о том, что мы живём в мире из интерфейсов, и коммуникация — нечто гораздо более базовое, чем язык или кнопки. Но оставлю это для следующего раза.

Мне бы очень хотелось, чтобы мы научились учиться и делать себе полезно без страхов ошибок. И без глубокой рефлексии несовершенства того, что получается. Мне нравится, что у дочки в Яндекс-учебнике программирование начинается с жёсткой привязки к предметной области, к тому, что можно пощупать и получить немедленно обратную связь. Мне нравится, что на рисовании её учили криво-косо набросать экспозицию, потом силуэты, потом контуры, а потом уже аккуратно закрасить цветом.

Нарисовал нас с женой для афиши первого концерта на новом месте. MyPaint, стилус :D
Нарисовал нас с женой для афиши первого концерта на новом месте. MyPaint, стилус :D

Эти простые лайфхаки получения чего-то из ничего я зарабатывал ночами побитового сложения чисел на листочке и постоянного переписывания программ с нуля. Я почти не писал музыки, пока мне не стали попадаться откровенные халтуры, на которых было не стыдно расписаться. И совсем не рисовал до тех пор, пока не понадобилось дать хоть какую-то афишу нашего концерта. Да блин, у меня дома 15 лет лежала скрипка, и я на ней не играл, потому что: «это ж надо заниматься».

Возможно, один из самых крутых жизненных поворотов я сделал  тот вечер, когда мы с Артёмом на шару поехали писать сэмплы. Просто потому что хотелось сделать свои треки чуточку лучше. С этого начался путь к свободе в работе, контролю над своими инструментами, и ответственности за окружение, будь то DAW, IDE, природа или родина. Очень надеюсь, что последующая конфронтация с режимом и попытка к бегству — всё-таки побочный эффект, а не симптом болезни.

Автор: Тимофей Казанцев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js