Add FM synthesizer support

This adds support for "real" FM sound that can be used instead of MIDI
sound. (Currently only available on platforms where SDL_mixer is used.)

The MakoFM class interprets AMUS.DAT to create a write sequence to the
OPNA registers, from which fmgen generates an audio stream.
This commit is contained in:
kichikuou
2020-11-21 13:40:21 +09:00
parent f4f8b7d104
commit 893f716ef3
11 changed files with 1049 additions and 2 deletions
+2
View File
@@ -11,6 +11,8 @@ jobs:
name: Linux ${{ matrix.build-type }}
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Install Deps
run: |
+3
View File
@@ -0,0 +1,3 @@
[submodule "deps/fmgen"]
path = deps/fmgen
url = https://github.com/kichikuou/fmgen.git
+4
View File
@@ -6,6 +6,8 @@ This is a SDL2 port of [System3 for Win32](http://takeda-toshiya.my.coocan.jp/al
### Linux (Debian, Ubuntu)
$ git submodule init
$ git submodule update
$ sudo apt install g++ cmake libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev
$ mkdir -p out/debug
$ cd out/debug
@@ -15,6 +17,8 @@ This is a SDL2 port of [System3 for Win32](http://takeda-toshiya.my.coocan.jp/al
### MacOS
$ git submodule init
$ git submodule update
$ brew install cmake pkg-config sdl2 sdl2_ttf sdl2_mixer
$ mkdir -p out/debug
$ cd out/debug
Vendored Submodule
+1
Submodule deps/fmgen added at d74357ec4b
+7 -2
View File
@@ -56,9 +56,14 @@ else() # NOT (ANDROID OR EMSCRIPTEN)
target_link_libraries(system3 PRIVATE winmm)
set(CMAKE_EXE_LINKER_FLAGS "-static-libgcc -static-libstdc++ -static")
else()
add_subdirectory(../deps/fmgen fmgen)
pkg_check_modules(SDL2MIXER REQUIRED IMPORTED_TARGET SDL2_mixer)
target_sources(system3 PRIVATE linux/nact_linux.cpp linux/mako.cpp)
target_link_libraries(system3 PRIVATE PkgConfig::SDL2MIXER)
target_sources(system3 PRIVATE
linux/nact_linux.cpp
linux/mako.cpp
fm/makofm.cpp
fm/mako_fmgen.cpp)
target_link_libraries(system3 PRIVATE PkgConfig::SDL2MIXER fmgen)
set(RESOURCE_PATH ${CMAKE_INSTALL_PREFIX}/share/system3/)
install(TARGETS system3 RUNTIME DESTINATION bin)
install(DIRECTORY ../resources/
+62
View File
@@ -0,0 +1,62 @@
#include "mako_fmgen.h"
#include <algorithm>
#include <assert.h>
#include <string.h>
const int SAMPLE_RATE = 44100;
const int OPNA_CLOCK = 3993600 * 2;
MakoFMgen::MakoFMgen(const uint8_t* data) :
MakoFM(data),
sync_ms(0),
samples_left(0) {
bool ok = opna.Init(OPNA_CLOCK, SAMPLE_RATE, false);
assert(ok);
opna.Reset();
}
void MakoFMgen::Process(int16* stream, int len) {
output = stream;
out_samples = len;
if (samples_left > 0) {
int samples = samples_left;
samples_left = 0;
Mix(samples);
}
while (out_samples > 0) {
MainLoop();
Sync();
}
}
void MakoFMgen::SetReg(RegType type, uint8_t addr, uint8_t val) {
opna.SetReg(addr + (type == OPNA_SLAVE ? 0x100 : 0), val);
}
void MakoFMgen::Sync() {
int t = static_cast<int>(GetTime());
if (t == sync_ms)
return;
int ms = t - sync_ms;
sync_ms = t;
Mix(SAMPLE_RATE * ms / 1000);
}
void MakoFMgen::Mix(int samples) {
assert(samples_left == 0);
if (samples > out_samples) {
samples_left = samples - out_samples;
samples = out_samples;
}
FM_SAMPLETYPE* buf = new FM_SAMPLETYPE[samples * 2];
memset(buf, 0, samples * sizeof(FM_SAMPLETYPE) * 2);
opna.Mix(buf, samples);
for (int i = 0; i < samples * 2; i += 2) {
output[i] = std::max(-0x7fff, std::min(0x7fff, buf[i]));
output[i+1] = std::max(-0x7fff, std::min(0x7fff, buf[i+1]));
}
out_samples -= samples;
output += samples * 2;
delete[] buf;
}
+24
View File
@@ -0,0 +1,24 @@
#ifndef MAKOFMGEN_H_
#define MAKOFMGEN_H_
#include "fmgen/opna.h"
#include "makofm.h"
class MakoFMgen : private MakoFM {
public:
MakoFMgen(const uint8_t* data);
void Process(int16* stream, int len);
private:
void SetReg(RegType type, uint8_t addr, uint8_t val) override;
void Sync();
void Mix(int samples);
FM::OPNA opna;
int sync_ms;
int16* output;
int out_samples;
int samples_left;
};
#endif // MAKOFMGEN_H_
+783
View File
@@ -0,0 +1,783 @@
#include "makofm.h"
#include <algorithm>
#include <assert.h>
#include <string.h>
#include "common.h"
namespace {
const int PART_DISABLED = 2;
enum Flags {
ALT_Q_FORMAT = 1 << 0,
SKIP_KEYOFF = 1 << 1,
LFO_FLAG = 1 << 2,
SW_FRQLFO_ENABLED = 1 << 4,
SW_VOLLFO_ENABLED = 1 << 5,
USE_HW_LFO = 1 << 6,
FRQLFO_DIRECTION = 1 << 7,
VOLLFO_DIRECTION = 1 << 8,
HW_LFO_WRITE_REQUEST = 1 << 9,
};
const uint16_t FMFreqTable[12] = {
0x0269, 0x028E, 0x02B4, 0x02DE, 0x0309, 0x0338,
0x0369, 0x039C, 0x03D3, 0x040E, 0x044B, 0x048D,
};
const uint16_t PSGFreqTable[12] = {
0x0EED, 0x0E17, 0x0D4C, 0x0C8D, 0x0BD9, 0x0B2F,
0x0A8E, 0x09F6, 0x0967, 0x08E0, 0x0860, 0x07E8,
};
const int TONE_DATA_SIZE = 43;
const uint8_t PresetFMTone[TONE_DATA_SIZE] = {
// alg, keyon, reserved*5
0x03, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00,
// DT/ML, TL, KS/AR, DR, SR, SL/RR, reserved*3
0x02, 0x0F, 0x1F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00,
0x06, 0x28, 0x1F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00,
0x04, 0x28, 0x1F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00,
0x02, 0x00, 0x1F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00,
};
} // namespace
MakoFM::MakoFM(const uint8_t* data) :
data(data),
tone_offset(GetWord(0)),
ver(GetWord(2)) {
memset(work, 0, sizeof(work));
for (int ch = 0; ch < MAKO_MAXCH; ch++) {
work[ch].Q = 7;
work[ch].default_length = 64;
work[ch].octave = 4;
work[ch].octave_temp = 4;
work[ch].vol = 120;
work[ch].panpot = 0x40;
}
}
void MakoFM::MainLoop() {
if (time_ms == 0.0) {
InitOPNA();
for (int ch = MAKO_MAXCH - 1; ch >= 0; ch--) {
InitChannel(ch);
ExecPan(ch);
SetFMTone(ch, PresetFMTone);
if (work[ch].part_enable == PART_DISABLED)
continue;
ExecPan(ch);
ExecSequence(ch, true);
}
} else {
for (int ch = MAKO_MAXCH - 1; ch >= 0; ch--) {
ExecSequence(ch, false);
ExecFLFO(ch);
ExecVLFO(ch);
}
}
time_ms += 545455.0 / (480.0 * tempo);
}
void MakoFM::InitOPNA() {
WritePSG(0x00, 0x57);
WritePSG(0x00, 0x41);
WritePSG(0x00, 0x4f);
WriteFM(0, 0x27, 0x30);
WritePSG(0x07, 0xBf);
WriteFM(0, 0x90, 0x00);
WriteFM(0, 0x91, 0x00);
WriteFM(0, 0x92, 0x00);
WriteFM(0, 0x29, 0x83);
}
void MakoFM::InitChannel(int ch) {
work[ch].part_offset = GetWord(4 + ch * 4);
if (!work[ch].part_offset) {
work[ch].part_enable = PART_DISABLED;
part_looped |= 1 << ch;
return;
}
uint16_t block0_offset = GetWord(work[ch].part_offset);
if (block0_offset >= 0xff00) {
work[ch].part_enable = PART_DISABLED;
part_looped |= 1 << ch;
return;
}
work[ch].jump_ptr = work[ch].part_offset + 2;
work[ch].seq_ptr = block0_offset;
}
void MakoFM::ExecSequence(int ch, bool execcmd) {
if (execcmd)
goto execcmd;
if (work[ch].part_enable != 1)
return;
if (work[ch].length_counter > 1) {
work[ch].length_counter--;
return;
}
if (!work[ch].rest)
goto note1;
execcmd:
cmdFF = 0;
work[ch].seq_ptr = ExecCmd(ch, data + work[ch].seq_ptr) - data;
if (cmdFF) {
uint16_t block_addr = GetWord(work[ch].jump_ptr);
work[ch].jump_ptr += 2;
if (block_addr >> 8 == 0xff) {
part_looped |= 1 << ch;
if (work[ch].part_enable == PART_DISABLED)
return;
if (work[ch].part_enable == 0) {
work[ch].part_enable = 0xff;
cmdFF = 0;
return;
}
work[ch].jump_ptr = work[ch].part_offset + (block_addr & 0xff) * 2;
block_addr = GetWord(work[ch].jump_ptr);
work[ch].jump_ptr += 2;
}
work[ch].seq_ptr = block_addr;
cmdFF = 0;
goto execcmd;
}
if (work[ch].rest) {
if (work[ch].off_length) {
work[ch].length_counter = work[ch].off_length;
return;
}
KeyOff(ch);
goto execcmd;
}
// .note3
if (work[ch].on_length) {
work[ch].length_counter = work[ch].on_length;
} else {
note1:
work[ch].rest = 1;
if (work[ch].off_length == 0) {
KeyOff(ch);
goto execcmd;
}
// .note2
work[ch].length_counter = work[ch].off_length;
}
// .note4
if (work[ch].rest) {
KeyOff(ch);
return;
}
if (IsFMchannel(ch)) {
uint8_t reg = 0xa4 + ch % 6;
uint16_t freq = work[ch].blk_fnum + work[ch].detune;
WriteFM(ch, reg, freq >> 8);
WriteFM(ch, reg - 4, freq & 0xff);
if (work[ch].flags & LFO_FLAG) {
work[ch].flags &= ~LFO_FLAG;
return;
}
SetFLFO(ch, false);
SetVLFO(ch, false);
if (work[ch].flags & USE_HW_LFO) {
work[ch].flags |= HW_LFO_WRITE_REQUEST;
ExecPan(ch);
}
WriteFM(ch, 0x28, work[ch].keyon + (ch < 6 ? ch : ch - 2));
} else {
uint16_t freq = work[ch].blk_fnum - work[ch].detune / 4;
WritePSG((ch - 3) * 2 + 1, freq >> 8);
WritePSG((ch - 3) * 2, freq & 0xff);
if (work[ch].flags & LFO_FLAG) {
work[ch].flags &= ~LFO_FLAG;
return;
}
SetFLFO(ch, false);
SetVLFO(ch, false);
mixer &= (ch - 2 == 3 ? 4 : ch - 2) ^ 0xff;
mixer |= 0xb8;
WritePSG(7, mixer);
}
}
const uint8_t* MakoFM::ExecCmd(int ch, const uint8_t* code) {
bool cont = true;
while (cont) {
if (*code < 0xe0)
return ExecNote(ch, code);
cont = false;
uint8_t a = *code++;
switch (a) {
case 0xec: // Part end
work[ch].part_enable = PART_DISABLED;
part_looped |= 1 << ch;
break;
case 0xff: // Jump
cmdFF = 0xff;
break;
case 0xeb: // Pan
work[ch].panpot = *code++;
ExecPan(ch);
cont = true;
break;
case 0xee: // Octave++
work[ch].octave++;
cont = true;
break;
case 0xef: // Octave--
work[ch].octave--;
cont = true;
break;
case 0xf0: // Octave
work[ch].octave = *code++;
cont = true;
break;
case 0xf1: // Temporary octave
work[ch].octave_temp = *code++;
cont = true;
break;
case 0xf2: // Q (1-8)
work[ch].flags &= ~ALT_Q_FORMAT;
work[ch].Q = *code++ + 1;
cont = true;
break;
case 0xea: // Q (alt)
work[ch].flags |= ALT_Q_FORMAT;
work[ch].Q = *code++;
cont = true;
break;
case 0xe9: // Tie
work[ch].flags |= SKIP_KEYOFF;
cont = true;
break;
case 0xe7: // Frq LFO
work[ch].flfo_params.delay = code[0] | code[1] << 8;
work[ch].flfo_params.counter = code[2] | code[3] << 8;
work[ch].flfo_params.value = code[4] | code[5] << 8;
work[ch].flfo_params.max = code[6] | code[7] << 8;
code += 8;
work[ch].flags |= SW_FRQLFO_ENABLED;
work[ch].flags &= ~FRQLFO_DIRECTION;
SetFLFO(ch, false);
cont = true;
break;
case 0xe6: // Vol LFO (PSG)
work[ch].vlfo_params.delay = code[0] | code[1] << 8;
work[ch].vlfo_params.counter = code[2] | code[3] << 8;
work[ch].vlfo_params.value = code[4] | code[5] << 8;
work[ch].vlfo_params.max = code[6] | code[7] << 8;
code += 8;
work[ch].fm_delay = work[ch].vlfo_params.delay;
work[ch].sw_lfo_val = 0;
work[ch].flags |= SW_VOLLFO_ENABLED;
work[ch].flags &= ~VOLLFO_DIRECTION;
SetVLFO(ch, false);
cont = true;
break;
case 0xf3: // Default length
{
uint8_t a = *code++;
if (a < 0x80)
work[ch].default_length = a;
else
work[ch].default_length = (a - 0x80) << 8 | *code++;
}
cont = true;
break;
case 0xe5: // LFO flags
work[ch].flags &= ~0x70;
work[ch].flags |= (*code++ & 7) << 4;
cont = true;
break;
case 0xe8: // Vol LFO (FM)
work[ch].vlfo_params.delay = code[0] | code[1] << 8;
work[ch].vlfo_params.counter = code[2] | code[3] << 8;
work[ch].vlfo_params.value = code[4] | code[5] << 8;
work[ch].vlfo_params.max = code[6] | code[7] << 8;
work[ch].fm_delay = code[8] | code[9] << 8;
code += 10;
work[ch].flags |= SW_VOLLFO_ENABLED;
cont = true;
break;
case 0xf4: // Tempo
tempo = *code++;
cont = true;
break;
case 0xf5: // FM tone set
work[ch].tone_num = *code++;
SetFMTone(ch, data + tone_offset + TONE_DATA_SIZE * work[ch].tone_num);
ExecHWLFO(ch);
cont = true;
break;
case 0xf7: // Temp octave--
work[ch].octave_temp = work[ch].octave - 1;
work[ch].use_temp_octave = 1;
cont = true;
break;
case 0xf8: // Temp octave++
work[ch].octave_temp = work[ch].octave + 1;
work[ch].use_temp_octave = 1;
cont = true;
break;
case 0xf9: // Absolute vol
work[ch].vol = work[ch].tl4_vol = std::min(*code++, static_cast<uint8_t>(127));
SetVol(ch);
cont = true;
break;
case 0xe1: // Relative vol
work[ch].vol = work[ch].tl4_vol =
std::min(static_cast<uint8_t>(work[ch].vol + *code++),
static_cast<uint8_t>(127));
SetVol(ch);
cont = true;
break;
case 0xe4: // HW_LFO
hwlfo = *code++ | 8;
work[ch].pms_param = *code++;
work[ch].ams_param = *code++;
work[ch].flags |= USE_HW_LFO;
ExecHWLFO(ch);
cont = true;
break;
case 0xfc: // Detune
work[ch].detune = *(const int8_t*)code++;
cont = true;
break;
case 0xfe:
code += 3;
cont = true;
break;
case 0xf6:
code += 2;
cont = true;
break;
case 0xfa:
case 0xfd:
code++;
cont = true;
break;
case 0xe0:
current_mark = *code++;
cont = true;
break;
default:
WARNING("MakoFM: unknown command 0x%02x", a);
work[ch].part_enable = PART_DISABLED;
part_looped |= 1 << ch;
break;
}
}
return code;
}
const uint8_t* MakoFM::ExecNote(int ch, const uint8_t* code) {
uint8_t note = *code++;
if (note < 13) {
work[ch].note = note;
work[ch].length = *code++;
} else if (note < 26) {
work[ch].note = note - 13;
work[ch].length = code[0] | code[1] << 8;
code += 2;
} else if (0x80 <= note && note < 0x80 + 13) {
work[ch].note = note - 0x80;
work[ch].length = work[ch].default_length;
} else {
WARNING("MakoFM: unknown command: 0x%02x", note);
work[ch].part_enable = PART_DISABLED;
part_looped |= 1 << ch;
return code;
}
if (!work[ch].use_temp_octave)
work[ch].octave_temp = work[ch].octave;
work[ch].use_temp_octave = 0;
uint16_t freq = 0;
if (work[ch].note) {
if (IsFMchannel(ch))
freq = FMFreqTable[work[ch].note - 1] | work[ch].octave_temp << 11;
else
freq = PSGFreqTable[work[ch].note - 1] >> work[ch].octave_temp;
}
work[ch].blk_fnum = freq;
if (work[ch].part_enable != PART_DISABLED)
work[ch].part_enable = 1;
if (!work[ch].note) {
work[ch].on_length = 0;
work[ch].off_length = work[ch].length;
work[ch].rest = 1;
return code;
}
uint16_t on_length, off_length;
if (work[ch].length <= 1) {
on_length = work[ch].length;
off_length = 0;
} else {
if (work[ch].flags & ALT_Q_FORMAT) {
if (work[ch].length >= work[ch].Q) {
on_length = work[ch].length - work[ch].Q;
off_length = work[ch].Q;
} else {
on_length = 1;
off_length = work[ch].length - 1;
}
} else {
if (work[ch].Q == 8) {
on_length = work[ch].length - 1;
off_length = 1;
} else {
assert(1 <= work[ch].Q && work[ch].Q <= 7);
on_length = work[ch].length / 8 * work[ch].Q;
if (!on_length)
on_length = 1;
off_length = work[ch].length - on_length;
if (off_length == 0) {
on_length--;
off_length++;
}
}
}
}
work[ch].rest = 0;
work[ch].on_length = on_length;
work[ch].off_length = off_length;
return code;
}
void MakoFM::KeyOff(int ch) {
if (work[ch].flags & SKIP_KEYOFF) {
work[ch].flags &= ~SKIP_KEYOFF;
work[ch].flags |= LFO_FLAG;
return;
}
if (IsFMchannel(ch)) {
if (work[ch].flags & USE_HW_LFO) {
work[ch].flags &= ~HW_LFO_WRITE_REQUEST;
ExecPan(ch);
}
WriteFM(ch, 0x28, ch < 3 ? ch : ch - 2);
} else {
if (work[ch].flags & SW_VOLLFO_ENABLED &&
work[ch].vlfo_mode & 0x0f &&
(work[ch].vlfo_mode & 8) == 0) {
work[ch].vlfo_val = work[ch].vlfo_params.max;
work[ch].vlfo_mode = 8; // SW-VLFO mode 3
return;
}
mixer |= (ch == 5 ? 4 : ch - 2) | 0xB8;
WritePSG(7, mixer);
}
}
void MakoFM::SetVol(int ch) {
work[ch].vol = work[ch].tl4_vol;
assert(work[ch].vol <= 127);
if (IsFMchannel(ch)) {
uint8_t reg = 0x40 + ch % 6;
uint8_t val = 127 - work[ch].vol;
switch (work[ch].alg) {
case 7:
WriteFM(ch, reg + 0x00, val);
// fall through
case 5: case 6:
WriteFM(ch, reg + 0x04, val);
// fall through
case 4:
WriteFM(ch, reg + 0x08, val);
// fall through
case 0: case 1: case 2: case 3:
WriteFM(ch, reg + 0x0c, val);
}
} else {
if (work[ch].flags & SW_VOLLFO_ENABLED)
return;
uint8_t val = work[ch].vol >> 3;
WritePSG(ch + 5, val);
}
}
void MakoFM::ExecPan(int ch) {
if (!IsFMchannel(ch))
return;
uint8_t reg = 0xb4 + ch % 6;
uint8_t panpot = work[ch].panpot;
uint8_t val = (panpot == 0 || panpot == 0x40) ? 0xc0 // LR
: (panpot < 0x40) ? 0x80 // L
: 0x40; // R
if ((work[ch].flags & USE_HW_LFO) && (work[ch].flags & HW_LFO_WRITE_REQUEST))
val |= work[ch].ams_param << 4 | work[ch].pms_param;
WriteFM(ch, reg, val);
}
void MakoFM::SetFMTone(int ch, const uint8_t* tone) {
if (!IsFMchannel(ch))
return;
uint8_t alg = *tone++;
work[ch].alg = alg & 7;
WriteFM(ch, 0xb0 + ch % 6, alg);
work[ch].keyon = *tone * 16;
tone += 6;
work[ch].tl1 = tone[9 * 0 + 1];
work[ch].tl2 = tone[9 * 1 + 1];
work[ch].tl3 = tone[9 * 2 + 1];
work[ch].tl4_vol = tone[9 * 3 + 1];
const uint8_t regtop[] = {0x30, 0x38, 0x34, 0x3c};
for (int op = 0; op < 4; op++) {
uint8_t reg = regtop[op] + ch % 6;
for (int i = 0; i < 6; i++) { // 6 parameters (DT/ML,TL,KS/AR,DR,SR,SL/RR)
uint8_t val = *tone++;
// Since ver.0x300, write TL only to the modulator
if (ver >= 0x300 && i == 1) {
switch (work[ch].alg) {
case 0: case 1: case 2: case 3:
if (op < 3)
WriteFM(ch, reg, val);
break;
case 4:
if (op == 0 || op == 2)
WriteFM(ch, reg, val);
break;
case 5: case 6:
if (op == 0)
WriteFM(ch, reg, val);
break;
case 7: default:
break;
}
} else {
WriteFM(ch, reg, val);
}
reg += 0x10;
}
tone += 3;
}
}
void MakoFM::ExecFLFO(int ch) {
if ((work[ch].flags & SW_FRQLFO_ENABLED) == 0)
return;
if ((work[ch].flags & (SKIP_KEYOFF | LFO_FLAG)) == 0) {
if (work[ch].part_enable == PART_DISABLED)
return;
if (work[ch].rest || work[ch].note == 0)
return;
}
if (work[ch].flq_lfo_counter-- > 0)
return;
work[ch].flq_lfo_counter = work[ch].flfo_params.counter;
work[ch].flq_lfo_result += work[ch].flfo_params.value;
if (work[ch].flfo_params.value & 0x8000) {
// .minus
if ((int16_t)work[ch].flfo_params.max >= (int16_t)work[ch].flq_lfo_result) {
work[ch].flq_lfo_result = work[ch].flfo_params.max;
SetFLFO(ch, true);
work[ch].flags &= ~FRQLFO_DIRECTION;
}
} else {
if ((int16_t)work[ch].flq_lfo_result >= (int16_t)work[ch].flfo_params.max) {
work[ch].flq_lfo_result = work[ch].flfo_params.max;
SetFLFO(ch, true);
work[ch].flags |= FRQLFO_DIRECTION;
}
}
// .output
uint16_t result = work[ch].flq_lfo_result + work[ch].detune;
if (IsFMchannel(ch)) {
result += work[ch].blk_fnum;
uint8_t reg = 0xa4 + ch % 6;
WriteFM(ch, reg, result >> 8);
WriteFM(ch, reg - 4, result & 0xff);
} else {
uint8_t reg = (ch - 3) * 2 + 1;
result = -result;
int16_t s_result = result;
s_result >>= 2;
s_result += work[ch].blk_fnum;
result = s_result;
WritePSG(reg, (result >> 8) & 0xf);
WritePSG(reg - 1, result & 0xff);
}
}
void MakoFM::ExecVLFO(int ch) {
if ((work[ch].flags & SW_VOLLFO_ENABLED) == 0)
return;
if (work[ch].part_enable == PART_DISABLED)
return;
work[ch].vol = work[ch].tl4_vol;
if (!IsFMchannel(ch)) {
ExecVLFOp(ch);
return;
}
if (work[ch].fm_delay) {
work[ch].fm_delay--; // counter--
return;
}
work[ch].fm_delay = work[ch].vlfo_params.counter;
work[ch].sw_lfo_val = work[ch].vlfo_params.value; // bug (should be +=)?
if (work[ch].vlfo_params.value & 0x8000) {
if (work[ch].vlfo_params.max >= work[ch].sw_lfo_val) {
work[ch].sw_lfo_val = work[ch].vlfo_params.max;
SetVLFO(ch, true); // negate add_value, max
work[ch].flags |= VOLLFO_DIRECTION;
}
} else {
if (work[ch].vlfo_params.max >= work[ch].sw_lfo_val) {
work[ch].sw_lfo_val = work[ch].vlfo_params.max;
SetVLFO(ch, true); // negate add_value, max
work[ch].flags |= VOLLFO_DIRECTION;
}
}
// .skip1
uint8_t val = 127 - work[ch].vol - (work[ch].sw_lfo_val & 0xff) & 0x7f;
uint8_t reg = 0x40 + ch % 6;
switch (work[ch].alg) {
case 7:
WriteFM(ch, reg, val);
// fall through
case 5: case 6:
WriteFM(ch, reg + 4, val);
// fall through
case 4:
WriteFM(ch, reg + 8, val);
// fall through
case 0: case 1: case 2: case 3:
WriteFM(ch, reg + 12, val);
}
}
void MakoFM::ExecVLFOp(int ch) {
if (work[ch].vlfo_mode & 1) {
// .mode0
if (work[ch].sw_lfo_val < 0x3fff) {
work[ch].sw_lfo_val += work[ch].vlfo_val;
} else {
work[ch].sw_lfo_val = 0x4000;
work[ch].vlfo_counter = work[ch].vlfo_params.counter; // counter = counter source
work[ch].vlfo_val = work[ch].fm_delay;
work[ch].vlfo_mode = 2; // to mode1
}
} else if (work[ch].vlfo_mode & 2) {
// .mode1
if (work[ch].sw_lfo_val < work[ch].vlfo_val) {
work[ch].vlfo_mode = 0;
work[ch].sw_lfo_val = 0;
KeyOff(ch);
return;
}
work[ch].sw_lfo_val -= work[ch].vlfo_val;
if (work[ch].vlfo_counter--) {
work[ch].vlfo_val = work[ch].vlfo_params.value; // value2
work[ch].vlfo_mode = 4; // to mode2
}
} else if (work[ch].vlfo_mode & 4) {
// .mode2
if (work[ch].sw_lfo_val < work[ch].vlfo_val) {
work[ch].vlfo_mode = 0;
work[ch].sw_lfo_val = 0;
KeyOff(ch);
return;
}
work[ch].sw_lfo_val -= work[ch].vlfo_val;
} else if (work[ch].vlfo_mode & 8) {
// .mode3, same as mode2
if (work[ch].sw_lfo_val < work[ch].vlfo_val) {
work[ch].vlfo_mode = 0;
work[ch].sw_lfo_val = 0;
KeyOff(ch);
return;
}
work[ch].sw_lfo_val -= work[ch].vlfo_val;
} else {
return;
}
// .output
uint8_t val = ((work[ch].sw_lfo_val >> 6) * work[ch].vol) >> 8;
WritePSG(ch + 5, (val & 0x78) >> 3);
}
void MakoFM::SetFLFO(int ch, bool neg) {
if (!neg) {
if ((work[ch].flags & SW_FRQLFO_ENABLED) == 0)
return;
work[ch].flq_lfo_counter = work[ch].flfo_params.delay;
work[ch].flq_lfo_result = 0;
if ((work[ch].flags & FRQLFO_DIRECTION) == 0)
return;
work[ch].flags &= ~FRQLFO_DIRECTION;
}
work[ch].flfo_params.value = -work[ch].flfo_params.value;
work[ch].flfo_params.max = -work[ch].flfo_params.max;
}
void MakoFM::SetVLFO(int ch, bool neg) {
if (neg)
goto neg;
if ((work[ch].flags & SW_VOLLFO_ENABLED) == 0)
return;
if (IsFMchannel(ch)) {
work[ch].fm_delay = work[ch].vlfo_params.delay;
work[ch].sw_lfo_val = 0;
if ((work[ch].flags & VOLLFO_DIRECTION) == 0)
return;
work[ch].flags &= ~VOLLFO_DIRECTION;
neg:
work[ch].vlfo_params.value = -work[ch].vlfo_params.value;
work[ch].vlfo_params.max = -work[ch].vlfo_params.max;
} else {
work[ch].vlfo_mode = 1;
work[ch].vlfo_val = work[ch].vlfo_params.delay;
work[ch].sw_lfo_val = work[ch].vlfo_params.delay;
}
}
void MakoFM::ExecHWLFO(int ch) {
if ((work[ch].flags & USE_HW_LFO) == 0)
return;
assert(!"not implemented");
}
+133
View File
@@ -0,0 +1,133 @@
#ifndef MAKOFM_H_
#define MAKOFM_H_
#include <stdint.h>
const int MAKO_MAXCH = 9;
// MakoFM emulates the AliceSoft Music Driver (MAKO.COM) for PC-98
// version 2.67 [1].
// The implementation is based on mako60.z80 by Bookworm [2].
// [1] http://hp.vector.co.jp/authors/VA004111/filelist.htm
// [2] http://mydocuments.g2.xrea.com/html/p6/makop6.html
class MakoFM {
public:
MakoFM(const uint8_t* data);
void MainLoop();
float GetTime() const { return time_ms; }
bool Looped() const { return part_looped == (1 << MAKO_MAXCH) - 1; }
protected:
enum RegType {
OPNA_MASTER,
OPNA_SLAVE,
};
virtual void SetReg(RegType type, uint8_t addr, uint8_t val) = 0;
private:
uint16_t GetWord(int offset) const {
return data[offset] | data[offset + 1] << 8;
}
bool IsFMchannel(int ch) const {
return ch < 3 || 6 <= ch;
}
void WriteFM(int ch, uint8_t addr, uint8_t val) {
// Use OPNA_MASTER for control registers / PSG
RegType type = (addr < 0x30 || ch < 6) ? OPNA_MASTER : OPNA_SLAVE;
SetReg(type, addr, val);
}
void WritePSG(uint8_t addr, uint8_t val) {
SetReg(OPNA_MASTER, addr, val);
}
void InitOPNA();
void InitChannel(int ch);
void ExecSequence(int ch, bool execcmd);
const uint8_t* ExecCmd(int ch, const uint8_t* code);
const uint8_t* ExecNote(int ch, const uint8_t* code);
void KeyOff(int ch);
void SetVol(int ch);
void ExecPan(int ch);
void SetFMTone(int ch, const uint8_t* tone);
void ExecFLFO(int ch);
void ExecVLFO(int ch);
void ExecVLFOp(int ch);
void SetFLFO(int ch, bool neg);
void SetVLFO(int ch, bool neg);
void ExecHWLFO(int ch);
struct Work {
uint16_t part_offset; // W20A3
uint16_t jump_ptr; // W20A5
uint16_t seq_ptr; // W20A7
uint16_t Q; // W20A9
uint16_t flags; // W20AB
uint8_t tone_num; // W20AD
uint8_t vol; // W20AE
uint8_t tl1; // W20AF
uint8_t tl2; // W20B0
uint8_t tl3; // W20B1
uint8_t tl4_vol; // W20B2
uint8_t use_temp_octave; // W20B3
uint8_t octave; // W20B4
uint8_t octave_temp; // W20B5
uint8_t note; // W20B6 0=rest, 1-12=scale (cdefgab)
uint16_t blk_fnum; // W20B7
uint16_t default_length; // W20B9
uint16_t length; // W20BB
uint16_t on_length; // W20BD
uint16_t off_length; // W20BF
uint16_t length_counter; // W20C1
uint8_t rest; // W20C3 1=rest, 0=note
uint8_t keyon; // W20C4 key-on status
int16_t detune; // W20C5
uint8_t alg; // W20C7
uint8_t panpot; // W20C8
uint8_t part_enable; // W20C9 02=disable 01,FF=enable
struct {
uint16_t delay; // W20DB
uint16_t counter; // W20DD
uint16_t value; // W20DF
uint16_t max; // W20E1
} flfo_params;
uint16_t flq_lfo_counter; // W20E3
uint16_t flq_lfo_result; // W20E5
struct {
uint16_t delay; // W20E7
uint16_t counter; // W20E9
uint16_t value; // W20EB
uint16_t max; // W20ED
} vlfo_params;
uint16_t fm_delay; // W20EF
uint16_t sw_lfo_val; // W20F1
uint16_t vlfo_val; // W20F3
uint16_t vlfo_counter; // W20F5
uint16_t vlfo_mode; // W20F7
uint8_t ams_param; // W20F9
uint8_t pms_param; // W20FA
};
Work work[9];
const uint8_t* data;
uint16_t tone_offset;
uint16_t ver;
uint8_t mixer = 0xbf;
uint8_t hwlfo = 0;
uint8_t tempo = 120;
uint8_t cmdFF;
unsigned part_looped = 0;
float time_ms = 0;
uint8_t current_mark;
};
#endif // MAKOFM_H_
+28
View File
@@ -4,11 +4,13 @@
[ MAKO ]
*/
#include <memory>
#include <SDL.h>
#include <SDL_mixer.h>
#include "mako.h"
#include "mako_midi.h"
#include "fm/mako_fmgen.h"
#include "dri.h"
// MIX_INIT_FLUIDSYNTH was renamed to MIX_INIT_MID in SDL_mixer 2.0.2
@@ -21,10 +23,20 @@ namespace {
std::vector<uint8> smf;
Mix_Music *mix_music;
Mix_Chunk *mix_chunk;
SDL_mutex* fm_mutex;
std::unique_ptr<MakoFMgen> fm;
uint8* fmdata;
void FMHook(void *udata, Uint8 *stream, int len) {
SDL_LockMutex(fm_mutex);
fm->Process(reinterpret_cast<int16*>(stream), len / 4);
SDL_UnlockMutex(fm_mutex);
}
} // namespace
MAKO::MAKO(NACT* parent, const char* playlist) :
use_fm(true),
current_music(0),
next_loop(0),
nact(parent)
@@ -92,6 +104,21 @@ void MAKO::play_music(int page)
return;
}
}
} else if (use_fm) {
DRI dri;
int size;
uint8* data = dri.load(amus, page, &size);
if (!data)
return;
if (!fm_mutex)
fm_mutex = SDL_CreateMutex();
SDL_LockMutex(fm_mutex);
free(fmdata);
fmdata = data;
fm = std::make_unique<MakoFMgen>(fmdata);
Mix_HookMusic(&FMHook, this);
SDL_UnlockMutex(fm_mutex);
} else {
MAKOMidi midi(nact, amus);
if (midi.load_mml(page)) {
@@ -120,6 +147,7 @@ void MAKO::stop_music()
mix_music = NULL;
smf.clear();
}
Mix_HookMusic(NULL, NULL);
current_music = 0;
}
+2
View File
@@ -42,6 +42,8 @@ public:
void on_mci_notify(SDL_SysWMmsg* msg);
#endif
bool use_fm;
// AMUS.DAT, AMSE.DAT
char amus[16];
char amse[16];