FPGA implementation

An investigation into the behaviour of the Yamaha OPL3 chipset.
With a view to a more accurate emulator

Re: FPGA implementation

Postby sto » Sun Sep 27, 2015 7:09 pm

Hey,

I've read that comments about the rhythm mode problems in your Descent video, and played around with it again, and I think I have found an acceptable solution with this commit: https://github.com/stohrendorf/ppplay/c ... 5331a1c8b3

I have also checked the noise generation logic, and it's definitively the one from the OPL3, not the OPL2 one from DBOPL or MAME. I have also exchanged the snare drum and top cymbal operators, because this feels more natural when they're using the same phase register, and I think this also sounds much more like the sound samples I have found so far.
sto
 
Posts: 60
Joined: Thu Nov 08, 2012 4:33 am

Re: FPGA implementation

Postby sto » Mon Sep 28, 2015 3:39 pm

Sorry, false alert. Descent uses MIDI-style music, where each logical MIDI channel can be mapped to multiple physical channels (slots). The default strategy, when running out of channels, is to stop rhythm sounds, which is because some snares seem to be missing in your DRO dump. The sounds are fully feedback-driven, so that there is no rhythm mode involved at all.
sto
 
Posts: 60
Joined: Thu Nov 08, 2012 4:33 am

Re: FPGA implementation

Postby synthop » Mon Sep 28, 2015 4:20 pm

Okay that makes sense, sort of. If Descent is not using rhythm mode, wouldn't the dropouts occur on any channel really? I think I may have noticed this occurring on other songs, but I'm not totally sure due to my lack of having original hardware. Also there are other sources of error such as the DosBOX DRO dump and the imfplay software port, so hard to say. I'll still go ahead and implement your rhythm mode changes.

So you think the dropouts would have matched the original OPL3? It would be so nice to have some old original hardware to compare. I have very little room though in our NYC apartment...
synthop
 
Posts: 33
Joined: Sun Oct 26, 2014 9:21 pm
Location: Brooklyn, New York, USA

Re: FPGA implementation

Postby sto » Mon Sep 28, 2015 6:22 pm

The timing may indeed affect the feedback "noise timbre" due to timing differences of the KON events, but that should only be audible in very few cases.

About the percussion: If you think about it, it's a better experience to loose some short HHs or SDs than to loose full notes of melody. If you don't listen very consciously (like it's mostly the case when playing a game), your brain even starts to fill the gaps automatically, or you may think it's only the song writer's decision to put some dynamics into the song. Even Duke Nukem 3D did this trick; I converted their MIDI engine for testing purposes some months ago, and it's dropping percussion samples when running out of physical channels.

By the way, Monster Bash (http://www.vgmpf.com/Wiki/index.php?tit ... _%28DOS%29) uses rhythm instruments, which I use for comparison.
sto
 
Posts: 60
Joined: Thu Nov 08, 2012 4:33 am

Re: FPGA implementation

Postby sto » Tue Oct 06, 2015 5:52 am

Seems like I finally found an acceptable implementation for the rhythm mode. As I can't commit the code right now, I'll post the relevant code here.

Operator handling:
Code: Select all
int16_t Operator::handleTopCymbal( uint8_t ws )
{
    // The Top Cymbal operator uses its own phase together with the High Hat phase.

    const uint16_t hhPhase = m_opl->highHatOperator()->m_phase;
    const uint16_t phaseBit = ( ( ( hhPhase & 0x88 ) ^ ( ( hhPhase << 5 ) & 0x80 ) ) | ( ( hhPhase ^ ( hhPhase << 2 ) ) & 0x20 ) ) ? 0x02 : 0x00;

    auto phase = ( 1 + phaseBit ) << 8;
    return getOutput( phase, ws );
}

int16_t Operator::handleHighHat( uint8_t ws )
{
    const uint16_t phaseBit = ( ( ( m_phase & 0x88 ) ^ ( ( m_phase << 5 ) & 0x80 ) ) | ( ( m_phase ^ ( m_phase << 2 ) ) & 0x20 ) ) ? 0x02 : 0x00;
    const uint16_t noiseBit = m_opl->randBit() << 1;

    auto phase = ( phaseBit << 8 ) | ( 0x34 << ( phaseBit ^ noiseBit ) );
    return getOutput( phase, ws );
}

int16_t Operator::handleSnareDrum( uint8_t ws )
{
    const uint16_t hhPhase = m_opl->highHatOperator()->m_phase;
    const uint16_t noiseBit = m_opl->randBit() << 8;
    auto phase = ( 0x100 + ( hhPhase & 0x100 ) ) ^ noiseBit;
    return getOutput( phase, ws );
}

int16_t Operator::nextSample( uint16_t modulator )
{
    const bool isRhythm = m_opl->ryt() && m_operatorBaseAddress>=0x10 && m_operatorBaseAddress<=0x15;
    m_envelopeGenerator.advance( m_egt && !isRhythm, m_am );
    m_phase = m_phaseGenerator.advance( m_vib );

    // If it is in OPL2 mode, use first four waveforms only:
    const uint8_t ws = m_opl->isNew() ? m_ws : ( m_ws & 0x03 );

    if( isRhythm ) {
        static constexpr int BassDrumOperator1 = 0x10; // Channel 7, operator 13
        static constexpr int HighHatOperator   = 0x11; // Channel 8, operator 14
        static constexpr int TomTomOperator    = 0x12; // Channel 9, operator 15
        static constexpr int BassDrumOperator2 = 0x13; // Channel 7, operator 16
        static constexpr int SnareDrumOperator = 0x14; // Channel 9, operator 18
        static constexpr int TopCymbalOperator = 0x15; // Channel 8, operator 17

        switch( m_operatorBaseAddress ) {
            case BassDrumOperator1:
            case BassDrumOperator2:
            case TomTomOperator:
                return getOutput( modulator + m_phase, ws );
            case HighHatOperator:
                return handleHighHat( ws ) + modulator;
            case SnareDrumOperator:
                return handleSnareDrum( ws ) + modulator;
            case TopCymbalOperator:
                return handleTopCymbal( ws ) + modulator;
        }
    }

    return getOutput( modulator + m_phase, ws );
}


Slot handling:
Code: Select all
void Channel::nextSample2Op( std::array< int16_t, 4 >* dest )
{
    int16_t channelOutput = m_operators[0]->nextSample( feedback() );
    pushFeedback( channelOutput );

    if( !m_cnt ) {
        // CNT = 0, the operators are in series, with the first in feedback.
        channelOutput = m_operators[1]->nextSample( channelOutput );
    }
    else {
        // CNT = 1, the operators are in parallel, with the first in feedback.
        if( isRhythmChannel() && m_operators[0] == m_opl->bassDrumOp1() )
            channelOutput = 0; // the first bass drum operator is ignored when in parallel
        channelOutput += m_operators[1]->nextSample();
    }
    if(isRhythmChannel())
        channelOutput *= 2;

    getInFourChannels( dest, channelOutput );
}


Please note that the highhat phase is used everywhere, and that there is no phase manipulation anymore. TC and HH use the same phase bit, but different noise calculations, and SD uses a completely different noise calculation without phase bit calculation. Also, in rhythm mode, there's no feedback calculated at all, except for TT and BD, and the sample output is doubled in any case.

The SD sounds a bit off sometimes, but I believe this could be an issue of the player code.
sto
 
Posts: 60
Joined: Thu Nov 08, 2012 4:33 am

Previous

Return to Yamaha OPL-3 research

Who is online

Users browsing this forum: No registered users and 1 guest