Первая часть про железо здесь
Итак, я решил довести до реально работающего проекта превосходный эмулятор синтезатора Roland JV880. Это рэковый модуль (без клавиатурный), сделанный на базе синтезатора JV80. Соответственно он поддерживает все карты расширения этого синтезатора и почти все MIDI команды.

Почему именно этот синтезатор? Мне показалось, что используя сэмплы и их разнообразную обработку, можно добиться гораздо большего, чем просто менять форму сигнала в обычном синтезаторе. Если честно, до последнего времени я был фанатом FM синтеза и очень часто использовал например FM8 VST в своих мелодиях. Но став старше склоняюсь к более разнообразному звуку. К тому же в JV880 намного интереснее реализация ручки модуляции и Aftertouch. Ну вот например схема обработки одного тона (сэмпла):

А их в патче 4. К тому же у патча еще есть и общие обработки сигнала. Не скрою, порой в этом сложно разобраться. Но если все понять, то рулить звуком становится очень интересно.
Начало
И тут возникает другой вопрос - я ведь не программист особо. За 20 лет программирование ушло семимильными шагами в какую то несусветную даль. Хотя С++ то особо не изменился. Но тут надо программировать под конкретное железо, да еще и на голом железе, без системы. Ну и деваться то мне некуда, а хотелки растут. И окунулся я с головой в вайб кодинг. С помощью него я смог внедрить SerialMIDI в код и попробовать звук. Скажу сразу Giulio Zausa всех нае обманул. Эмулятор не работал. Звук выходил с какими то щелчками. Как потом он признался - некоторые патчи работали без щелчков и он их использовал в видео. Пламен Джорев (Plamikcho) использовал RPi4, может быть поэтому у него нет щелчков.
Небольшое отступление
Тут надо сделать небольшое отступление. Изначально меня немного удивляло то, что нельзя загнать все эмуляторы на одну карту и просто переключаться между ними. Я даже выдвинул идею создать проект MultiSynth на страницах обсуждения MiniDexed. Но меня никто не поддержал. Кто-то даже предложил уйти от концепции bare metal в обычный linux и уже на базе linux создать такой проект. Но с помощью вайб кодинга я решил попробовать скрестить MiniDexed и Mini-JV880. Вся проблема подобного проекта состояла в том, что в них использовались функции и переменные с одинаковыми именами, так как Mini-JV880 был построен на базе MiniDexed. Пришлось столкнуться с тотальным переименованием всего. В принципе у меня получилось. И по отдельности я их запускал переключением переменной в ini файле. Но тут я зашел в тупик.
Стандартная инициализация в используемой библиотеке Circle происходит по файлам main.cpp -> kernel.cpp -> minidexed.cpp
Main.cpp это запускающий файл. Kernel.cpp содержит главные функции верхнего уровня I2C, SPI, USB, a в minidexed.cpp уже инициализируется клавиатура (uibuttons.cpp), дисплей (userinterface.cpp) ну и все остальное. А чтобы на старте включить дисплей и кнопки и организовать переключение синтезаторов, такое для меня было достаточно сложно. В принципе все как бы получилось и оба синтезатора запускались с одной SD карты, только нужно было поменять номер запускаемого синтезатора в ini файле. Но тем не менее Mini-JV880 при этом нормально не работал, потому что я не вмешивался в код. И я решил все-таки доделать отдельный не работающий проект Mini-JV880 до нормального релиза, где все бы работало как нужно.
Но перед этим я еще попробовал сделать проект multikernel, в котором загрузчик смог бы запустить нужное ядро синтезатора: minidexed, mini-jv880 или mt32-pi. И хотя все AI твердили, что это возможно, Circle (а на нем основаны все эти синтезаторы) отказался это делать и постоянно падал в панику.
Битва MCU vs DSP
Начал разбираться. Эмулятор использует прямую эмуляцию процессора Hitachi H8/300H и непрямую DSP процессора. Причем прямая эмуляция использует прерывания (несколько штук), таймеры и еще кучу компонентов железного процессора. Дело в том, что в железном JV880 процессор и DSP связаны между собой и тактовыми сигналами и дополнительными сигналами ожидания. А в эмуляторе процессор эмулируется в нативном коде, а DSP написан на C++ и не эмулируется. Ну он и не может, потому-что его вообще вскрывали с помощью фотографий ROM. Проблема в том, что синхронизации между MCU и DSP нет никакой. NukeyKT (автор эмулятора Nuked SC-55) писал эмулятор для PC. Там видимо быстродействия хватало. Но RPi это почти калькулятор, хотя и мощный, и с ресурсами немного похуже. Стало понятно, что MCU и DSP не синхронизируются и идет или переполнение буфера или потеря данных.
Изначальный код Giulioz
void CMiniJV880::Run(unsigned nCore) { assert(1 <= nCore && nCore < CORES); if (nCore == 1) { // while (true) { // if (m_pMIDIDevice != 0) { // assert(m_pMIDIDevice->hostDevice != 0); // m_pMIDIDevice->hostDevice->Update(); // } // } } else if (nCore == 2) { // emulator while (true) { unsigned nFrames = m_nQueueSizeFrames - m_pSoundDevice->GetQueueFramesAvail(); if (nFrames >= m_nQueueSizeFrames / 2) { // unsigned int startT = CTimer::GetClockTicks(); nSamples = (int)nFrames * 2; // mcu.updateSC55(nSamples); mcu.sample_write_ptr = 0; while (mcu.sample_write_ptr < nSamples) { if (!mcu.mcu.ex_ignore) mcu.MCU_Interrupt_Handle(); else mcu.mcu.ex_ignore = 0; if (!mcu.mcu.sleep) mcu.MCU_ReadInstruction(); mcu.mcu.cycles += 12; // FIXME: assume 12 cycles per instruction mcu.TIMER_Clock(mcu.mcu.cycles); mcu.MCU_UpdateUART_RX(); mcu.MCU_UpdateUART_TX(); mcu.MCU_UpdateAnalog(mcu.mcu.cycles); // mcu.pcm.PCM_Update(mcu.mcu.cycles); } // unsigned int endT = CTimer::GetClockTicks(); // avg = avg == 0 ? (endT - startT) : avg * 0.99 + (endT - startT) * // 0.01; if (cnt++ == 100) { // LOGNOTE("%f\n", avg); // cnt = 0; // } int len = nSamples * sizeof(int16_t); if (m_pSoundDevice->Write(mcu.sample_buffer, len) != len) { LOGERR("Sound data dropped"); } } } // LOGNOTE("%d samples in %d time", nFrames, m_GetChunkTimer); } else if (nCore == 3) { // pcm chip while (true) { // while (mcu.sample_write_ptr >= nSamples) { // } mcu.pcm.PCM_Update(mcu.mcu.cycles); } } }
Изначальный код вывода сэмпла, который вызывается внутри PCM_update()
inline void MCU_PostSample(int *sample) { sample_buffer[sample_write_ptr++] = sample[0] >> 16; sample_buffer[sample_write_ptr++] = sample[1] >> 16; sample_write_ptr %= audio_buffer_size; // int ptr = SDL_AtomicGet(&sample_write_ptr); // sample_buffer[ptr] = sample[0] >> 16; // sample_buffer[ptr + 1] = sample[1] >> 16; // SDL_AtomicSet(&sample_write_ptr, (ptr + 2) % audio_buffer_size); }
И тут выходит GPT5. Я делаю анализ кода, уязвимые критичные места. И он мне с ходу выдает кольцевой буфер с раскидкой MCU и DSP по ядрам с атомной синхронизацией по заполнению буфера. Хотя они и так были раскиданы по ядрам и Giulio Zauza пытался синхронизировать, но в моем варианте это не работало. Я подстроил еще несколько параметров и эмулятор перестал трещать. Звук стал идеальным.
Код после изменения
void CMiniJV880::Run(unsigned nCore) { assert(1 <= nCore && nCore < CORES); //int nSamples = 0; if (nCore == 2) { // 2nd core - MCU + audio output const int MCU_INSTR_BURST = 64; while (true) { if (m_bAudioPaused.load(std::memory_order_acquire)) { CTimer::SimpleMsDelay(1); continue; } unsigned nFrames = m_nQueueSizeFrames - m_pSoundDevice->GetQueueFramesAvail(); if (nFrames < m_nQueueSizeFrames / 2) { CTimer::SimpleMsDelay(1); continue; } int nSamples = (int)nFrames * 2; if (nSamples >= (int)AUDIO_BUFFER_SIZE) nSamples = (int)AUDIO_BUFFER_SIZE - 2; int16_t *out_buf = (int16_t*)malloc(nSamples * sizeof(int16_t)); if (!out_buf) { CTimer::SimpleMsDelay(1); continue; } int out_pos = 0; while (out_pos < nSamples) { // 1) try to copy available audio first uint64_t w = __atomic_load_n(&sample_write_idx, __ATOMIC_ACQUIRE); uint64_t r = __atomic_load_n(&sample_read_idx, __ATOMIC_RELAXED); uint64_t avail = w - r; if (avail > 0) { uint32_t need = (uint32_t)(nSamples - out_pos); uint32_t to_copy = (avail < need) ? (uint32_t)avail : need; uint32_t idx = (uint32_t)(r & AUDIO_BUFFER_MASK); uint32_t first = AUDIO_BUFFER_SIZE - idx; if (first > to_copy) first = to_copy; memcpy(&out_buf[out_pos], &sample_buffer[idx], first * sizeof(int16_t)); out_pos += first; r += first; uint32_t rem = to_copy - first; if (rem) { memcpy(&out_buf[out_pos], &sample_buffer[r & AUDIO_BUFFER_MASK], rem * sizeof(int16_t)); out_pos += rem; r += rem; } __atomic_store_n(&sample_read_idx, r, __ATOMIC_RELEASE); continue; // loop to fill remaining samples } // 2) if no samples ready, run bounded MCU burst to allow producer to progress int instr = 0; while (instr < MCU_INSTR_BURST) { if (m_bAudioPaused.load(std::memory_order_acquire)) { break; // Exit MCU burst immediately } if (!mcu.mcu.ex_ignore) mcu.MCU_Interrupt_Handle(); else mcu.mcu.ex_ignore = 0; if (!mcu.mcu.sleep) mcu.MCU_ReadInstruction(); mcu.mcu.cycles += n_mMCUcycles; __atomic_store_n(&mcu.mcu.cycles, mcu.mcu.cycles, __ATOMIC_RELEASE); mcu.TIMER_Clock(mcu.mcu.cycles); mcu.MCU_UpdateUART_RX(); mcu.MCU_UpdateUART_TX(); mcu.MCU_UpdateAnalog(mcu.mcu.cycles); ++instr; } } // write to audio device int len = nSamples * sizeof(int16_t); if (m_pSoundDevice->Write(out_buf, len) != len) { LOGERR("Sound data dropped"); } free(out_buf); } } else if (nCore == 3) { // 3rd core - PCM Update constexpr uint64_t MCU_CLOCK_HZ = 12000000ull; // if your MCU clock differs, set accordingly constexpr uint32_t AUDIO_RATE = 64000u; constexpr uint64_t CYCLES_PER_SAMPLE = MCU_CLOCK_HZ / AUDIO_RATE; // 375 typical for H8@12MHz const uint32_t MAX_SAMPLES_PER_ITER = 128; // bound to avoid huge bursts uint64_t last_generated_cycles = __atomic_load_n(&mcu.mcu.cycles, __ATOMIC_RELAXED); while (true) { if (m_bAudioPaused.load(std::memory_order_acquire)) { last_generated_cycles = __atomic_load_n(&mcu.mcu.cycles, __ATOMIC_ACQUIRE); // Синхронизация! CTimer::SimpleMsDelay(1); continue; } uint64_t cycles_target = __atomic_load_n(&mcu.mcu.cycles, __ATOMIC_ACQUIRE); if (cycles_target <= last_generated_cycles) { CTimer::SimpleMsDelay(0); continue; } uint64_t cycles_avail = cycles_target - last_generated_cycles; uint64_t samples_to_gen = cycles_avail / CYCLES_PER_SAMPLE; while (samples_to_gen > 0) { uint32_t gen = (uint32_t) (samples_to_gen > MAX_SAMPLES_PER_ITER ? MAX_SAMPLES_PER_ITER : samples_to_gen); uint64_t pcm_target = last_generated_cycles + gen * CYCLES_PER_SAMPLE; mcu.pcm.PCM_Update(pcm_target); last_generated_cycles = pcm_target; samples_to_gen -= gen; CTimer::SimpleMsDelay(0); } } } }
Ну и так же претерпел изменения код вывода сэмпла.
inline void MCU_PostSample(int *sample32) { // write left/right 32->16 (with saturation) then publish new write index (release) uint64_t cur = __atomic_load_n(&sample_write_idx, __ATOMIC_RELAXED); uint32_t i0 = (uint32_t)(cur & AUDIO_BUFFER_MASK); uint32_t i1 = (i0 + 1u) & AUDIO_BUFFER_MASK; auto sat16 = [](int v)->int16_t { if (v > 32767) return (int16_t)32767; if (v < -32768) return (int16_t)-32768; return (int16_t)v; }; sample_buffer[i0] = sat16(sample32[0] >> 16); sample_buffer[i1] = sat16(sample32[1] >> 16); uint64_t next = cur + 2ull; // protect against overflow: if next - read > buffer_size then advance read (drop oldest) uint64_t r = __atomic_load_n(&sample_read_idx, __ATOMIC_ACQUIRE); if ((next - r) > AUDIO_BUFFER_SIZE) { uint64_t newr = next - AUDIO_BUFFER_SIZE; __atomic_store_n(&sample_read_idx, newr, __ATOMIC_RELEASE); __atomic_fetch_add(&sample_overflow_count, 1ull, __ATOMIC_RELAXED); } __atomic_store_n(&sample_write_idx, next, __ATOMIC_RELEASE); }
Хотя я и до этого почти смог синхронизировать процессоры без кольцевого буфера и звук был почти идеальным. Но на долгом периоде он мог начать щелкать, либо искажаться. То есть 10 минут работает нормально. Потом идут щелчки, либо искажения примерно минуту, потом опять все нормально. Синхронизация терялась с очень низкой частотой. Чтобы выловить проблемы синхронизации, я запускал встроенное демо на долгий срок и ловил проблемы со звуком:
Встроенное демо
Про вайб кодинг
Вносить правки в проект я начал примерно весной 2025г. В то время основным достаточно мощным LLM был только GPT4. Но и с ним возникали постоянные проблемы. Может быть потому-что я использовал только бесплатные варианты. О постоянно терял контекст, через какое то время переходил в режим более низшей модели, которая безбожно тупила. Приходилось бросать его на сутки и продолжать работу на следующий день.
Потом вышел очередной убийца всех LLM DeepSeek. Вот он мне реально помог решить большинство поставленных задач. Гениальностью он конечно не отличался, но программировал уверенно. Конечно случались и потеря памяти и тупизм на ровном месте, но потом как то все решалось. Видимо на сервере снижали производительность модели для пользователей, которые использовали модель долгое время. По крайней мере я так это заметил. По началу сервера даже были перегружены и сообщения просто не доходили.
Потом вышел GPT5. Он то и помог решить мне главную проблему эмулятора JV880 - синхронизацию между MCU и PCM. причем сделал это качественно, проанализировав кучу мест кода и внеся оригинальные изменения. Но так же как и предшественник со временем переходил на более тупую модель и отказывался принимать сообщения в бесплатной версии. На данный момент бесконечно тупой AI с маленьким контекстом. Очевидно бесплатную версию понизили до нуля.
Потом вышел Qwen. Сначала 2.5, потом 3, потом Coder. Coder держал контекст лучше, видимо в токены попадали не просто буквы и слова, но так же и функции целиком. Он тоже нормально писал код, но иногда лучше, а иногда хуже чем DeepSeek.
Поначалу я пробовал использовать Claude, но он вообще ничего реально работающего не предлагал. Причем не доходило даже до стадии тестирования. Сразу было видно, что ничего работать не будет. Не знаю из-за чего. То ли из-за маленького контекста, то ли из за плохого обучения. Но в начале 2026 года вышла версия Claude 4.5, которая кардинально изменилась. И, хотя ограничения на контекст сильно порезаны, эта модель может сжимать каким то образом на лету контекст для лучшего понимания. Она рассудительна. Если есть ошибка - копает по полной. Если бы она еще могла репы читать с гитхаба - цены бы ей не было. Хотя, возможно в платной версии она такое может. После выхода этой модели, я работаю только с ней. И она реально пишет конкретный работающий код и не выдает каждый раз разные имена переменных, как это делали Qwen и Deepseek. Важно только правильно описать задачу и дать максимум нужных входных данных.
В итоге при написании кода я использовал почти все LLM. Какие то больше, какие то меньше. Интересно, что все LLM в определенный день тупили безбожно. Причем с элементарным кодом. Или например забывали код после нескольких сообщений и придумывали новый. Ну то что они придумывали несуществующие функции и API про это можно и не писать. Достаточно было поставить их на место. Самое полезное качество в LLM - это то, что не надо писать килобайты текста. Достаточно копипасты. Можно полностью поменять функцию за пару кликов.
Самое интересное, что так же тупила даже SUNO. В один день она выдает просто идеальные треки на Грэмми, в другие дни жрет слова пачками, не попадает в ноты и делает всё не красиво.
А вообще вайб кодинг - это немного другой уровень программирования. Ты сидишь как босс, дае��ь задания студенту написать код, он приносит тебе его на тарелочке. Ты его проверяешь и начинаешь тыкать носом в ошибки. И так, раз за разом продвигаешься к конечному результату. Но для этого надо знать полностью код, архитектуру, взаимодействие с железом, зависимости частей кода друг от друга. Вслепую, не понимая, такое сделать не получится. И я даже, бывает, учусь. Когда он выдает мне то, чего я не понимаю, я прошу объяснить и разложить по полочкам. Так вот с весны 2025г и до сегодняшнего времени я очень хорошо изучил проект и знаю буквально каждый кусочек кода.
Но хорошее рано или поздно кончается. В данный момент (январь2026) все LLM как будто отупели и плохо справляются с запросами. Такое ощущение, что все держатели LLM перевели свои модели на самый тупой пресет. Исключая только Claude 4.5, но и у него в бесплатной версии окно контекста порезано и ограничено, что впрочем не мешает до сих пор реализовывать нужные задачи.
Ну а с выходом Claude Sonnet 4.6 все стало совсем идеально. Это крутейший помощник, который адекватно реагирует на запросы и нормально коротко отвечает, а не предлагает золотые горы в каждом сообщении, как GPT5.
Дисплей
Ну так как звука я добился нормального, что то надо было делать с дисплеем. Изначально в Roland JV880 используется дисплей на HD44780 24х2. Но дело в том, что сейчас в самоделках используются самые дешевые и популярные дисплеи 1602 и OLED на чипе SSD1306 128x64 или 128x32. OLED дисплей графический, но библиотека Circle выводит на него 20 символов в 2 или 4 строки. В качестве поддержки так же заявлен TFT SPI ST7789, но в аннотации к драйверу указано, что не только лишь все такие дисплеи могут корректно работать и их работоспособность с данным драйвером не гарантируется. Да и шина SPI изначально занимает больше пинов, хотя и быстрее. И так же у всех таких дисплеев проблемы с подсветкой. Либо она без драйвера, либо есть драйвер и им надо как то управлять.
Изначально Plamikcho вывел на дисплей SSD1306 изображение из буфера JV880 со скроллингом. То есть изображение постоянно двигалось, но только так можно было увидеть его полностью. Он видимо не использовал все функции самого эмулятора, а не уходил дальше главного экрана, потому-что позиция курсора, который выводится поверх готового изображения, в некоторых режимах оказывалась не там где надо. Он так же убирал пробелы в какой то из версий. Я тоже так делал, но иногда пробелов не было, при длинных названиях инструментов или пунктов меню. В итоге я все-таки вернул назад скроллинг, хотя он и ужасно выглядел, полное отображение информации было важнее.
Я использовал графический дисплей SSD1305 128x32. Изначально библиотека Circle выводит на такой дисплей символы 20х2. То есть каждый символ 6х8(16 с double высотой). Я подумал, что можно уменьшить символы до 5х7 и получить заполнение символами 25х4. Я просто взял драйвер Circle, положил в свой проект и с помощью AI изменил его, заставив выводить текст шрифтом 5х7. Особенной болью был шрифт. Я не мог найти в интернете растровый шрифт 5х7 (или может быть плохо искал) именно в коде С и мне пришлось генерить его так же с помощью AI. Это отдельный вид извращений. Уже потом, спустя несколько версий я замечал что какие то символы нарисованы не совсем правильно и я исправлял их. Ед��нственная проблема такого шрифта - неправильное отображение широких символов, потому что для отображения, например, символа М нужно минимум 5 точек по горизонтали, но в шрифте одна из точек нужна для отделения символов друг от друга, поэтому либо буква М (W) будет не отделена от других символов, либо будет состоять из 4 точек и будет непонятной. Но человеческое зрение адаптивно и мне хватило 4 точки. И тут я сразу вспоминаю CP/M на ZX Spectrum. Там тоже использовался такой шрифт. Но на телевизоре это было как то размыто и нечитабельно. В итоге я нарисовал шрифт, подправил драйвер и сделал выбор в ini файле. Когда используется дисплей 1305/6, то настройка 25х4(8) будет включать этот драйвер. Я долго бился с нейросетями, чтобы программа вывода изображения на дисплей корректно работала в нативном 24х2, в скроллинговом 16(20)х2 и в нативном 25х4 (5х7). Они то портили прямой формат со скроллингом, то ломали виртуальный 25х4. Проблема еще и в том, что 25х4, а не 24х4. Один символ по горизонтали создавал достаточно проблем. Кстати, 24 на 4 еще никто из пользователей не применял, а вот 2402 отлично работает и в обсуждениях даже есть фото.
Уже перед самым релизом версии 1.1.0 меня осенило. Я ведь использовал всего 2 строки из четырех для эмулятора, а еще 2 строки всегда были пустые. Как то не экономно расходуются OLED пиксели. Сначала решил вывести системные сообщения. На обычный двухстрочный дисплей поверх эмулятора на 3 секунды, а на 4 и более строчный в нижние 2 строки. Из за этого, кстати была еще одна месячная битва с AI. Строки то не выводились, то выводились не все, то выводились частично. Но на 4-х строчном выводились, а на 2-х строчном нет. В итоге я победил ценой многих нервных клеток.
Так же я написал NukeyKT насчет опроса состояния оригинальных светодиодов. Это тоже достаточно важно, потому-что от их состояния зависит режим работы синтезатора. А 4 светодиода под кнопками показывают какие тонгенераторы включены. Но видимо автор эмулятора давно забил на все и какой то проходящий мимо пользователь кинул кусок кода, который я и использовал. Я превратил нижние 2 строки в 10 полей по 4 символа + пробел, в которых отобразил надписи светодиодов. Причем в режиме Patch светодиоды Mute, Monitor, Compare, Enter отображают тонгенераторы, а в режиме Performance - ��еальные режимы кнопок.

Управление
В самом начале, после внедрения Serial MIDI, встал вопрос управления эмулятором. Изначально эмулятор содержит кучу кнопок и энкодер для управления, мне это показалось очень сложным. И я вообще не хотел никаких лишних изменений во внешний вид своей MIDI клавиатуры, как например сделал Soundplantage. Ну и в общем то я их и не сделал, если не считать заднюю панель. Дисплей закрыт темным стеклом и его не видно. Но так как нет физических кнопок, я решил все управление сделать на MIDI командах. Сделал простейший обработчик команд и дергал определенные кнопки. Либо энкодер. Все кнопки настраиваемые. Используются команды MIDI Control Change N так же как и в MiniDexed.
MIDI парсер
void CMiniJV880::ParseMIDIData(CMiniJV880* pThis, const u8* pData, unsigned nLength) { for (unsigned i = 0; i < nLength; i++) { u8 status = pData[i]; if ((status & 0xF0) == 0xB0 && i + 2 < nLength) { u8 ccNumber = pData[i + 1]; u8 ccValue = pData[i + 2]; if (pThis->m_UI.m_nMIDIButtonChannel != 0) { u8 channel = status & 0x0F; u8 expectedChannel = pThis->m_UI.m_nMIDIButtonChannel - 1; // OMNI (17) if (pThis->m_UI.m_nMIDIButtonChannel == 17 || expectedChannel == channel) { auto handleButton = [&](u8 confCC, CUIButton::BtnEvent ev) { if (ccNumber == confCC) { if (ccValue < 64) { // нажали pThis->m_UI.TriggerUIButtonEvent(ev); } else { pThis->m_UI.TriggerUIButtonEvent(CUIButton::BtnEventNone); } i += 2; return true; } return false; }; if (handleButton(pThis->m_UI.m_nMIDIPreview, CUIButton::BtnEventPreview)) continue; if (handleButton(pThis->m_UI.m_nMIDILeft, CUIButton::BtnEventLeft)) continue; if (handleButton(pThis->m_UI.m_nMIDIRight, CUIButton::BtnEventRight)) continue; if (handleButton(pThis->m_UI.m_nMIDIData, CUIButton::BtnEventData)) continue; if (handleButton(pThis->m_UI.m_nMIDIToneSelect, CUIButton::BtnEventToneSelect)) continue; if (handleButton(pThis->m_UI.m_nMIDIPatchPerform, CUIButton::BtnEventPatchPerform)) continue; if (handleButton(pThis->m_UI.m_nMIDIEdit, CUIButton::BtnEventEdit)) continue; if (handleButton(pThis->m_UI.m_nMIDISystem, CUIButton::BtnEventSystem)) continue; if (handleButton(pThis->m_UI.m_nMIDIRhythm, CUIButton::BtnEventRhythm)) continue; if (handleButton(pThis->m_UI.m_nMIDIUtility, CUIButton::BtnEventUtility)) continue; if (handleButton(pThis->m_UI.m_nMIDIMute, CUIButton::BtnEventMute)) continue; if (handleButton(pThis->m_UI.m_nMIDIMonitor, CUIButton::BtnEventMonitor)) continue; if (handleButton(pThis->m_UI.m_nMIDICompare, CUIButton::BtnEventCompare)) continue; if (handleButton(pThis->m_UI.m_nMIDIEnter, CUIButton::BtnEventEnter)) continue; // Encoder if (ccNumber == pThis->m_UI.m_nMIDIUp && ccValue < 64) { pThis->m_UI.TriggerUIButtonEvent(CUIButton::BtnEventNone); pThis->mcu.MCU_EncoderTrigger(1); i += 2; continue; } if (ccNumber == pThis->m_UI.m_nMIDIDown && ccValue < 64) { pThis->m_UI.TriggerUIButtonEvent(CUIButton::BtnEventNone); pThis->mcu.MCU_EncoderTrigger(0); i += 2; continue; } } } i += 2; } }
И я ведь не менял оригинальный код GPIO кнопок который написал кто то до меня. И он, предсказуемо, работал не так как надо. Предыдущий автор забывал "отпускать" кнопку. Я это замечал на MIDI кнопках, но думал, что это у меня где то проблема в моем коде. Но один из пользователей написал, что эта же проблема присутствует в GPIO кнопках. Выглядело это так: когда я нажимал на кнопку влево (перемещение курсора по экрану), например, то второе нажатие на это кнопку уже не перемещало курсор, пока не нажмешь любую другую. Оказывается бит передавался в эмулятор и там застревал. А другой бит, переданный в эмулятор, сбрасывал все остальные. Проблема была еще в том, что эмулятор не всегда читал кнопки, а только тогда, когда ему прерывание разрешит, поэтому послать бит и потом сразу выключить его не подходило. А в классе, который был выдран из MiniDexed не было события Release, чтобы бит обнулялся, когда кнопка отпущена. Пришлось полностью переделывать класс, учитывая и doubleclick и longpress события. И после этого все заработало как надо. После, по просьбе одного из пользователей я добавил GPIO кнопки, заменяющие энкодер, так как он купил плату с джойстиком и экраном, а без энкодера в этом синтезаторе делать нечего. И еще добавил кнопку Save NVRAM.
Ну и уже в самом конце моего противостояния я сделал MIDI энкодер. В клавиатуре Novation SL61 MKII есть такая настройка, причем она удивительно прикольная. При повороте энкодера в одну сторону клавиатура выдает значение 01-04 в зависимости от скорости. А в другую 64-67. Но я не стал заморачиваться со скоростью, его хватало и на первой. Просто некоторые позиции в эмуляторе стоят так далеко, что мне стало жалко кнопки и время на их тыканье.
Память
Изначально в синтезаторе JV880 используется энергон��зависимая память (возможно CMOS), которая никогда не сбрасывается сама. То есть при включении синтезатора она содержит то состояние, которое было при выключении. В эмуляторе этого сделать невозможно, потому что нельзя определить, когда пользователь выключит синтезатор. Поэтому я и сделал сохранение по кнопке. Инкрементальное. То есть счетчик в имени файла все время увеличивается. При желании его можно загрузить, если переименовать и положить в папку с загрузочными файлами. Проблема еще и в том, что память NVRAM может содержать ссылки на патчи и сэмплы, которые лежат в расширенной памяти. И если она будет пуста, ничего не будет правильно работать. В случае оригинального JV880 расширенная память представляла собой картридж, который крепился винтами.
Ресэмплер
А в это время Giulioz давно забросил Mini-JV880 и переключился на VST эмулятор VirtualJV и он сделал ресемпл выхода в 44100, 48000, так как в PC не угадаешь, кто и как работает в DAW. И я решил тоже попробовать сделать ресемплирование. Но так как нормально преобразовать некратный звук достаточно сложно, я решил сделать преобразование 32000*3 = 96000 и потом 96000/2=48000. И почти сделал, пока не обнаружил в коде Nuked-SC55 такой фрагмент:
Ресемплер
if (pcm.config_reg_3c & 0x40) // oversampling { pcm.ram2[30][10] = shifter; pcm.ram1[30][0] = pcm.accum_l & write_mask; pcm.ram1[30][1] = pcm.accum_r & write_mask; tt[0] = (int)((pcm.ram1[30][3] & ~write_mask) << 12); tt[1] = (int)((pcm.ram1[30][5] & ~write_mask) << 12); MCU_PostSample(tt); }
которого в коде Mini-JV880 изначально не было. Я так до конца и не понял как происходит ресемплинг и что подмешивается в эти дополнительные сэмплы. А так же что включает ресемплинг, что за настройка. Но включил по дефолту. И переключил PCM5102 на 64000. И на этом остановился, потому-что качественный ресемплинг в не кратные частоты достаточно сложен, а простой убил бы звук. Можно конечно использовать ARM NEON, но мне кажется это избыточно, потому-что звук выходит аналоговый. Но что самое интересное - по сравнению с VurtualJV на 44.1 или на 48 кгц, эмулятор на RPi звучал намного прозрачнее и чище даже на 32 кгц. Но VirtualJV standalone версия работала только на 96 кгц и звучала так же красиво. Видимо есть неточности в ресемплинге.
Все звуки мира
Giulioz в VirtualJV организовал загрузку всех существующих карт расширения в плагин VST. Они все есть в сети.
VirtualJV

Сначала я сделал просто загрузку карты расширения в память эмулятора. И с помощью синтезатора копировал в основную память патчи (инструменты). Они копировались в память синтезатора по 64 шт (карта расширения может содержать до 255 патчей). Giulioz опять нае обманул систему. Он грузил все банки 19 штук по 8 мб в память, считывал из них все настройки патчей и выводил их на экран. После этого он, при выборе определенного патча, загружал его в позицию I01 и подставлял эмулятору на лету банк, содержащий сэмплы для этого патча и можно было играть. То есть эмуляция поддерживалась на уровне извлечения звука. Полной эмуляции JV-880 нет. Хотя есть полный редактор патча.
Особенности организации памяти JV-880
Изначально в памяти синтезатора содержится 192 патча и 3 набора ударных. Это заводская установка и никак ее не изменить. Вся информация содержится в файлах:
jv880_rom1.bin jv880_rom2.bin jv880_waverom1.bin jv880_waverom2.bin jv880_nvram.bin
Rom1 - основная программа эмулятора.
Rom2 - дополнительные данные о патчах и сэмплах, параметры инициализации, дополнительные данные (тексты, обозначения, константы)
Waverom1 и 2 - сэмплы или как их называет Роланд тоны. Ну это не совсем сэмплы, там еще кроме сэмплов куча разных параметров для них.
NVRam - память текущего состояния синтезатора. Может быть сброшена в исходное состояние через меню синтезатора.
Так вот патчи хранятся в трех банках по 64 штуки A, B, I. Есть еще один банк, он называется Datacard - банк C. Но он в эмуляторе не эмулируется, потому-что эмулятору надо указать что в слот вставлена карта и загрузить какие то данные в память. А это какой то бит в какой то из переменных или регистре. Я так же задал вопрос NukeyKT, но до сих пор тишина. Но я думаю, что там структура типа NVRam.
Банки A и B не изменяемые. Банк I можно менять. Можно редактировать через меню сами инструменты. Можно скопировать из карты расширения по 64 патча. Можно по одному.
Мне тоже захотелось загрузить все карты расширения и переключать их на лету. Я даже попробовал так сделать. Но 150+ мб грузятся с SD карты уж очень долго. А их еще нужно дескремблить. Потому-что даже тогда были DRM в своеобразном виде. Но я пошел немного другим путем. Я загружал карту расширения в память, копировал по 64 патча в банк I и сохранял NVRam и у меня для каждой карты расширения было несколько NVRam. Я их переименовал в XXnvramYY.bin, где XX был порядковый номер, а YY номер карты расширения.
Карты расширения
RomInfo{sz8M, "SR-JV80-01 Pop - CS 0x3F1CF705.bin", "aeb02a1af5031194c723030b133fda6bfb5463f6", true, "b1a825c60cebedd5bcb0709f4ed874322df4da9b", false}, RomInfo{sz8M, "SR-JV80-02 Orchestral - CS 0x3F0E09E2.BIN", "6f8e3113e7f53b0df1f7f0bd9a0627cc655a6a1c", true, "635850b86e9682f090ad49e87cb1f59ab1d31b4d", false}, RomInfo{sz8M, "SR-JV80-03 Piano - CS 0x3F8DB303.bin", "899b68001674b92d82ad86dfb69c62365e368284", true, "3450d81ffeec8f2e46fd1711ea636956c19f17ca", false}, RomInfo{sz8M, "SR-JV80-04 Vintage Synth - CS 0x3E23B90C.BIN", "29fe6c1dde042ff2147c73f1c9d5fcf58092879a", true, "461907c2208abe2e66f11112810667c13da890bf", false}, RomInfo{sz8M, "SR-JV80-05 World - CS 0x3E8E8A0D.bin", "c0a64d6ab04fac96ceba09becd4be888304c72a6", true, "f91d717d0ac8b530b9887a1b0b751fc15c80fea0", false}, RomInfo{sz8M, "SR-JV80-06 Dance - CS 0x3EC462E0.bin", "e2bed925027ed2f73e15aba3c46bf951c93a0716", true, "b18c3ec3d5d0308de393dee8e8e603126d51f01f", false}, RomInfo{sz8M, "SR-JV80-07 Super Sound Set - CS 0x3F1EE208.bin", "be45e76154cee6571e7bacb5633b21945b055843", true, "d403b1e8d77dd374a8fa753f13ba83577410d377", false}, RomInfo{sz8M, "SR-JV80-08 Keyboards of the 60s and 70s - CS 0x3F1E3F0A.BIN", "9652aa26ed091c11d3a89449e8feba5ab73b4bc7", true, "03054a4695f8e451368fb621b0a36e7026c688a0", false}, RomInfo{sz8M, "SR-JV80-09 Session - CS 0x3F381791.BIN", "50e67516a0c996b9813dfdac6c1d08709057e405", true, "ffb8957b0fb894aa36e4879a9b5eb800b4185cb7", false}, RomInfo{sz8M, "SR-JV80-10 Bass & Drum - CS 0x3D83D02A.BIN", "0719e32f001d012769cc92b5dc1ea75882fa0656", true, "1a1af6a99cd34d149ccf37f0de70d862ce6e48ca", false}, RomInfo{sz8M, "SR-JV80-11 Techno - CS 0x3F046250.bin", "880d032b9b2ae97869358161b427c6c8d529f2f8", true, "8987499c2d1c77e70ac7c386f194858375e53a19", false}, RomInfo{sz8M, "SR-JV80-12 HipHop - CS 0x3EA08A19.BIN", "d3d4ff39659bbe993cf6582c145d0db1f1e79b26", true, "bfea6a3e20d7ba4d0601a4948ecdf67ab6756980", false}, RomInfo{sz8M, "SR-JV80-13 Vocal - CS 0x3ECE78AA.bin", "6fd05df901127291e0b74304038ccea79c9a8812", true, "4421ed9fd1b59c79d00530f186fe49eff191cbf2", false}, RomInfo{sz8M, "SR-JV80-14 Asia - CS 0x3C8A1582.bin", "4127864976393052f74fddf9e0a3bbe27f9324df", true, "0841a2415fa28c02c2b62b998e6567569f4f98dc", false}, RomInfo{sz8M, "SR-JV80-15 Special FX - CS 0x3F591CE4.bin", "54b898f76e698e7bafbf093d616ad2df4df6dc82", true, "3648b448f29d7a59db733d8d676ddb82caeefff0", false}, RomInfo{sz8M, "SR-JV80-16 Orchestral II - CS 0x3F35B03B.bin", "216cde7393d5fd57cabe4c2d4bbc5f65f0c07e90", true, "e0a3da9e41677606810acd37300f0671a212f46d", false}, RomInfo{sz8M, "SR-JV80-17 Country - CS 0x3ED75089.bin", "07eaf0a7f822d8369c33b77a9fe2f2a3ea2f7713", true, "a779038e3f6dcca89db26fe98767579be74f6713", false}, RomInfo{sz8M, "SR-JV80-18 Latin - CS 0x3EA51033.BIN", "bb177db61f6f32d7bcc693e4c23d162a3ade3801", true, "402eacaecdaa7d04747887700b23d9f1edda2acf", false}, RomInfo{sz8M, "SR-JV80-19 House - CS 0x3E330C41.BIN", "23ce90ce898ef59a6268070644ab18c7b7834509", true, "1aded637852277ec600a7504fa10cee4053c36e1", false},
В качестве действия я решил использовать команду смены банка. В JV-880 есть MIDI поддержка двух банков. Это команда Bank select MSB 80 или 81, которая переключает банки A+B и I+C. Это можно делать с любого устройства. Я решил, чтобы не ломать MIDI поддержку синтезатора переключать той же командой, но LSB. Прикол в том, что команда Bank Select посылает и LSB и MSB одновременно. То есть состоит сразу из двух команд. И я решил, что LSB 00 будет грузить оригинальный NVRam, а все остальные мною изготовленные по индексу XX. А карту расширения будет грузить уже по последним цифрам файла NVRam. И все прекрасно получалось. Но только при запуске эмулятора. Я мог выбрать произвольный 25nvram14.bin переименовать его в jv880_nvram.bin и подгрузить карту расширения 14. После запуска все прекрасно работало до горячей перезагрузки эмулятора. После каждого изменения памяти (загрузки нужного nvram и expanded rom) нужно было перезагружать эмулятор, для приема новых значений памяти и инициализации. Giulioz делал это легко. У него все программы крутились в одном потоке. У меня было 2 потока - MCU и PCM. И после сброса они теряли друг друга, буфера, указатели. Непонятно что, но это не работало. Хотя я останавливал ядра, ждал пока они встанут. Грузил файлы в память, потом делал сброс и запускал опять ядра. Эмулятор мог подвиснуть на минуту, мог работать, но выдавать странные несоответствующие звуки. По миганию курсора четко было видно когда MCU работает, а когда нет. AI предлагали различные версии, почему это происходит. Причем все выдвигали свои, но несколько месяцев экспериментов не увенчались успехом. Пока не вышел Claude 4.5. Он попросил все исходники и анализировал их, предлагая различные варианты сброса. И вот в один момент, когда как всегда всё зависало при переключении он предложил логгировать каждое событие - и вот что странно, при логгировании ошибок не было. И тут я все понял - нужно после определенных действий делать паузы. Удивительно, но в итоге это сработало. Загрузка практически не замедлилась, а вот переключение стало стабильным.
Процедура сброса эмулятора
// 4. Full reset (clears mcu.mcu.cycles!) mcu.SC55_Reset(); CTimer::SimpleMsDelay(300); // 5. CRITICAL: Zero sample_write_idx AFTER reset __atomic_store_n(&sample_write_idx, 0, __ATOMIC_RELEASE); __atomic_store_n(&sample_read_idx, 0, __ATOMIC_RELEASE); std::atomic_thread_fence(std::memory_order_seq_cst); CTimer::SimpleMsDelay(50); // Small delay before resume // 6. Resume - Core 3 synchronizes automatically m_bAudioPaused.store(false, std::memory_order_release); CTimer::SimpleMsDelay(20); std::atomic_thread_fence(std::memory_order_seq_cst); size_t freeAfter = CMemorySystem::Get()->GetHeapFreeSpace(HEAP_ANY); LOGNOTE("=== BANK SWITCHED TO: %d ROM index %d (%s), free mem=%.2f MB ===", bankNumber, romIndex, rom.filename, (float)freeAfter/(1024.0f*1024.0f));
Переключение банков было сделано просто. В начале загрузки программа сканировала папку nvram и находила файлы ??nvram??.bin и складывала их в память. Потом при переключении на банк XX искала в памяти имя файла XXnvram??.bin и читала последние 2 символа имени. По этим цифрам грузился нужный expanded rom на лету. Если он уже был в памяти и был загружен заранее, то expanded rom просто копировался в память эмулятора. В expanded rom обычно 4 банка по 64 патча (но часто меньше). Вернее их там 255, но банк I в эмуляторе содержит всего 64. Поэтому когда загружался первый по порядку банк из этого exp rom, то 8 мб грузились с задержкой. А следующие банки этого exp rom загружались менее секунды, потому-что 8 мб уже были в памяти. Загрузив по порядку все банки, все exp rom были в памяти, я их специально не выгружал. И теперь все переключения между банками происходили моментально. 512 мб памяти хватает для всех банков. Но теоретически, если будет разрабатываться мультисинтезатор, то можно выгружать все банки, кроме текущего. Либо вообще не пользоваться банками.
Проблемы с MIDI
Еще одна проблема возникла с MIDI. Обработчик у меня был простой и просто отправлял команду в парсер или транзитом в эмулятор. Поэтому я не мог понять почему у меня иногда банк переключается, а иногда нет. А потом когда включил логи увидел, что в буфере парсера не вся команда, поэтому он ее не может опознать. А команда переключения банка состоит из 6 байт. Вернее это даже 2 команды: переключение банка LSB и MSB. Пришлось сооружать дополнительный класс, который собирал миди со всех источников: Serial, USB, UDP, разбивал на команды и покомандно отдавал парсеру. Еще одна проблема с MIDI была с Implementation Chart. У JV880 она очень большая. Основная часть обычная, маленькая, а вот Sysex секция просто огромна. Там содержатся команды для изменения практически всех параметров тонов и патчей в синтезаторе. А их там туева хуча. Но Роланд очень сильно осложнил ситуацию со сторонними миди устройствами. Он требовал контрольную сумму Sysex сообщений, когда все распространенные синтезаторы ее не просят. И вот я программирую таблицу кнопок на MIDI клавиатуре, задаю одну из команд Sysex, вместо контрольной суммы пишу рандомное число и эмулятор выкидывает на дисплей что то там про чексумму. А вариантов посчитать ее в MIDI клавиатуре нет. Пришлось перехватывать все Sysex сообщения в парсере и дописывать контрольную сумму. При чем для совместимости вместо контрольной суммы можно посылать любой байт. Парсер исправит и пошлет эмулятору. А вот так я запрограммировать клавиатуру уже мог.

На скриншоте примерно видно структуру Sysex сообщения JV880 в HEX формате:
41 - Roland
10 - Dev ID. Идентификатор устройства. Задается в меню синтезатора, чтобы можно их было использовать 16 штук в одной MIDI сети и они не пересекались. 10 - по умолчанию прописано в JV880
46 - Model ID JV880
12 - тип команды DT1 - передача параметров в синтезатор. Есть еще 11 - RQ1, в документации не описан. Но судя по формату - запрос или передача дампов патчей или сэмплов
00 00 00 00 - адрес команды или параметра
DV - изменяемое значение. Может иметь ограничение по диапазону. В данном случае 0 или 1
7A - контрольная сумма. Написал от балды, все равно парсер эмулятора исправит.
F0 и F7 - начало и конец Sysex сообщения в этой программе не пишутся, они подразумеваются.
Сеть
Сеть реализовать было непросто. Несмотря на то, что ее реализация в MiniDexed (а из него вырос Mini-JV880) достаточно проста и прозрачна, документация Circle не содержит явно неочевидные вещи. Я просто скопировал сетевую под��ержку из MiniDexed, но постоянно получал ошибки. Сначала это были драйверы. Оказывается их много и они разные. И к определенной версии Circle они нужны свои, другие не подойдут. С этим я разобрался. Интернет большой, поисковики еще работают. Но потом он начал зависать и падать в панику. Неочевидным фактом была необходимость использования Scheduler(), который как раз и рулит сетью в фоне. Нет Scheduler - нет сети и ядрёная паника. Причем это была уже вторая попытка. Первую я делал пол года назад, но так и плюнул. Но в этот раз решил добить.
Из сетевых плюшек есть WiFi/Ethernet, DHCP адрес, FTP сервер, Syslog клиент, UDP MIDI и RTP MIDI. Syslog работает, по FTP можно обновить ядро. И если послать команду "отключиться", то эмулятор перегружается. Не нужно выключать и включать, чтобы запустить новое ядро. UDP и RTP тоже работают, но пользоваться невозможно. Статический адрес не работает, не пойму почему. Вроде все как в оригинале. Но пока не до этого. Назначенный адрес эмулятор пишет на дисплее при старте, поэтому не ошибетесь. Только не включайте Syslog, если у вас нет сервера. Пингами задолбит, засрет весь лог и будут сбои в звуке во время пинга. И сетью нельзя пользоваться во время игры. Нет, конечно же можно, но никто не гарантирует чистый звук.
Конфигуратор
Во время тестирования из-за изначально не проработанной системы выбора дисплея, многие тестеры неправильно прописывали настройки в ini файле. В последней версии я сделал HTML конфигуратор. Закидываете туда свой ini файл настроек, ставите нужные настройки и скачиваете его у себя же. Секрет в том, что в конфигураторе нельзя выбрать, например 2 дисплея, потому-что включение дисплеев разных типов зависит от разных неочевидных параметров. Это не я так придумал, просто не хотел кардинально все менять. Впрочем, MiniDexed от этого тоже страдает. Но там как то пользователи терпят. Впрочем если вы точно знаете чего вы хотите, можете прописать все настройки в блокноте. Подсказки в ini файле есть для каждого пункта.

Будущее
Вообще в идеале нужно переделать MCU в нормальный C++ код, вынуть его из эмуляции и повторить MiniDexed. Эмуляция старого процессора, да еще и в рилтайме - дело неблагодарное. Он медленный, но иногда быстрый. Дисплей медленный. Тратит кучу ресурсов зря. C++ движок можно оптимизировать и ускорить. Применить например ARM NEON в особо критичных местах. А то он как то простаивает в этом проекте.
Небольшое отступление
Я уже начал разбор прошивки и кода и выяснил, что Roland тоже всех обманули. Реальная полифония не 28 голосов. Точнее - 28 тонов (сэмплов). А так как патч может состоять из 4 тонов, то, используя такие патчи полифония будет всего 7 голосов. Многосэмпловые инструменты, такие как ударные, могут занимать все голоса, если включить сразу все ударные. Но надо отдать должное - если даже включать все инструменты с зажатой педалью, инструменты отключаются практически незаметно. Используется хитрая логика - если определенная нота играет и приходит такая же, то отключается этот канал. Конечно же при отсутствии свободных. Но ожидать чего-то лучшего от 92 года не приходится, программисты и конструкторы и так сделали что-то невообразимое, что и сейчас звучит очень достойно и в чем достаточно сложно разобраться.
Что хотелось бы сделать в первую очередь:
Увеличить число голосов с 28 до .... (в зависимости от быстродействия эмулятора)
Реализовать не банки патчей, а по одному. Чтобы патч содержал в себе только свою информацию вместе с сэмплами. Был автономным.
Разработать формат инструмента: патч+сэмплы . Изначально он не существует, потому-что все эти железки JV, JD, XD не имеют формата сохранения кроме sysex. Но это немного не то.
Добавить дополнительную DSP обработку. Ревербератор нормальный, компрессор, дисторшн.
А может быть расширить инструмент за счет этих DSP. Сделать огибающие этих обработок. Добавить какие то перекрестные связи. Хотя их и так там дохрена.
Сделать битность и частоту сэмплов повыше (поддержку в новых патчах)
Ну и т.д. Фантазия не остановима. Главное чтобы потом это не превратилось в DreamDexed. Я сколько не пытался, так и не смог понять нахрена там столько DSP добавили.
Не знаю, хватит ли у меня терпения и нужно ли это кому-то, но реализовать запланированное будет очень сложно, в несколько раз сложнее всего, что я сделал. Посмотрим реакцию сообщества. Да и вы отпишите в комментариях.
Если интересно, то все изменения в проекте есть здесь.
Страница проекта Mini-JV880pi
Страница проекта MT32-pi
Страница проекта MiniDexed
Страница проекта DreamDexed
