view 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 source

// 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;
            }
        }
    }
}