From bfb032366dc08ebe3e20337665bd70603d25816f Mon Sep 17 00:00:00 2001 From: Tim Stirrat Date: Fri, 23 Aug 2024 16:16:28 +0800 Subject: [PATCH] Allow loading custom waveforms via sysex --- Source/io/midi.c | 10 ++++ Source/io/midi.h | 1 + Source/io/midi_sysex.c | 84 ++++++++++++++++++++++++++++++++ Source/io/midi_sysex.h | 46 ++++++++++++++++++ Source/io/sram.c | 87 ++++++++++++++++++++++++---------- Source/io/sram.h | 19 +++++++- Source/io/sram/sram.b0.c | 8 +++- Source/mGB.c | 2 +- Source/mGB.h | 1 + Source/synth/common.c | 1 + Source/synth/wav.c | 100 ++++++++++++++++++++++++++------------- Source/synth/wav.h | 8 +++- 12 files changed, 302 insertions(+), 65 deletions(-) create mode 100644 Source/io/midi_sysex.c create mode 100644 Source/io/midi_sysex.h diff --git a/Source/io/midi.c b/Source/io/midi.c index 8ccdbec..b9f3bbe 100644 --- a/Source/io/midi.c +++ b/Source/io/midi.c @@ -1,6 +1,7 @@ #include "midi.h" #include "../mGB.h" #include "midi_asm.h" +#include "midi_sysex.h" #include "serial.h" uint8_t statusByte; @@ -17,8 +18,17 @@ void updateMidiBuffer(void) { uint8_t byte = serialBuffer[serialBufferReadPosition]; + if (sysexBytesCount) { + captureSysexByte(byte); + return; + } + // STATUS BYTE if (byte & MIDI_STATUS_BIT) { + if (byte == MIDI_STATUS_SYSEX) { + sysexBytesCount = 1; + return; + } if ((byte & MIDI_STATUS_SYSTEM) == MIDI_STATUS_SYSTEM) { return; } diff --git a/Source/io/midi.h b/Source/io/midi.h index cb809ed..9a4020b 100644 --- a/Source/io/midi.h +++ b/Source/io/midi.h @@ -1,6 +1,7 @@ #pragma once #include +#include #define MIDI_STATUS_BIT 0x80 #define MIDI_STATUS_NOTE_ON 0x09 diff --git a/Source/io/midi_sysex.c b/Source/io/midi_sysex.c new file mode 100644 index 0000000..d20c27b --- /dev/null +++ b/Source/io/midi_sysex.c @@ -0,0 +1,84 @@ +#include "midi_sysex.h" +#include "../mGB.h" + +uint8_t sysexBytesCount; +sysex_payload_t sysexPayload; +uint8_t sysexBuffer[24]; + +// adapted from +// https://github.com/FortySevenEffects/arduino_midi_library/blob/2d64cc3c2ff85bbee654a7054e36c59694d8d8e4/src/MIDI.cpp#L87 +// (MIT licensed) +// Reduced parameter count to improve perf + +/*! + \brief Decode System Exclusive messages. + + SysEx messages are encoded to guarantee transmission of data bytes higher + than 127 without breaking the MIDI protocol. Use this static method to + reassemble your received message. + + \param outData The output buffer where to + store the decrypted message. + \param inLength The length of the input + buffer. + \return The length of the output buffer. + @see encodeSysEx @see getSysExArrayLength + Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com + */ +static uint8_t decodeSysEx(uint8_t *outData, uint8_t inLength) { + uint8_t count = 0; + uint8_t msbStorage = 0; + uint8_t byteIndex = 0; + + for (uint8_t i = 0; i < inLength; ++i) { + if ((i % 8) == 0) { + msbStorage = sysexBuffer[i]; + byteIndex = 6; + } else { + const uint8_t body = sysexBuffer[i]; + const uint8_t msb = (((msbStorage >> byteIndex) & 1) << 7); + byteIndex--; + outData[count++] = msb | body; + } + } + return count; +} + +void captureSysexByte(uint8_t byte) { + sysexBytesCount++; + systemIdle = false; + + const uint8_t bufferIndex = sysexBytesCount - SYSEX_HEADER_SIZE; + + if (byte == SYSEX_EOF) { + // EMU_printf("sysex EOF: %#02x\n", byte); + sysexPayload.size = decodeSysEx(&sysexPayload.data[0], bufferIndex); + sysexPayload.ready = true; + sysexBytesCount = 0; + return; + } + + switch (sysexBytesCount) { + case 1: + // EMU_printf("sysex status byte: %#02x\n", byte); + // ignored in payload + break; + case 2: + // EMU_printf("sysex id: %#02x\n", byte); + sysexPayload.id = byte; + break; + case 3: + // EMU_printf("sysex device_id: %#02x\n", byte); + sysexPayload.device_id = byte; + break; + case 4: + // EMU_printf("sysex channel: %#02x\n", byte); + sysexPayload.channel = byte; + break; + + default: + // EMU_printf("sysex data: %#02x\n", byte); + sysexBuffer[bufferIndex] = byte; + break; + } +} diff --git a/Source/io/midi_sysex.h b/Source/io/midi_sysex.h new file mode 100644 index 0000000..f6c45fb --- /dev/null +++ b/Source/io/midi_sysex.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#define MIDI_STATUS_SYSEX 0xF0 +#define SYSEX_UNIVERSAL_REALTIME 0x7F +#define SYSEX_UNIVERSAL_NON_REALTIME 0x7E +#define SYSEX_NON_COMMERCIAL 0x7D +#define SYSEX_EOF 0xF7 + +#define SYSEX_MGB_ID 0x69 + +// The size of the SysEx message header before the payload begins +#define SYSEX_HEADER_SIZE 5 + +extern uint8_t sysexBytesCount; + +typedef struct sysex_payload { + // the sysex message id (or manufacturer id) + uint8_t id; + // the payload device id (0xBB for mGB) + uint8_t device_id; + // the device or channel id + uint8_t channel; + // the size of the data + uint8_t size; + // the data payload (24 raw sysex bytes = 21 (3*7) + 3 header blocks) + uint8_t data[21]; + // true if the payload has finished reading from the buffer (has seen an EOF) + bool ready; +} sysex_payload_t; + +// A SysEx payload, captures up to 16 bytes of sysex data. Currently this is +// used to modify the active WAV channel waveform. +extern sysex_payload_t sysexPayload; + +// The buffer to hold the sysexBytes before decoding +// contains enough space to hold 16 bytes = MSB, 7 bytes, MSB, 7 bytes, MSB, 2 +// bytes = 19 bytes +extern uint8_t sysexBuffer[24]; + +// Reads SysEx messages (F0) up until a SysEx EOF packet +// (F7). Data is stored in `sysexPayload` and will set `sysexPayload.ready` when +// it sees the EOF +void captureSysexByte(uint8_t byte); diff --git a/Source/io/sram.c b/Source/io/sram.c index 30a35b2..2b15a5f 100644 --- a/Source/io/sram.c +++ b/Source/io/sram.c @@ -2,7 +2,9 @@ #include "../mGB.h" #include "../screen/main.h" #include "../synth/data.h" +#include "../synth/wav.h" #include +#include void saveDataSet(uint8_t synth) { // EMU_printf("saveDataSet(synth %d)\n", synth); @@ -80,7 +82,46 @@ void loadDataSet(uint8_t synth) { DISABLE_RAM_MBC1; } -void checkMemory(void) { +// Init V1 SRAM data (`saveData`) +static void initV1(void) { + for (x = 0; x != 128; x += 8) { + l = 0; + for (i = 0; i < 7; i++) { + saveData[(x + l)] = dataSet[i]; + l++; + } + l = 0; + for (i = 7; i != 13; i++) { + saveData[(x + 128U + l)] = dataSet[i]; + l++; + } + l = 0; + for (i = 13; i != 20; i++) { + saveData[(x + 256U + l)] = dataSet[i]; + l++; + } + l = 0; + for (i = 20; i != 24; i++) { + saveData[(x + 384U + l)] = dataSet[i]; + l++; + } + } + saveData[SRAM_SENTINEL_INDEX] = SRAM_INITIALIZED; + sram_version = 1; +} + +// Init V2 SRAM data (`sram_wavData`) +static void initV2(void) { + memcpy(&sram_wavData, &wavData, sizeof(sram_wavData)); + sram_version = 2; +} + +// load the SRAM `sram_wavData` data into the runtime `wavData` +static void sramLoadAllWavData(void) { + memcpy(&wavData, &sram_wavData, sizeof(wavData)); +} + +void initMemory(void) { ENABLE_RAM_MBC1; // fixes some SRAM Bugs @@ -88,33 +129,29 @@ void checkMemory(void) { SWITCH_RAM(0); // end fix - if (saveData[512] != SRAM_INITIALIZED) { - for (x = 0; x != 128; x += 8) { - l = 0; - for (i = 0; i < 7; i++) { - saveData[(x + l)] = dataSet[i]; - l++; - } - l = 0; - for (i = 7; i != 13; i++) { - saveData[(x + 128U + l)] = dataSet[i]; - l++; - } - l = 0; - for (i = 13; i != 20; i++) { - saveData[(x + 256U + l)] = dataSet[i]; - l++; - } - l = 0; - for (i = 20; i != 24; i++) { - saveData[(x + 384U + l)] = dataSet[i]; - l++; - } - } - saveData[512] = SRAM_INITIALIZED; + if (sram_version == 2) { + // do nothing + } else if (saveData[SRAM_SENTINEL_INDEX] == SRAM_INITIALIZED) { + // legacy SRAM format (== V1) + initV2(); + } else { + // no SRAM initialized + saveData[SRAM_SENTINEL_INDEX] = SRAM_INITIALIZED; + initV1(); + initV2(); } + + sramLoadAllWavData(); DISABLE_RAM_MBC1; for (j = 0; j != 24; j++) dataSetSnap[j] = dataSet[j]; } + +void sramStoreWaveform(uint8_t index) { + const uint8_t offset = index * WAVEFORM_LENGTH; + + ENABLE_RAM_MBC1; + memcpy(&sram_wavData[offset], &wavData[offset], WAVEFORM_LENGTH); + DISABLE_RAM_MBC1; +} diff --git a/Source/io/sram.h b/Source/io/sram.h index fe68841..b3ae118 100644 --- a/Source/io/sram.h +++ b/Source/io/sram.h @@ -2,11 +2,26 @@ #include +// the position in SRAM where the Version sentinel sits +#define SRAM_SENTINEL_INDEX 512 + +// SRAM sentinel value to determine if anything is initialized #define SRAM_INITIALIZED 0xF7 // 512 + sentinel -extern uint8_t saveData[513U]; +extern volatile uint8_t saveData[513]; + +// SRAM version, so that we can determine if a partial migration/init is needed +extern volatile uint8_t sram_version; + +// SRAM stored wav data, can be modified at runtime +extern volatile uint8_t sram_wavData[256]; void saveDataSet(uint8_t synth); void loadDataSet(uint8_t synth); -void checkMemory(void); \ No newline at end of file + +// initialises the SRAM +void initMemory(void); + +// Copies a wave from `wavData` into SRAM stored `sram_wavData` +void sramStoreWaveform(uint8_t index); diff --git a/Source/io/sram/sram.b0.c b/Source/io/sram/sram.b0.c index 8999edf..0b21b66 100644 --- a/Source/io/sram/sram.b0.c +++ b/Source/io/sram/sram.b0.c @@ -2,4 +2,10 @@ #include -uint8_t saveData[513]; \ No newline at end of file +uint8_t saveData[513]; + +// SRAM version +uint8_t sram_version; + +// SRAM stored wav data, can be modified at runtime +uint8_t sram_wavData[256]; diff --git a/Source/mGB.c b/Source/mGB.c index 5d18e78..a9ed3a2 100644 --- a/Source/mGB.c +++ b/Source/mGB.c @@ -42,7 +42,7 @@ void main(void) { CRITICAL { cpu_fast(); - checkMemory(); + initMemory(); displaySetup(); initMainScreen(); setSoundDefaults(); diff --git a/Source/mGB.h b/Source/mGB.h index bbdd5c6..a604db7 100644 --- a/Source/mGB.h +++ b/Source/mGB.h @@ -1,5 +1,6 @@ #pragma once +// #include #include #include diff --git a/Source/synth/common.c b/Source/synth/common.c index 55720bb..8ca41b4 100644 --- a/Source/synth/common.c +++ b/Source/synth/common.c @@ -139,6 +139,7 @@ void updateSynths(void) { updateVibratoPosition(NOI); updateWavSweep(); + updateWavSysex(); } inline void setOutputSwitch(void) { diff --git a/Source/synth/wav.c b/Source/synth/wav.c index 39f4430..9c2db17 100644 --- a/Source/synth/wav.c +++ b/Source/synth/wav.c @@ -1,7 +1,10 @@ #include "wav.h" #include "../io/midi.h" +#include "../io/midi_sysex.h" +#include "../io/sram.h" #include "../mGB.h" #include "common.h" +#include "data.h" #include #include @@ -22,54 +25,55 @@ uint16_t counterWav; bool cueWavSweep; uint8_t wavStepCounter; uint8_t counterWavStart; -// initial waveforms -const uint8_t wavData[256] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, - 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, - 0x22, 0x55, 0x77, 0xAA, 0xBB, 0xDD, 0xEE, 0xFF, - 0xEE, 0xDD, 0xBB, 0xAA, 0x77, 0x66, 0x44, 0x00, +// runtime waveforms (loaded from SRAM on init, can be modified) +uint8_t wavData[256] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, - 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x22, 0x55, 0x77, 0xAA, 0xBB, 0xDD, 0xEE, 0xFF, + 0xEE, 0xDD, 0xBB, 0xAA, 0x77, 0x66, 0x44, 0x00, - 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88, - 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88, + 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x16, 0x13, 0xAA, 0xB3, 0x25, 0x81, 0xE8, 0x2A, - 0x1B, 0xEB, 0xF8, 0x85, 0xE1, 0x28, 0xFF, 0xA4, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x34, 0x09, 0x91, 0xA7, 0x63, 0xB8, 0x99, 0xA1, - 0x3F, 0xE4, 0xD0, 0x61, 0x66, 0x73, 0x59, 0x7C, + 0x16, 0x13, 0xAA, 0xB3, 0x25, 0x81, 0xE8, 0x2A, + 0x1B, 0xEB, 0xF8, 0x85, 0xE1, 0x28, 0xFF, 0xA4, - 0x86, 0x04, 0x2F, 0xAC, 0x85, 0x17, 0xD6, 0xA1, - 0x03, 0xCF, 0x27, 0xE4, 0xF8, 0x27, 0x89, 0x2C, + 0x34, 0x09, 0x91, 0xA7, 0x63, 0xB8, 0x99, 0xA1, + 0x3F, 0xE4, 0xD0, 0x61, 0x66, 0x73, 0x59, 0x7C, - 0x30, 0x1B, 0xD4, 0x93, 0xD3, 0x6E, 0x35, 0x13, - 0x53, 0x05, 0x75, 0xB9, 0x79, 0xCF, 0x36, 0x00, + 0x86, 0x04, 0x2F, 0xAC, 0x85, 0x17, 0xD6, 0xA1, + 0x03, 0xCF, 0x27, 0xE4, 0xF8, 0x27, 0x89, 0x2C, - 0xD4, 0x2C, 0xC6, 0x4E, 0x2C, 0x12, 0xE2, 0x15, - 0x8B, 0xAF, 0x3D, 0xEF, 0x6E, 0xF1, 0xBF, 0xD9, + 0x30, 0x1B, 0xD4, 0x93, 0xD3, 0x6E, 0x35, 0x13, + 0x53, 0x05, 0x75, 0xB9, 0x79, 0xCF, 0x36, 0x00, - 0x43, 0x17, 0x2B, 0xF3, 0x12, 0xC2, 0xCB, 0x8C, - 0x3B, 0x43, 0xF2, 0xDF, 0x5D, 0xF9, 0xEF, 0x31, + 0xD4, 0x2C, 0xC6, 0x4E, 0x2C, 0x12, 0xE2, 0x15, + 0x8B, 0xAF, 0x3D, 0xEF, 0x6E, 0xF1, 0xBF, 0xD9, - 0x6D, 0x46, 0xF6, 0x7A, 0xEE, 0x17, 0x35, 0xF4, - 0xDA, 0xFE, 0x7C, 0x28, 0xB8, 0x55, 0x12, 0x57, + 0x43, 0x17, 0x2B, 0xF3, 0x12, 0xC2, 0xCB, 0x8C, + 0x3B, 0x43, 0xF2, 0xDF, 0x5D, 0xF9, 0xEF, 0x31, - 0xFF, 0x82, 0xBB, 0x85, 0xEF, 0xD4, 0x7C, 0xA1, - 0x05, 0xB4, 0xFF, 0xC1, 0x95, 0x27, 0x30, 0x03}; + 0x6D, 0x46, 0xF6, 0x7A, 0xEE, 0x17, 0x35, 0xF4, + 0xDA, 0xFE, 0x7C, 0x28, 0xB8, 0x55, 0x12, 0x57, + + 0xFF, 0x82, 0xBB, 0x85, 0xEF, 0xD4, 0x7C, 0xA1, + 0x05, 0xB4, 0xFF, 0xC1, 0x95, 0x27, 0x30, 0x03}; // _asmUpdateWav was: 653/628 clock cycles, 134 bytes // now: 684/658 clock cycles, 144 bytes @@ -199,6 +203,28 @@ void updateWavSweep(void) { } } +/** + * Checks for a sysex message to overwrite the current wav shape with a custom + * 16 byte payload + */ +void updateWavSysex(void) { + if (sysexPayload.ready) { + sysexPayload.ready = false; + // EMU_printf("SYSEX read %d bytes for channel %d\n", sysexPayload.size, + // sysexPayload.channel); + + if (sysexPayload.id == SYSEX_NON_COMMERCIAL && + sysexPayload.device_id == SYSEX_MGB_ID && sysexPayload.channel == WAV && + sysexPayload.size == WAVEFORM_LENGTH) { + // EMU_printf("SYSEX payload looks correct, updatingwaveform...\n"); + storeWav(dataSet[WAV_Shape], &sysexPayload.data); + loadWav(wavDataOffset); + } else { + // EMU_printf("SYSEX payload was not correctly formed\n"); + } + } +} + // _asmLoadWav was: 697 clock cycles, 159 bytes // now: 235 clock cycles, 53 bytes // + _memcpy: 247/227 clock cycles, 44 bytes @@ -210,7 +236,7 @@ void loadWav(uint8_t offset) { // Turn off wave channel rAUD3ENA = AUDENA_OFF; - memcpy(&_AUD3WAVERAM, &wavData[offset], 16U); + memcpy(&_AUD3WAVERAM, &wavData[offset], WAVEFORM_LENGTH); // Turn on wave channel rAUD3ENA = AUDENA_ON; @@ -221,3 +247,9 @@ void loadWav(uint8_t offset) { systemIdle = false; } + +// Store a custom waveform in wavData +void storeWav(uint8_t index, uint8_t *source[WAVEFORM_LENGTH]) { + memcpy(&wavData[index * WAVEFORM_LENGTH], source, WAVEFORM_LENGTH); + sramStoreWaveform(index); +} diff --git a/Source/synth/wav.h b/Source/synth/wav.h index 817b218..8ab76b1 100644 --- a/Source/synth/wav.h +++ b/Source/synth/wav.h @@ -15,6 +15,8 @@ // The WAV oscilator is 1 octave higher #define WAV_OCTAVE_OFFSET 12U +#define WAVEFORM_LENGTH 16 + extern uint16_t wavCurrentFreq; extern int8_t wavOct; @@ -34,10 +36,12 @@ extern bool cueWavSweep; extern uint8_t wavStepCounter; extern uint8_t counterWavStart; -// initial waveforms -extern const uint8_t wavData[256]; +// waveforms +extern uint8_t wavData[256]; void updateWav(void); +void storeWav(uint8_t index, uint8_t *source[WAVEFORM_LENGTH]); void loadWav(uint8_t offset); void playNoteWav(void); void updateWavSweep(void); +void updateWavSysex(void);