You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MiniDexed/src/sysexfileloader.cpp

393 lines
12 KiB

//
// sysexfileloader.cpp
//
Restructure code and add new features (#37) * Make synth parameters configurable * Add class CConfig, which holds the configuration * Add template config file minidexed.ini * Register panic handler in CKernel to allow to display assertions * Fix: Performance timer did not show correct percent value with HDMI * Add class CDexedAdapter Some Dexed methods require to be guarded from being interrupted by other Dexed calls. This is done in the class CDexedAdapter. * Add class CUserInterface The user interface should be implemented here. As a start it supports showing the program number and name on the LCD display. The LCD output is buffered, so that LCD writes from an IRQ handler are possible. * Move MIDI handling from CMiniDexed to specific classes * CMIDIDevice is the generic MIDI handler * CMIDIKeyboard handles USB audio class MIDI devices * CSerialMIDIDevice handles the serial MIDI device * Now all MIDI inputs can work simultaneous * Program change and bank select work with serial MIDI * Add headers to all files * Include voices.c in sysexfileloader.cpp * Cleanup Makefile * Support headless operation on Raspberry Pi 4 Some code cleanup for src/kernel.* * Code cleanup for src/minidexed.* Move implementation of constructors to minidexed.cpp Reorder member variables * Support multiple USB MIDI inputs at once * Maximum 2 inputs on Raspberry Pi 1-3 * Maximum 4 inputs on Raspberry Pi 4 * Suppress frequent messages in MIDI dump * Use minidexed.txt * Document `SoundDevice` in `minidexed.ini` Co-authored-by: probonopd <probonopd@users.noreply.github.com>
3 years ago
// MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi
// Copyright (C) 2022 The MiniDexed Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "sysexfileloader.h"
#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <assert.h>
#include <circle/logger.h>
Restructure code and add new features (#37) * Make synth parameters configurable * Add class CConfig, which holds the configuration * Add template config file minidexed.ini * Register panic handler in CKernel to allow to display assertions * Fix: Performance timer did not show correct percent value with HDMI * Add class CDexedAdapter Some Dexed methods require to be guarded from being interrupted by other Dexed calls. This is done in the class CDexedAdapter. * Add class CUserInterface The user interface should be implemented here. As a start it supports showing the program number and name on the LCD display. The LCD output is buffered, so that LCD writes from an IRQ handler are possible. * Move MIDI handling from CMiniDexed to specific classes * CMIDIDevice is the generic MIDI handler * CMIDIKeyboard handles USB audio class MIDI devices * CSerialMIDIDevice handles the serial MIDI device * Now all MIDI inputs can work simultaneous * Program change and bank select work with serial MIDI * Add headers to all files * Include voices.c in sysexfileloader.cpp * Cleanup Makefile * Support headless operation on Raspberry Pi 4 Some code cleanup for src/kernel.* * Code cleanup for src/minidexed.* Move implementation of constructors to minidexed.cpp Reorder member variables * Support multiple USB MIDI inputs at once * Maximum 2 inputs on Raspberry Pi 1-3 * Maximum 4 inputs on Raspberry Pi 4 * Suppress frequent messages in MIDI dump * Use minidexed.txt * Document `SoundDevice` in `minidexed.ini` Co-authored-by: probonopd <probonopd@users.noreply.github.com>
3 years ago
#include "voices.c"
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 ")
};
*/
uint8_t CSysExFileLoader::s_DefaultVoice[SizeSingleVoice] = // INIT VOICE
{
99, 99, 99, 99, 99, 99, 99, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 01, 00, 07, // 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
99, 99, 99, 99, 99, 99, 99, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 01, 00, 07, // OP5
99, 99, 99, 99, 99, 99, 99, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 01, 00, 07, // OP4
99, 99, 99, 99, 99, 99, 99, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 01, 00, 07, // OP3
99, 99, 99, 99, 99, 99, 99, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 01, 00, 07, // OP2
99, 99, 99, 99, 99, 99, 99, 00, 00, 00, 00, 00, 00, 00, 00, 00, 99, 00, 01, 00, 07, // OP1
99, 99, 99, 99, 50, 50, 50, 50, // 4 * pitch EG rates, 4 * pitch EG level
00, 00, 01, // algorithm, feedback, osc sync
35, 00, 00, 00, 01, 00, // lfo speed, lfo delay, lfo pitch_mod_depth, lfo_amp_mod_depth, lfo_sync, lfo_waveform
03, 24, // pitch_mod_sensitivity, transpose
73, 78, 73, 84, 32, 86, 79, 73, 67, 69 // 10 * char for name
};
CSysExFileLoader::CSysExFileLoader (const char *pDirName)
: m_DirName (pDirName)
{
m_DirName += "/voice";
m_nNumHighestBank = 0;
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 (bool bHeaderlessSysExVoices)
{
m_nNumHighestBank = 0;
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"
|| strcasecmp (&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 > MaxVoiceBankID)
{
LOGWARN ("Bank #%u is not supported", nBank);
continue;
}
if (m_pVoiceBank[nBank])
{
LOGWARN ("Bank #%u already loaded", nBank);
continue;
}
m_pVoiceBank[nBank] = new TVoiceBank;
assert (m_pVoiceBank[nBank]);
assert (sizeof(TVoiceBank) == VoiceSysExHdrSize + VoiceSysExSize);
std::string Filename (m_DirName);
Filename += "/";
Filename += pEntry->d_name;
FILE *pFile = fopen (Filename.c_str (), "rb");
if (pFile)
{
bool bBankLoaded = false;
if ( fread (m_pVoiceBank[nBank], VoiceSysExHdrSize+VoiceSysExSize, 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);
m_BankFileName[nBank] = pEntry->d_name;
if (nBank > m_nNumHighestBank)
{
// This is the bank ID of the highest loaded bank
m_nNumHighestBank = nBank;
}
bBankLoaded = true;
}
else if (bHeaderlessSysExVoices)
{
// Config says to accept headerless SysEx Voice Banks
// so reset file pointer and try again.
fseek (pFile, 0, SEEK_SET);
if (fread (m_pVoiceBank[nBank]->Voice, VoiceSysExSize, 1, pFile) == 1)
{
LOGDBG ("Bank #%u successfully loaded (headerless)", nBank);
// Add in the missing header items.
// Naturally it isn't possible to validate these!
m_pVoiceBank[nBank]->StatusStart = 0xF0;
m_pVoiceBank[nBank]->CompanyID = 0x43;
m_pVoiceBank[nBank]->Format = 0x09;
m_pVoiceBank[nBank]->ByteCountMS = 0x20;
m_pVoiceBank[nBank]->ByteCountLS = 0x00;
m_pVoiceBank[nBank]->Checksum = 0x00;
m_pVoiceBank[nBank]->StatusEnd = 0xF7;
m_BankFileName[nBank] = pEntry->d_name;
if (nBank > m_nNumHighestBank)
{
// This is the bank ID of the highest loaded bank
m_nNumHighestBank = nBank;
}
bBankLoaded = true;
}
}
if (!bBankLoaded)
{
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);
}
std::string CSysExFileLoader::GetBankName (unsigned nBankID)
{
if (nBankID <= MaxVoiceBankID)
{
std::string Result = m_BankFileName[nBankID];
size_t nLen = Result.length ();
if (nLen > 4)
{
Result.resize (nLen-4); // remove file extension
unsigned nBank;
char BankName[30+1];
if (sscanf (Result.c_str (), "%u_%30s", &nBank, BankName) == 2)
{
Result = BankName;
return Result;
}
}
}
return "NO NAME";
}
unsigned CSysExFileLoader::GetNextBankUp (unsigned nBankID)
{
// Find the next loaded bank "up" from the provided bank ID
for (unsigned id=nBankID+1; id <= m_nNumHighestBank; id++)
{
if (IsValidBank(id))
{
return id;
}
}
// Handle wrap-around
for (unsigned id=0; id<nBankID; id++)
{
if (IsValidBank(id))
{
return id;
}
}
// If we get here there are no other banks!
return nBankID;
}
unsigned CSysExFileLoader::GetNextBankDown (unsigned nBankID)
{
// Find the next loaded bank "down" from the provided bank ID
for (int id=((int)nBankID)-1; id >= 0; id--)
{
if (IsValidBank((unsigned)id))
{
return id;
}
}
// Handle wrap-around
for (unsigned id=m_nNumHighestBank; id>nBankID; id--)
{
if (IsValidBank(id))
{
return id;
}
}
// If we get here there are no other banks!
return nBankID;
}
bool CSysExFileLoader::IsValidBank (unsigned nBankID)
{
if (m_pVoiceBank[nBankID])
{
// Use a valid "status start/end" as an indicator of a valid loaded bank
if ((m_pVoiceBank[nBankID]->StatusStart == 0xF0) &&
(m_pVoiceBank[nBankID]->StatusEnd == 0xF7))
{
return true;
}
}
return false;
}
unsigned CSysExFileLoader::GetNumHighestBank (void)
{
return m_nNumHighestBank;
}
void CSysExFileLoader::GetVoice (unsigned nBankID, unsigned nVoiceID, uint8_t *pVoiceData)
{
if ( nBankID <= MaxVoiceBankID
&& nVoiceID <= VoicesPerBank)
{
if (IsValidBank(nBankID))
{
DecodePackedVoice (m_pVoiceBank[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 (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];
}
}
}