Mercurial > pmdwin
diff fmgen/psg.c @ 0:c55ea9478c80
Hello Gensokyo!
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Tue, 21 May 2013 10:29:21 +0200 |
parents | |
children | 8ad174416431 |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/fmgen/psg.c @@ -0,0 +1,358 @@ +// FIXME: move ugly-ass legalese somewhere where it won't be seen +// by anyone other than lawyers. (/dev/null would be ideal but sadly +// we live in an imperfect world). +/* Copyright (c) 2012/2013, Peter Barfuss +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +// Quick, somewhat hacky PSG implementation. Seems to work fine in most cases. +// Known bugs: volume *may* still be off for a lot of samples. Importantly, +// waveform volume is too quiet but setting it at the correct volume makes +// the noise volume too loud and vice-versa. I *think* what I have currently +// is mostly correct (I'm basing this mostly on how good Strawberry Crisis +// sounds with the given settings), but it's possible that more fine-tuning +// is needed. Apart from that, this is probably the sketchiest part of all +// of my emulator code, but then again there's a bit-exact VHDL core of +// the YM2149F/AY-3-8910, so while I do want to make this as good +// as the code in opna.c, it's the lowest-priority of all of the code here. +// --bofh +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> +#include <unistd.h> +#include "op.h" +#include "psg.h" +#define Max(a,b) ((a>b)?a:b) +#define Min(a,b) ((a<b)?a:b) + +// --------------------------------------------------------------------------- +// テーブル +// +int EmitTable[0x20] = { -1, }; +uint enveloptable[16][64] = { 0, }; + +// --------------------------------------------------------------------------- +// PSG reset to power-on defaults +// +void PSGReset(PSG *psg) +{ + int i; + for (i=0; i<14; i++) + PSGSetReg(psg, i, 0); + PSGSetReg(psg, 7, 0xff); + PSGSetReg(psg, 14, 0xff); + PSGSetReg(psg, 15, 0xff); +} + +// --------------------------------------------------------------------------- +// This code is strongly inspired by some random PSG emulator code I found, +// and is probably not the optimal way to define periods. It *is* at least +// among the fastest, given that it uses the hilarious hack of using the +// integer overflow on a 32-bit unsigned integer to compute ""moduli"". +// +void PSGSetClock(PSG *psg, uint32_t clock, uint32_t rate) +{ + psg->tperiodbase = (uint32_t)((1 << toneshift ) / 4.0f * clock / rate); + psg->eperiodbase = (uint32_t)((1 << envshift ) / 4.0f * clock / rate); + + // 各データの更新 + int tmp; + tmp = ((psg->reg[0] + psg->reg[1] * 256) & 0xfff); + psg->speriod[0] = tmp ? psg->tperiodbase / tmp : psg->tperiodbase; + tmp = ((psg->reg[2] + psg->reg[3] * 256) & 0xfff); + psg->speriod[1] = tmp ? psg->tperiodbase / tmp : psg->tperiodbase; + tmp = ((psg->reg[4] + psg->reg[5] * 256) & 0xfff); + psg->speriod[2] = tmp ? psg->tperiodbase / tmp : psg->tperiodbase; + tmp = psg->reg[6] & 0x1f; + psg->nperiod = tmp; + tmp = ((psg->reg[11] + psg->reg[12] * 256) & 0xffff); + psg->eperiod = tmp ? psg->eperiodbase / tmp : psg->eperiodbase * 2; +} + +// --------------------------------------------------------------------------- +// エンベロープ波形テーブル +// +static uint8_t table3[4] = { 0, 1, -1, 0 }; +void MakeEnvelopTable(void) +{ + // 0 lo 1 up 2 down 3 hi + static uint8_t table1[16*2] = + { + 2,0, 2,0, 2,0, 2,0, 1,0, 1,0, 1,0, 1,0, + 2,2, 2,0, 2,1, 2,3, 1,1, 1,3, 1,2, 1,0, + }; + int i, j; + + if (!enveloptable[0][0]) + { + uint* ptr = enveloptable[0]; + + for (i=0; i<16*2; i++) + { + uint8_t v = ((table1[i] & 0x2) ? 31 : 0); + + for (j=0; j<32; j++) + { + *ptr++ = EmitTable[v]; + v += table3[table1[i]]; + } + } + } +} + +// --------------------------------------------------------------------------- +// Sets the channel output mask for the PSG device. +// c is a bitvector where the 3 LSBs are set to 0 to disable a given +// PSG channel and 1 to enable it. +// TODO: Possibly allow enabling tone/noise output for each channel independently? +// +void PSGSetChannelMask(PSG *psg, int c) +{ + int i; + psg->mask = c; + for (i=0; i<3; i++) + psg->olevel[i] = psg->mask & (1 << i) ? EmitTable[(psg->reg[8+i] & 15) * 2 + 1] : 0; +} + +// --------------------------------------------------------------------------- +// Sets the PSG volume. It's a fairly standard dB -> internal linear scale +// conversion, followed by generating a table with volume levels. +// The "magic number" 1.189207115 is just sqrt(sqrt(2)) +// TODO: is this table really needed? We have fast floating-point now. +// +void SetVolumePSG(PSG *psg, int volume) +{ + int i; + float base = 0x4000 / 3.0f * expf((float)M_LN10*(volume / 40.0f)); + for (i=31; i>=2; i--) + { + EmitTable[i] = lrintf(base); + base /= 1.189207115f; + } + EmitTable[1] = 0; + EmitTable[0] = 0; + MakeEnvelopTable(); + + PSGSetChannelMask(psg, psg->mask); +} + +// --------------------------------------------------------------------------- +// PSG register set routine. Mostly just what you'd expect from reading the manual. +// Fairly boring code overall. regnum can be 0 - 15, data can be 0x00 - 0xFF. +// (This should not be surprising - the YM2149F *did* use an 8-bit bus, after all). +// Interesting quirk: the task of register 7 (channel enable/disable) is basically +// entirely duplicated by other registers, to the point where you can basically +// just ignore any writes to register 7 entirely. I save it here in case some +// braindead routine wants to read its value and do something based on that +// (Another curiosity: register 7 on the PSG appears to be the only register +// between *both* the OPNA and the PSG which is actually *read from* by +// pmdwin.cpp and not just written to. Amusingly enough, the only reason +// that it is ever read is so that it can then OR something with what it just read +// and then write that back to register 7. Hilarity). +// HACK ALERT: The output levels for channels 0 and 1 are increased by a factor of 4 +// to make them match the actual chip in loudness, but without causing the noise channel +// to overtake everything in intensity. This is almost certainly wrong, and moreover +// it assumes that channel 2 will be playing back Speak Board effects which usually means +// drum kit only (for the most part, at least), and not being used as a separate tonal +// channel in its own right. To the best of my knowledge, this does hold for all of ZUN's +// songs, however, once you step outside that set of music, it's trivial to find +// all sorts of counterexamples to that assumption. Therefore, this should be fixed ASAP. +// +void PSGSetReg(PSG *psg, uint8_t regnum, uint8_t data) +{ + if (regnum < 0x10) + { + psg->reg[regnum] = data; + switch (regnum) + { + int tmp; + + case 0: // ChA Fine Tune + case 1: // ChA Coarse Tune + tmp = ((psg->reg[0] + psg->reg[1] * 256) & 0xfff); + psg->speriod[0] = tmp ? psg->tperiodbase / tmp : psg->tperiodbase; + break; + + case 2: // ChB Fine Tune + case 3: // ChB Coarse Tune + tmp = ((psg->reg[2] + psg->reg[3] * 256) & 0xfff); + psg->speriod[1] = tmp ? psg->tperiodbase / tmp : psg->tperiodbase; + break; + + case 4: // ChC Fine Tune + case 5: // ChC Coarse Tune + tmp = ((psg->reg[4] + psg->reg[5] * 256) & 0xfff); + psg->speriod[2] = tmp ? psg->tperiodbase / tmp : psg->tperiodbase; + break; + + case 6: // Noise generator control + data &= 0x1f; + psg->nperiod = data; + break; + + case 8: + psg->olevel[0] = psg->mask & 1 ? EmitTable[(data & 15) * 2 + 1] : 0; + break; + + case 9: + psg->olevel[1] = psg->mask & 2 ? EmitTable[(data & 15) * 2 + 1] : 0; + break; + + case 10: + psg->olevel[2] = psg->mask & 4 ? EmitTable[(data & 15) * 2 + 1] : 0; + break; + + case 11: // Envelope period + case 12: + tmp = ((psg->reg[11] + psg->reg[12] * 256) & 0xffff); + psg->eperiod = tmp ? psg->eperiodbase / tmp : psg->eperiodbase * 2; + break; + + case 13: // Envelope shape + psg->ecount = 0; + psg->envelop = enveloptable[data & 15]; + break; + } + } +} + +// --------------------------------------------------------------------------- +// Init code. Set volume to 0, reset the chip, enable all channels, seed the RNG. +// RNG seed lifted from MAME's YM2149F emulation routine, appears to be correct. +// +void PSGInit(PSG *psg) +{ + SetVolumePSG(psg, 0); + psg->rng = 14231; + psg->ncount = 0; + PSGReset(psg); + psg->mask = 0x3f; +} + +// --------------------------------------------------------------------------- +// The main output routine for the PSG emulation. +// dest should be an array of size nsamples, and of type Sample +// (one of int16_t, int32_t or float - any will work here without causing +// clipping/precision problems). +// Everything is implemented using some form of fixed-point arithmetic +// that currently needs no more than 32-bits for its implementation, +// but I'm pretty certain that you can get by with much less than that +// and still have mostly correct-to-fully-correct emulation. +// +// TODO: In the future, test the veracity of the above statement. Moreover, +// if it turns out to be correct, rewrite this routine to not use more than +// the required precision. This is irrelevant for any PC newer than, well, +// a 386DX/68040, but important for efficient hardware implementation. +// +void PSGMix(PSG *psg, Sample* dest, uint32_t nsamples) +{ + uint8_t chenable[3]; + uint8_t r7 = ~psg->reg[7]; + int i; + + if ((r7 & 0x3f) | ((psg->reg[8] | psg->reg[9] | psg->reg[10]) & 0x1f)) { + chenable[0] = (r7 & 0x01) && (psg->speriod[0] <= (1 << toneshift)); + chenable[1] = (r7 & 0x02) && (psg->speriod[1] <= (1 << toneshift)); + chenable[2] = (r7 & 0x04) && (psg->speriod[2] <= (1 << toneshift)); + + int noise, sample; + uint env; + uint* p1 = ((psg->mask & 1) && (psg->reg[ 8] & 0x10)) ? &env : &psg->olevel[0]; + uint* p2 = ((psg->mask & 2) && (psg->reg[ 9] & 0x10)) ? &env : &psg->olevel[1]; + uint* p3 = ((psg->mask & 4) && (psg->reg[10] & 0x10)) ? &env : &psg->olevel[2]; + #define SCOUNT(ch) (psg->scount[ch] >> toneshift) + + if (p1 != &env && p2 != &env && p3 != &env) { + // ノイズ有り + for (i=0; i<nsamples; i++) { + psg->ncount++; + if(psg->ncount >= psg->nperiod) { + if(psg->rng & 1) + psg->rng ^= 0x24000; + psg->rng >>= 1; + psg->ncount = 0; + } + noise = (psg->rng & 1); + sample = 0; + { + int x, y, z; + x = ((SCOUNT(0) & chenable[0]) | ((r7 >> 3) & noise)) - 1; // 0 or -1 + sample += (psg->olevel[0] + x) ^ x; + psg->scount[0] += psg->speriod[0]; + y = ((SCOUNT(1) & chenable[1]) | ((r7 >> 4) & noise)) - 1; + sample += (psg->olevel[1] + y) ^ y; + psg->scount[1] += psg->speriod[1]; + z = ((SCOUNT(2) & chenable[2]) | ((r7 >> 5) & noise)) - 1; + sample += (psg->olevel[2] + z) ^ z; + psg->scount[2] += psg->speriod[2]; + } + dest[0] += sample; + dest += 1; + } + + // エンベロープの計算をさぼった帳尻あわせ + psg->ecount = (psg->ecount >> 8) + (psg->eperiod >> 8) * nsamples; + if (psg->ecount >= (1 << (envshift+6-8))) { + if ((psg->reg[0x0d] & 0x0b) != 0x0a) + psg->ecount |= (1 << (envshift+5-8)); + psg->ecount &= (1 << (envshift+6-8)) - 1; + } + psg->ecount <<= 8; + } else { + // エンベロープあり + for (i=0; i<nsamples; i++) { + psg->ncount++; + if(psg->ncount >= psg->nperiod) { + if(psg->rng & 1) + psg->rng ^= 0x24000; + psg->rng >>= 1; + psg->ncount = 0; + } + noise = (psg->rng & 1); + sample = 0; + { + env = psg->envelop[psg->ecount >> envshift]; + psg->ecount += psg->eperiod; + if (psg->ecount >= (1 << (envshift+6))) { + if ((psg->reg[0x0d] & 0x0b) != 0x0a) + psg->ecount |= (1 << (envshift+5)); + psg->ecount &= (1 << (envshift+6)) - 1; + } + int x, y, z; + x = ((SCOUNT(0) & chenable[0]) | ((r7 >> 3) & noise)) - 1; // 0 or -1 + sample += (*p1 + x) ^ x; + psg->scount[0] += psg->speriod[0]; + y = ((SCOUNT(1) & chenable[1]) | ((r7 >> 4) & noise)) - 1; + sample += (*p2 + y) ^ y; + psg->scount[1] += psg->speriod[1]; + z = ((SCOUNT(2) & chenable[2]) | ((r7 >> 5) & noise)) - 1; + sample += (*p3 + z) ^ z; + psg->scount[2] += psg->speriod[2]; + } + dest[0] += sample; + dest += 1; + } + } + } +} +