gigatron/rom/Contrib/at67/tools/gtmidi/gtmidi.cpp
2025-01-28 19:17:01 +03:00

557 lines
20 KiB
C++

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <sstream>
#include <iomanip>
#include <fstream>
#include <string>
#include <algorithm>
#include <map>
#include "../../cpu.h"
#include "../../memory.h"
#include "../../expression.h"
#include "../../midi.h"
#include "../../gtmidi.h"
#define GTMIDI_MAJOR_VERSION "0.6"
#define GTMIDI_MINOR_VERSION "0"
#define GTMIDI_VERSION_STR "gtmidi v" GTMIDI_MAJOR_VERSION "." GTMIDI_MINOR_VERSION
#define MAX_ELEMENTS 16
enum Format {gtMID=0, vCPU, GBAS, GCL, CPP, PY, NumFormats};
int _elementCount = 0;
std::string _segmentName;
std::map<std::string, int> _formatName =
{
{"GTMID", gtMID},
{"VCPU", vCPU },
{"GBAS", GBAS },
{"GCL" , GCL },
{"CPP" , CPP },
{"PY" , PY },
};
uint8_t _midiBuffer[MIDI_MAX_BUFFER_SIZE];
std::vector<uint8_t> _outBuffer;
void padString(std::string &str, size_t num, char pad=' ')
{
if(num > str.size()) str.insert(0, num - str.size(), pad);
}
void addString(std::string &str, size_t num, char add=' ')
{
str.append(num, add);
}
// gtMID output
void outputGTMIDheader(std::ofstream& outfile, const std::string& name, bool hasVolume)
{
GtMidiHdr gtMidiHdr;
// Write GtMiDiHdr, upto HiSize
strncpy((char *)gtMidiHdr._name, name.c_str(), int(name.length()) % GTMIDI_NAME_SIZE);
gtMidiHdr._hasVolume = hasVolume;
outfile.write((char *)&gtMidiHdr, GTMIDI_HI_SIZE_OFFSET);
}
void outputGTMIDcommand(uint8_t command)
{
_outBuffer.push_back(command);
}
void outputGTMIDbuffer(std::ofstream& outfile)
{
// Write HiSize and LoSize, (which are part of GtMiDiHdr), can't be written until size is calculated
uint8_t loSize = uint8_t(_outBuffer.size() & 0xFF);
uint8_t hiSize = uint8_t((_outBuffer.size() >>8) & 0xFF);
outfile.write((char *)&hiSize, 1);
outfile.write((char *)&loSize, 1);
// Write MIDI stream
outfile.write((char *)&_outBuffer[0], _outBuffer.size());
}
// vCPU output
void outputVCPUheader(std::ofstream& outfile, const std::string& name, uint16_t address, uint16_t segmentSize, uint16_t segmentIndex)
{
_segmentName = name;
if(segmentSize)
{
std::stringstream ss;
ss << _segmentName << std::setfill('0') << std::setw(2) << std::to_string(segmentIndex);
_segmentName = ss.str();
}
addString(_segmentName, 16 - _segmentName.size());
outfile << _segmentName.c_str() << "EQU 0x" << std::hex << std::setw(4) << std::setfill('0') << address << std::endl;
outfile << _segmentName.c_str() << "DB ";
}
void outputVCPUnewLine(std::ofstream& outfile, bool newLine)
{
if(!newLine)
{
_elementCount = 0;
return;
}
if(++_elementCount < MAX_ELEMENTS) return;
_elementCount = 0;
std::string str;
addString(str, _segmentName.size());
outfile << std::endl << str << "DB ";
}
void outputVCPUcommand(std::ofstream& outfile, uint8_t command, bool newLine=true)
{
outfile << " 0x" << std::hex << std::setw(2) << std::setfill('0') << uint16_t(command);
outputVCPUnewLine(outfile, newLine);
}
// GBAS output
void outputGBASheader(std::ofstream& outfile, uint16_t address)
{
outfile << "def byte" << "(&h" << std::hex << std::setw(4) << std::setfill('0') << address << ") = ";
}
void outputGBASnewLine(std::ofstream& outfile, bool newLine)
{
if(!newLine)
{
_elementCount = 0;
return;
}
if(++_elementCount < MAX_ELEMENTS) return;
_elementCount = 0;
std::string str;
outfile << std::endl << str << "def byte = ";
}
void outputGBAScommand(std::ofstream& outfile, uint8_t command, bool newLine=true)
{
outfile << " &h" << std::hex << std::setw(2) << std::setfill('0') << uint16_t(command) << ",";
outputGBASnewLine(outfile, newLine);
}
// GCL output
void outputGCLheader(std::ofstream& outfile, uint16_t address)
{
outfile << "$" << std::hex << std::setw(4) << std::setfill('0') << address << ":" << std::endl;
outfile << "[def" << std::endl << " ";
}
void outputGCLnewLine(std::ofstream& outfile, bool newLine)
{
if(!newLine)
{
_elementCount = 0;
return;
}
if(++_elementCount < MAX_ELEMENTS) return;
_elementCount = 0;
outfile << std::endl << " ";
}
void outputGCLcommand(std::ofstream& outfile, uint8_t command, bool newLine=true)
{
outfile << " $" << std::hex << std::setw(2) << std::setfill('0') << uint16_t(command) << "#";
outputGCLnewLine(outfile, newLine);
}
void outputGCLfooter(std::ofstream& outfile, const std::string& name)
{
UNREFERENCED_PARAM(name);
outfile << std::endl << "]" << std::endl;
}
// CPP output
void outputCPPheader(std::ofstream& outfile, const std::string& name, uint16_t segmentSize, uint16_t segmentIndex)
{
_segmentName = name;
if(segmentSize)
{
std::stringstream ss;
ss << _segmentName << std::setfill('0') << std::setw(2) << std::to_string(segmentIndex);
_segmentName = ss.str() + "[] = ";
}
outfile << "uint8_t " << _segmentName.c_str() << std::endl;
outfile << "{" << std::endl;
outfile << " ";
}
void outputCPPnewLine(std::ofstream& outfile, bool newLine)
{
if(!newLine)
{
_elementCount = 0;
return;
}
if(++_elementCount < MAX_ELEMENTS) return;
_elementCount = 0;
outfile << std::endl << " ";
}
void outputCPPcommand(std::ofstream& outfile, uint8_t command, bool newLine=true)
{
outfile << "0x" << std::hex << std::setw(2) << std::setfill('0') << uint16_t(command) << ",";
outputCPPnewLine(outfile, newLine);
}
void outputCPPfooter(std::ofstream& outfile)
{
outfile << std::endl << "};" << std::endl;
}
// PY output
void outputPYheader(std::ofstream& outfile, const std::string& name, uint16_t segmentSize, uint16_t segmentIndex)
{
_segmentName = name;
if(segmentSize)
{
std::stringstream ss;
ss << _segmentName << std::setfill('0') << std::setw(2) << std::to_string(segmentIndex);
_segmentName = ss.str() + " = bytearray([";
}
outfile << _segmentName.c_str() << std::endl;
outfile << " ";
}
void outputPYnewLine(std::ofstream& outfile, bool newLine)
{
if(!newLine)
{
_elementCount = 0;
return;
}
if(++_elementCount < MAX_ELEMENTS) return;
_elementCount = 0;
outfile << std::endl << " ";
}
void outputPYcommand(std::ofstream& outfile, uint8_t command, bool newLine=true)
{
outfile << "0x" << std::hex << std::setw(2) << std::setfill('0') << uint16_t(command) << ",";
outputPYnewLine(outfile, newLine);
}
void outputPYfooter(std::ofstream& outfile)
{
outfile << std::endl << "])" << std::endl;
}
void outputDelay(std::ofstream& outfile, Format format, uint8_t delay8, double timingAdjust, double totalTime16, double& totalTime8)
{
// Adjust delay8 to try and keep overall timing as accurate as possible
if(timingAdjust)
{
if(totalTime16 > totalTime8 + double(delay8)*16.6666666667 + 16.6666666667*timingAdjust && delay8 < 0x7F) delay8++;
if(totalTime16 < totalTime8 + double(delay8)*16.6666666667 - 16.6666666667*timingAdjust && delay8 > 0x01) delay8--;
}
totalTime8 += double(delay8) * 16.6666666667;
switch(format)
{
case Format::gtMID: outputGTMIDcommand(delay8); break;
case Format::vCPU: outputVCPUcommand(outfile, delay8); break;
case Format::GBAS: outputGBAScommand(outfile, delay8); break;
case Format::GCL: outputGCLcommand(outfile, delay8); break;
case Format::CPP: outputCPPcommand(outfile, delay8); break;
case Format::PY: outputPYcommand(outfile, delay8); break;
default: break;
}
}
int main(int argc, char* argv[])
{
if(argc < 9 || argc > 10)
{
fprintf(stderr, "Version: %s\n", GTMIDI_VERSION_STR);
fprintf(stderr, "Usage: gtmidi <input filename> <output filename> <midi name> <format name> <uint16_t start_address in hex>\n <uint16_t segment_offset in hex> <int segment_size> <float timing_adjust> <optional -v>\n\n");
fprintf(stderr, "Example1: gtmidi game_over.bin game_over.i gameOver vCPU 0x08A0 0x0100 96 0.5\n");
fprintf(stderr, "Example2: gtmidi game_over.bin game_over.i gameOver vCPU 0x8000 0 0 0.5 -v\n\n");
fprintf(stderr, "Input: miditones binary file produced with miditones, e.g. miditones -t4 -b -s1 -pi -v <filename>.bin\n");
fprintf(stderr, "Format: 'gtMID', 'vCPU', 'GBAS', 'GCL', 'CPP', 'Py'\n");
fprintf(stderr, "Optional: -v use volume/velocity values, (use -v with miditones)\n");
return 1;
}
std::string inFilename = std::string(argv[1]);
std::string outFilename = std::string(argv[2]);
std::string midiName = std::string(argv[3]);
std::string formatName = argv[4];
formatName = Expression::strToUpper(formatName);
if(_formatName.find(formatName) == _formatName.end())
{
fprintf(stderr, "Format must be one of : 'gtMID', 'vCPU', 'GBAS', 'GCL', 'CPP', 'Py'\n");
return 1;
}
Format format = (Format)_formatName[formatName];
// Handles hex numbers
uint16_t startAddress, segmentOffset;
std::stringstream ss0, ss1;
ss0 << std::hex << argv[5];
ss0 >> startAddress;
ss1 << std::hex << argv[6];
ss1 >> segmentOffset;
uint16_t segment = startAddress;
uint16_t segmentSize = uint16_t(strtol(argv[7], nullptr, 10));
double timingAdjust = strtod(argv[8], nullptr);
bool hasVolume = false;
if(argc == 10)
{
std::string volume = argv[9];
Expression::strToUpper(volume);
if(volume != "-V")
{
fprintf(stderr, "Optional volume specifier must be '-v'\n");
return 1;
}
hasVolume = true;
}
std::ifstream infile(inFilename, std::ios::binary | std::ios::in);
if(!infile.is_open())
{
fprintf(stderr, "Failed to open input file '%s'\n", inFilename.c_str());
return 1;
}
std::ofstream outfile(outFilename, std::ios::binary | std::ios::out);
if(!outfile.is_open())
{
fprintf(stderr, "Failed to open output file '%s'\n", outFilename.c_str());
return 1;
}
infile.read((char *)&_midiBuffer, MIDI_MAX_BUFFER_SIZE);
if(infile.bad())
{
fprintf(stderr, "Failed to read input file '%s'\n", inFilename.c_str());
return 1;
}
std::streamsize midiSize = infile.gcount();
uint8_t* midiPtr = _midiBuffer;
uint16_t gigaSize = 0;
double totalTime16 = 0;
double totalTime8 = 0;
uint16_t segmentIndex = 0;
// Header
switch(format)
{
case Format::gtMID: outputGTMIDheader(outfile, midiName, hasVolume); break;
case Format::vCPU: outputVCPUheader(outfile, midiName, startAddress, segmentSize, segmentIndex); break;
case Format::GBAS: outputGBASheader(outfile, startAddress); break;
case Format::GCL: outputGCLheader(outfile, startAddress); break;
case Format::CPP: outputCPPheader(outfile, midiName, segmentSize, segmentIndex); break;
case Format::PY: outputPYheader(outfile, midiName, segmentSize, segmentIndex); break;
default: break;
}
// Commands
int noteOnCount = 0;
uint16_t segmentCount = 0;
uint16_t consecutiveDelays = 0;
while(midiSize)
{
uint8_t command = *midiPtr++; midiSize--;
if(command & 0x80)
{
// Start note
if((command & 0xF0) == MIDI_CMD_START_NOTE)
{
noteOnCount++;
uint8_t note = *midiPtr++; midiSize--;
if(note >= MIDI_PERCUSSION_NOTES) note -= MIDI_PERCUSSION_NOTES;
if(note < MIDI_MIN_GIGA_NOTE) note = MIDI_MIN_GIGA_NOTE;
if(note > MIDI_MAX_GIGA_NOTE) note = MIDI_MAX_GIGA_NOTE;
gigaSize += 2;
segmentCount += 2;
// Volume, (0 -> 127), needs to be scaled, inverted and offset into (127 -> 64)
uint8_t volume = 0;
if(hasVolume)
{
volume = *midiPtr++; midiSize--;
volume = (63 - ((volume & 127) >>1)) + 64;
gigaSize += 1;
segmentCount += 1;
}
switch(format)
{
case Format::gtMID: outputGTMIDcommand(command); outputGTMIDcommand(note); if(hasVolume) outputGTMIDcommand(volume); break;
case Format::vCPU: outputVCPUcommand(outfile, command); outputVCPUcommand(outfile, note); if(hasVolume) outputVCPUcommand(outfile, volume); break;
case Format::GBAS: outputGBAScommand(outfile, command); outputGBAScommand(outfile, note); if(hasVolume) outputGBAScommand(outfile, volume); break;
case Format::GCL: outputGCLcommand(outfile, command); outputGCLcommand(outfile, note); if(hasVolume) outputGCLcommand(outfile, volume); break;
case Format::CPP: outputCPPcommand(outfile, command); outputCPPcommand(outfile, note); if(hasVolume) outputCPPcommand(outfile, volume); break;
case Format::PY: outputPYcommand(outfile, command); outputPYcommand(outfile, note); if(hasVolume) outputPYcommand(outfile, volume); break;
default: break;
}
}
// Stop note
else if((command & 0xF0) == MIDI_CMD_STOP_NOTE)
{
gigaSize += 1;
segmentCount += 1;
switch(format)
{
case Format::gtMID: outputGTMIDcommand(command); break;
case Format::vCPU: outputVCPUcommand(outfile, command); break;
case Format::GBAS: outputGBAScommand(outfile, command); break;
case Format::GCL: outputGCLcommand(outfile, command); break;
case Format::CPP: outputCPPcommand(outfile, command); break;
case Format::PY: outputPYcommand(outfile, command); break;
default: break;
}
}
// Stop midi events are ignored
else if((command & 0xF0) == 0xF0)
{
}
// Restart midi events are ignored
else if((command & 0xF0) == 0xE0)
{
}
}
// Delay n milliseconds where n = 16bit value, converted to multiple, (if necessary), 7bit values, (0x00 <-> 0x7F)
else
{
// Coalesce sequence of delays together
int index = 0;
uint16_t coalescedDelay = ((command<<8) | *midiPtr++); midiSize--;
while(midiSize)
{
if(midiPtr[index] & 0x80) break;
coalescedDelay += (midiPtr[index]<<8) + midiPtr[index + 1];
index += 2;
midiSize -= 2;
consecutiveDelays++;
}
midiPtr += index;
// Break up coalesced delay into bytes
if(coalescedDelay)
{
totalTime16 += double(coalescedDelay);
coalescedDelay = uint16_t(double(coalescedDelay)/16.6666666667 + 0.5);
// Ignore zero delays
if(coalescedDelay)
{
uint8_t div = uint8_t(coalescedDelay / 0x7F);
uint8_t rem = uint8_t(coalescedDelay % 0x7F);
gigaSize += div + 1;
segmentCount += div + 1;
for(uint8_t i=0; i<div; i++)
{
outputDelay(outfile, format, 0x7f, timingAdjust, totalTime16, totalTime8);
}
outputDelay(outfile, format, rem, timingAdjust, totalTime16, totalTime8);
}
}
}
// Segmented stream
if(midiSize && segmentSize && segmentOffset)
{
int size = 1;
uint8_t nextCommand = *midiPtr;
if((nextCommand & 0xF0) == 0x90) size = 3;
else if((nextCommand & 0xF0) == 0x80) size = 2;
// If segment size is approaching count, leave room for segment command + next command
if(segmentCount >= (segmentSize-(size+2)))
{
gigaSize += 3;
segmentCount = 0;
segmentIndex++;
segment += segmentOffset;
// Commands
switch(format)
{
case Format::vCPU: outputVCPUcommand(outfile, MIDI_CMD_JMP_SEG); outputVCPUcommand(outfile, LO_BYTE(segment)); outputVCPUcommand(outfile, HI_BYTE(segment), false); break;
case Format::GBAS: outputGBAScommand(outfile, MIDI_CMD_JMP_SEG); outputGBAScommand(outfile, LO_BYTE(segment)); outputGBAScommand(outfile, HI_BYTE(segment), false); break;
case Format::GCL: outputGCLcommand(outfile, MIDI_CMD_JMP_SEG); outputGCLcommand(outfile, LO_BYTE(segment)); outputGCLcommand(outfile, HI_BYTE(segment), false); break;
case Format::CPP: outputCPPcommand(outfile, MIDI_CMD_JMP_SEG); outputCPPcommand(outfile, LO_BYTE(segment)); outputCPPcommand(outfile, HI_BYTE(segment), false); break;
case Format::PY: outputPYcommand(outfile, MIDI_CMD_JMP_SEG); outputPYcommand(outfile, LO_BYTE(segment)); outputPYcommand(outfile, HI_BYTE(segment), false); break;
default: break;
}
// Old segment footer
switch(format)
{
case Format::GCL: outputGCLfooter(outfile, midiName); break;
case Format::CPP: outputCPPfooter(outfile); break;
case Format::PY: outputPYfooter(outfile); break;
default: break;
}
outfile << std::endl << std::endl;
// New segment header
switch(format)
{
case Format::vCPU: outputVCPUheader(outfile, midiName, segment, segmentSize, segmentIndex); break;
case Format::GBAS: outputGBASheader(outfile, segment); break;
case Format::GCL: outputGCLheader(outfile, segment); break;
case Format::CPP: outputCPPheader(outfile, midiName, segmentSize, segmentIndex); break;
case Format::PY: outputPYheader(outfile, midiName, segmentSize, segmentIndex); break;
default: break;
}
}
}
// Last segment points back to address, (can be user edited in output source file to point to a different MIDI stream)
if(midiSize == 0)
{
switch(format)
{
case Format::vCPU: outputVCPUcommand(outfile, MIDI_CMD_JMP_SEG); outputVCPUcommand(outfile, LO_BYTE(startAddress)); outputVCPUcommand(outfile, HI_BYTE(startAddress)); break;
case Format::GBAS: outputGBAScommand(outfile, MIDI_CMD_JMP_SEG); outputGBAScommand(outfile, LO_BYTE(startAddress)); outputGBAScommand(outfile, HI_BYTE(startAddress)); break;
case Format::GCL: outputGCLcommand(outfile, MIDI_CMD_JMP_SEG); outputGCLcommand(outfile, LO_BYTE(startAddress)); outputGCLcommand(outfile, HI_BYTE(startAddress)); break;
case Format::CPP: outputCPPcommand(outfile, MIDI_CMD_JMP_SEG); outputCPPcommand(outfile, LO_BYTE(startAddress)); outputCPPcommand(outfile, HI_BYTE(startAddress)); break;
case Format::PY: outputPYcommand(outfile, MIDI_CMD_JMP_SEG); outputPYcommand(outfile, LO_BYTE(startAddress)); outputPYcommand(outfile, HI_BYTE(startAddress)); break;
default: break;
}
}
}
// Footer
switch(format)
{
case Format::gtMID: outputGTMIDbuffer(outfile); break;
case Format::GCL: outputGCLfooter(outfile, midiName); break;
case Format::CPP: outputCPPfooter(outfile); break;
case Format::PY: outputPYfooter(outfile); break;
default: break;
}
fprintf(stderr, "Note On count:%d Consecutive Delays Eliminated:%d Original size:%d New size:%d Original time:%.1lfms New time:%.1lfms Error:%.1lfms Start Address:0x%04x End Address:0x%04x\n",
noteOnCount, consecutiveDelays, int(infile.gcount()), gigaSize, totalTime16, totalTime8, totalTime8 - totalTime16, startAddress, segment+segmentSize);
return 0;
}