moe-bius/vgmplay/lxmplay/main.cpp
2025-08-21 02:04:43 +07:00

557 lines
17 KiB
C++

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <conio.h>
#include <stdint.h>
#include <portaudio.h>
#include <math.h>
#include <vector>
#include <fstream>
#include <queue>
#include "wavehead.h"
#include "opmplay.h"
#include "vgm.h"
#include "rss.h" // YM2608 internal ADPCM-A ROM dump
#include "ymfm/src/ymfm_opn.h"
#define WIN32_LEAN_AND_MEAN
#define WIN32_EXTRA_LEAN
#define NOMINMAX
#include <Windows.h>
enum {
CHANNELS = 2,
FRAMES_PER_BUFFER = 512,
MAX_FRAMES_PER_BUFFER = 4096,
};
const double OPN_CLOCK_RATE = 3500000;
PaStreamParameters outputParameters;
PaStream* stream;
// console stuff
struct {
HANDLE hStdout, hScreenBuffer;
COORD bufcoord, bufsize;
SMALL_RECT bufDestRect;
CHAR_INFO* buffer; // the main buffer to write to
} console;
struct opm_context_t {
// full context
opmplay_context_t opm;
opmplay_io_t io;
// delay count relative to sample rate
int32_t delay_count;
};
struct vgm_context_t {
std::vector<uint8_t> vgmfile;
std::vector<uint8_t>::iterator vgmfile_it;
VGMHeader* header;
uint32_t loop_pos;
uint32_t start, end; // offsets
// delay count
int32_t delay_count;
// rescaler for 44100hz delays
double rescaler;
};
vgm_context_t vgmctx;
opm_context_t opmctx;
class opna_interface_t : public ymfm::ymfm_interface {
public:
opna_interface_t() {};
uint8_t ymfm_external_read(ymfm::access_class type, uint32_t address) {
switch (type) {
case ymfm::ACCESS_ADPCM_A: return YM2608_ADPCM_ROM[address & 0x1FFF];
default: return 0;
}
}
};
// register write queue
class opnx_register_queue_t {
private:
uint64_t write_delay;
uint64_t clock;
struct reg_entry_t {
uint64_t clock;
int chip, reg, data;
};
std::queue<reg_entry_t> reg_queue;
ymfm::ym2203* chip;
public:
opnx_register_queue_t() : opnx_register_queue_t(nullptr) {}
opnx_register_queue_t(ymfm::ym2203* _chip) : clock(0), write_delay(0), chip(_chip) {}
void set_chip(ymfm::ym2203* _chip) { chip = _chip; }
void reset() {
clock = 0;
write_delay = 0;
while (!reg_queue.empty()) reg_queue.pop(); // flush queue
}
void add(int chip, int reg, int data, uint64_t delay) {
reg_entry_t r;
r.clock = clock + write_delay;
r.chip = chip;
r.reg = reg;
r.data = data;
reg_queue.push(r);
write_delay += delay;
}
void pop_clock() {
if (!reg_queue.empty() && (reg_queue.front().clock <= clock)) {
auto &r = reg_queue.front();
if (chip) {
chip->write_address(r.reg);
chip->write_data(r.data);
}
reg_queue.pop();
}
clock++; write_delay = 0;
}
};
opna_interface_t opna_interface[2];
ymfm::ym2203 *opnachip[2];
opnx_register_queue_t opna_regqueue[2];
ymfm::ym2203::output_data opna_out[MAX_FRAMES_PER_BUFFER][2];
// ------------------
// draw plain string
void drawstring(const char* str, unsigned long x, unsigned long y, unsigned char attr) {
CHAR_INFO* p = (CHAR_INFO*)console.buffer + (console.bufsize.X * y) + x;
while (*str != '\0') {
p->Char.AsciiChar = *str++;
p->Attributes = attr;
p++;
}
}
// draw string with attributes
// '\0' - end, '\xFF\xaa' - set attribute byte 'aa'
void drawastring(const char* str, unsigned long x, unsigned long y) {
CHAR_INFO* p = (CHAR_INFO*)console.buffer + (console.bufsize.X * y) + x;
unsigned short attr = 0x07;
while (*str != '\0') if (*str == '\xFF') {
attr = (*++str); str++;
}
else {
p->Char.AsciiChar = *str++;
p->Attributes = attr;
p++;
}
}
// printf
int tprintf(uint32_t x, uint32_t y, const char* format, ...) {
char buffer[1024]; // large enough
va_list arglist;
va_start(arglist, format);
int rtn = vsnprintf(buffer, sizeof(buffer), format, arglist);
drawastring(buffer, x, y);
va_end(arglist);
return rtn;
};
// -------------------
int console_open() {
// Get a handle to the STDOUT screen buffer to copy from and
// create a new screen buffer to copy to.
console.hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
console.hScreenBuffer = CreateConsoleScreenBuffer(
GENERIC_READ | // read/write access
GENERIC_WRITE,
FILE_SHARE_READ |
FILE_SHARE_WRITE, // shared
NULL, // default security attributes
CONSOLE_TEXTMODE_BUFFER, // must be TEXTMODE
NULL); // reserved; must be NULL
if (console.hStdout == INVALID_HANDLE_VALUE ||
console.hScreenBuffer == INVALID_HANDLE_VALUE)
{
printf("CreateConsoleScreenBuffer failed - (%d)\n", GetLastError());
return 1;
}
// resize
console.bufsize.X = 80;
console.bufsize.Y = 40;
SetConsoleScreenBufferSize(console.hScreenBuffer, console.bufsize);
// allocate console buffer
console.buffer = new CHAR_INFO[console.bufsize.X * console.bufsize.Y];
memset(console.buffer, 0, sizeof(CHAR_INFO) * console.bufsize.X * console.bufsize.Y);
// Make the new screen buffer the active screen buffer.
if (!SetConsoleActiveScreenBuffer(console.hScreenBuffer))
{
printf("SetConsoleActiveScreenBuffer failed - (%d)\n", GetLastError());
return 1;
}
return 0;
}
void console_update() {
console.bufDestRect.Top = 0;
console.bufDestRect.Left = 0;
console.bufDestRect.Bottom = console.bufsize.Y - 1;
console.bufDestRect.Right = console.bufsize.X - 1;
console.bufcoord.X = console.bufcoord.Y = 0;
WriteConsoleOutput(
console.hScreenBuffer, // screen buffer to write to
console.buffer, // buffer to copy from
console.bufsize, // col-row size of chiBuffer
console.bufcoord, // top left src cell in chiBuffer
&console.bufDestRect); // dest. screen buffer rectangle
}
void console_done() {
SetConsoleActiveScreenBuffer(console.hStdout);
}
// ----------------------------
int vgm_parse_frame() {
auto it = vgmctx.vgmfile_it;
while (it < vgmctx.vgmfile.begin() + vgmctx.end) {
// flush delays
vgmctx.delay_count = 0;
// first, do OPL cases
switch ((VGM_Stream_Opcode)*it) {
case VGM_Stream_Opcode::YM2203_WRITE: {
// get register and data
int reg = *(it + 1), data = *(it + 2);
opna_regqueue[0].add(0, reg, data, 16);
//opnachip->write(2, reg);
//opnachip->write(3, data);
break;
}
case VGM_Stream_Opcode::YM2203_CHIP2_WRITE: {
// get register and data
int reg = *(it + 1), data = *(it + 2);
opna_regqueue[1].add(0, reg, data, 16);
//opnachip->write(0, reg);
//opnachip->write(1, data);
break;
}
default: break;
}
// then parse everything else
if (((*it >= 0x30) && (*it <= 0x3F)) || (*it == 0x4F) || (*it == 0x50)) it += 2; else
if (((*it >= 0x40) && (*it <= 0x4E)) || ((*it >= 0x51) && (*it <= 0x5F)) ||
((*it >= 0xA0) && (*it <= 0xAF)) || ((*it >= 0xB0) && (*it <= 0xBF))) it += 3; else
if (((*it >= 0xC0) && (*it <= 0xCF)) || ((*it >= 0xD0) && (*it <= 0xDF))) it += 4; else
if (((*it >= 0x70) && (*it <= 0x7F))) {
// accumulate short delays
vgmctx.delay_count += (*it) - 0x70; it++;
} else
switch ((VGM_Stream_Opcode)*it) {
case VGM_Stream_Opcode::DELAY_LONG:
vgmctx.delay_count = (uint32_t)((*(it + 1)) | (*(it + 2) << 8));
it += 3;
break;
case VGM_Stream_Opcode::DELAY_60HZ:
vgmctx.delay_count = 735U;
it += 1;
break;
case VGM_Stream_Opcode::DELAY_50HZ:
vgmctx.delay_count = 882U;
it += 1;
break;
case VGM_Stream_Opcode::END_OF_DATA:
it = vgmctx.vgmfile.begin() + vgmctx.end; // hack but should work
break;
case VGM_Stream_Opcode::DATA_BLOCK:
// skip data block
{
uint32_t datasize = (*(it + 3)) | (*(it + 4) << 8) | (*(it + 5) << 16) | (*(it + 6) << 24);
it += datasize + 7;
}
break;
default:
printf("unknown opcode %02X at offset %X!\n", *it, it - vgmctx.vgmfile.begin());
return 1;
break;
}
if (vgmctx.delay_count > 0) {
vgmctx.delay_count *= vgmctx.rescaler;
break;
}
}
// save current pointer
vgmctx.vgmfile_it = it;
return 0;
}
// -------------------
// synth render
int synth_render(int16_t* buffer, uint32_t num_samples) {
int samples_to_render = num_samples;
memset(buffer, 0, sizeof(int16_t) * 2 * num_samples);
while (samples_to_render > 0) {
if (samples_to_render < vgmctx.delay_count) {
for (int i = 0; i < samples_to_render; i++) {
for (int chip = 0; chip < 2; chip++) {
opna_regqueue[chip].pop_clock();
opnachip[chip]->generate(opna_out[chip] + i, 1);
opna_out[chip][i].clamp16();
buffer[2*i+0] += 0.5*(0.5 * opna_out[chip][i].data[0] + 0.5 * (1.0*opna_out[chip][i].data[1] + 0.5*opna_out[chip][i].data[2] + 0.0*opna_out[chip][i].data[3])); // mix FM and SSG
buffer[2*i+1] += 0.5*(0.5 * opna_out[chip][i].data[0] + 0.5 * (0.0*opna_out[chip][i].data[1] + 0.5*opna_out[chip][i].data[2] + 1.0*opna_out[chip][i].data[3]));
}
}
vgmctx.delay_count -= samples_to_render;
buffer += 2 * samples_to_render;
break;
}
else {
// calculate new delay
for (int i = 0; i < vgmctx.delay_count; i++) {
for (int chip = 0; chip < 2; chip++) {
opna_regqueue[chip].pop_clock();
opnachip[chip]->generate(opna_out[chip] + i, 1);
opna_out[chip][i].clamp16();
buffer[2*i+0] += 0.5*(0.5 * opna_out[chip][i].data[0] + 0.5 * (1.0*opna_out[chip][i].data[1] + 0.5*opna_out[chip][i].data[2] + 0.0*opna_out[chip][i].data[3])); // mix FM and SSG
buffer[2*i+1] += 0.5*(0.5 * opna_out[chip][i].data[0] + 0.5 * (0.0*opna_out[chip][i].data[1] + 0.5*opna_out[chip][i].data[2] + 1.0*opna_out[chip][i].data[3]));
}
}
samples_to_render -= vgmctx.delay_count;
buffer += 2 * vgmctx.delay_count;
// parse VGM stream
vgm_parse_frame();
}
}
return 0;
}
int pa_init(double sample_rate) {
PaError err;
// init portaudio
err = Pa_Initialize();
if (err != paNoError) return 1;
outputParameters.device = Pa_GetDefaultOutputDevice(); /* default output device */
if (outputParameters.device == paNoDevice) {
fprintf(stderr, "Error: No default output device.\n");
return 1;
}
outputParameters.channelCount = CHANNELS;
outputParameters.sampleFormat = paInt16;
outputParameters.suggestedLatency = 0.04;
outputParameters.hostApiSpecificStreamInfo = NULL;
err = Pa_OpenStream(
&stream,
NULL, /* no input */
&outputParameters,
sample_rate,
FRAMES_PER_BUFFER,
0, /* we won't output out of range samples so don't bother clipping them */
NULL, /* no callback, use blocking API */
NULL); /* no callback, so no callback userData */
if (err != paNoError) return 1;
err = Pa_StartStream(stream);
if (err != paNoError) return 1;
return 0;
}
int pa_write(void* data, int32_t count) {
PaError err;
err = Pa_WriteStream(stream, data, count);
return 0;
}
int pa_done() {
PaError err;
err = Pa_StopStream(stream);
if (err != paNoError) return 1;
// deinit portaudio
err = Pa_CloseStream(stream);
if (err != paNoError) return 1;
Pa_Terminate();
return 0;
}
int main(int argc, char* argv[])
{
bool render_to_wave = (argc >= 3);
uint32_t sample_rate;
for (int chip = 0; chip < 2; chip++) {
opnachip[chip] = new ymfm::ym2203(opna_interface[chip]);
if (opnachip[chip] == nullptr) {
printf("error: unable to init ymfm!\n");
return 1;
}
opnachip[chip]->reset();
opnachip[chip]->set_fidelity(ymfm::OPN_FIDELITY_MIN);
sample_rate = opnachip[chip]->sample_rate(OPN_CLOCK_RATE);
vgmctx.rescaler = ((double)sample_rate / 44100.0);
printf("sample rate - %d hz\n", sample_rate);
opna_regqueue[chip].reset();
opna_regqueue[chip].set_chip(opnachip[chip]);
}
if (!render_to_wave) {
if (pa_init(sample_rate) != 0) {
printf("error: unable to init PortAudio!\n");
return 1;
}
}
FILE* f = fopen(argv[1], "rb");
if (f == NULL) {
printf("error: unable to open file!\n");
return 1;
}
#if 0
opmctx.io.type = OPMPLAY_IO_FILE;
opmctx.io.io = f;
int rtn;
if ((rtn = opmplay_init(&opmctx.opm, &opl3)) != OPMPLAY_ERR_OK) {
printf("unable to init OPMPlay (error = %d)\n", rtn);
return 1;
}
if ((rtn = opmplay_load_header(&opmctx.opm, &opmctx.io)) != OPMPLAY_ERR_OK) {
printf("unable to load OPM header (error = %d)\n", rtn);
return 1;
};
if ((rtn = opmplay_load_module(&opmctx.opm, &opmctx.io)) != OPMPLAY_ERR_OK) {
printf("unable to load OPM module (error = %d)\n", rtn);
return 1;
};
opmctx.delay_count = 0;
#else
// open VGM file, ready to parse
std::ifstream infile(argv[1], std::ios::in | std::ios::binary);
infile.unsetf(std::ios::skipws);
// get filesize
infile.seekg(0, std::ios::end);
uint64_t fsize = infile.tellg();
infile.seekg(0, std::ios::beg);
// read whole file
vgmctx.vgmfile.reserve(fsize);
vgmctx.vgmfile.insert(vgmctx.vgmfile.begin(), std::istream_iterator<uint8_t>(infile), std::istream_iterator<uint8_t>());
// get header
vgmctx.header = reinterpret_cast<VGMHeader*>(vgmctx.vgmfile.data());
// check header
if (memcmp(vgmctx.header->id, "Vgm\x20", sizeof(vgmctx.header->id)) != 0) {
printf("not a vaild VGM file!\n");
return 1;
}
// parse basic VGM structure
printf("VGM %d.%d file found\n", (vgmctx.header->version >> 8) & 0xFF, vgmctx.header->version & 0xFF);
if (vgmctx.header->loopOffset != 0) vgmctx.loop_pos = vgmctx.header->loopOffset + offsetof(VGMHeader, loopOffset);
vgmctx.end = vgmctx.header->eofOffset + offsetof(VGMHeader, eofOffset);
vgmctx.start = ((vgmctx.header->version < 0x150) ? 0x40 : vgmctx.header->dataOffset + offsetof(VGMHeader, dataOffset));
vgmctx.vgmfile_it = vgmctx.vgmfile.begin() + vgmctx.start;
vgmctx.delay_count = 0;
#endif
console_open();
std::vector<int16_t> wavedata;
int ff_pos = 0, ff_counter = 0;
int16_t buf[FRAMES_PER_BUFFER * CHANNELS] = { 0 };
while (1) {
int rtn = synth_render(buf, FRAMES_PER_BUFFER);
if (render_to_wave) {
wavedata.insert(wavedata.end(), buf, buf + FRAMES_PER_BUFFER * CHANNELS);
}
else {
pa_write(buf, FRAMES_PER_BUFFER);
}
ff_pos += FRAMES_PER_BUFFER;
// update console
memset(console.buffer, 0, sizeof(CHAR_INFO) * console.bufsize.X * console.bufsize.Y);
tprintf(0, 0, "frame = %d", ff_pos);
console_update();
if (_kbhit()) {
_getch();
break;
}
}
// write wave file
if (render_to_wave) {
// create headers
RIFF_Header riffHeader;
memcpy(&riffHeader.id, "RIFF", sizeof(riffHeader.id));
memcpy(&riffHeader.fourcc, "WAVE", sizeof(riffHeader.fourcc));
riffHeader.size = sizeof(riffHeader.fourcc) + sizeof(fmt_Header) + sizeof(chunk_Header) + (wavedata.size() * sizeof(decltype(wavedata)::value_type));
fmt_Header fmtHeader;
memcpy(&fmtHeader.id, "fmt ", sizeof(fmtHeader.id));
fmtHeader.size = sizeof(fmtHeader) - 8;
fmtHeader.wFormatTag = 1; // plain uncompressed PCM
fmtHeader.nSamplesPerSec = sample_rate;
fmtHeader.nBlockAlign = CHANNELS;
fmtHeader.nAvgBytesPerSec = sample_rate * CHANNELS;
fmtHeader.nChannels = CHANNELS;
fmtHeader.wBitsPerSample = 8;
chunk_Header dataHeader;
memcpy(&dataHeader.id, "data", sizeof(dataHeader.id));
dataHeader.size = (wavedata.size() * sizeof(decltype(wavedata)::value_type));
// write wave file
FILE* outfile = fopen("out.wav", "wb");
fwrite(&riffHeader, sizeof(riffHeader), 1, outfile);
fwrite(&fmtHeader, sizeof(fmtHeader), 1, outfile);
fwrite(&dataHeader, sizeof(dataHeader), 1, outfile);
fwrite(wavedata.data(), (wavedata.size() * sizeof(decltype(wavedata)::value_type)), 1, outfile);
fclose(outfile);
} else
pa_done();
console_done();
return 0;
}