From 86c54a6f489fb79e202addf69da98f5c4adbd861 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/screen/main.c | 2 +- Source/synth/common.c | 1 + Source/synth/wav.c | 100 ++++++++++++++++++++++++++------------- Source/synth/wav.h | 8 +++- mGB-v1.4.1-ts.gb | Bin 0 -> 32768 bytes 14 files changed, 303 insertions(+), 66 deletions(-) create mode 100644 Source/io/midi_sysex.c create mode 100644 Source/io/midi_sysex.h create mode 100644 mGB-v1.4.1-ts.gb 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/screen/main.c b/Source/screen/main.c index b472962..28669f6 100644 --- a/Source/screen/main.c +++ b/Source/screen/main.c @@ -9,7 +9,7 @@ #include "../synth/wav.h" #include "screen.h" -static const uint8_t VERSION_NUMBER[] = "v.1.4.0ts"; +static const uint8_t VERSION_NUMBER[] = "v.1.4.1 sysex"; static const uint8_t HELP_DATA[10][18] = { "octave ", 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); diff --git a/mGB-v1.4.1-ts.gb b/mGB-v1.4.1-ts.gb new file mode 100644 index 0000000000000000000000000000000000000000..b9ddde121ce7c2309127138df4410a3b2a70f699 GIT binary patch literal 32768 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