Add WonderSwan support

This commit is contained in:
Natt Akuma 2022-03-06 23:13:47 +07:00
parent 2453426d03
commit 840a6fa306
18 changed files with 1231 additions and 6 deletions

View file

@ -265,6 +265,8 @@ src/engine/platform/sound/lynx/Mikey.cpp
src/engine/platform/sound/qsound.c
src/engine/platform/sound/ws.cpp
src/engine/platform/ym2610Interface.cpp
src/engine/blip_buf.c
@ -306,8 +308,9 @@ src/engine/platform/amiga.cpp
src/engine/platform/pcspkr.cpp
src/engine/platform/segapcm.cpp
src/engine/platform/qsound.cpp
src/engine/platform/dummy.cpp
src/engine/platform/lynx.cpp
src/engine/platform/ws.cpp
src/engine/platform/dummy.cpp
)
if (WIN32)

View file

@ -22,6 +22,7 @@ this is a work-in-progress chiptune tracker compatible with DefleMask modules (.
- Philips SAA1099
- Amiga
- TIA (Atari 2600/7800)
- WonderSwan
- multiple sound chips in a single song!
- clean-room design (guesswork and ABX tests only, no decompilation involved)
- bug/quirk implementation for increased playback accuracy

View file

@ -10,12 +10,13 @@ double-click to open the instrument editor.
every instrument can be renamed and have its type changed.
depending on the instrument type, there are currently 10 different types of an instrument editor:
depending on the instrument type, there are currently 12 different types of an instrument editor:
- [FM synthesis](fm.md) - for use with YM2612, YM2151 and FM block portion of YM2610.
- [Standard](standard.md) - for use with NES and Sega Master System's PSG sound source and its derivatives.
- [Game Boy](game-boy.md) - for use with Game Boy APU.
- [PC Engine/TurboGrafx-16](pce.md) - for use with PC Engine's wavetable synthesizer.
- [WonderSwan](wonderswan.md) - for use with WonderSwan's wavetable synthesizer.
- [AY8930](8930.md) - for use with Microchip AY8930 E-PSG sound source.
- [Commodore 64](c64.md) - for use with Commodore 64 SID.
- [SAA1099](saa.md) - for use with Philips SAA1099 PSG sound source.

View file

@ -0,0 +1,8 @@
# WonderSwan instrument editor
WS instrument editor consists of only four macros, similar to PCE but with different volume and noise range:
- [Volume] - volume sequence
- [Arpeggio] - pitch sequencr
- [Noise] - noise LFSR tap sequence
- [Waveform] - spicifies wavetables sequence

View file

@ -1,5 +1,5 @@
# wavetable editor
Wavetable synthizers, in context of Furnace, are sound sources that operate on extremely short n-bit PCM streams. By extremely short, no more than 256 bytes. This amount of space is nowhere near enough to store an actual sampled sound, it allows certain amount of freedom to define a waveform shape. As of Furnace 0.5.5, wavetable editor affects PC Engine and channel 3 of Game Boy.
Wavetable synthizers, in context of Furnace, are sound sources that operate on extremely short n-bit PCM streams. By extremely short, no more than 256 bytes. This amount of space is nowhere near enough to store an actual sampled sound, it allows certain amount of freedom to define a waveform shape. As of Furnace 0.5.8, wavetable editor affects PC Engine, WonderSwan and channel 3 of Game Boy.
Furnace's wavetable editor is rather simple, you can draw the waveform using mouse or by pasting an MML bit stream in the input field. Maximum wave width (length) is 256 bytes, and maximum wave height (depth) is 256. NOTE: both Game Boy and PCE can handle max 32 byte waveforms as of now, width 16-level height for GB and 32-level height for PCE. If larger wave will be defined for these two systems, it will be squashed to fit in the constrains of the system.
Furnace's wavetable editor is rather simple, you can draw the waveform using mouse or by pasting an MML bit stream in the input field. Maximum wave width (length) is 256 bytes, and maximum wave height (depth) is 256. NOTE: Game Boy, PCE and WonderSwan can handle max 32 byte waveforms as of now, with 16-level height for GB and WS, and 32-level height for PCE. If larger wave will be defined for these two systems, it will be squashed to fit within the constraints of the system.

View file

@ -18,5 +18,6 @@ this is a list of systems that Furnace supports, including each system's effects
- [Atari 2600](tia.md)
- [Philips SAA1099](saa1099.md)
- [Microchip AY8930](ay8930.md)
- [WonderSwan](wonderswan.md)
Furnace also reads .dmf files with the [Yamaha YMU759](ymu759.md) system, but does not emulate the chip at all.

View file

@ -0,0 +1,20 @@
# WonderSwan
A handheld console released only in Japan by Bandai. Designed by the same
people behind Game Boy and Virtual Boy, it has lots of similar elements from
those two systems in the sound department.
It has 4 wavetable channels, one channel could play PCM, the other has hardware
sweep and the other could play noise.
# effects
- `10xx`: change wave.
- `11xx`: setup noise mode (channel 4 only).
- 0: disable.
- 1-8: enable and set tap preset.
- `12xx`: setup sweep period (channel 3 only).
- 0: disable.
- 1-32: enable and set period.
- `13xx`: setup sweep amount (channel 3 only).
- `17xx`: toggle PCM mode (channel 2 only).

View file

@ -109,6 +109,9 @@ enum DivDispatchCmds {
DIV_CMD_QSOUND_ECHO_DELAY,
DIV_CMD_QSOUND_ECHO_LEVEL,
DIV_CMD_WS_SWEEP_TIME,
DIV_CMD_WS_SWEEP_AMOUNT,
DIV_ALWAYS_SET_VOLUME,
DIV_CMD_MAX

View file

@ -41,6 +41,7 @@
#include "platform/pcspkr.h"
#include "platform/segapcm.h"
#include "platform/qsound.h"
#include "platform/ws.h"
#include "platform/dummy.h"
#include "platform/lynx.h"
#include "../ta-log.h"
@ -234,6 +235,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do
case DIV_SYSTEM_SEGAPCM_COMPAT:
dispatch=new DivPlatformSegaPCM;
break;
case DIV_SYSTEM_SWAN:
dispatch=new DivPlatformWS;
break;
default:
logW("this system is not supported yet! using dummy platform.\n");
dispatch=new DivPlatformDummy;

View file

@ -0,0 +1,412 @@
/******************************************************************************/
/* Mednafen - Multi-system Emulator */
/******************************************************************************/
/* sound.cpp - WonderSwan Sound Emulation
** Copyright (C) 2007-2017 Mednafen Team
** Copyright (C) 2016 Alex 'trap15' Marshall - http://daifukkat.su/
**
** 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 "ws.h"
#include <string.h>
#define MK_SAMPLE_CACHE \
{ \
int sample; \
sample = (((wsRAM[(/*(SampleRAMPos << 6) + */(sample_pos[ch] >> 1) + (ch << 4)) ] >> ((sample_pos[ch] & 1) ? 4 : 0)) & 0x0F)); \
sample_cache[ch][0] = sample * ((volume[ch] >> 4) & 0x0F); \
sample_cache[ch][1] = sample * ((volume[ch] >> 0) & 0x0F); \
}
#define MK_SAMPLE_CACHE_NOISE \
{ \
int sample; \
sample = ((nreg & 1) ? 0xF : 0x0); \
sample_cache[ch][0] = sample * ((volume[ch] >> 4) & 0x0F); \
sample_cache[ch][1] = sample * ((volume[ch] >> 0) & 0x0F); \
}
#define MK_SAMPLE_CACHE_VOICE \
{ \
int sample, half; \
sample = volume[ch]; \
half = sample >> 1; \
sample_cache[ch][0] = (voice_volume & 4) ? sample : (voice_volume & 8) ? half : 0; \
sample_cache[ch][1] = (voice_volume & 1) ? sample : (voice_volume & 2) ? half : 0; \
}
#define SYNCSAMPLE(wt) /* \
{ \
int32_t left = sample_cache[ch][0], right = sample_cache[ch][1]; \
WaveSynth.offset_inline(wt, left - last_val[ch][0], sbuf[0]); \
WaveSynth.offset_inline(wt, right - last_val[ch][1], sbuf[1]); \
last_val[ch][0] = left; \
last_val[ch][1] = right; \
} */
#define SYNCSAMPLE_NOISE(wt) SYNCSAMPLE(wt)
void WSwan::SoundUpdate(uint32_t v30mz_timestamp)
{
int32_t run_time;
//printf("%d\n", v30mz_timestamp);
//printf("%02x %02x\n", control, noise_control);
run_time = v30mz_timestamp - last_ts;
for(int y = 0; y < 2; y++)
sbuf[y] = 0;
for(unsigned int ch = 0; ch < 4; ch++)
{
// Channel is disabled?
if(!(control & (1 << ch)))
continue;
if(ch == 1 && (control & 0x20)) // Direct D/A mode?
{
MK_SAMPLE_CACHE_VOICE;
SYNCSAMPLE(v30mz_timestamp);
}
else if(ch == 2 && (control & 0x40) && sweep_value) // Sweep
{
uint32_t tmp_pt = 2048 - period[ch];
uint32_t meow_timestamp = v30mz_timestamp - run_time;
uint32_t tmp_run_time = run_time;
while(tmp_run_time)
{
int32_t sub_run_time = tmp_run_time;
if(sub_run_time > sweep_8192_divider)
sub_run_time = sweep_8192_divider;
sweep_8192_divider -= sub_run_time;
if(sweep_8192_divider <= 0)
{
sweep_8192_divider += 8192;
sweep_counter--;
if(sweep_counter <= 0)
{
sweep_counter = sweep_step + 1;
period[ch] = (period[ch] + (int8_t)sweep_value) & 0x7FF;
}
}
meow_timestamp += sub_run_time;
if(tmp_pt > 4)
{
period_counter[ch] -= sub_run_time;
while(period_counter[ch] <= 0)
{
sample_pos[ch] = (sample_pos[ch] + 1) & 0x1F;
MK_SAMPLE_CACHE;
SYNCSAMPLE(meow_timestamp + period_counter[ch]);
period_counter[ch] += tmp_pt;
}
}
tmp_run_time -= sub_run_time;
}
}
else if(ch == 3 && (control & 0x80) && (noise_control & 0x10)) // Noise
{
uint32_t tmp_pt = 2048 - period[ch];
period_counter[ch] -= run_time;
while(period_counter[ch] <= 0)
{
static const uint8_t stab[8] = { 14, 10, 13, 4, 8, 6, 9, 11 };
nreg = ((nreg << 1) | ((1 ^ (nreg >> 7) ^ (nreg >> stab[noise_control & 0x7])) & 1)) & 0x7FFF;
if(control & 0x80)
{
MK_SAMPLE_CACHE_NOISE;
SYNCSAMPLE_NOISE(v30mz_timestamp + period_counter[ch]);
}
else if(tmp_pt > 4)
{
sample_pos[ch] = (sample_pos[ch] + 1) & 0x1F;
MK_SAMPLE_CACHE;
SYNCSAMPLE(v30mz_timestamp + period_counter[ch]);
}
period_counter[ch] += tmp_pt;
}
}
else
{
uint32_t tmp_pt = 2048 - period[ch];
if(tmp_pt > 4)
{
period_counter[ch] -= run_time;
while(period_counter[ch] <= 0)
{
sample_pos[ch] = (sample_pos[ch] + 1) & 0x1F;
MK_SAMPLE_CACHE;
SYNCSAMPLE(v30mz_timestamp + period_counter[ch]); // - period_counter[ch]);
period_counter[ch] += tmp_pt;
}
}
}
sbuf[0] += sample_cache[ch][0];
sbuf[1] += sample_cache[ch][1];
}
if(HVoiceCtrl & 0x80)
{
int16_t sample = (uint8_t)HyperVoice;
switch(HVoiceCtrl & 0xC)
{
case 0x0: sample = (uint16_t)sample << (8 - (HVoiceCtrl & 3)); break;
case 0x4: sample = (uint16_t)(sample | -0x100) << (8 - (HVoiceCtrl & 3)); break;
case 0x8: sample = (uint16_t)((int8_t)sample) << (8 - (HVoiceCtrl & 3)); break;
case 0xC: sample = (uint16_t)sample << 8; break;
}
// bring back to 11bit, keeping signedness
sample >>= 5;
int32_t left, right;
left = (HVoiceChanCtrl & 0x40) ? sample : 0;
right = (HVoiceChanCtrl & 0x20) ? sample : 0;
// WaveSynth.offset_inline(v30mz_timestamp, left - last_hv_val[0], sbuf[0]);
// WaveSynth.offset_inline(v30mz_timestamp, right - last_hv_val[1], sbuf[1]);
// last_hv_val[0] = left;
// last_hv_val[1] = right;
sbuf[0] += left;
sbuf[1] += right;
}
last_ts = v30mz_timestamp;
}
void WSwan::SoundWrite(uint32_t A, uint8_t V)
{
if(A >= 0x80 && A <= 0x87)
{
int ch = (A - 0x80) >> 1;
if(A & 1)
period[ch] = (period[ch] & 0x00FF) | ((V & 0x07) << 8);
else
period[ch] = (period[ch] & 0x0700) | ((V & 0xFF) << 0);
//printf("Period %d: 0x%04x --- %f\n", ch, period[ch], 3072000.0 / (2048 - period[ch]));
}
else if(A >= 0x88 && A <= 0x8B)
{
volume[A - 0x88] = V;
}
else if(A == 0x8C)
sweep_value = V;
else if(A == 0x8D)
{
sweep_step = V;
sweep_counter = sweep_step + 1;
sweep_8192_divider = 8192;
}
else if(A == 0x8E)
{
//printf("NOISECONTROL: %02x\n", V);
if(V & 0x8)
nreg = 0;
noise_control = V & 0x17;
}
else if(A == 0x90)
{
for(int n = 0; n < 4; n++)
{
if(!(control & (1 << n)) && (V & (1 << n)))
{
period_counter[n] = 1;
sample_pos[n] = 0x1F;
}
}
control = V;
//printf("Sound Control: %02x\n", V);
}
else if(A == 0x91)
{
output_control = V & 0xF;
//printf("%02x, %02x\n", V, (V >> 1) & 3);
}
else if(A == 0x92)
nreg = (nreg & 0xFF00) | (V << 0);
else if(A == 0x93)
nreg = (nreg & 0x00FF) | ((V & 0x7F) << 8);
else if(A == 0x94)
{
voice_volume = V & 0xF;
//printf("%02x\n", V);
}
else switch(A)
{
case 0x6A: HVoiceCtrl = V; break;
case 0x6B: HVoiceChanCtrl = V & 0x6F; break;
case 0x8F: SampleRAMPos = V; break;
case 0x95: HyperVoice = V; break; // Pick a port, any port?!
//default: printf("%04x:%02x\n", A, V); break;
}
}
uint8_t WSwan::SoundRead(uint32_t A)
{
if(A >= 0x80 && A <= 0x87)
{
int ch = (A - 0x80) >> 1;
if(A & 1)
return(period[ch] >> 8);
else
return(period[ch]);
}
else if(A >= 0x88 && A <= 0x8B)
return(volume[A - 0x88]);
else switch(A)
{
default: /*printf("SoundRead: %04x\n", A);*/ return(0);
case 0x6A: return(HVoiceCtrl);
case 0x6B: return(HVoiceChanCtrl);
case 0x8C: return(sweep_value);
case 0x8D: return(sweep_step);
case 0x8E: return(noise_control);
case 0x8F: return(SampleRAMPos);
case 0x90: return(control);
case 0x91: return(output_control | 0x80);
case 0x92: return((nreg >> 0) & 0xFF);
case 0x93: return((nreg >> 8) & 0xFF);
case 0x94: return(voice_volume);
}
}
void WSwan::RAMWrite(uint32_t A, uint8_t V)
{
wsRAM[A & 0x3F] = V;
}
int32_t WSwan::SoundFlush(int16_t *SoundBuf, const int32_t MaxSoundFrames)
{
int32_t FrameCount = 0;
if(SoundBuf)
{
for(int y = 0; y < 2; y++)
{
// sbuf[y]->end_frame(v30mz_timestamp);
// FrameCount = sbuf[y]->read_samples(SoundBuf + y, MaxSoundFrames, true);
int32_t left = sbuf[0];
int32_t right = sbuf[1];
if (left >= 0x400) left = 0x3FF;
else if (left < -0x400) left = -0x400;
if (right >= 0x400) left = 0x3FF;
else if (right < -0x400) left = -0x400;
SoundBuf[0] = (int16_t)left << 5;
SoundBuf[1] = (int16_t)right << 5;
}
}
last_ts = 0;
return(FrameCount);
}
// Call before wsRAM is updated
// void WSwan::SoundCheckRAMWrite(uint32_t A)
// {
// if((A >> 6) == SampleRAMPos)
// SoundUpdate();
// }
// static void RedoVolume(void)
// {
// WaveSynth.volume(2.5);
// }
// void WSwan::SoundInit(void)
// {
// for(int i = 0; i < 2; i++)
// {
// sbuf[i] = new Blip_Buffer();
// sbuf[i]->set_sample_rate(0 ? 0 : 44100, 60);
// sbuf[i]->clock_rate((long)(3072000));
// sbuf[i]->bass_freq(20);
// }
// RedoVolume();
// }
// void WSwan::SoundKill(void)
// {
// for(int i = 0; i < 2; i++)
// {
// if(sbuf[i])
// {
// delete sbuf[i];
// sbuf[i] = NULL;
// }
// }
// }
// bool WSwan::SetSoundRate(uint32_t rate)
// {
// for(int i = 0; i < 2; i++)
// sbuf[i]->set_sample_rate(rate?rate:44100, 60);
// return(true);
// }
void WSwan::SoundReset(void)
{
memset(period, 0, sizeof(period));
memset(volume, 0, sizeof(volume));
voice_volume = 0;
sweep_step = 0;
sweep_value = 0;
noise_control = 0;
control = 0;
output_control = 0;
sweep_8192_divider = 8192;
sweep_counter = 1;
SampleRAMPos = 0;
for(unsigned ch = 0; ch < 4; ch++)
period_counter[ch] = 1;
memset(sample_pos, 0, sizeof(sample_pos));
nreg = 0;
memset(sample_cache, 0, sizeof(sample_cache));
// memset(last_val, 0, sizeof(last_val));
last_v_val = 0;
HyperVoice = 0;
last_hv_val[0] = last_hv_val[1] = 0;
HVoiceCtrl = 0;
HVoiceChanCtrl = 0;
for(int y = 0; y < 2; y++)
// sbuf[y]->clear();
sbuf[y] = 0;
last_ts = 0;
}

