Implement QSound support

This commit is contained in:
Ian Karlsson 2022-02-22 10:01:57 +01:00
parent 721445cf07
commit b3908216f8
17 changed files with 1746 additions and 15 deletions

View file

@ -272,6 +272,8 @@ src/engine/platform/sound/ymfm/ymfm_opn.cpp
src/engine/platform/sound/ymfm/ymfm_opz.cpp src/engine/platform/sound/ymfm/ymfm_opz.cpp
src/engine/platform/sound/ymfm/ymfm_ssg.cpp src/engine/platform/sound/ymfm/ymfm_ssg.cpp
src/engine/platform/sound/qsound.c
src/engine/platform/ym2610Interface.cpp src/engine/platform/ym2610Interface.cpp
src/engine/blip_buf.c src/engine/blip_buf.c
@ -306,6 +308,7 @@ src/engine/platform/ay8930.cpp
src/engine/platform/tia.cpp src/engine/platform/tia.cpp
src/engine/platform/saa.cpp src/engine/platform/saa.cpp
src/engine/platform/amiga.cpp src/engine/platform/amiga.cpp
src/engine/platform/qsound.cpp
src/engine/platform/dummy.cpp src/engine/platform/dummy.cpp
) )

View file

@ -97,6 +97,9 @@ enum DivDispatchCmds {
DIV_CMD_SAA_ENVELOPE, DIV_CMD_SAA_ENVELOPE,
DIV_CMD_QSOUND_ECHO_FEEDBACK,
DIV_CMD_QSOUND_ECHO_LEVEL,
DIV_ALWAYS_SET_VOLUME, DIV_ALWAYS_SET_VOLUME,
DIV_CMD_MAX DIV_CMD_MAX
@ -217,6 +220,13 @@ class DivDispatch {
*/ */
virtual int getRegisterPoolSize(); virtual int getRegisterPoolSize();
/**
* get the bit depth of the register pool of this dispatch.
* If the result is 16, it should be casted to unsigned short
* @return the depth. Default value is 8
*/
virtual int getRegisterPoolDepth();
/** /**
* get this dispatch's state. DO NOT IMPLEMENT YET. * get this dispatch's state. DO NOT IMPLEMENT YET.
* @return a pointer to the dispatch's state. must be deallocated manually! * @return a pointer to the dispatch's state. must be deallocated manually!

View file

@ -34,6 +34,7 @@
#include "platform/tia.h" #include "platform/tia.h"
#include "platform/saa.h" #include "platform/saa.h"
#include "platform/amiga.h" #include "platform/amiga.h"
#include "platform/qsound.h"
#include "platform/dummy.h" #include "platform/dummy.h"
#include "../ta-log.h" #include "../ta-log.h"
#include "song.h" #include "song.h"
@ -199,6 +200,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do
((DivPlatformSAA1099*)dispatch)->setCore((DivSAACores)saaCore); ((DivPlatformSAA1099*)dispatch)->setCore((DivSAACores)saaCore);
break; break;
} }
case DIV_SYSTEM_QSOUND:
dispatch=new DivPlatformQSound;
break;
default: default:
logW("this system is not supported yet! using dummy platform.\n"); logW("this system is not supported yet! using dummy platform.\n");
dispatch=new DivPlatformDummy; dispatch=new DivPlatformDummy;

View file

@ -597,6 +597,41 @@ void DivEngine::renderSamples() {
memPos+=s->adpcmRendLength; memPos+=s->adpcmRendLength;
} }
adpcmMemLen=memPos+256; adpcmMemLen=memPos+256;
// step 4: allocate qsound pcm samples
if (qsoundMem==NULL) qsoundMem=new unsigned char[16777216];
memset(qsoundMem, 0, 16777216);
memPos=0;
for (int i=0; i<song.sampleLen; i++) {
DivSample* s=song.sample[i];
int length = s->rendLength;
if(length > 65536-16)
length = 65536-16;
if ((memPos&0xff0000)!=((memPos+length)&0xff0000)) {
memPos=(memPos+0xffff)&0xff0000;
}
if (memPos>=16777216) {
logW("out of QSound PCM memory for sample %d!\n",i);
break;
}
if (memPos+length>=16777216) {
for(unsigned int i=0; i<16777216-(memPos+length); i++)
{
qsoundMem[(memPos + i) ^ 0x8000] = s->rendData[i] >> ((s->depth == 16) ? 8 : 0);
}
logW("out of QSound PCM memory for sample %d!\n",i);
} else {
for(int i=0; i<length; i++)
{
qsoundMem[(memPos + i) ^ 0x8000] = s->rendData[i] >> ((s->depth == 16) ? 8 : 0);
}
}
s->rendOffQsound=memPos ^ 0x8000;
memPos+=length+16;
}
qsoundMemLen=memPos+256;
} }
void DivEngine::createNew() { void DivEngine::createNew() {
@ -729,10 +764,11 @@ void* DivEngine::getDispatchChanState(int ch) {
return disCont[dispatchOfChan[ch]].dispatch->getChanState(dispatchChanOfChan[ch]); return disCont[dispatchOfChan[ch]].dispatch->getChanState(dispatchChanOfChan[ch]);
} }
unsigned char* DivEngine::getRegisterPool(int sys, int& size) { unsigned char* DivEngine::getRegisterPool(int sys, int& size, int& depth) {
if (sys<0 || sys>=song.systemLen) return NULL; if (sys<0 || sys>=song.systemLen) return NULL;
if (disCont[sys].dispatch==NULL) return NULL; if (disCont[sys].dispatch==NULL) return NULL;
size=disCont[sys].dispatch->getRegisterPoolSize(); size=disCont[sys].dispatch->getRegisterPoolSize();
depth=disCont[sys].dispatch->getRegisterPoolDepth();
return disCont[sys].dispatch->getRegisterPool(); return disCont[sys].dispatch->getRegisterPool();
} }
@ -1887,7 +1923,7 @@ bool DivEngine::addSampleFromFile(const char* path) {
delete[] buf; delete[] buf;
sample->rate=si.samplerate; sample->rate=si.samplerate;
if (sample->rate<4000) sample->rate=4000; if (sample->rate<4000) sample->rate=4000;
if (sample->rate>32000) sample->rate=32000; if (sample->rate>96000) sample->rate=96000;
song.sample.push_back(sample); song.sample.push_back(sample);
song.sampleLen=sampleCount+1; song.sampleLen=sampleCount+1;

View file

@ -531,7 +531,7 @@ class DivEngine {
void* getDispatchChanState(int chan); void* getDispatchChanState(int chan);
// get register pool // get register pool
unsigned char* getRegisterPool(int sys, int& size); unsigned char* getRegisterPool(int sys, int& size, int& depth);
// enable command stream dumping // enable command stream dumping
void enableCommandStream(bool enable); void enableCommandStream(bool enable);
@ -624,6 +624,8 @@ class DivEngine {
size_t adpcmMemLen; size_t adpcmMemLen;
unsigned char* adpcmBMem; unsigned char* adpcmBMem;
size_t adpcmBMemLen; size_t adpcmBMemLen;
unsigned char* qsoundMem;
size_t qsoundMemLen;
DivEngine(): DivEngine():
output(NULL), output(NULL),
@ -681,6 +683,8 @@ class DivEngine {
adpcmMem(NULL), adpcmMem(NULL),
adpcmMemLen(0), adpcmMemLen(0),
adpcmBMem(NULL), adpcmBMem(NULL),
adpcmBMemLen(0) {} adpcmBMemLen(0),
qsoundMem(NULL),
qsoundMemLen(0) {}
}; };
#endif #endif

View file

@ -956,7 +956,7 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) {
// while version 32 stored this value, it was unused. // while version 32 stored this value, it was unused.
if (ds.version>=38) { if (ds.version>=38) {
sample->centerRate=reader.readS(); sample->centerRate=(unsigned short) reader.readS();
} else { } else {
reader.readS(); reader.readS();
} }

View file

@ -37,6 +37,10 @@ int DivDispatch::getRegisterPoolSize() {
return 0; return 0;
} }
int DivDispatch::getRegisterPoolDepth() {
return 8;
}
void* DivDispatch::getState() { void* DivDispatch::getState() {
return NULL; return NULL;
} }

View file

@ -0,0 +1,619 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2022 tildearrow and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "qsound.h"
#include "../engine.h"
#include "../../ta-log.h"
#include <math.h>
#include <map>
#define CHIP_DIVIDER (1248*2)
#define QS_NOTE_FREQUENCY(x) parent->calcBaseFreq(440,0x1000,(x)-3,false)
#define rWrite(a,v) {if(!skipRegisterWrites) {qsound_write_data(&chip,a,v); if(dumpWrites) addWrite(a,v); }}
#define immWrite(a,v) {qsound_write_data(&chip,a,v); if(dumpWrites) addWrite(a,v);}
const char* regCheatSheetQSound[]={
"Ch15_Bank", "00",
"Ch00_Start", "01",
"Ch00_Freq", "02",
"Ch00_Phase", "03",
"Ch00_Loop", "04",
"Ch00_End", "05",
"Ch00_Volume", "06",
"Ch00_Bank", "08",
"Ch01_Start", "09",
"Ch01_Freq", "0A",
"Ch01_Phase", "0B",
"Ch01_Loop", "0C",
"Ch01_End", "0D",
"Ch01_Volume", "0E",
"Ch01_Bank", "10",
"Ch02_Start", "11",
"Ch02_Freq", "12",
"Ch02_Phase", "13",
"Ch02_Loop", "14",
"Ch02_End", "15",
"Ch02_Volume", "16",
"Ch02_Bank", "18",
"Ch03_Start", "19",
"Ch03_Freq", "1A",
"Ch03_Phase", "1B",
"Ch03_Loop", "1C",
"Ch03_End", "1D",
"Ch03_Volume", "1E",
"Ch03_Bank", "20",
"Ch04_Start", "21",
"Ch04_Freq", "22",
"Ch04_Phase", "23",
"Ch04_Loop", "24",
"Ch04_End", "25",
"Ch04_Volume", "26",
"Ch04_Bank", "28",
"Ch05_Start", "29",
"Ch05_Freq", "2A",
"Ch05_Phase", "2B",
"Ch05_Loop", "2C",
"Ch05_End", "2D",
"Ch05_Volume", "2E",
"Ch05_Bank", "30",
"Ch06_Start", "31",
"Ch06_Freq", "32",
"Ch06_Phase", "33",
"Ch06_Loop", "34",
"Ch06_End", "35",
"Ch06_Volume", "36",
"Ch06_Bank", "38",
"Ch07_Start", "39",
"Ch07_Freq", "3A",
"Ch07_Phase", "3B",
"Ch07_Loop", "3C",
"Ch07_End", "3D",
"Ch07_Volume", "3E",
"Ch07_Bank", "40",
"Ch08_Start", "41",
"Ch08_Freq", "42",
"Ch08_Phase", "43",
"Ch08_Loop", "44",
"Ch08_End", "45",
"Ch08_Volume", "46",
"Ch08_Bank", "48",
"Ch09_Start", "49",
"Ch09_Freq", "4A",
"Ch09_Phase", "4B",
"Ch09_Loop", "4C",
"Ch09_End", "4D",
"Ch09_Volume", "4E",
"Ch09_Bank", "50",
"Ch10_Start", "51",
"Ch10_Freq", "52",
"Ch10_Phase", "53",
"Ch10_Loop", "54",
"Ch10_End", "55",
"Ch10_Volume", "56",
"Ch10_Bank", "58",
"Ch11_Start", "59",
"Ch11_Freq", "5A",
"Ch11_Phase", "5B",
"Ch11_Loop", "5C",
"Ch11_End", "5D",
"Ch11_Volume", "5E",
"Ch11_Bank", "60",
"Ch12_Start", "61",
"Ch12_Freq", "62",
"Ch12_Phase", "63",
"Ch12_Loop", "64",
"Ch12_End", "65",
"Ch12_Volume", "66",
"Ch12_Bank", "68",
"Ch13_Start", "69",
"Ch13_Freq", "6A",
"Ch13_Phase", "6B",
"Ch13_Loop", "6C",
"Ch13_End", "6D",
"Ch13_Volume", "6E",
"Ch13_Bank", "70",
"Ch14_Start", "71",
"Ch14_Freq", "72",
"Ch14_Phase", "73",
"Ch14_Loop", "74",
"Ch14_End", "75",
"Ch14_Volume", "76",
"Ch14_Bank", "78",
"Ch15_Start", "79",
"Ch15_Freq", "7A",
"Ch15_Phase", "7B",
"Ch15_Loop", "7C",
"Ch15_End", "7D",
"Ch15_Volume", "7E",
"Ch00_Panning", "80",
"Ch01_Panning", "81",
"Ch02_Panning", "82",
"Ch03_Panning", "83",
"Ch04_Panning", "84",
"Ch05_Panning", "85",
"Ch06_Panning", "86",
"Ch07_Panning", "87",
"Ch08_Panning", "88",
"Ch09_Panning", "89",
"Ch10_Panning", "8A",
"Ch11_Panning", "8B",
"Ch12_Panning", "8C",
"Ch13_Panning", "8D",
"Ch14_Panning", "8E",
"Ch15_Panning", "8F",
"Adpcm0_Panning","90",
"Adpcm1_Panning","91",
"Adpcm2_Panning","92",
"Echo_Feedback","93",
"Ch00_Echo", "BA",
"Ch01_Echo", "BB",
"Ch02_Echo", "BC",
"Ch03_Echo", "BD",
"Ch04_Echo", "BE",
"Ch05_Echo", "BF",
"Ch06_Echo", "C0",
"Ch07_Echo", "C1",
"Ch08_Echo", "C2",
"Ch09_Echo", "C3",
"Ch10_Echo", "C4",
"Ch11_Echo", "C5",
"Ch12_Echo", "C6",
"Ch13_Echo", "C7",
"Ch14_Echo", "C8",
"Ch15_Echo", "C9",
"Adpcm0_Start", "CA",
"Adpcm0_End", "CB",
"Adpcm0_Bank", "CC",
"Adpcm0_Volume","CD",
"Adpcm1_Start", "CE",
"Adpcm1_End", "CF",
"Adpcm1_Bank", "D0",
"Adpcm1_Volume","D1",
"Adpcm2_Start", "D2",
"Adpcm2_End", "D3",
"Adpcm2_Bank", "D4",
"Adpcm2_Volume","D5",
"Adpcm0_KeyOn", "D6",
"Adpcm1_KeyOn", "D7",
"Adpcm2_KeyOn", "D8",
"Echo_Delay", "D9",
"L_Wet_Filter", "DA",
"L_Dry_Filter", "DB",
"R_Wet_Filter", "DC",
"R_Dry_Filter", "DD",
"L_Wet_Delay", "DE",
"L_Dry_Delay", "DF",
"R_Wet_Delay", "E0",
"R_Dry_Delay", "E1",
"Delay_Flag", "E2",
"Mode_Select", "E3", //valid: 0000,0288,0039,061A,004F
"L_Wet_Volume", "E4",
"L_Dry_Volume", "E5",
"R_Wet_Volume", "E6",
"R_Dry_Volume", "E7",
NULL
};
enum q1_register_name {
Q1V_BANK = 0,
Q1V_START = 1,
Q1V_FREQ = 2,
Q1V_PHASE = 3,
Q1V_LOOP = 4,
Q1V_END = 5,
Q1V_VOL = 6,
Q1V_REG_COUNT = 7,
Q1_PAN = 0x80,
Q1_ECHO = 0xba,
Q1A_PAN = 0x90,
Q1A_START = 0xca,
Q1A_END = 0xcb,
Q1A_BANK = 0xcc,
Q1A_VOL = 0xcd,
Q1A_KEYON = 0xd6,
Q1_ECHO_FEEDBACK = 0x93,
Q1_ECHO_LENGTH = 0xd9,
};
const unsigned char q1_reg_map[Q1V_REG_COUNT][16] = {
{0x78,0x00,0x08,0x10,0x18,0x20,0x28,0x30,0x38,0x40,0x48,0x50,0x58,0x60,0x68,0x70},
{0x01,0x09,0x11,0x19,0x21,0x29,0x31,0x39,0x41,0x49,0x51,0x59,0x61,0x69,0x71,0x79},
{0x02,0x0a,0x12,0x1a,0x22,0x2a,0x32,0x3a,0x42,0x4a,0x52,0x5a,0x62,0x6a,0x72,0x7a},
{0x03,0x0b,0x13,0x1b,0x23,0x2b,0x33,0x3b,0x43,0x4b,0x53,0x5b,0x63,0x6b,0x73,0x7b},
{0x04,0x0c,0x14,0x1c,0x24,0x2c,0x34,0x3c,0x44,0x4c,0x54,0x5c,0x64,0x6c,0x74,0x7c},
{0x05,0x0d,0x15,0x1d,0x25,0x2d,0x35,0x3d,0x45,0x4d,0x55,0x5d,0x65,0x6d,0x75,0x7d},
{0x06,0x0e,0x16,0x1e,0x26,0x2e,0x36,0x3e,0x46,0x4e,0x56,0x5e,0x66,0x6e,0x76,0x7e},
};
const char** DivPlatformQSound::getRegisterSheet() {
return regCheatSheetQSound;
}
const char* DivPlatformQSound::getEffectName(unsigned char effect) {
switch (effect) {
case 0x10:
return "10xx: Set echo feedback level (00 to FF)";
break;
case 0x11:
return "11xx: Set channel echo level (00 to FF)";
break;
}
return NULL;
}
void DivPlatformQSound::acquire(short* bufL, short* bufR, size_t start, size_t len) {
chip.rom_data = parent->qsoundMem;
chip.rom_mask = 0xffffff;
for (size_t h=start; h<start+len; h++) {
qsound_update(&chip);
bufL[h]=chip.out[0];
bufR[h]=chip.out[1];
}
}
void DivPlatformQSound::tick() {
for (int i=0; i<16; i++) {
chan[i].std.next();
if (chan[i].std.hadVol) {
chan[i].outVol=((chan[i].vol%256)*MIN(255,chan[i].std.vol << 2))>>8;
// Check if enabled and write volume
if(chan[i].active)
{
rWrite(q1_reg_map[Q1V_VOL][i], chan[i].outVol << 5);
//logW("ch %d vol=%04x (hadVol)!\n",i,chan[i].outVol << 5);
}
}
uint16_t qsound_bank = 0;
uint16_t qsound_addr = 0;
uint16_t qsound_loop = 0;
uint16_t qsound_end = 0;
double off=1.0;
if (chan[i].sample>=0 && chan[i].sample<parent->song.sampleLen) {
DivSample* s=parent->song.sample[chan[i].sample];
if (s->centerRate<1) {
off=1.0;
} else {
off=(double)s->centerRate/24038.0/16.0;
}
qsound_bank = 0x8000 | (s->rendOffQsound >> 16);
qsound_addr = s->rendOffQsound & 0xffff;
int length = s->length;
if(length > 65536 - 16)
length = 65536 - 16;
if(s->loopStart == -1 || s->loopStart >= length)
{
qsound_end = s->rendOffQsound + length + 15;
qsound_loop = 15;
}
else
{
qsound_end = s->rendOffQsound + length;
qsound_loop = length - s->loopStart;
}
}
if (chan[i].std.hadArp) {
if (!chan[i].inPorta) {
if (chan[i].std.arpMode) {
chan[i].baseFreq=off*QS_NOTE_FREQUENCY(chan[i].std.arp);
} else {
chan[i].baseFreq=off*QS_NOTE_FREQUENCY(chan[i].note+chan[i].std.arp);
}
}
chan[i].freqChanged=true;
} else {
if (chan[i].std.arpMode && chan[i].std.finishedArp) {
chan[i].baseFreq=off*QS_NOTE_FREQUENCY(chan[i].note);
chan[i].freqChanged=true;
}
}
if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) {
//DivInstrument* ins=parent->getIns(chan[i].ins);
chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false);
if (chan[i].freq>0xffff) chan[i].freq=0xffff;
//if (chan[i].note>0x5d) chan[i].freq=0x01; //????
if (chan[i].keyOn) {
rWrite(q1_reg_map[Q1V_BANK][i], qsound_bank);
rWrite(q1_reg_map[Q1V_END][i], qsound_end);
rWrite(q1_reg_map[Q1V_LOOP][i], qsound_loop);
rWrite(q1_reg_map[Q1V_START][i], qsound_addr);
rWrite(q1_reg_map[Q1V_PHASE][i], 0x8000);
//logW("ch %d bank=%04x, addr=%04x, end=%04x, loop=%04x!\n",i,qsound_bank,qsound_addr,qsound_end,qsound_loop);
// Write sample address. Enable volume
if (!chan[i].std.hadVol) {
rWrite(q1_reg_map[Q1V_VOL][i], chan[i].vol << 5);
//logW("ch %d vol=%04x (!hadVol)!\n",i,chan[i].vol << 5);
}
}
if (chan[i].keyOff) {
rWrite(q1_reg_map[Q1V_VOL][i], 0);
rWrite(q1_reg_map[Q1V_FREQ][i], 0);
// Disable volume
}
else if (chan[i].active) {
//logW("ch %d frequency set to %04x, off=%f, note=%d, %04x!\n",i,chan[i].freq,off,chan[i].note,QS_NOTE_FREQUENCY(chan[i].note));
rWrite(q1_reg_map[Q1V_FREQ][i], chan[i].freq);
}
if (chan[i].keyOn) chan[i].keyOn=false;
if (chan[i].keyOff) chan[i].keyOff=false;
chan[i].freqChanged=false;
}
}
}
int DivPlatformQSound::dispatch(DivCommand c) {
switch (c.cmd) {
case DIV_CMD_NOTE_ON: {
DivInstrument* ins=parent->getIns(chan[c.chan].ins);
chan[c.chan].sample=ins->amiga.initSample;
double off=1.0;
if (chan[c.chan].sample>=0 && chan[c.chan].sample<parent->song.sampleLen) {
DivSample* s=parent->song.sample[chan[c.chan].sample];
if (s->centerRate<1) {
off=1.0;
} else {
off=(double)s->centerRate/24038.0/16.0;
}
}
if (c.value!=DIV_NOTE_NULL) {
chan[c.chan].baseFreq=off*QS_NOTE_FREQUENCY(c.value);
}
if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) {
chan[c.chan].sample=-1;
}
if (c.value!=DIV_NOTE_NULL) {
chan[c.chan].freqChanged=true;
chan[c.chan].note=c.value;
}
chan[c.chan].active=true;
chan[c.chan].keyOn=true;
chan[c.chan].std.init(ins);
break;
}
case DIV_CMD_NOTE_OFF:
chan[c.chan].sample=-1;
chan[c.chan].active=false;
chan[c.chan].keyOff=true;
chan[c.chan].std.init(NULL);
break;
case DIV_CMD_NOTE_OFF_ENV:
case DIV_CMD_ENV_RELEASE:
chan[c.chan].std.release();
break;
case DIV_CMD_INSTRUMENT:
if (chan[c.chan].ins!=c.value || c.value2==1) {
chan[c.chan].ins=c.value;
}
break;
case DIV_CMD_VOLUME:
if (chan[c.chan].vol!=c.value) {
chan[c.chan].vol=c.value;
if (!chan[c.chan].std.hasVol) {
// Check if enabled and write volume
chan[c.chan].outVol=c.value;
if(chan[c.chan].active && c.chan < 16)
{
rWrite(q1_reg_map[Q1V_VOL][c.chan], chan[c.chan].outVol << 5);
//logW("ch %d vol=%04x (cmd vol)!\n",c.chan,chan[c.chan].outVol << 5);
}
}
}
break;
case DIV_CMD_GET_VOLUME:
if (chan[c.chan].std.hasVol) {
return chan[c.chan].vol;
}
return chan[c.chan].outVol;
break;
case DIV_CMD_PANNING:
immWrite(Q1_PAN+c.chan, c.value + 0x110);
break;
case DIV_CMD_QSOUND_ECHO_LEVEL:
immWrite(Q1_ECHO+c.chan, c.value << 7);
break;
case DIV_CMD_QSOUND_ECHO_FEEDBACK:
immWrite(Q1_ECHO_FEEDBACK, c.value << 6);
break;
case DIV_CMD_PITCH:
chan[c.chan].pitch=c.value;
chan[c.chan].freqChanged=true;
break;
case DIV_CMD_NOTE_PORTA: {
double off=1.0;
if (chan[c.chan].sample>=0 && chan[c.chan].sample<parent->song.sampleLen) {
DivSample* s=parent->song.sample[chan[c.chan].sample];
if (s->centerRate<1) {
off=1.0;
} else {
off=(double)s->centerRate/24038.0/16.0;
}
}
int destFreq=off*QS_NOTE_FREQUENCY(c.value2);
bool return2=false;
if (destFreq>chan[c.chan].baseFreq) {
chan[c.chan].baseFreq+=c.value;
if (chan[c.chan].baseFreq>=destFreq) {
chan[c.chan].baseFreq=destFreq;
return2=true;
}
} else {
chan[c.chan].baseFreq-=c.value;
if (chan[c.chan].baseFreq<=destFreq) {
chan[c.chan].baseFreq=destFreq;
return2=true;
}
}
chan[c.chan].freqChanged=true;
if (return2) {
chan[c.chan].inPorta=false;
return 2;
}
break;
}
case DIV_CMD_LEGATO: {
double off=1.0;
if (chan[c.chan].sample>=0 && chan[c.chan].sample<parent->song.sampleLen) {
DivSample* s=parent->song.sample[chan[c.chan].sample];
if (s->centerRate<1) {
off=1.0;
} else {
off=(double)s->centerRate/24038.0/16.0;
}
}
chan[c.chan].baseFreq=off*QS_NOTE_FREQUENCY(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp-12):(0)));
chan[c.chan].freqChanged=true;
chan[c.chan].note=c.value;
break;
}
case DIV_CMD_PRE_PORTA:
if (chan[c.chan].active && c.value2) {
if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins));
}
chan[c.chan].inPorta=c.value;
break;
case DIV_CMD_GET_VOLMAX:
return 255;
break;
case DIV_ALWAYS_SET_VOLUME:
return 1;
break;
default:
break;
}
return 1;
}
void DivPlatformQSound::muteChannel(int ch, bool mute) {
if(mute)
chip.mute_mask |= (1 << ch);
else
chip.mute_mask &= ~(1 << ch);
}
void DivPlatformQSound::forceIns() {
for (int i=0; i<4; i++) {
chan[i].insChanged=true;
chan[i].freqChanged=true;
chan[i].sample=-1;
}
}
void* DivPlatformQSound::getChanState(int ch) {
return &chan[ch];
}
void DivPlatformQSound::reset() {
for (int i=0; i<16; i++) {
chan[i]=DivPlatformQSound::Channel();
}
qsound_reset(&chip);
while(!chip.ready_flag)
qsound_update(&chip);
immWrite(Q1_ECHO_LENGTH, 0xfff - (2725 - echoDelay));
immWrite(Q1_ECHO_FEEDBACK, echoFeedback << 6);
}
bool DivPlatformQSound::isStereo() {
return true;
}
bool DivPlatformQSound::keyOffAffectsArp(int ch) {
return true;
}
void DivPlatformQSound::notifyInsChange(int ins) {
for (int i=0; i<4; i++) {
if (chan[i].ins==ins) {
chan[i].insChanged=true;
}
}
}
void DivPlatformQSound::notifyWaveChange(int wave) {
// TODO when wavetables are added
}
void DivPlatformQSound::notifyInsDeletion(void* ins) {
for (int i=0; i<4; i++) {
chan[i].std.notifyInsDeletion((DivInstrument*)ins);
}
}
void DivPlatformQSound::setFlags(unsigned int flags) {
echoDelay = 2725 - (flags & 0xfff);
echoFeedback = (flags >> 12) & 255;
if(echoDelay < 0)
echoDelay = 0;
if(echoDelay > 2725)
echoDelay = 2725;
//rate=chipClock/CHIP_DIVIDER;
}
void DivPlatformQSound::poke(unsigned int addr, unsigned short val) {
immWrite(addr, val);
immWrite(addr, val);
}
void DivPlatformQSound::poke(std::vector<DivRegWrite>& wlist) {
for (DivRegWrite& i: wlist) immWrite(i.addr,i.val);
}
unsigned char* DivPlatformQSound::getRegisterPool() {
unsigned short* regPoolPtr = regPool;
for(int i=0; i<256; i++)
{
uint16_t data = qsound_read_data(&chip, i);
*regPoolPtr++ = data;
}
return (unsigned char*)regPool;
}
int DivPlatformQSound::getRegisterPoolSize() {
return 256;
}
int DivPlatformQSound::getRegisterPoolDepth() {
return 16;
}
int DivPlatformQSound::init(DivEngine* p, int channels, int sugRate, unsigned int flags) {
parent=p;
dumpWrites=false;
skipRegisterWrites=false;
// for (int i=0; i<16; i++) {
// isMuted[i]=false;
// }
setFlags(flags);
chipClock=60000000;
rate = qsound_start(&chip, chipClock);
chip.rom_data = (unsigned char*)&chip.rom_mask;
chip.rom_mask = 0;
reset();
return 19;
}
void DivPlatformQSound::quit() {
}

View file

@ -0,0 +1,93 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2022 tildearrow and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef _QSOUND_H
#define _QSOUND_H
#include "../dispatch.h"
#include <queue>
#include "../macroInt.h"
#include "sound/qsound.h"
class DivPlatformQSound: public DivDispatch {
struct Channel {
int freq, baseFreq, pitch;
unsigned short audLen;
unsigned int audPos;
int sample, wave;
unsigned char ins;
int note;
int panning;
bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave;
int vol, outVol;
DivMacroInt std;
Channel():
freq(0),
baseFreq(0),
pitch(0),
audLen(0),
audPos(0),
sample(-1),
ins(-1),
note(0),
panning(0x10),
active(false),
insChanged(true),
freqChanged(false),
keyOn(false),
keyOff(false),
inPorta(false),
vol(255),
outVol(255) {}
};
Channel chan[19];
int echoDelay;
int echoFeedback;
struct qsound_chip chip;
unsigned short regPool[512];
friend void putDispatchChan(void*,int,int);
public:
void acquire(short* bufL, short* bufR, size_t start, size_t len);
int dispatch(DivCommand c);
void* getChanState(int chan);
unsigned char* getRegisterPool();
int getRegisterPoolSize();
int getRegisterPoolDepth();
void reset();
void forceIns();
void tick();
void muteChannel(int ch, bool mute);
bool isStereo();
bool keyOffAffectsArp(int ch);
void setFlags(unsigned int flags);
void notifyInsChange(int ins);
void notifyWaveChange(int wave);
void notifyInsDeletion(void* ins);
void poke(unsigned int addr, unsigned short val);
void poke(std::vector<DivRegWrite>& wlist);
const char** getRegisterSheet();
const char* getEffectName(unsigned char effect);
int init(DivEngine* parent, int channels, int sugRate, unsigned int flags);
void quit();
};
#endif

View file

@ -0,0 +1,698 @@
/*
Capcom DL-1425 QSound emulator
==============================
by superctr (Ian Karlsson)
with thanks to Valley Bell
2018-05-12 - 2018-05-15
*/
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <math.h>
#include "qsound.h"
#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
// ============================================================================
static const int16_t qsound_dry_mix_table[33] = {
-16384,-16384,-16384,-16384,-16384,-16384,-16384,-16384,
-16384,-16384,-16384,-16384,-16384,-16384,-16384,-16384,
-16384,-14746,-13107,-11633,-10486,-9175,-8520,-7209,
-6226,-5226,-4588,-3768,-3277,-2703,-2130,-1802,
0
};
static const int16_t qsound_wet_mix_table[33] = {
0,-1638,-1966,-2458,-2949,-3441,-4096,-4669,
-4915,-5120,-5489,-6144,-7537,-8831,-9339,-9830,
-10240,-10322,-10486,-10568,-10650,-11796,-12288,-12288,
-12534,-12648,-12780,-12829,-12943,-13107,-13418,-14090,
-16384
};
static const int16_t qsound_linear_mix_table[33] = {
-16379,-16338,-16257,-16135,-15973,-15772,-15531,-15251,
-14934,-14580,-14189,-13763,-13303,-12810,-12284,-11729,
-11729,-11144,-10531,-9893,-9229,-8543,-7836,-7109,
-6364,-5604,-4829,-4043,-3246,-2442,-1631,-817,
0
};
static const int16_t qsound_filter_data[5][95] = {
{ // d53 - 0
0,0,0,6,44,-24,-53,-10,59,-40,-27,1,39,-27,56,127,174,36,-13,49,
212,142,143,-73,-20,66,-108,-117,-399,-265,-392,-569,-473,-71,95,-319,-218,-230,331,638,
449,477,-180,532,1107,750,9899,3828,-2418,1071,-176,191,-431,64,117,-150,-274,-97,-238,165,
166,250,-19,4,37,204,186,-6,140,-77,-1,1,18,-10,-151,-149,-103,-9,55,23,
-102,-97,-11,13,-48,-27,5,18,-61,-30,64,72,0,0,0,
},
{ // db2 - 1 - default left filter
0,0,0,85,24,-76,-123,-86,-29,-14,-20,-7,6,-28,-87,-89,-5,100,154,160,
150,118,41,-48,-78,-23,59,83,-2,-176,-333,-344,-203,-66,-39,2,224,495,495,280,
432,1340,2483,5377,1905,658,0,97,347,285,35,-95,-78,-82,-151,-192,-171,-149,-147,-113,
-22,71,118,129,127,110,71,31,20,36,46,23,-27,-63,-53,-21,-19,-60,-92,-69,
-12,25,29,30,40,41,29,30,46,39,-15,-74,0,0,0,
},
{ // e11 - 2 - default right filter
0,0,0,23,42,47,29,10,2,-14,-54,-92,-93,-70,-64,-77,-57,18,94,113,
87,69,67,50,25,29,58,62,24,-39,-131,-256,-325,-234,-45,58,78,223,485,496,
127,6,857,2283,2683,4928,1328,132,79,314,189,-80,-90,35,-21,-186,-195,-99,-136,-258,
-189,82,257,185,53,41,84,68,38,63,77,14,-60,-71,-71,-120,-151,-84,14,29,
-8,7,66,69,12,-3,54,92,52,-6,-15,-2,0,0,0,
},
{ // e70 - 3
0,0,0,2,-28,-37,-17,0,-9,-22,-3,35,52,39,20,7,-6,2,55,121,
129,67,8,1,9,-6,-16,16,66,96,118,130,75,-47,-92,43,223,239,151,219,
440,475,226,206,940,2100,2663,4980,865,49,-33,186,231,103,42,114,191,184,116,29,
-47,-72,-21,60,96,68,31,32,63,87,76,39,7,14,55,85,67,18,-12,-3,
21,34,29,6,-27,-49,-37,-2,16,0,-21,-16,0,0,0,
},
{ // ecf - 4
0,0,0,48,7,-22,-29,-10,24,54,59,29,-36,-117,-185,-213,-185,-99,13,90,
83,24,-5,23,53,47,38,56,67,57,75,107,16,-242,-440,-355,-120,-33,-47,152,
501,472,-57,-292,544,1937,2277,6145,1240,153,47,200,152,36,64,134,74,-82,-208,-266,
-268,-188,-42,65,74,56,89,133,114,44,-3,-1,17,29,29,-2,-76,-156,-187,-151,
-85,-31,-5,7,20,32,24,-5,-20,6,48,62,0,0,0,
}
};
static const int16_t qsound_filter_data2[209] = {
// f2e - following 95 values used for "disable output" filter
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,
// f73 - following 45 values used for "mode 2" filter (overlaps with f2e)
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,
-371,-196,-268,-512,-303,-315,-184,-76,276,-256,298,196,990,236,1114,-126,4377,6549,791,
// fa0 - filtering disabled (for 95-taps) (use fa3 or fa4 for mode2 filters)
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,-16384,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const int16_t adpcm_step_table[16] = {
154, 154, 128, 102, 77, 58, 58, 58,
58, 58, 58, 58, 77, 102, 128, 154
};
// DSP states
enum {
STATE_INIT1 = 0x288,
STATE_INIT2 = 0x61a,
STATE_REFRESH1 = 0x039,
STATE_REFRESH2 = 0x04f,
STATE_NORMAL1 = 0x314,
STATE_NORMAL2 = 0x6b2,
};
enum {
PANTBL_LEFT = 0,
PANTBL_RIGHT = 1,
PANTBL_DRY = 0,
PANTBL_WET = 1,
};
static void init_pan_tables(struct qsound_chip *chip);
static void init_register_map(struct qsound_chip *chip);
static void state_init(struct qsound_chip *chip);
static void state_refresh_filter_1(struct qsound_chip *chip);
static void state_refresh_filter_2(struct qsound_chip *chip);
static void state_normal_update(struct qsound_chip *chip);
static inline int16_t get_sample(struct qsound_chip *chip, uint16_t bank,uint16_t address);
static inline int16_t* get_filter_table(struct qsound_chip *chip, uint16_t offset);
static inline int16_t pcm_update(struct qsound_chip *chip, int voice_no, int32_t *echo_out);
static inline void adpcm_update(struct qsound_chip *chip, int voice_no, int nibble);
static inline int16_t echo(struct qsound_echo *r,int32_t input);
static inline int32_t fir(struct qsound_fir *f, int16_t input);
static inline int32_t delay(struct qsound_delay *d, int32_t input);
static inline void delay_update(struct qsound_delay *d);
// ============================================================================
long qsound_start(struct qsound_chip *chip, int clock)
{
memset(chip,0,sizeof(*chip));
init_pan_tables(chip);
init_register_map(chip);
return clock / 2 / 1248; // DSP program uses 1248 machine cycles per iteration
}
void qsound_reset(struct qsound_chip *chip)
{
chip->ready_flag = 0;
chip->out[0] = chip->out[1] = 0;
chip->state = 0;
chip->state_counter = 0;
}
uint8_t qsound_stream_update(struct qsound_chip *chip, int16_t **outputs, int samples)
{
// Clear the buffers
memset(outputs[0], 0, samples * sizeof(*outputs[0]));
memset(outputs[1], 0, samples * sizeof(*outputs[1]));
for (int i = 0; i < samples; i ++)
{
qsound_update(chip);
outputs[0][i] = chip->out[0];
outputs[1][i] = chip->out[1];
}
}
uint8_t qsound_w(struct qsound_chip *chip, uint8_t offset, uint8_t data)
{
switch (offset)
{
case 0:
chip->data_latch = (chip->data_latch & 0x00ff) | (data << 8);
break;
case 1:
chip->data_latch = (chip->data_latch & 0xff00) | data;
break;
case 2:
qsound_write_data(chip, data, chip->data_latch);
break;
default:
break;
}
}
uint8_t qsound_r(struct qsound_chip *chip)
{
// ready bit (0x00 = busy, 0x80 == ready)
return chip->ready_flag;
}
void qsound_write_data(struct qsound_chip *chip, uint8_t address, uint16_t data)
{
uint16_t *destination = chip->register_map[address];
if(destination)
*destination = data;
chip->ready_flag = 0;
}
uint16_t qsound_read_data(struct qsound_chip *chip, uint8_t address)
{
uint16_t data = 0;
uint16_t *source = chip->register_map[address];
if(source)
data = *source;
return data;
}
// ============================================================================
static void init_pan_tables(struct qsound_chip *chip)
{
int i;
for(i=0;i<33;i++)
{
// dry mixing levels
chip->pan_tables[PANTBL_LEFT][PANTBL_DRY][i] = qsound_dry_mix_table[i];
chip->pan_tables[PANTBL_RIGHT][PANTBL_DRY][i] = qsound_dry_mix_table[32-i];
// wet mixing levels
chip->pan_tables[PANTBL_LEFT][PANTBL_WET][i] = qsound_wet_mix_table[i];
chip->pan_tables[PANTBL_RIGHT][PANTBL_WET][i] = qsound_wet_mix_table[32-i];
// linear panning, only for dry component. wet component is muted.
chip->pan_tables[PANTBL_LEFT][PANTBL_DRY][i+0x30] = qsound_linear_mix_table[i];
chip->pan_tables[PANTBL_RIGHT][PANTBL_DRY][i+0x30] = qsound_linear_mix_table[32-i];
}
}
static void init_register_map(struct qsound_chip *chip)
{
int i;
// unused registers
for(i=0;i<256;i++)
chip->register_map[i] = NULL;
// PCM registers
for(i=0;i<16;i++) // PCM voices
{
chip->register_map[(i<<3)+0] = (uint16_t*)&chip->voice[(i+1)%16].bank; // Bank applies to the next channel
chip->register_map[(i<<3)+1] = (uint16_t*)&chip->voice[i].addr; // Current sample position and start position.
chip->register_map[(i<<3)+2] = (uint16_t*)&chip->voice[i].rate; // 4.12 fixed point decimal.
chip->register_map[(i<<3)+3] = (uint16_t*)&chip->voice[i].phase;
chip->register_map[(i<<3)+4] = (uint16_t*)&chip->voice[i].loop_len;
chip->register_map[(i<<3)+5] = (uint16_t*)&chip->voice[i].end_addr;
chip->register_map[(i<<3)+6] = (uint16_t*)&chip->voice[i].volume;
chip->register_map[(i<<3)+7] = NULL; // unused
chip->register_map[i+0x80] = (uint16_t*)&chip->voice_pan[i];
chip->register_map[i+0xba] = (uint16_t*)&chip->voice[i].echo;
}
// ADPCM registers
for(i=0;i<3;i++) // ADPCM voices
{
// ADPCM sample rate is fixed to 8khz. (one channel is updated every third sample)
chip->register_map[(i<<2)+0xca] = (uint16_t*)&chip->adpcm[i].start_addr;
chip->register_map[(i<<2)+0xcb] = (uint16_t*)&chip->adpcm[i].end_addr;
chip->register_map[(i<<2)+0xcc] = (uint16_t*)&chip->adpcm[i].bank;
chip->register_map[(i<<2)+0xcd] = (uint16_t*)&chip->adpcm[i].volume;
chip->register_map[i+0xd6] = (uint16_t*)&chip->adpcm[i].flag; // non-zero to start ADPCM playback
chip->register_map[i+0x90] = (uint16_t*)&chip->voice_pan[16+i];
}
// QSound registers
chip->register_map[0x93] = (uint16_t*)&chip->echo.feedback;
chip->register_map[0xd9] = (uint16_t*)&chip->echo.end_pos;
chip->register_map[0xe2] = (uint16_t*)&chip->delay_update; // non-zero to update delays
chip->register_map[0xe3] = (uint16_t*)&chip->next_state;
for(i=0;i<2;i++) // left, right
{
// Wet
chip->register_map[(i<<1)+0xda] = (uint16_t*)&chip->filter[i].table_pos;
chip->register_map[(i<<1)+0xde] = (uint16_t*)&chip->wet[i].delay;
chip->register_map[(i<<1)+0xe4] = (uint16_t*)&chip->wet[i].volume;
// Dry
chip->register_map[(i<<1)+0xdb] = (uint16_t*)&chip->alt_filter[i].table_pos;
chip->register_map[(i<<1)+0xdf] = (uint16_t*)&chip->dry[i].delay;
chip->register_map[(i<<1)+0xe5] = (uint16_t*)&chip->dry[i].volume;
}
}
static inline int16_t get_sample(struct qsound_chip *chip, uint16_t bank,uint16_t address)
{
uint32_t rom_addr;
uint8_t sample_data;
if (! chip->rom_mask)
return 0; // no ROM loaded
if (! (bank & 0x8000))
return 0; // ignore attempts to read from DSP program ROM
bank &= 0x7FFF;
rom_addr = (bank << 16) | (address << 0);
sample_data = chip->rom_data[rom_addr];
return (int16_t)((sample_data << 8) | (sample_data << 0)); // MAME currently expands the 8 bit ROM data to 16 bits this way.
}
static inline int16_t* get_filter_table(struct qsound_chip *chip, uint16_t offset)
{
int index;
if (offset >= 0xf2e && offset < 0xfff)
return (int16_t*)&qsound_filter_data2[offset-0xf2e]; // overlapping filter data
index = (offset-0xd53)/95;
if(index >= 0 && index < 5)
return (int16_t*)&qsound_filter_data[index]; // normal tables
return NULL; // no filter found.
}
/********************************************************************/
// updates one DSP sample
void qsound_update(struct qsound_chip *chip)
{
switch(chip->state)
{
default:
case STATE_INIT1:
case STATE_INIT2:
state_init(chip); return;
case STATE_REFRESH1:
state_refresh_filter_1(chip); return;
case STATE_REFRESH2:
state_refresh_filter_2(chip); return;
case STATE_NORMAL1:
case STATE_NORMAL2:
state_normal_update(chip); return;
}
}
// Initialization routine
static void state_init(struct qsound_chip *chip)
{
int mode = (chip->state == STATE_INIT2) ? 1 : 0;
int i;
// we're busy for 4 samples, including the filter refresh.
if(chip->state_counter >= 2)
{
chip->state_counter = 0;
chip->state = chip->next_state;
return;
}
else if(chip->state_counter == 1)
{
chip->state_counter++;
return;
}
memset(chip->voice, 0, sizeof(chip->voice));
memset(chip->adpcm, 0, sizeof(chip->adpcm));
memset(chip->filter, 0, sizeof(chip->filter));
memset(chip->alt_filter, 0, sizeof(chip->alt_filter));
memset(chip->wet, 0, sizeof(chip->wet));
memset(chip->dry, 0, sizeof(chip->dry));
memset(&chip->echo, 0, sizeof(chip->echo));
for(i=0;i<19;i++)
{
chip->voice_pan[i] = 0x120;
chip->voice_output[i] = 0;
}
for(i=0;i<16;i++)
chip->voice[i].bank = 0x8000;
for(i=0;i<3;i++)
chip->adpcm[i].bank = 0x8000;
if(mode == 0)
{
// mode 1
chip->wet[0].delay = 0;
chip->dry[0].delay = 46;
chip->wet[1].delay = 0;
chip->dry[1].delay = 48;
chip->filter[0].table_pos = 0xdb2;
chip->filter[1].table_pos = 0xe11;
chip->echo.end_pos = 0x554 + 6;
chip->next_state = STATE_REFRESH1;
}
else
{
// mode 2
chip->wet[0].delay = 1;
chip->dry[0].delay = 0;
chip->wet[1].delay = 0;
chip->dry[1].delay = 0;
chip->filter[0].table_pos = 0xf73;
chip->filter[1].table_pos = 0xfa4;
chip->alt_filter[0].table_pos = 0xf73;
chip->alt_filter[1].table_pos = 0xfa4;
chip->echo.end_pos = 0x53c + 6;
chip->next_state = STATE_REFRESH2;
}
chip->wet[0].volume = 0x3fff;
chip->dry[0].volume = 0x3fff;
chip->wet[1].volume = 0x3fff;
chip->dry[1].volume = 0x3fff;
chip->delay_update = 1;
chip->ready_flag = 0;
chip->state_counter = 1;
}
// Updates filter parameters for mode 1
static void state_refresh_filter_1(struct qsound_chip *chip)
{
const int16_t *table;
for(int ch=0; ch<2; ch++)
{
chip->filter[ch].delay_pos = 0;
chip->filter[ch].tap_count = 95;
table = get_filter_table(chip,chip->filter[ch].table_pos);
if (table != NULL)
memcpy(chip->filter[ch].taps, table, 95 * sizeof(int16_t));
}
chip->state = chip->next_state = STATE_NORMAL1;
}
// Updates filter parameters for mode 2
static void state_refresh_filter_2(struct qsound_chip *chip)
{
const int16_t *table;
for(int ch=0; ch<2; ch++)
{
chip->filter[ch].delay_pos = 0;
chip->filter[ch].tap_count = 45;
table = get_filter_table(chip,chip->filter[ch].table_pos);
if (table != NULL)
memcpy(chip->filter[ch].taps, table, 45 * sizeof(int16_t));
chip->alt_filter[ch].delay_pos = 0;
chip->alt_filter[ch].tap_count = 44;
table = get_filter_table(chip,chip->alt_filter[ch].table_pos);
if (table != NULL)
memcpy(chip->alt_filter[ch].taps, table, 44 * sizeof(int16_t));
}
chip->state = chip->next_state = STATE_NORMAL2;
}
// Updates a PCM voice. There are 16 voices, each are updated every sample
// with full rate and volume control.
static inline int16_t pcm_update(struct qsound_chip *chip, int voice_no, int32_t *echo_out)
{
struct qsound_voice* v = &chip->voice[voice_no];
int32_t new_phase;
int16_t output = 0;
if(!(chip->mute_mask & (1<<voice_no)))
{
// Read sample from rom and apply volume
output = (v->volume * get_sample(chip, v->bank, v->addr))>>14;
*echo_out += (output * v->echo)<<2;
}
// Add delta to the phase and loop back if required
new_phase = v->rate + ((v->addr<<12) | (v->phase>>4));
if((new_phase>>12) >= v->end_addr)
new_phase -= (v->loop_len<<12);
new_phase = CLAMP(new_phase, -0x8000000, 0x7FFFFFF);
v->addr = new_phase>>12;
v->phase = (new_phase<<4)&0xffff;
return output;
}
// Updates an ADPCM voice. There are 3 voices, one is updated every sample
// (effectively making the ADPCM rate 1/3 of the master sample rate), and
// volume is set when starting samples only.
// The ADPCM algorithm is supposedly similar to Yamaha ADPCM. It also seems
// like Capcom never used it, so this was not emulated in the earlier QSound
// emulators.
static inline void adpcm_update(struct qsound_chip *chip, int voice_no, int nibble)
{
struct qsound_adpcm *v = &chip->adpcm[voice_no];
int32_t delta;
int8_t step;
if(!nibble)
{
// Mute voice when it reaches the end address.
if(v->cur_addr == v->end_addr)
v->cur_vol = 0;
// Playback start flag
if(v->flag)
{
chip->voice_output[16+voice_no] = 0;
v->flag = 0;
v->step_size = 10;
v->cur_vol = v->volume;
v->cur_addr = v->start_addr;
}
// get top nibble
step = get_sample(chip, v->bank, v->cur_addr) >> 8;
}
else
{
// get bottom nibble
step = get_sample(chip, v->bank, v->cur_addr++) >> 4;
}
// shift with sign extend
step >>= 4;
// delta = (0.5 + abs(v->step)) * v->step_size
delta = ((1+abs(step<<1)) * v->step_size)>>1;
if(step <= 0)
delta = -delta;
delta += chip->voice_output[16+voice_no];
delta = CLAMP(delta,-32768,32767);
if(chip->mute_mask & (1<<(16+voice_no)))
chip->voice_output[16+voice_no] = 0;
else
chip->voice_output[16+voice_no] = (delta * v->cur_vol)>>16;
v->step_size = (adpcm_step_table[8+step] * v->step_size) >> 6;
v->step_size = CLAMP(v->step_size, 1, 2000);
}
// The echo effect is pretty simple. A moving average filter is used on
// the output from the delay line to smooth samples over time.
static inline int16_t echo(struct qsound_echo *r,int32_t input)
{
// get average of last 2 samples from the delay line
int32_t new_sample;
int32_t old_sample = r->delay_line[r->delay_pos];
int32_t last_sample = r->last_sample;
r->last_sample = old_sample;
old_sample = (old_sample+last_sample) >> 1;
// add current sample to the delay line
new_sample = input + ((old_sample * r->feedback)<<2);
r->delay_line[r->delay_pos++] = new_sample>>16;
if(r->delay_pos >= r->length)
r->delay_pos = 0;
return old_sample;
}
// Process a sample update
static void state_normal_update(struct qsound_chip *chip)
{
int v, ch;
int32_t echo_input = 0;
int16_t echo_output;
chip->ready_flag = 0x80;
// recalculate echo length
if(chip->state == STATE_NORMAL2)
chip->echo.length = chip->echo.end_pos - 0x53c;
else
chip->echo.length = chip->echo.end_pos - 0x554;
chip->echo.length = CLAMP(chip->echo.length, 0, 1024);
// update PCM voices
for(v=0; v<16; v++)
{
chip->voice_output[v] = pcm_update(chip, v, &echo_input);
}
// update ADPCM voices (one every third sample)
adpcm_update(chip, chip->state_counter % 3, chip->state_counter / 3);
echo_output = echo(&chip->echo,echo_input);
// now, we do the magic stuff
for(ch=0; ch<2; ch++)
{
// Echo is output on the unfiltered component of the left channel and
// the filtered component of the right channel.
int32_t wet = (ch == 1) ? echo_output<<16 : 0;
int32_t dry = (ch == 0) ? echo_output<<16 : 0;
int32_t output = 0;
for(int v=0; v<19; v++)
{
uint16_t pan_index = chip->voice_pan[v]-0x110;
if(pan_index > 97)
pan_index = 97;
// Apply different volume tables on the dry and wet inputs.
dry -= (chip->voice_output[v] * chip->pan_tables[ch][PANTBL_DRY][pan_index])<<2;
wet -= (chip->voice_output[v] * chip->pan_tables[ch][PANTBL_WET][pan_index])<<2;
}
// Apply FIR filter on 'wet' input
wet = fir(&chip->filter[ch], wet >> 16);
// in mode 2, we do this on the 'dry' input too
if(chip->state == STATE_NORMAL2)
dry = fir(&chip->alt_filter[ch], dry >> 16);
// output goes through a delay line and attenuation
output = (delay(&chip->wet[ch], wet) + delay(&chip->dry[ch], dry));
// DSP round function
output = (output + 0x2000) >> 14;
chip->out[ch] = CLAMP(output, -0x7fff, 0x7fff);
if(chip->delay_update)
{
delay_update(&chip->wet[ch]);
delay_update(&chip->dry[ch]);
}
}
chip->delay_update = 0;
// after 6 samples, the next state is executed.
chip->state_counter++;
if(chip->state_counter > 5)
{
chip->state_counter = 0;
chip->state = chip->next_state;
}
}
// Apply the FIR filter used as the Q1 transfer function
static inline int32_t fir(struct qsound_fir *f, int16_t input)
{
int32_t output = 0, tap = 0;
for(; tap < (f->tap_count-1); tap++)
{
output -= (f->taps[tap] * f->delay_line[f->delay_pos++])<<2;
if(f->delay_pos >= f->tap_count-1)
f->delay_pos = 0;
}
output -= (f->taps[tap] * input)<<2;
f->delay_line[f->delay_pos++] = input;
if(f->delay_pos >= f->tap_count-1)
f->delay_pos = 0;
return output;
}
// Apply delay line and component volume
static inline int32_t delay(struct qsound_delay *d, int32_t input)
{
int32_t output;
d->delay_line[d->write_pos++] = input>>16;
if(d->write_pos >= 51)
d->write_pos = 0;
output = d->delay_line[d->read_pos++]*d->volume;
if(d->read_pos >= 51)
d->read_pos = 0;
return output;
}
// Update the delay read position to match new delay length
static inline void delay_update(struct qsound_delay *d)
{
int16_t new_read_pos = (d->write_pos - d->delay) % 51;
if(new_read_pos < 0)
new_read_pos += 51;
d->read_pos = new_read_pos;
}

View file

@ -0,0 +1,121 @@
#ifndef QSOUND_H
#define QSOUND_H
#ifdef __cplusplus
extern "C" {
#endif
/*
Capcom DL-1425 QSound emulator
==============================
by superctr (Ian Karlsson)
with thanks to Valley Bell
2018-05-12 - 2018-05-15
*/
#include <stdint.h>
struct qsound_voice {
uint16_t bank;
int16_t addr; // top word is the sample address
uint16_t phase;
uint16_t rate;
int16_t loop_len;
int16_t end_addr;
int16_t volume;
int16_t echo;
};
struct qsound_adpcm {
uint16_t start_addr;
uint16_t end_addr;
uint16_t bank;
int16_t volume;
uint16_t flag;
int16_t cur_vol;
int16_t step_size;
uint16_t cur_addr;
};
// Q1 Filter
struct qsound_fir {
int tap_count; // usually 95
int delay_pos;
int16_t table_pos;
int16_t taps[95];
int16_t delay_line[95];
};
// Delay line
struct qsound_delay {
int16_t delay;
int16_t volume;
int16_t write_pos;
int16_t read_pos;
int16_t delay_line[51];
};
struct qsound_echo {
uint16_t end_pos;
int16_t feedback;
int16_t length;
int16_t last_sample;
int16_t delay_line[1024];
int16_t delay_pos;
};
struct qsound_chip {
unsigned long rom_mask;
uint8_t *rom_data;
uint32_t mute_mask;
uint16_t data_latch;
int16_t out[2];
int16_t pan_tables[2][2][98];
struct qsound_voice voice[16];
struct qsound_adpcm adpcm[3];
uint16_t voice_pan[16+3];
int16_t voice_output[16+3];
struct qsound_echo echo;
struct qsound_fir filter[2];
struct qsound_fir alt_filter[2];
struct qsound_delay wet[2];
struct qsound_delay dry[2];
uint16_t state;
uint16_t next_state;
uint16_t delay_update;
int state_counter;
int ready_flag;
uint16_t *register_map[256];
};
long qsound_start(struct qsound_chip *chip, int clock);
void qsound_reset(struct qsound_chip *chip);
void qsound_update(struct qsound_chip *chip);
uint8_t qsound_stream_update(struct qsound_chip *chip, int16_t **outputs, int samples);
uint8_t qsound_w(struct qsound_chip *chip, uint8_t offset, uint8_t data);
uint8_t qsound_r(struct qsound_chip *chip);
void qsound_write_data(struct qsound_chip *chip, uint8_t address, uint16_t data);
uint16_t qsound_read_data(struct qsound_chip *chip, uint8_t address);
#ifdef __cplusplus
};
#endif
#endif

View file

@ -109,6 +109,9 @@ const char* cmdName[DIV_CMD_MAX]={
"SAA_ENVELOPE", "SAA_ENVELOPE",
"QSOUND_ECHO_FEEDBACK",
"QSOUND_ECHO_LEVEL",
"ALWAYS_SET_VOLUME" "ALWAYS_SET_VOLUME"
}; };
@ -218,6 +221,18 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe
return false; return false;
} }
break; break;
case DIV_SYSTEM_QSOUND:
switch (effect) {
case 0x10: // echo feedback
dispatchCmd(DivCommand(DIV_CMD_QSOUND_ECHO_FEEDBACK,ch,effectVal));
break;
case 0x11: // echo level
dispatchCmd(DivCommand(DIV_CMD_QSOUND_ECHO_LEVEL,ch,effectVal));
break;
default:
return false;
}
break;
default: default:
return false; return false;
} }

