OPL3 rendering path: authentic Sound Blaster FM, no soundfont
- tools/opl/render_opl.cpp: libADLMIDI harness - renders MIDI through OPL3 emulation with game-driver volume models (DMX/Doom, Apogee/Duke, AIL, HMI), normalised output, WAV header patched to actual length - tools/opl/build.sh: one-shot fetch+build of libADLMIDI - tools/render_opl.sh: batch render + check_wav for all tunes - README: OPL3 section
This commit is contained in:
1 parent
9b1e61b2ce
commit
72a337f50f
5 files changed
+152
No files matched your search
@@ -104,6 +104,21 @@ on a peak of 32767.
|
|||||||
- A bar-length linter in the score parser catches more composition bugs
|
- A bar-length linter in the score parser catches more composition bugs
|
||||||
than any amount of staring at note lists.
|
than any amount of staring at note lists.
|
||||||
|
|
||||||
|
## OPL3 rendering (the authentic one)
|
||||||
|
|
||||||
|
For real Sound Blaster flavour — FM synthesis, no soundfont, like the
|
||||||
|
actual YMF262 chip:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/opl/build.sh # one-time: fetches + builds libADLMIDI
|
||||||
|
./tools/render_opl.sh # renders all midi/*.mid through OPL3
|
||||||
|
OPL_MODEL=4 ./tools/render_opl.sh midi/03_late_equaliser.mid
|
||||||
|
```
|
||||||
|
|
||||||
|
Volume models mimic real game sound drivers: `2` NativeOPL3 (id-style),
|
||||||
|
`3` DMX (Doom, default), `4` APOGEE (Duke Nukem 3D), `8` AIL, `10` HMI.
|
||||||
|
Output is normalised (OPL runs quiet). Needs `cmake` + `g++` to build.
|
||||||
|
|
||||||
## Licence & soundfonts
|
## Licence & soundfonts
|
||||||
|
|
||||||
MIT — see [LICENSE](LICENSE). Rendering needs a General MIDI soundfont,
|
MIT — see [LICENSE](LICENSE). Rendering needs a General MIDI soundfont,
|
||||||
|
|||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# build.sh — fetch and build libADLMIDI, then compile render_opl.
|
||||||
|
# Idempotent: skips anything already present. Run once per machine.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
LIBDIR="${OPL_LIBDIR:-/opt/libADLMIDI}"
|
||||||
|
|
||||||
|
if [ ! -f "$LIBDIR/build/libADLMIDI.a" ]; then
|
||||||
|
echo "cloning libADLMIDI..."
|
||||||
|
git clone --depth 1 -q https://github.com/Wohlstand/libADLMIDI.git "$LIBDIR"
|
||||||
|
echo "building (a few minutes)..."
|
||||||
|
cmake -B "$LIBDIR/build" -S "$LIBDIR" -DCMAKE_BUILD_TYPE=Release >/dev/null
|
||||||
|
cmake --build "$LIBDIR/build" -j"$(nproc)" --target ADLMIDI_static >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
g++ -O2 -I "$LIBDIR/include" render_opl.cpp "$LIBDIR/build/libADLMIDI.a" \
|
||||||
|
-o render_opl -lpthread
|
||||||
|
echo "built: $(pwd)/render_opl"
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,92 @@
|
|||||||
|
// 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 <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<short> buf(2048 * 2);
|
||||||
|
std::vector<short> 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;
|
||||||
|
}
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# render_opl.sh — render tunes through OPL3 FM (authentic Sound Blaster
|
||||||
|
# flavour, no soundfont). Usage:
|
||||||
|
# ./tools/render_opl.sh [tune.mid ...] (default: all midi/*.mid)
|
||||||
|
# OPL_MODEL=4 ./tools/render_opl.sh ... (4 = Apogee/Duke Nukem volumes)
|
||||||
|
#
|
||||||
|
# Volume models: 2 NativeOPL3 (id-style) 3 DMX (Doom, default)
|
||||||
|
# 4 APOGEE (Duke Nukem 3D) 8 AIL 10 HMI
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
[ -x tools/opl/render_opl ] || ./tools/opl/build.sh
|
||||||
|
mkdir -p build/opl
|
||||||
|
MODEL="${OPL_MODEL:-3}"
|
||||||
|
|
||||||
|
files=("$@")
|
||||||
|
if [ ${#files[@]} -eq 0 ]; then files=(midi/*.mid); fi
|
||||||
|
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
dur=$(python3 -c "
|
||||||
|
import sys; sys.path.insert(0,'lib')
|
||||||
|
from midikit import read
|
||||||
|
print(read('$f')['duration_sec'] + 3)")
|
||||||
|
out="build/opl/$(basename "${f%.mid}").wav"
|
||||||
|
echo "== $f -> $out (model $MODEL)"
|
||||||
|
./tools/opl/render_opl "$f" "$out" "$dur" "$MODEL"
|
||||||
|
python3 tools/check_wav.py "$out" "$dur"
|
||||||
|
done
|
||||||
Reference in new issue
Block a user