View file

@ -0,0 +1,82 @@
/******************************************************************************/
/* Mednafen - Multi-system Emulator */
/******************************************************************************/
/* sound.h - WonderSwan Sound Emulation
** Copyright (C) 2007-2016 Mednafen Team
**
** 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 __WSWAN_SOUND_H
#define __WSWAN_SOUND_H
#include <stdint.h>
class WSwan
{
public:
int32_t SoundFlush(int16_t *SoundBuf, const int32_t MaxSoundFrames);
// void SoundInit(void);
// void SoundKill(void);
// void SetSoundMultiplier(double multiplier);
// bool SetSoundRate(uint32_t rate);
void SoundWrite(uint32_t, uint8_t);
uint8_t SoundRead(uint32_t);
void SoundReset(void);
// void SoundCheckRAMWrite(uint32_t A);
void SoundUpdate(uint32_t);
void RAMWrite(uint32_t, uint8_t);
private:
// Blip_Synth<blip_good_quality, 4096> WaveSynth;
// Blip_Buffer *sbuf[2] = { NULL };
int32_t sbuf[2];
uint16_t period[4];
uint8_t volume[4]; // left volume in upper 4 bits, right in lower 4 bits
uint8_t voice_volume;
uint8_t sweep_step, sweep_value;
uint8_t noise_control;
uint8_t control;
uint8_t output_control;
int32_t sweep_8192_divider;
uint8_t sweep_counter;
uint8_t SampleRAMPos;
int32_t sample_cache[4][2];
int32_t last_v_val;
uint8_t HyperVoice;
int32_t last_hv_val[2];
uint8_t HVoiceCtrl, HVoiceChanCtrl;
int32_t period_counter[4];
// int32_t last_val[4][2]; // Last outputted value, l&r
uint8_t sample_pos[4];
uint16_t nreg;
uint32_t last_ts;
uint8_t wsRAM[64];
int16_t sBuf[2];
};
#endif

522
src/engine/platform/ws.cpp Normal file
View file

@ -0,0 +1,522 @@
/**
* 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 "ws.h"
#include "../engine.h"
#include <math.h>
#define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);}}
#define CHIP_DIVIDER 32
const char* regCheatSheetWS[]={
"CH1_Pitch", "00",
"CH2_Pitch", "02",
"CH3_Pitch", "04",
"CH4_Pitch", "06",
"CH1_Vol", "08",
"CH2_Vol", "09",
"CH3_Vol", "0A",
"CH4_Vol", "0B",
"Sweep_Value", "0C",
"Sweep_Time", "0D",
"Noise", "0E",
"Wave_Base", "0F",
"Ctrl", "10",
"Output", "11",
"Random", "12",
"Voice_Ctrl", "14",
"Wave_Mem", "40",
NULL
};
const char** DivPlatformWS::getRegisterSheet() {
return regCheatSheetWS;
}
const char* DivPlatformWS::getEffectName(unsigned char effect) {
switch (effect) {
case 0x10:
return "10xx: Change waveform";
break;
case 0x11:
return "11xx: Setup noise mode (0: disabled; 1-8: enabled/tap)";
break;
case 0x12:
return "12xx: Setup sweep period (0: disabled; 1-20: enabled/period)";
break;
case 0x13:
return "13xx: Set sweep amount";
break;
case 0x17:
return "17xx: Toggle PCM mode";
break;
}
return NULL;
}
void DivPlatformWS::acquire(short* bufL, short* bufR, size_t start, size_t len) {
for (size_t h=start; h<start+len; h++) {
// PCM part
if (pcm && dacSample!=-1) {
dacPeriod+=dacRate;
while (dacPeriod>rate) {
DivSample* s=parent->getSample(dacSample);
if (s->samples<=0) {
dacSample=-1;
continue;
}
rWrite(0x09,(unsigned char)s->data8[dacPos++]+0x80);
if (dacPos>=s->samples) {
if (s->loopStart>=0 && s->loopStart<=(int)s->samples) {
dacPos=s->loopStart;
} else {
dacSample=-1;
}
}
dacPeriod-=rate;
}
}
// the rest
while (!writes.empty()) {
QueuedWrite w=writes.front();
if (regPool[w.addr]!=w.val) {
if (w.addr<0x40) ws->SoundWrite(w.addr|0x80,w.val);
else ws->RAMWrite(w.addr&0x3f,w.val);
regPool[w.addr]=w.val;
}
writes.pop();
}
int16_t samp[2]{0, 0};
ws->SoundUpdate(16);
ws->SoundFlush(samp, 1);
bufL[h]=samp[0];
bufR[h]=samp[1];
}
}
void DivPlatformWS::updateWave(int ch) {
DivWavetable* wt=parent->getWave(chan[ch].wave);
unsigned char addr=0x40+ch*16;
if (wt->max<1 || wt->len<1) {
for (int i=0; i<16; i++) {
rWrite(addr+i,0);
}
} else {
for (int i=0; i<16; i++) {
unsigned char nibble1=(wt->data[(i*2)*wt->len/32]*15)/wt->max;
unsigned char nibble2=(wt->data[(1+i*2)*wt->len/32]*15)/wt->max;
rWrite(addr+i,nibble1|(nibble2<<4));
}
}
}
void DivPlatformWS::calcAndWriteOutVol(int ch, int env) {
int vl=chan[ch].vol*((chan[ch].pan>>4)&0x0f)*env/225;
int vr=chan[ch].vol*(chan[ch].pan&0x0f)*env/225;
if (ch==1&&pcm) {
vl=(vl>0)?((vl>7)?3:2):0;
vr=(vr>0)?((vr>7)?3:2):0;
chan[1].outVol=vr|(vl<<2);
} else {
chan[ch].outVol=vr|(vl<<4);
}
writeOutVol(ch);
}
void DivPlatformWS::writeOutVol(int ch) {
unsigned char val=isMuted[ch]?0:chan[ch].outVol;
if (ch==1&&pcm) {
rWrite(0x14,val)
} else {
rWrite(0x08+ch,val);
}
}
void DivPlatformWS::tick() {
unsigned char sndCtrl=(pcm?0x20:0)|(sweep?0x40:0)|((noise>0)?0x80:0);
for (int i=0; i<4; i++) {
chan[i].std.next();
if (chan[i].std.hadVol) {
int env=chan[i].std.vol;
if(parent->getIns(chan[i].ins)->type==DIV_INS_AMIGA) {
env=MIN(env/4,15);
}
calcAndWriteOutVol(i,env);
}
if (chan[i].std.hadArp) {
if (!chan[i].inPorta) {
if (chan[i].std.arpMode) {
chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp);
} else {
chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp);
}
}
chan[i].freqChanged=true;
} else {
if (chan[i].std.arpMode && chan[i].std.finishedArp) {
chan[i].baseFreq=NOTE_PERIODIC(chan[i].note);
chan[i].freqChanged=true;
}
}
if (chan[i].std.hadWave && !(i==1 && pcm)) {
if (chan[i].wave!=chan[i].std.wave) {
chan[i].wave=chan[i].std.wave;
updateWave(i);
}
}
if (chan[i].active) {
sndCtrl|=(1<<i);
}
if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) {
chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true);
if (i==1 && furnaceDac) {
double off=1.0;
if (dacSample>=0 && dacSample<parent->song.sampleLen) {
DivSample* s=parent->getSample(dacSample);
if (s->centerRate<1) {
off=1.0;
} else {
off=8363.0/(double)s->centerRate;
}
}
dacRate=((double)chipClock/2)/MAX(1,off*chan[i].freq);
if (dumpWrites) addWrite(0xffff0001,dacRate);
}
if (chan[i].freq>2048) chan[i].freq=2048;
if (chan[i].freq<1) chan[i].freq=1;
int rVal=2048-chan[i].freq;
rWrite(i*2,rVal&0xff);
rWrite(i*2+1,rVal>>8);
if (chan[i].keyOn) {
if (!chan[i].std.hasVol) {
calcAndWriteOutVol(i,15);
}
if (chan[i].wave<0) {
chan[i].wave=0;
updateWave(i);
}
chan[i].keyOn=false;
}
if (chan[i].keyOff) {
chan[i].keyOff=false;
}
chan[i].freqChanged=false;
}
}
if (chan[3].std.hadDuty) {
noise=chan[3].std.duty;
if (noise>0) {
rWrite(0x0e,(noise-1)&0x07|0x18);
sndCtrl|=0x80;
} else {
sndCtrl&=~0x80;
}
}
rWrite(0x10,sndCtrl);
}
int DivPlatformWS::dispatch(DivCommand c) {
switch (c.cmd) {
case DIV_CMD_NOTE_ON: {
DivInstrument* ins=parent->getIns(chan[c.chan].ins);
if (c.chan==1 && ins->type==DIV_INS_AMIGA) {
pcm=true;
} else if (furnaceDac) {
pcm=false;
}
if (c.chan==1 && pcm) {
if (skipRegisterWrites) break;
dacPos=0;
dacPeriod=0;
if (ins->type==DIV_INS_AMIGA) {
dacSample=ins->amiga.initSample;
if (dacSample<0 || dacSample>=parent->song.sampleLen) {
dacSample=-1;
if (dumpWrites) addWrite(0xffff0002,0);
break;
} else {
if (dumpWrites) {
addWrite(0xffff0000,dacSample);
}
}
if (c.value!=DIV_NOTE_NULL) {
chan[1].baseFreq=NOTE_PERIODIC(c.value);
chan[1].freqChanged=true;
chan[1].note=c.value;
}
chan[1].active=true;
chan[1].keyOn=true;
chan[1].std.init(ins);
furnaceDac=true;
} else {
if (c.value!=DIV_NOTE_NULL) {
chan[1].note=c.value;
}
dacSample=12*sampleBank+chan[1].note%12;
if (dacSample>=parent->song.sampleLen) {
dacSample=-1;
if (dumpWrites) addWrite(0xffff0002,0);
break;
} else {
if (dumpWrites) addWrite(0xffff0000,dacSample);
}
dacRate=parent->getSample(dacSample)->rate;
if (dumpWrites) {
addWrite(0xffff0001,dacRate);
}
chan[1].active=true;
chan[1].keyOn=true;
furnaceDac=false;
}
break;
}
if (c.value!=DIV_NOTE_NULL) {
chan[c.chan].baseFreq=NOTE_PERIODIC(c.value);
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:
if (c.chan==1&&pcm) {
dacSample=-1;
if (dumpWrites) addWrite(0xffff0002,0);
pcm=false;
}
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) {
calcAndWriteOutVol(c.chan,15);
}
}
break;
case DIV_CMD_GET_VOLUME:
return chan[c.chan].vol;
break;
case DIV_CMD_PITCH:
chan[c.chan].pitch=c.value;
chan[c.chan].freqChanged=true;
break;
case DIV_CMD_WAVE:
chan[c.chan].wave=c.value;
updateWave(c.chan);
chan[c.chan].keyOn=true;
break;
case DIV_CMD_WS_SWEEP_TIME:
if (c.chan==2) {
if (c.value==0) {
sweep=false;
} else {
sweep=true;
rWrite(0x0d,(c.value-1)&0xff);
}
}
break;
case DIV_CMD_WS_SWEEP_AMOUNT:
if (c.chan==2) {
rWrite(0x0c,c.value&0xff);
}
break;
case DIV_CMD_NOTE_PORTA: {
int destFreq=NOTE_PERIODIC(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_STD_NOISE_MODE:
if (c.chan==3) {
noise=c.value&0xff;
if (noise>0) rWrite(0x0e,(noise-1)&0x07|0x18);
}
break;
case DIV_CMD_SAMPLE_MODE:
if (c.chan==1) pcm=c.value;
break;
case DIV_CMD_SAMPLE_BANK:
sampleBank=c.value;
if (sampleBank>(parent->song.sample.size()/12)) {
sampleBank=parent->song.sample.size()/12;
}
break;
case DIV_CMD_PANNING: {
chan[c.chan].pan=c.value;
if (!chan[c.chan].std.hasVol) {
calcAndWriteOutVol(c.chan,15);
}
break;
}
case DIV_CMD_LEGATO:
chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(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 15;
break;
case DIV_ALWAYS_SET_VOLUME:
return 0;
break;
default:
break;
}
return 1;
}
void DivPlatformWS::muteChannel(int ch, bool mute) {
isMuted[ch]=mute;
writeOutVol(ch);
}
void DivPlatformWS::forceIns() {
for (int i=0; i<4; i++) {
chan[i].insChanged=true;
chan[i].freqChanged=true;
updateWave(i);
writeOutVol(i);
}
}
void* DivPlatformWS::getChanState(int ch) {
return &chan[ch];
}
unsigned char* DivPlatformWS::getRegisterPool() {
// get Random from emulator
regPool[0x12]=ws->SoundRead(0x92);
regPool[0x13]=ws->SoundRead(0x93);
return regPool;
}
int DivPlatformWS::getRegisterPoolSize() {
return 128;
}
void DivPlatformWS::reset() {
while (!writes.empty()) writes.pop();
memset(regPool,0,128);
for (int i=0; i<4; i++) {
chan[i]=Channel();
chan[i].vol=15;
chan[i].pan=0xff;
}
if (dumpWrites) {
addWrite(0xffffffff,0);
}
ws->SoundReset();
pcm=false;
sweep=false;
furnaceDac=false;
noise=0;
dacPeriod=0;
dacRate=0;
dacPos=0;
dacSample=-1;
sampleBank=0;
rWrite(0x0f,0x00); // wave table at 0x0000
rWrite(0x11,0x09); // enable speakers
}
bool DivPlatformWS::isStereo() {
return true;
}
void DivPlatformWS::notifyWaveChange(int wave) {
for (int i=0; i<4; i++) {
if (chan[i].wave==wave) {
updateWave(i);
}
}
}
void DivPlatformWS::notifyInsDeletion(void* ins) {
for (int i=0; i<4; i++) {
chan[i].std.notifyInsDeletion((DivInstrument*)ins);
}
}
void DivPlatformWS::poke(unsigned int addr, unsigned short val) {
rWrite(addr,val);
}
void DivPlatformWS::poke(std::vector<DivRegWrite>& wlist) {
for (DivRegWrite& i: wlist) rWrite(i.addr,i.val);
}
int DivPlatformWS::init(DivEngine* p, int channels, int sugRate, unsigned int flags) {
parent=p;
dumpWrites=false;
skipRegisterWrites=false;
chipClock=3072000;
rate=chipClock/16; // = 192000kHz, should be enough
for (int i=0; i<4; i++) {
isMuted[i]=false;
}
ws=new WSwan();
reset();
return 4;
}
void DivPlatformWS::quit() {
delete ws;
}
DivPlatformWS::~DivPlatformWS() {
}

95
src/engine/platform/ws.h Normal file
View file

@ -0,0 +1,95 @@
/**
* 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 _WS_H
#define _WS_H
#include "../dispatch.h"
#include "../macroInt.h"
#include "sound/ws.h"
#include <queue>
class DivPlatformWS: public DivDispatch {
struct Channel {
int freq, baseFreq, pitch, note;
unsigned char ins, pan;
bool active, insChanged, freqChanged, keyOn, keyOff, inPorta;
int vol, outVol, wave;
DivMacroInt std;
Channel():
freq(0),
baseFreq(0),
pitch(0),
note(0),
ins(-1),
pan(255),
active(false),
insChanged(true),
freqChanged(false),
keyOn(false),
keyOff(false),
inPorta(false),
vol(15),
outVol(15),
wave(-1) {}
};
Channel chan[4];
bool isMuted[4];
bool pcm, sweep, furnaceDac;
unsigned char sampleBank, noise;
int dacPeriod, dacRate;
unsigned int dacPos;
int dacSample;
unsigned char regPool[0x80];
struct QueuedWrite {
unsigned char addr;
unsigned char val;
QueuedWrite(unsigned char a, unsigned char v): addr(a), val(v) {}
};
std::queue<QueuedWrite> writes;
WSwan* ws;
void updateWave(int ch);
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();
void reset();
void forceIns();
void tick();
void muteChannel(int ch, bool mute);
void notifyWaveChange(int wave);
void notifyInsDeletion(void* ins);
bool isStereo();
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();
~DivPlatformWS();
private:
void calcAndWriteOutVol(int ch, int env);
void writeOutVol(int ch);
};
#endif

View file

@ -251,6 +251,27 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe
break;
}
break;
case DIV_SYSTEM_SWAN:
switch (effect) {
case 0x10: // select waveform
dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal));
break;
case 0x11: // noise mode
dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_MODE,ch,effectVal));
break;
case 0x12: // sweep period
dispatchCmd(DivCommand(DIV_CMD_WS_SWEEP_TIME,ch,effectVal));
break;
case 0x13: // sweep amount
dispatchCmd(DivCommand(DIV_CMD_WS_SWEEP_AMOUNT,ch,effectVal));
break;
case 0x17: // PCM enable
dispatchCmd(DivCommand(DIV_CMD_SAMPLE_MODE,ch,(effectVal>0)));
break;
default:
return false;
}
break;
default:
return false;
}

View file

@ -1616,6 +1616,7 @@ bool DivEngine::isVGMExportable(DivSystem which) {
case DIV_SYSTEM_OPLL:
case DIV_SYSTEM_OPLL_DRUMS:
case DIV_SYSTEM_VRC7:
case DIV_SYSTEM_SWAN:
return true;
default:
return false;

View file

@ -443,6 +443,18 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write
w->writeC(write.val&0xff);
w->writeC(write.addr&0xff);
break;
case DIV_SYSTEM_SWAN:
if ((write.addr&0x7f)<0x40) {
w->writeC(0xbc);
w->writeC(baseAddr2|(write.addr&0x3f));
w->writeC(write.val&0xff);
} else {
// (Wave) RAM write
w->writeC(0xc6);
w->writeS(baseAddr2S|(write.addr&0x3f));
w->writeC(write.val&0xff);
}
break;
default:
logW("write not handled!\n");
break;
@ -746,6 +758,21 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) {
addWarning("dual QSound is not supported by the VGM format");
}
break;
case DIV_SYSTEM_SWAN:
if (!hasSwan) {
hasSwan=disCont[i].dispatch->chipClock;
willExport[i]=true;
// funny enough, VGM doesn't have support for WSC's sound DMA by design
// so DAC stream it goes
// since WS has the same PCM format as YM2612 DAC, I can just reuse this flag
writeDACSamples=true;
} else if (!(hasSwan&0x40000000)) {
isSecond[i]=true;
willExport[i]=true;
hasSwan|=0x40000000;
howManyChips++;
}
break;
default:
break;
}
@ -1031,6 +1058,24 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) {
streamID++;
}
break;
case DIV_SYSTEM_SWAN:
w->writeC(0x90);
w->writeC(streamID);
w->writeC(33);
w->writeC(0); // port
w->writeC(isSecond[i]?0x89:0x09); // DAC
w->writeC(0x91);
w->writeC(streamID);
w->writeC(0);
w->writeC(1);
w->writeC(0);
w->writeC(0x92);
w->writeC(streamID);
w->writeI(24000); // default
streamID++;
break;
default:
break;
}

View file

@ -4634,6 +4634,7 @@ bool FurnaceGUI::loop() {
sysAddOption(DIV_SYSTEM_AY8930);
sysAddOption(DIV_SYSTEM_LYNX);
sysAddOption(DIV_SYSTEM_QSOUND);
sysAddOption(DIV_SYSTEM_SWAN);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("configure system...")) {
@ -4979,6 +4980,7 @@ bool FurnaceGUI::loop() {
sysChangeOption(i,DIV_SYSTEM_AY8930);
sysChangeOption(i,DIV_SYSTEM_LYNX);
sysChangeOption(i,DIV_SYSTEM_QSOUND);
sysChangeOption(i,DIV_SYSTEM_SWAN);
ImGui::EndMenu();
}
}

View file

@ -1358,6 +1358,10 @@ void FurnaceGUI::drawInsEdit() {
if (ins->type==DIV_INS_PCE) {
dutyMax=1;
}
if (ins->type==DIV_INS_SWAN) {
dutyLabel="Noise";
dutyMax=8;
}
if (ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPL) {
dutyMax=0;
}
@ -1777,7 +1781,7 @@ void FurnaceGUI::drawWaveEdit() {
DivWavetable* wave=e->song.wave[curWave];
ImGui::Text("Width");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("use a width of 32 on Game Boy and PC Engine.\nany other widths will be scaled during playback.");
ImGui::SetTooltip("use a width of 32 on Game Boy, PC Engine and WonderSwan.\nany other widths will be scaled during playback.");
}
ImGui::SameLine();
ImGui::SetNextItemWidth(128.0f*dpiScale);
@ -1791,7 +1795,7 @@ void FurnaceGUI::drawWaveEdit() {
ImGui::SameLine();
ImGui::Text("Height");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("use a height of:\n- 15 for Game Boy\n- 31 for PC Engine\nany other heights will be scaled during playback.");
ImGui::SetTooltip("use a height of:\n- 15 for Game Boy and WonderSwan\n- 31 for PC Engine\nany other heights will be scaled during playback.");
}
ImGui::SameLine();
ImGui::SetNextItemWidth(128.0f*dpiScale);