From 79fe4aabbf1b311d22195852688255e2d61f916e Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 26 Feb 2022 17:31:39 +0100 Subject: [PATCH] Load voices from .syx files in /sysex/voice directory https://github.com/probonopd/MiniDexed/issues/11#issuecomment-1052239623 Thanks @rsta2 --- src/Makefile | 4 +- src/minidexed.cpp | 29 ++++- src/minidexed.h | 2 + src/sysexfileloader.cpp | 237 ++++++++++++++++++++++++++++++++++++++++ src/sysexfileloader.h | 61 +++++++++++ 5 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 src/sysexfileloader.cpp create mode 100644 src/sysexfileloader.h diff --git a/src/Makefile b/src/Makefile index 86f62a0..8204121 100644 --- a/src/Makefile +++ b/src/Makefile @@ -5,8 +5,8 @@ CIRCLE_STDLIB_DIR = ../circle-stdlib SYNTH_DEXED_DIR = ../Synth_Dexed/src -OBJS = main.o kernel.o minidexed.o pckeyboard.o $(SYNTH_DEXED_DIR)/synth_dexed.o \ - perftimer.o +OBJS = main.o kernel.o minidexed.o pckeyboard.o sysexfileloader.o perftimer.o \ + $(SYNTH_DEXED_DIR)/synth_dexed.o INCLUDE += -I $(SYNTH_DEXED_DIR) diff --git a/src/minidexed.cpp b/src/minidexed.cpp index ddf1e28..cbf0fb4 100644 --- a/src/minidexed.cpp +++ b/src/minidexed.cpp @@ -12,6 +12,9 @@ #define MIDI_NOTE_OFF 0b1000 #define MIDI_NOTE_ON 0b1001 #define MIDI_AFTERTOUCH 0xA0 +#define MIDI_CONTROL_CHANGE 0xB0 + #define MIDI_CC_BANK_SELECT_MSB 0 // TODO: not supported + #define MIDI_CC_BANK_SELECT_LSB 32 #define MIDI_PROGRAM_CHANGE 0xC0 #define MIDI_PITCH_BEND 0xE0 @@ -25,6 +28,8 @@ CPerformanceTimer GetChunkTimer ("GetChunk", 1000000U * CHUNK_SIZE/2 / SAMPLE_RA bool CMiniDexed::Initialize (void) { + m_SysExFileLoader.Load (); + if (!m_Serial.Initialize(31250)) { return false; @@ -144,15 +149,33 @@ void CMiniDexed::MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLeng } #endif + if (pPacket[0] == MIDI_CONTROL_CHANGE) + { + if (pPacket[1] == MIDI_CC_BANK_SELECT_LSB) + { + if (pPacket[2] < 1 || pPacket[2] > 128) + { + return; + } + + printf ("Select voice bank %u\n", (unsigned) pPacket[2]); + s_pThis->m_SysExFileLoader.SelectVoiceBank (pPacket[2]-1); + } + + return; + } + if (pPacket[0] == MIDI_PROGRAM_CHANGE) { if(pPacket[1] < 1 || pPacket[1] > 32) { return; } - printf ("Loading voice %d\n", (unsigned) pPacket[1]); - s_pThis->loadVoiceParameters(voices_bank[0][(unsigned) pPacket[1] - 1]); + printf ("Loading voice %u\n", (unsigned) pPacket[1]); + uint8_t Buffer[156]; + s_pThis->m_SysExFileLoader.GetVoice (pPacket[1]-1, Buffer); + s_pThis->loadVoiceParameters(Buffer); char buf_name[11]; - memset(reinterpret_cast(buf_name), 0, 11); // Initialize with 0x00 chars + memset(buf_name, 0, 11); // Initialize with 0x00 chars s_pThis->setName(buf_name); printf ("%s\n", buf_name); return; diff --git a/src/minidexed.h b/src/minidexed.h index 251addc..a8529c7 100644 --- a/src/minidexed.h +++ b/src/minidexed.h @@ -15,6 +15,7 @@ #include #include #include +#include "sysexfileloader.h" #include "pckeyboard.h" #define SAMPLE_RATE 48000 @@ -51,6 +52,7 @@ class CMiniDexed : public Dexed boolean m_bUseSerial; unsigned m_nSerialState; u8 m_SerialMessage[3]; + CSysExFileLoader m_SysExFileLoader; static CMiniDexed *s_pThis; }; diff --git a/src/sysexfileloader.cpp b/src/sysexfileloader.cpp new file mode 100644 index 0000000..062a720 --- /dev/null +++ b/src/sysexfileloader.cpp @@ -0,0 +1,237 @@ +// +// sysexfileloader.cpp +// +#include "sysexfileloader.h" +#include +#include +#include +#include +#include +#include + +extern uint8_t voices_bank[1][32][156]; + +LOGMODULE ("syxfile"); + +uint8_t CSysExFileLoader::s_DefaultVoice[SizeSingleVoice] = // FM-Piano +{ + 95, 29, 20, 50, 99, 95, 00, 00, 41, 00, 19, 00, 00, 03, 00, 06, 79, 00, 01, 00, 14, // OP6 eg_rate_1-4, level_1-4, kbd_lev_scl_brk_pt, kbd_lev_scl_lft_depth, kbd_lev_scl_rht_depth, kbd_lev_scl_lft_curve, kbd_lev_scl_rht_curve, kbd_rate_scaling, amp_mod_sensitivity, key_vel_sensitivity, operator_output_level, osc_mode, osc_freq_coarse, osc_freq_fine, osc_detune + 95, 20, 20, 50, 99, 95, 00, 00, 00, 00, 00, 00, 00, 03, 00, 00, 99, 00, 01, 00, 00, // OP5 + 95, 29, 20, 50, 99, 95, 00, 00, 00, 00, 00, 00, 00, 03, 00, 06, 89, 00, 01, 00, 07, // OP4 + 95, 20, 20, 50, 99, 95, 00, 00, 00, 00, 00, 00, 00, 03, 00, 02, 99, 00, 01, 00, 07, // OP3 + 95, 50, 35, 78, 99, 75, 00, 00, 00, 00, 00, 00, 00, 03, 00, 07, 58, 00, 14, 00, 07, // OP2 + 96, 25, 25, 67, 99, 75, 00, 00, 00, 00, 00, 00, 00, 03, 00, 02, 99, 00, 01, 00, 10, // OP1 + 94, 67, 95, 60, 50, 50, 50, 50, // 4 * pitch EG rates, 4 * pitch EG level + 04, 06, 00, // algorithm, feedback, osc sync + 34, 33, 00, 00, 00, 04, // lfo speed, lfo delay, lfo pitch_mod_depth, lfo_amp_mod_depth, lfo_sync, lfo_waveform + 03, 24, // pitch_mod_sensitivity, transpose + 70, 77, 45, 80, 73, 65, 78, 79, 00, 00 // 10 * char for name ("DEFAULT ") +}; + +CSysExFileLoader::CSysExFileLoader (const char *pDirName) +: m_DirName (pDirName), + m_nBankID (0) +{ + m_DirName += "/voice"; + + for (unsigned i = 0; i <= MaxVoiceBankID; i++) + { + m_pVoiceBank[i] = nullptr; + } +} + +CSysExFileLoader::~CSysExFileLoader (void) +{ + for (unsigned i = 0; i <= MaxVoiceBankID; i++) + { + delete m_pVoiceBank[i]; + } +} + +void CSysExFileLoader::Load (void) +{ + DIR *pDirectory = opendir (m_DirName.c_str ()); + if (!pDirectory) + { + LOGWARN ("Directory %s not found", m_DirName.c_str ()); + + return; + } + + dirent *pEntry; + while ((pEntry = readdir (pDirectory)) != nullptr) + { + unsigned nBank; + size_t nLen = strlen (pEntry->d_name); + + if ( nLen < 5 // "[NNNN]N[_name].syx" + || strcmp (&pEntry->d_name[nLen-4], ".syx") != 0 + || sscanf (pEntry->d_name, "%u", &nBank) != 1) + { + LOGWARN ("%s: Invalid filename format", pEntry->d_name); + + continue; + } + + if ( nBank < 1 + || nBank > MaxVoiceBankID+1) + { + LOGWARN ("Bank #%u is not supported", nBank); + + continue; + } + + nBank--; // zero-based internally + + if (m_pVoiceBank[nBank]) + { + LOGWARN ("Bank #%u already loaded", nBank+1); + + continue; + } + + m_pVoiceBank[nBank] = new TVoiceBank; + assert (m_pVoiceBank[nBank]); + + std::string Filename (m_DirName); + Filename += "/"; + Filename += pEntry->d_name; + + FILE *pFile = fopen (Filename.c_str (), "rb"); + if (pFile) + { + if ( fread (m_pVoiceBank[nBank], sizeof (TVoiceBank), 1, pFile) == 1 + && m_pVoiceBank[nBank]->StatusStart == 0xF0 + && m_pVoiceBank[nBank]->CompanyID == 0x43 + && m_pVoiceBank[nBank]->Format == 0x09 + && m_pVoiceBank[nBank]->StatusEnd == 0xF7) + { + LOGDBG ("Bank #%u successfully loaded", nBank+1); + } + else + { + LOGWARN ("%s: Invalid size or format", Filename.c_str ()); + + delete m_pVoiceBank[nBank]; + m_pVoiceBank[nBank] = nullptr; + } + + fclose (pFile); + } + else + { + delete m_pVoiceBank[nBank]; + m_pVoiceBank[nBank] = nullptr; + } + } + + closedir (pDirectory); +} + +void CSysExFileLoader::SelectVoiceBank (unsigned nBankID) +{ + if (nBankID <= MaxVoiceBankID) + { + m_nBankID = nBankID; + } +} + +void CSysExFileLoader::GetVoice (unsigned nVoiceID, uint8_t *pVoiceData) +{ + if (nVoiceID <= VoicesPerBank) + { + assert (m_nBankID <= MaxVoiceBankID); + if (m_pVoiceBank[m_nBankID]) + { + DecodePackedVoice (m_pVoiceBank[m_nBankID]->Voice[nVoiceID], pVoiceData); + + return; + } + else + { + // Use default voices_bank instead of s_DefaultVoice for bank 0, + // if the bank was not successfully loaded from disk. + if (m_nBankID == 0) + { + memcpy (pVoiceData, voices_bank[0][nVoiceID], SizeSingleVoice); + + return; + } + } + } + + memcpy (pVoiceData, s_DefaultVoice, SizeSingleVoice); +} + +// See: https://github.com/bwhitman/learnfm/blob/master/dx7db.py +void CSysExFileLoader::DecodePackedVoice (const uint8_t *pPackedData, uint8_t *pDecodedData) +{ + const uint8_t *p = pPackedData; + uint8_t *o = pDecodedData; + + memset (o, 0, 156); + + for (unsigned op = 0; op < 6; op++) + { + memcpy(&o[op*21], &p[op*17], 11); + + uint8_t leftrightcurves = p[op*17+11]; + o[op * 21 + 11] = leftrightcurves & 3; + o[op * 21 + 12] = (leftrightcurves >> 2) & 3; + + uint8_t detune_rs = p[op * 17 + 12]; + o[op * 21 + 13] = detune_rs & 7; + o[op * 21 + 20] = detune_rs >> 3; + + uint8_t kvs_ams = p[op * 17 + 13]; + o[op * 21 + 14] = kvs_ams & 3; + o[op * 21 + 15] = kvs_ams >> 2; + o[op * 21 + 16] = p[op * 17 + 14]; + + uint8_t fcoarse_mode = p[op * 17 + 15]; + o[op * 21 + 17] = fcoarse_mode & 1; + o[op * 21 + 18] = fcoarse_mode >> 1; + o[op * 21 + 19] = p[op * 17 + 16]; + } + + memcpy (&o[126], &p[102], 9); + uint8_t oks_fb = p[111]; + o[135] = oks_fb & 7; + o[136] = oks_fb >> 3; + memcpy (&o[137], &p[112], 4); + uint8_t lpms_lfw_lks = p[116]; + o[141] = lpms_lfw_lks & 1; + o[142] = (lpms_lfw_lks >> 1) & 7; + o[143] = lpms_lfw_lks >> 4; + memcpy (&o[144], &p[117], 11); + o[155] = 0x3f; + + // Clamp the unpacked patches to a known max. + static uint8_t maxes[] = + { + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, // osc6 + 3, 3, 7, 3, 7, 99, 1, 31, 99, 14, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, // osc5 + 3, 3, 7, 3, 7, 99, 1, 31, 99, 14, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, // osc4 + 3, 3, 7, 3, 7, 99, 1, 31, 99, 14, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, // osc3 + 3, 3, 7, 3, 7, 99, 1, 31, 99, 14, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, // osc2 + 3, 3, 7, 3, 7, 99, 1, 31, 99, 14, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, // osc1 + 3, 3, 7, 3, 7, 99, 1, 31, 99, 14, + 99, 99, 99, 99, 99, 99, 99, 99, // pitch eg rate & level + 31, 7, 1, 99, 99, 99, 99, 1, 5, 7, 48, // algorithm etc + 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, // name + 127 // operator on/off + }; + + for (unsigned i = 0; i < 156; i++) + { + if (o[i] > maxes[i]) + { + o[i] = maxes[i]; + } + } +} diff --git a/src/sysexfileloader.h b/src/sysexfileloader.h new file mode 100644 index 0000000..c90af4d --- /dev/null +++ b/src/sysexfileloader.h @@ -0,0 +1,61 @@ +// +// sysexfileloader.h +// +// See: https://github.com/asb2m10/dexed/blob/master/Documentation/sysex-format.txt +// +#ifndef _sysexfileloader_h +#define _sysexfileloader_h + +#include +#include +#include + +class CSysExFileLoader // Loader for DX7 .syx files +{ +public: + static const unsigned MaxVoiceBankID = 127; // TODO? 16383 + static const unsigned VoicesPerBank = 32; + static const size_t SizePackedVoice = 128; + static const size_t SizeSingleVoice = 156; + + struct TVoiceBank + { + uint8_t StatusStart; // 0xF0 + uint8_t CompanyID; // 0x43 + uint8_t SubStatus; // 0x00 + uint8_t Format; // 0x09 + uint8_t ByteCountMS; // 0x20 + uint8_t ByteCountLS; // 0x00 + + uint8_t Voice[VoicesPerBank][SizePackedVoice]; + + uint8_t Checksum; + uint8_t StatusEnd; // 0xF7 + } + PACKED; + +public: + CSysExFileLoader (const char *pDirName = "/sysex"); + ~CSysExFileLoader (void); + + void Load (void); + + void SelectVoiceBank (unsigned nBankID); // 0 .. 127 + + void GetVoice (unsigned nVoiceID, // 0 .. 31 + uint8_t *pVoiceData); // returns unpacked format (156 bytes) + +private: + static void DecodePackedVoice (const uint8_t *pPackedData, uint8_t *pDecodedData); + +private: + std::string m_DirName; + + TVoiceBank *m_pVoiceBank[MaxVoiceBankID+1]; + + unsigned m_nBankID; + + static uint8_t s_DefaultVoice[SizeSingleVoice]; +}; + +#endif