// render_opl.cpp — render a MIDI file to WAV through OPL3 FM emulation // (libADLMIDI), i.e. the authentic Sound Blaster / AdLib sound. No // soundfont involved: pure FM synthesis, like the real 1993 silicon. // // Build: see build.sh in this directory. // Usage: render_opl in.mid out.wav seconds [volume_model] // // Volume models (the fun bit — they mimic real game sound drivers): // 0 auto 1 generic 2 NativeOPL3 (CMF/id-style) 3 DMX (Doom) // 4 APOGEE (Duke Nukem 3D) 5 9X 8 AIL (Another World era) // 10 HMI #include "adlmidi.h" #include #include #include #include static void wr32(FILE *f, unsigned v) { unsigned char b[4] = {ADL_UInt8(v), ADL_UInt8(v >> 8), ADL_UInt8(v >> 16), ADL_UInt8(v >> 24)}; fwrite(b, 1, 4, f); } static void wr16(FILE *f, unsigned v) { unsigned char b[2] = {ADL_UInt8(v), ADL_UInt8(v >> 8)}; fwrite(b, 1, 2, f); } int main(int argc, char **argv) { if (argc < 4) { fprintf(stderr, "usage: render_opl in.mid out.wav seconds [volume_model]\n"); return 2; } const long rate = 44100; double secs = atof(argv[3]); int model = (argc > 4) ? atoi(argv[4]) : 3; // default: DMX (Doom-ish) ADL_MIDIPlayer *adl = adl_init(rate); if (!adl) { fprintf(stderr, "adl_init failed\n"); return 1; } adl_setVolumeRangeModel(adl, model); adl_setNumChips(adl, 2); // two OPL3s: our arrangements are dense if (adl_openFile(adl, argv[1]) != 0) { fprintf(stderr, "cannot open %s: %s\n", argv[1], adl_errorString()); adl_close(adl); return 1; } FILE *f = fopen(argv[2], "wb"); if (!f) { fprintf(stderr, "cannot write %s\n", argv[2]); adl_close(adl); return 1; } // WAV header (sizes patched to actual bytes written at the end) const long data_off = 40; fwrite("RIFF", 1, 4, f); wr32(f, 0); fwrite("WAVE", 1, 4, f); fwrite("fmt ", 1, 4, f); wr32(f, 16); wr16(f, 1); wr16(f, 2); wr32(f, unsigned(rate)); wr32(f, unsigned(rate * 4)); wr16(f, 4); wr16(f, 16); fwrite("data", 1, 4, f); wr32(f, 0); std::vector buf(2048 * 2); std::vector all; int frames_done = 0, frames_total = int(secs * rate); while (frames_done < frames_total) { int n = frames_total - frames_done; if (n > 2048) n = 2048; if (adl_play(adl, n * 2, buf.data()) != 0) { all.insert(all.end(), buf.begin(), buf.begin() + n * 2); frames_done += n; } else { break; // end of sequence (or error; errorString empty at EOF) } } // Normalise: OPL FM runs quiet; bring the peak up to ~-17dBFS, capped // so we never amplify a near-silent track into hiss. int peak = 0; for (short s : all) peak = std::max(peak, std::abs(int(s))); double gain = (peak > 0 && peak < 24000) ? std::min(24000.0 / peak, 12.0) : 1.0; if (gain != 1.0) { for (short &s : all) { int v = int(s * gain); s = short(v > 32767 ? 32767 : (v < -32767 ? -32767 : v)); } } fprintf(stderr, "peak %d, gain x%.1f\n", peak, gain); long bytes_written = long(all.size()) * 2; fwrite(all.data(), 2, all.size(), f); fflush(f); fseek(f, 4, SEEK_SET); wr32(f, unsigned(36 + bytes_written)); fseek(f, data_off, SEEK_SET); wr32(f, unsigned(bytes_written)); fclose(f); adl_close(adl); fprintf(stderr, "rendered %d frames of OPL3\n", frames_done); return 0; }