View file

@ -33,7 +33,7 @@ struct DivSample {
// - 16: 16-bit PCM // - 16: 16-bit PCM
unsigned char depth; unsigned char depth;
short* data; short* data;
unsigned int rendLength, adpcmRendLength, rendOff, rendOffP, rendOffContiguous; unsigned int rendLength, adpcmRendLength, rendOff, rendOffP, rendOffContiguous, rendOffQsound;
short* rendData; short* rendData;
unsigned char* adpcmRendData; unsigned char* adpcmRendData;
@ -54,6 +54,7 @@ struct DivSample {
rendOff(0), rendOff(0),
rendOffP(0), rendOffP(0),
rendOffContiguous(0), rendOffContiguous(0),
rendOffQsound(0),
rendData(NULL), rendData(NULL),
adpcmRendData(NULL) {} adpcmRendData(NULL) {}
~DivSample(); ~DivSample();

View file

@ -87,6 +87,8 @@ enum DivSystem {
DIV_SYSTEM_YM2610_FULL, DIV_SYSTEM_YM2610_FULL,
DIV_SYSTEM_YM2610_FULL_EXT, DIV_SYSTEM_YM2610_FULL_EXT,
DIV_SYSTEM_OPLL_DRUMS, DIV_SYSTEM_OPLL_DRUMS,
DIV_SYSTEM_QSOUND
}; };
struct DivSong { struct DivSong {
@ -210,6 +212,12 @@ struct DivSong {
// - 1: Amiga 1200 // - 1: Amiga 1200
// - bit 8-14: stereo separation // - bit 8-14: stereo separation
// - 0 is 0% while 127 is 100% // - 0 is 0% while 127 is 100%
// - QSound:
// - bit 12-20: echo feedback
// - Valid values are 0-255
// - bit 0-11: echo delay length
// - Valid values are 0-2725
// - 0 is max length, 2725 is min length
unsigned int systemFlags[32]; unsigned int systemFlags[32];
// song information // song information

View file

@ -129,6 +129,8 @@ DivSystem DivEngine::systemFromFile(unsigned char val) {
return DIV_SYSTEM_YM2610_FULL_EXT; return DIV_SYSTEM_YM2610_FULL_EXT;
case 0xa7: case 0xa7:
return DIV_SYSTEM_OPLL_DRUMS; return DIV_SYSTEM_OPLL_DRUMS;
case 0xe0:
return DIV_SYSTEM_QSOUND;
} }
return DIV_SYSTEM_NULL; return DIV_SYSTEM_NULL;
} }
@ -242,6 +244,8 @@ unsigned char DivEngine::systemToFile(DivSystem val) {
return 0xa6; return 0xa6;
case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_OPLL_DRUMS:
return 0xa7; return 0xa7;
case DIV_SYSTEM_QSOUND:
return 0xe0;
case DIV_SYSTEM_NULL: case DIV_SYSTEM_NULL:
return 0; return 0;
@ -354,6 +358,8 @@ int DivEngine::getChannelCount(DivSystem sys) {
return 17; return 17;
case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_OPLL_DRUMS:
return 11; return 11;
case DIV_SYSTEM_QSOUND:
return 19;
} }
return 0; return 0;
} }
@ -474,6 +480,8 @@ const char* DivEngine::getSystemName(DivSystem sys) {
return "Yamaha OPL3 with drums"; return "Yamaha OPL3 with drums";
case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_OPLL_DRUMS:
return "Yamaha OPLL with drums"; return "Yamaha OPLL with drums";
case DIV_SYSTEM_QSOUND:
return "Capcom QSound";
} }
return "Unknown"; return "Unknown";
} }
@ -589,6 +597,8 @@ const char* DivEngine::getSystemChips(DivSystem sys) {
return "Yamaha YM2610 (extended channel 2)"; return "Yamaha YM2610 (extended channel 2)";
case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_OPLL_DRUMS:
return "Yamaha YM2413 with drums"; return "Yamaha YM2413 with drums";
case DIV_SYSTEM_QSOUND:
return "Capcom DL-1425";
} }
return "Unknown"; return "Unknown";
} }
@ -668,7 +678,7 @@ bool DivEngine::isSTDSystem(DivSystem sys) {
sys!=DIV_SYSTEM_YM2151); sys!=DIV_SYSTEM_YM2151);
} }
const char* chanNames[36][24]={ const char* chanNames[37][24]={
{"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8", "Channel 9", "Channel 10", "Channel 11", "Channel 12", "Channel 13", "Channel 14", "Channel 15", "Channel 16", "PCM"}, // YMU759/SegaPCM {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8", "Channel 9", "Channel 10", "Channel 11", "Channel 12", "Channel 13", "Channel 14", "Channel 15", "Channel 16", "PCM"}, // YMU759/SegaPCM
{"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis
{"FM 1", "FM 2", "FM 3 OP1", "FM 3 OP2", "FM 3 OP3", "FM 3 OP4", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis (extended channel 3) {"FM 1", "FM 2", "FM 3 OP1", "FM 3 OP2", "FM 3 OP3", "FM 3 OP4", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis (extended channel 3)
@ -705,9 +715,10 @@ const char* chanNames[36][24]={
{"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8", "FM 9", "FM 10", "FM 11", "FM 12", "FM 13", "FM 14", "FM 15", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 drums {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8", "FM 9", "FM 10", "FM 11", "FM 12", "FM 13", "FM 14", "FM 15", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 drums
{"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6"}, // OPL3 4-op {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6"}, // OPL3 4-op
{"FM 1", "FM 2", "FM 3", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 4-op + drums {"FM 1", "FM 2", "FM 3", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 4-op + drums
{"PCM 1", "PCM 2", "PCM 3", "PCM 4", "PCM 5", "PCM 6", "PCM 7", "PCM 8", "PCM 9", "PCM 10", "PCM 11", "PCM 12", "PCM 13", "PCM 14", "PCM 15", "PCM 16", "ADPCM 1", "ADPCM 2", "ADPCM 3"}, // QSound
}; };
const char* chanShortNames[36][24]={ const char* chanShortNames[37][24]={
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "PCM"}, // YMU759 {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "PCM"}, // YMU759
{"F1", "F2", "F3", "F4", "F5", "F6", "S1", "S2", "S3", "NO"}, // Genesis {"F1", "F2", "F3", "F4", "F5", "F6", "S1", "S2", "S3", "NO"}, // Genesis
{"F1", "F2", "O1", "O2", "O3", "O4", "F4", "F5", "F6", "S1", "S2", "S3", "S4"}, // Genesis (extended channel 3) {"F1", "F2", "O1", "O2", "O3", "O4", "F4", "F5", "F6", "S1", "S2", "S3", "S4"}, // Genesis (extended channel 3)
@ -744,9 +755,10 @@ const char* chanShortNames[36][24]={
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "BD", "SD", "TM", "TP", "HH"}, // OPL3 drums {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "BD", "SD", "TM", "TP", "HH"}, // OPL3 drums
{"F1", "F2", "F3", "F4", "F5", "F6", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6"}, // OPL3 4-op {"F1", "F2", "F3", "F4", "F5", "F6", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6"}, // OPL3 4-op
{"F1", "F2", "F3", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "BD", "SD", "TM", "TP", "HH"}, // OPL3 4-op + drums {"F1", "F2", "F3", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "BD", "SD", "TM", "TP", "HH"}, // OPL3 4-op + drums
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "A1", "A2", "A3"}, // QSound
}; };
const int chanTypes[36][24]={ const int chanTypes[37][24]={
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}, // YMU759 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}, // YMU759
{0, 0, 0, 0, 0, 0, 1, 1, 1, 2}, // Genesis {0, 0, 0, 0, 0, 0, 1, 1, 1, 2}, // Genesis
{0, 0, 5, 5, 5, 5, 0, 0, 0, 1, 1, 1, 2}, // Genesis (extended channel 3) {0, 0, 5, 5, 5, 5, 0, 0, 0, 1, 1, 1, 2}, // Genesis (extended channel 3)
@ -783,9 +795,10 @@ const int chanTypes[36][24]={
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 drums {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 drums
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // OPL3 4-op {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // OPL3 4-op
{0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 4-op + drums {0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 4-op + drums
{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, // QSound
}; };
const DivInstrumentType chanPrefType[42][24]={ const DivInstrumentType chanPrefType[43][24]={
{DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM}, // YMU759 {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM}, // YMU759
{DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis
{DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis (extended channel 3) {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis (extended channel 3)
@ -828,6 +841,7 @@ const DivInstrumentType chanPrefType[42][24]={
{DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER}, // ZX beeper {DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER}, // ZX beeper
{DIV_INS_SWAN, DIV_INS_SWAN, DIV_INS_SWAN, DIV_INS_SWAN}, // Swan {DIV_INS_SWAN, DIV_INS_SWAN, DIV_INS_SWAN, DIV_INS_SWAN}, // Swan
{DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ}, // Z {DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ}, // Z
{DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // QSound
}; };
const char* DivEngine::getChannelName(int chan) { const char* DivEngine::getChannelName(int chan) {
@ -956,6 +970,9 @@ const char* DivEngine::getChannelName(int chan) {
case DIV_SYSTEM_AY8930: case DIV_SYSTEM_AY8930:
return chanNames[17][dispatchChanOfChan[chan]]; return chanNames[17][dispatchChanOfChan[chan]];
break; break;
case DIV_SYSTEM_QSOUND:
return chanNames[36][dispatchChanOfChan[chan]];
break;
} }
return "??"; return "??";
} }
@ -1086,6 +1103,9 @@ const char* DivEngine::getChannelShortName(int chan) {
case DIV_SYSTEM_AY8930: case DIV_SYSTEM_AY8930:
return chanShortNames[17][dispatchChanOfChan[chan]]; return chanShortNames[17][dispatchChanOfChan[chan]];
break; break;
case DIV_SYSTEM_QSOUND:
return chanShortNames[36][dispatchChanOfChan[chan]];
break;
} }
return "??"; return "??";
} }
@ -1214,6 +1234,9 @@ int DivEngine::getChannelType(int chan) {
case DIV_SYSTEM_AY8930: case DIV_SYSTEM_AY8930:
return chanTypes[17][dispatchChanOfChan[chan]]; return chanTypes[17][dispatchChanOfChan[chan]];
break; break;
case DIV_SYSTEM_QSOUND:
return chanTypes[36][dispatchChanOfChan[chan]];
break;
} }
return 1; return 1;
} }
@ -1355,6 +1378,9 @@ DivInstrumentType DivEngine::getPreferInsType(int chan) {
case DIV_SYSTEM_OPZ: case DIV_SYSTEM_OPZ:
return chanPrefType[41][dispatchChanOfChan[chan]]; return chanPrefType[41][dispatchChanOfChan[chan]];
break; break;
case DIV_SYSTEM_QSOUND:
return chanPrefType[42][dispatchChanOfChan[chan]];
break;
} }
return DIV_INS_FM; return DIV_INS_FM;
} }
@ -1375,6 +1401,7 @@ bool DivEngine::isVGMExportable(DivSystem which) {
case DIV_SYSTEM_AY8910: case DIV_SYSTEM_AY8910:
case DIV_SYSTEM_AY8930: case DIV_SYSTEM_AY8930:
case DIV_SYSTEM_SAA1099: case DIV_SYSTEM_SAA1099:
case DIV_SYSTEM_QSOUND:
return true; return true;
default: default:
return false; return false;

View file

@ -260,6 +260,28 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write
w->writeC(0); w->writeC(0);
} }
break; break;
case DIV_SYSTEM_QSOUND:
for(int i=0; i<16; i++) {
w->writeC(0xc4);
w->writeC(0);
w->writeC(0);
w->writeC(2+(i*8));
w->writeC(0xc4);
w->writeC(0);
w->writeC(0);
w->writeC(6+(i*8));
}
for(int i=0; i<3; i++) {
w->writeC(0xc4);
w->writeC(0);
w->writeC(0);
w->writeC(0xcd+(i*4));
w->writeC(0xc4);
w->writeC(0x00);
w->writeC(0x01);
w->writeC(0xd6+i);
}
break;
default: default:
break; break;
} }
@ -377,6 +399,12 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write
w->writeC((isSecond?0x80:0)|(write.addr&0xff)); w->writeC((isSecond?0x80:0)|(write.addr&0xff));
w->writeC(write.val); w->writeC(write.val);
break; break;
case DIV_SYSTEM_QSOUND:
w->writeC(0xc4);
w->writeC((write.val >> 8)&0xff);
w->writeC(write.val&0xff);
w->writeC(write.addr&0xff);
break;
default: default:
logW("write not handled!\n"); logW("write not handled!\n");
break; break;
@ -489,6 +517,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) {
bool writePCESamples=false; bool writePCESamples=false;
bool writeADPCM=false; bool writeADPCM=false;
bool writeSegaPCM=false; bool writeSegaPCM=false;
bool writeQSound=false;
for (int i=0; i<song.systemLen; i++) { for (int i=0; i<song.systemLen; i++) {
willExport[i]=false; willExport[i]=false;
@ -668,6 +697,20 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) {
howManyChips++; howManyChips++;
} }
break; break;
case DIV_SYSTEM_QSOUND:
if (!hasQSound) {
// could set chipClock to 4000000 here for compatibility
// However I think it it not necessary because old VGM players will still
// not be able to handle the 64kb sample bank trick
hasQSound=disCont[i].dispatch->chipClock;
willExport[i]=true;
writeQSound=true;
} else if (!(hasQSound&0x40000000)) {
isSecond[i]=true;
willExport[i]=false;
addWarning("dual QSound is not supported by the VGM format");
}
break;
default: default:
break; break;
} }
@ -900,6 +943,20 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) {
w->write(adpcmMem,adpcmMemLen); w->write(adpcmMem,adpcmMemLen);
} }
if (writeQSound && qsoundMemLen>0) {
// always write a whole bank
unsigned int blockSize=(qsoundMemLen + 0xffff) & ~0xffff;
if(blockSize > 0x1000000)
blockSize = 0x1000000;
w->writeC(0x67);
w->writeC(0x66);
w->writeC(0x8F);
w->writeI(blockSize+8);
w->writeI(0x1000000);
w->writeI(0);
w->write(qsoundMem,blockSize);
}
// initialize streams // initialize streams
int streamID=0; int streamID=0;
for (int i=0; i<song.systemLen; i++) { for (int i=0; i<song.systemLen; i++) {

View file

@ -1267,11 +1267,11 @@ void FurnaceGUI::drawSampleEdit() {
ImGui::Text("Length: %d",sample->length); ImGui::Text("Length: %d",sample->length);
if (ImGui::InputInt("Rate (Hz)",&sample->rate,10,200)) { if (ImGui::InputInt("Rate (Hz)",&sample->rate,10,200)) {
if (sample->rate<100) sample->rate=100; if (sample->rate<100) sample->rate=100;
if (sample->rate>32000) sample->rate=32000; if (sample->rate>96000) sample->rate=96000;
} }
if (ImGui::InputInt("Pitch of C-4 (Hz)",&sample->centerRate,10,200)) { if (ImGui::InputInt("Pitch of C-4 (Hz)",&sample->centerRate,10,200)) {
if (sample->centerRate<100) sample->centerRate=100; if (sample->centerRate<100) sample->centerRate=100;
if (sample->centerRate>32000) sample->centerRate=32000; if (sample->centerRate>65535) sample->centerRate=65535;
} }
ImGui::Text("effective rate: %dHz",e->getEffectiveSampleRate(sample->rate)); ImGui::Text("effective rate: %dHz",e->getEffectiveSampleRate(sample->rate));
bool doLoop=(sample->loopStart>=0); bool doLoop=(sample->loopStart>=0);
@ -1486,7 +1486,7 @@ void FurnaceGUI::drawVolMeter() {
ImGui::End(); ImGui::End();
} }
const char* aboutLine[93]={ const char* aboutLine[94]={
"tildearrow", "tildearrow",
"is proud to present", "is proud to present",
"", "",
@ -1548,6 +1548,7 @@ const char* aboutLine[93]={
"puNES by FHorse", "puNES by FHorse",
"reSID by Dag Lem", "reSID by Dag Lem",
"Stella by Stella Team", "Stella by Stella Team",
"QSound emulator by Ian Karlsson and Valley Bell",
"", "",
"greetings to:", "greetings to:",
"Delek", "Delek",
@ -1898,12 +1899,16 @@ void FurnaceGUI::drawStats() {
if (ImGui::Begin("Statistics",&statsOpen)) { if (ImGui::Begin("Statistics",&statsOpen)) {
String adpcmUsage=fmt::sprintf("%d/16384KB",e->adpcmMemLen/1024); String adpcmUsage=fmt::sprintf("%d/16384KB",e->adpcmMemLen/1024);
String adpcmBUsage=fmt::sprintf("%d/16384KB",e->adpcmBMemLen/1024); String adpcmBUsage=fmt::sprintf("%d/16384KB",e->adpcmBMemLen/1024);
String qsoundUsage=fmt::sprintf("%d/16384KB",e->qsoundMemLen/1024);
ImGui::Text("ADPCM-A"); ImGui::Text("ADPCM-A");
ImGui::SameLine(); ImGui::SameLine();
ImGui::ProgressBar(((float)e->adpcmMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmUsage.c_str()); ImGui::ProgressBar(((float)e->adpcmMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmUsage.c_str());
ImGui::Text("ADPCM-B"); ImGui::Text("ADPCM-B");
ImGui::SameLine(); ImGui::SameLine();
ImGui::ProgressBar(((float)e->adpcmBMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmBUsage.c_str()); ImGui::ProgressBar(((float)e->adpcmBMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmBUsage.c_str());
ImGui::Text("QSound");
ImGui::SameLine();
ImGui::ProgressBar(((float)e->qsoundMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),qsoundUsage.c_str());
} }
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_STATS; if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_STATS;
ImGui::End(); ImGui::End();
@ -2085,7 +2090,9 @@ void FurnaceGUI::drawRegView() {
for (int i=0; i<e->song.systemLen; i++) { for (int i=0; i<e->song.systemLen; i++) {
ImGui::Text("%d. %s",i+1,getSystemName(e->song.system[i])); ImGui::Text("%d. %s",i+1,getSystemName(e->song.system[i]));
int size=0; int size=0;
unsigned char* regPool=e->getRegisterPool(i,size); int depth=8;
unsigned char* regPool=e->getRegisterPool(i,size,depth);
unsigned short* regPoolW=(unsigned short*) regPool;
if (regPool==NULL) { if (regPool==NULL) {
ImGui::Text("- no register pool available"); ImGui::Text("- no register pool available");
} else { } else {
@ -2104,7 +2111,12 @@ void FurnaceGUI::drawRegView() {
for (int j=0; j<16; j++) { for (int j=0; j<16; j++) {
ImGui::TableNextColumn(); ImGui::TableNextColumn();
if (i*16+j>=size) continue; if (i*16+j>=size) continue;
if(depth == 8)
ImGui::Text("%.2x",regPool[i*16+j]); ImGui::Text("%.2x",regPool[i*16+j]);
else if(depth == 16)
ImGui::Text("%.4x",regPoolW[i*16+j]);
else
ImGui::Text("??");
} }
} }
ImGui::EndTable(); ImGui::EndTable();
@ -4475,6 +4487,7 @@ bool FurnaceGUI::loop() {
sysAddOption(DIV_SYSTEM_TIA); sysAddOption(DIV_SYSTEM_TIA);
sysAddOption(DIV_SYSTEM_SAA1099); sysAddOption(DIV_SYSTEM_SAA1099);
sysAddOption(DIV_SYSTEM_AY8930); sysAddOption(DIV_SYSTEM_AY8930);
sysAddOption(DIV_SYSTEM_QSOUND);
ImGui::EndMenu(); ImGui::EndMenu();
} }
if (ImGui::BeginMenu("configure system...")) { if (ImGui::BeginMenu("configure system...")) {
@ -4640,6 +4653,23 @@ bool FurnaceGUI::loop() {
} }
break; break;
} }
case DIV_SYSTEM_QSOUND: {
ImGui::Text("Echo delay:");
int echoBufSize=2725 - (flags & 4095);
if (ImGui::SliderInt("##EchoBufSize",&echoBufSize,0,2725)) {
if (echoBufSize<0) echoBufSize=0;
if (echoBufSize>2725) echoBufSize=2725;
e->setSysFlags(i,(flags & ~4095) | ((2725 - echoBufSize) & 4095),restart);
}
ImGui::Text("Echo feedback:");
int echoFeedback=(flags>>12)&255;
if (ImGui::SliderInt("##EchoFeedback",&echoFeedback,0,255)) {
if (echoFeedback<0) echoFeedback=0;
if (echoFeedback>255) echoFeedback=255;
e->setSysFlags(i,(flags & ~(255 << 12)) | ((echoFeedback & 255) << 12),restart);
}
break;
}
case DIV_SYSTEM_GB: case DIV_SYSTEM_GB:
case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610:
case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_EXT:
@ -4682,6 +4712,7 @@ bool FurnaceGUI::loop() {
sysChangeOption(i,DIV_SYSTEM_TIA); sysChangeOption(i,DIV_SYSTEM_TIA);
sysChangeOption(i,DIV_SYSTEM_SAA1099); sysChangeOption(i,DIV_SYSTEM_SAA1099);
sysChangeOption(i,DIV_SYSTEM_AY8930); sysChangeOption(i,DIV_SYSTEM_AY8930);
sysChangeOption(i,DIV_SYSTEM_QSOUND);
ImGui::EndMenu(); ImGui::EndMenu();
} }
} }