diff --git a/CMakeLists.txt b/CMakeLists.txt index b1324e27c..e2e268244 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,8 @@ src/engine/platform/sound/c64/wave8580_PST.cc src/engine/platform/sound/c64/wave8580_P_T.cc src/engine/platform/sound/c64/wave8580__ST.cc +src/engine/platform/sound/tia/TIASnd.cpp + src/engine/platform/sound/ymfm/ymfm_adpcm.cpp src/engine/platform/sound/ymfm/ymfm_opm.cpp src/engine/platform/sound/ymfm/ymfm_opn.cpp @@ -119,6 +121,7 @@ src/engine/platform/ym2610.cpp src/engine/platform/ym2610ext.cpp src/engine/platform/ay.cpp src/engine/platform/ay8930.cpp +src/engine/platform/tia.cpp src/engine/platform/dummy.cpp) set(GUI_SOURCES diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index ef59ae63d..6480f0216 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -11,6 +11,7 @@ #include "platform/ym2610ext.h" #include "platform/ay.h" #include "platform/ay8930.h" +#include "platform/tia.h" #include "platform/dummy.h" #include "../ta-log.h" @@ -120,6 +121,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_AY8930: dispatch=new DivPlatformAY8930; break; + case DIV_SYSTEM_TIA: + dispatch=new DivPlatformTIA; + break; default: logW("this system is not supported yet! using dummy platform.\n"); dispatch=new DivPlatformDummy; diff --git a/src/engine/platform/sound/tia/TIASnd.cpp b/src/engine/platform/sound/tia/TIASnd.cpp new file mode 100644 index 000000000..22cf733b1 --- /dev/null +++ b/src/engine/platform/sound/tia/TIASnd.cpp @@ -0,0 +1,372 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2016 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +// +// $Id: TIASnd.cxx 3239 2015-12-29 19:22:46Z stephena $ +//============================================================================ + +#include "TIATables.h" +#include "TIASnd.h" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +TIASound::TIASound(int outputFrequency) + : myChannelMode(Hardware2Stereo), + myOutputFrequency(outputFrequency), + myVolumePercentage(100) +{ + reset(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIASound::reset() +{ + // Fill the polynomials + polyInit(Bit4, 4, 4, 3); + polyInit(Bit5, 5, 5, 3); + polyInit(Bit9, 9, 9, 5); + + // Initialize instance variables + for(int chan = 0; chan <= 1; ++chan) + { + myVolume[chan] = 0; + myDivNCnt[chan] = 0; + myDivNMax[chan] = 0; + myDiv3Cnt[chan] = 3; + myAUDC[chan] = 0; + myAUDF[chan] = 0; + myAUDV[chan] = 0; + myP4[chan] = 0; + myP5[chan] = 0; + myP9[chan] = 0; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIASound::outputFrequency(int freq) +{ + myOutputFrequency = freq; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +std::string TIASound::channels(unsigned int hardware, bool stereo) +{ + if(hardware == 1) + myChannelMode = Hardware1; + else + myChannelMode = stereo ? Hardware2Stereo : Hardware2Mono; + + switch(myChannelMode) + { + case Hardware1: return "Hardware1"; + case Hardware2Mono: return "Hardware2Mono"; + case Hardware2Stereo: return "Hardware2Stereo"; + default: return ""; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIASound::set(unsigned short address, unsigned char value) +{ + int chan = ~address & 0x1; + switch(address) + { + case TIARegister::AUDC0: + case TIARegister::AUDC1: + myAUDC[chan] = value & 0x0f; + break; + + case TIARegister::AUDF0: + case TIARegister::AUDF1: + myAUDF[chan] = value & 0x1f; + break; + + case TIARegister::AUDV0: + case TIARegister::AUDV1: + myAUDV[chan] = (value & 0x0f) << AUDV_SHIFT; + break; + + default: + return; + } + + unsigned short newVal = 0; + + // An AUDC value of 0 is a special case + if (myAUDC[chan] == SET_TO_1 || myAUDC[chan] == POLY5_POLY5) + { + // Indicate the clock is zero so no processing will occur, + // and set the output to the selected volume + newVal = 0; + myVolume[chan] = (myAUDV[chan] * myVolumePercentage) / 100; + } + else + { + // Otherwise calculate the 'divide by N' value + newVal = myAUDF[chan] + 1; + + // If bits 2 & 3 are set, then multiply the 'div by n' count by 3 + if((myAUDC[chan] & DIV3_MASK) == DIV3_MASK && myAUDC[chan] != POLY5_DIV3) + newVal *= 3; + } + + // Only reset those channels that have changed + if(newVal != myDivNMax[chan]) + { + // Reset the divide by n counters + myDivNMax[chan] = newVal; + + // If the channel is now volume only or was volume only, + // reset the counter (otherwise let it complete the previous) + if ((myDivNCnt[chan] == 0) || (newVal == 0)) + myDivNCnt[chan] = newVal; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +unsigned char TIASound::get(unsigned short address) const +{ + switch(address) + { + case TIARegister::AUDC0: return myAUDC[0]; + case TIARegister::AUDC1: return myAUDC[1]; + case TIARegister::AUDF0: return myAUDF[0]; + case TIARegister::AUDF1: return myAUDF[1]; + case TIARegister::AUDV0: return myAUDV[0] >> AUDV_SHIFT; + case TIARegister::AUDV1: return myAUDV[1] >> AUDV_SHIFT; + default: return 0; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIASound::volume(unsigned int percent) +{ + if(percent <= 100) + myVolumePercentage = percent; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIASound::process(short* buffer, unsigned int samples) +{ + // Make temporary local copy + unsigned char audc0 = myAUDC[0], audc1 = myAUDC[1]; + unsigned char p5_0 = myP5[0], p5_1 = myP5[1]; + unsigned char div_n_cnt0 = myDivNCnt[0], div_n_cnt1 = myDivNCnt[1]; + short v0 = myVolume[0], v1 = myVolume[1]; + + // Take external volume into account + short audv0 = (myAUDV[0] * myVolumePercentage) / 100, + audv1 = (myAUDV[1] * myVolumePercentage) / 100; + + // Loop until the sample buffer is full + while(samples > 0) + { + // Process channel 0 + if (div_n_cnt0 > 1) + { + div_n_cnt0--; + } + else if (div_n_cnt0 == 1) + { + int prev_bit5 = Bit5[p5_0]; + div_n_cnt0 = myDivNMax[0]; + + // The P5 counter has multiple uses, so we increment it here + p5_0++; + if (p5_0 == POLY5_SIZE) + p5_0 = 0; + + // Check clock modifier for clock tick + if ((audc0 & 0x02) == 0 || + ((audc0 & 0x01) == 0 && Div31[p5_0]) || + ((audc0 & 0x01) == 1 && Bit5[p5_0]) || + ((audc0 & 0x0f) == POLY5_DIV3 && Bit5[p5_0] != prev_bit5)) + { + if (audc0 & 0x04) // Pure modified clock selected + { + if ((audc0 & 0x0f) == POLY5_DIV3) // POLY5 -> DIV3 mode + { + if ( Bit5[p5_0] != prev_bit5 ) + { + myDiv3Cnt[0]--; + if ( !myDiv3Cnt[0] ) + { + myDiv3Cnt[0] = 3; + v0 = v0 ? 0 : audv0; + } + } + } + else + { + // If the output was set turn it off, else turn it on + v0 = v0 ? 0 : audv0; + } + } + else if (audc0 & 0x08) // Check for p5/p9 + { + if (audc0 == POLY9) // Check for poly9 + { + // Increase the poly9 counter + myP9[0]++; + if (myP9[0] == POLY9_SIZE) + myP9[0] = 0; + + v0 = Bit9[myP9[0]] ? audv0 : 0; + } + else if ( audc0 & 0x02 ) + { + v0 = (v0 || audc0 & 0x01) ? 0 : audv0; + } + else // Must be poly5 + { + v0 = Bit5[p5_0] ? audv0 : 0; + } + } + else // Poly4 is the only remaining option + { + // Increase the poly4 counter + myP4[0]++; + if (myP4[0] == POLY4_SIZE) + myP4[0] = 0; + + v0 = Bit4[myP4[0]] ? audv0 : 0; + } + } + } + + // Process channel 1 + if (div_n_cnt1 > 1) + { + div_n_cnt1--; + } + else if (div_n_cnt1 == 1) + { + int prev_bit5 = Bit5[p5_1]; + + div_n_cnt1 = myDivNMax[1]; + + // The P5 counter has multiple uses, so we increment it here + p5_1++; + if (p5_1 == POLY5_SIZE) + p5_1 = 0; + + // Check clock modifier for clock tick + if ((audc1 & 0x02) == 0 || + ((audc1 & 0x01) == 0 && Div31[p5_1]) || + ((audc1 & 0x01) == 1 && Bit5[p5_1]) || + ((audc1 & 0x0f) == POLY5_DIV3 && Bit5[p5_1] != prev_bit5)) + { + if (audc1 & 0x04) // Pure modified clock selected + { + if ((audc1 & 0x0f) == POLY5_DIV3) // POLY5 -> DIV3 mode + { + if ( Bit5[p5_1] != prev_bit5 ) + { + myDiv3Cnt[1]--; + if ( ! myDiv3Cnt[1] ) + { + myDiv3Cnt[1] = 3; + v1 = v1 ? 0 : audv1; + } + } + } + else + { + // If the output was set turn it off, else turn it on + v1 = v1 ? 0 : audv1; + } + } + else if (audc1 & 0x08) // Check for p5/p9 + { + if (audc1 == POLY9) // Check for poly9 + { + // Increase the poly9 counter + myP9[1]++; + if (myP9[1] == POLY9_SIZE) + myP9[1] = 0; + + v1 = Bit9[myP9[1]] ? audv1 : 0; + } + else if ( audc1 & 0x02 ) + { + v1 = (v1 || audc1 & 0x01) ? 0 : audv1; + } + else // Must be poly5 + { + v1 = Bit5[p5_1] ? audv1 : 0; + } + } + else // Poly4 is the only remaining option + { + // Increase the poly4 counter + myP4[1]++; + if (myP4[1] == POLY4_SIZE) + myP4[1] = 0; + + v1 = Bit4[myP4[1]] ? audv1 : 0; + } + } + } + + short byte = v0 + v1; + switch(myChannelMode) + { + case Hardware2Mono: // mono sampling with 2 hardware channels + *(buffer++) = byte; + *(buffer++) = byte; + samples--; + break; + + case Hardware2Stereo: // stereo sampling with 2 hardware channels + *(buffer++) = v0; + *(buffer++) = v1; + samples--; + break; + + case Hardware1: // mono/stereo sampling with only 1 hardware channel + *(buffer++) = v0 + v1; + samples--; + break; + } + } + + // Save for next round + myP5[0] = p5_0; + myP5[1] = p5_1; + myVolume[0] = v0; + myVolume[1] = v1; + myDivNCnt[0] = div_n_cnt0; + myDivNCnt[1] = div_n_cnt1; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIASound::polyInit(unsigned char* poly, int size, int f0, int f1) +{ + int mask = (1 << size) - 1, x = mask; + + for(int i = 0; i < mask; i++) + { + int bit0 = ( ( size - f0 ) ? ( x >> ( size - f0 ) ) : x ) & 0x01; + int bit1 = ( ( size - f1 ) ? ( x >> ( size - f1 ) ) : x ) & 0x01; + poly[i] = x & 1; + // calculate next bit + x = ( x >> 1 ) | ( ( bit0 ^ bit1 ) << ( size - 1) ); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const unsigned char TIASound::Div31[POLY5_SIZE] = { + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; diff --git a/src/engine/platform/sound/tia/TIASnd.h b/src/engine/platform/sound/tia/TIASnd.h new file mode 100644 index 000000000..1b3877812 --- /dev/null +++ b/src/engine/platform/sound/tia/TIASnd.h @@ -0,0 +1,186 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2016 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +// +// $Id: TIASnd.hxx 3239 2015-12-29 19:22:46Z stephena $ +//============================================================================ + +#ifndef TIASOUND_HXX +#define TIASOUND_HXX + +#include + +/** + This class implements a fairly accurate emulation of the TIA sound + hardware. This class uses code/ideas from z26 and MESS. + + Currently, the sound generation routines work at 31400Hz only. + Resampling can be done by passing in a different output frequency. + + @author Bradford W. Mott, Stephen Anthony, z26 and MESS teams + @version $Id: TIASnd.hxx 3239 2015-12-29 19:22:46Z stephena $ +*/ +class TIASound +{ + public: + /** + Create a new TIA Sound object using the specified output frequency + */ + TIASound(int outputFrequency = 31400); + + public: + /** + Reset the sound emulation to its power-on state + */ + void reset(); + + /** + Set the frequency output samples should be generated at + */ + void outputFrequency(int freq); + + /** + Selects the number of audio channels per sample. There are two factors + to consider: hardware capability and desired mixing. + + @param hardware The number of channels supported by the sound system + @param stereo Whether to output the internal sound signals into 1 + or 2 channels + + @return Status of the channel configuration used + */ + std::string channels(unsigned int hardware, bool stereo); + + public: + /** + Sets the specified sound register to the given value + + @param address Register address + @param value Value to store in the register + */ + void set(unsigned short address, unsigned char value); + + /** + Gets the specified sound register's value + + @param address Register address + */ + unsigned char get(unsigned short address) const; + + /** + Create sound samples based on the current sound register settings + in the specified buffer. NOTE: If channels is set to stereo then + the buffer will need to be twice as long as the number of samples. + + @param buffer The location to store generated samples + @param samples The number of samples to generate + */ + void process(short* buffer, unsigned int samples); + + /** + Set the volume of the samples created (0-100) + */ + void volume(unsigned int percent); + + private: + void polyInit(unsigned char* poly, int size, int f0, int f1); + + private: + // Definitions for AUDCx (15, 16) + enum AUDCxRegister + { + SET_TO_1 = 0x00, // 0000 + POLY4 = 0x01, // 0001 + DIV31_POLY4 = 0x02, // 0010 + POLY5_POLY4 = 0x03, // 0011 + PURE1 = 0x04, // 0100 + PURE2 = 0x05, // 0101 + DIV31_PURE = 0x06, // 0110 + POLY5_2 = 0x07, // 0111 + POLY9 = 0x08, // 1000 + POLY5 = 0x09, // 1001 + DIV31_POLY5 = 0x0a, // 1010 + POLY5_POLY5 = 0x0b, // 1011 + DIV3_PURE = 0x0c, // 1100 + DIV3_PURE2 = 0x0d, // 1101 + DIV93_PURE = 0x0e, // 1110 + POLY5_DIV3 = 0x0f // 1111 + }; + + enum { + POLY4_SIZE = 0x000f, + POLY5_SIZE = 0x001f, + POLY9_SIZE = 0x01ff, + DIV3_MASK = 0x0c, + AUDV_SHIFT = 10 // shift 2 positions for AUDV, + // then another 8 for 16-bit sound + }; + + enum ChannelMode { + Hardware2Mono, // mono sampling with 2 hardware channels + Hardware2Stereo, // stereo sampling with 2 hardware channels + Hardware1 // mono/stereo sampling with only 1 hardware channel + }; + + private: + // Structures to hold the 6 tia sound control bytes + unsigned char myAUDC[2]; // AUDCx (15, 16) + unsigned char myAUDF[2]; // AUDFx (17, 18) + short myAUDV[2]; // AUDVx (19, 1A) + + short myVolume[2]; // Last output volume for each channel + + unsigned char myP4[2]; // Position pointer for the 4-bit POLY array + unsigned char myP5[2]; // Position pointer for the 5-bit POLY array + unsigned short myP9[2]; // Position pointer for the 9-bit POLY array + + unsigned char myDivNCnt[2]; // Divide by n counter. one for each channel + unsigned char myDivNMax[2]; // Divide by n maximum, one for each channel + unsigned char myDiv3Cnt[2]; // Div 3 counter, used for POLY5_DIV3 mode + + ChannelMode myChannelMode; + int myOutputFrequency; + int myOutputCounter; + unsigned int myVolumePercentage; + + /* + Initialize the bit patterns for the polynomials (at runtime). + + The 4bit and 5bit patterns are the identical ones used in the tia chip. + Though the patterns could be packed with 8 bits per byte, using only a + single bit per byte keeps the math simple, which is important for + efficient processing. + */ + unsigned char Bit4[POLY4_SIZE]; + unsigned char Bit5[POLY5_SIZE]; + unsigned char Bit9[POLY9_SIZE]; + + /* + The 'Div by 31' counter is treated as another polynomial because of + the way it operates. It does not have a 50% duty cycle, but instead + has a 13:18 ratio (of course, 13+18 = 31). This could also be + implemented by using counters. + */ + static const unsigned char Div31[POLY5_SIZE]; + + private: + // Following constructors and assignment operators not supported + TIASound(const TIASound&) = delete; + TIASound(TIASound&&) = delete; + TIASound& operator=(const TIASound&) = delete; + TIASound& operator=(TIASound&&) = delete; +}; + +#endif diff --git a/src/engine/platform/sound/tia/TIATables.h b/src/engine/platform/sound/tia/TIATables.h new file mode 100644 index 000000000..782a24de9 --- /dev/null +++ b/src/engine/platform/sound/tia/TIATables.h @@ -0,0 +1,219 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2016 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +// +// $Id: TIATables.hxx 3239 2015-12-29 19:22:46Z stephena $ +//============================================================================ + +#ifndef TIA_TABLES_HXX +#define TIA_TABLES_HXX + +enum TIABit { + P0Bit = 0x01, // Bit for Player 0 + M0Bit = 0x02, // Bit for Missle 0 + P1Bit = 0x04, // Bit for Player 1 + M1Bit = 0x08, // Bit for Missle 1 + BLBit = 0x10, // Bit for Ball + PFBit = 0x20, // Bit for Playfield + ScoreBit = 0x40, // Bit for Playfield score mode + PriorityBit = 0x80 // Bit for Playfield priority +}; + +enum TIAColor { + BKColor = 0, // Color index for Background + PFColor = 1, // Color index for Playfield + P0Color = 2, // Color index for Player 0 + P1Color = 3, // Color index for Player 1 + M0Color = 4, // Color index for Missle 0 + M1Color = 5, // Color index for Missle 1 + BLColor = 6, // Color index for Ball + HBLANKColor = 7 // Color index for HMove blank area +}; + +enum CollisionBit +{ + Cx_M0P1 = 1 << 0, // Missle0 - Player1 collision + Cx_M0P0 = 1 << 1, // Missle0 - Player0 collision + Cx_M1P0 = 1 << 2, // Missle1 - Player0 collision + Cx_M1P1 = 1 << 3, // Missle1 - Player1 collision + Cx_P0PF = 1 << 4, // Player0 - Playfield collision + Cx_P0BL = 1 << 5, // Player0 - Ball collision + Cx_P1PF = 1 << 6, // Player1 - Playfield collision + Cx_P1BL = 1 << 7, // Player1 - Ball collision + Cx_M0PF = 1 << 8, // Missle0 - Playfield collision + Cx_M0BL = 1 << 9, // Missle0 - Ball collision + Cx_M1PF = 1 << 10, // Missle1 - Playfield collision + Cx_M1BL = 1 << 11, // Missle1 - Ball collision + Cx_BLPF = 1 << 12, // Ball - Playfield collision + Cx_P0P1 = 1 << 13, // Player0 - Player1 collision + Cx_M0M1 = 1 << 14 // Missle0 - Missle1 collision +}; + +// TIA Write/Read register names +enum TIARegister { + VSYNC = 0x00, // Write: vertical sync set-clear (D1) + VBLANK = 0x01, // Write: vertical blank set-clear (D7-6,D1) + WSYNC = 0x02, // Write: wait for leading edge of hrz. blank (strobe) + RSYNC = 0x03, // Write: reset hrz. sync counter (strobe) + NUSIZ0 = 0x04, // Write: number-size player-missle 0 (D5-0) + NUSIZ1 = 0x05, // Write: number-size player-missle 1 (D5-0) + COLUP0 = 0x06, // Write: color-lum player 0 (D7-1) + COLUP1 = 0x07, // Write: color-lum player 1 (D7-1) + COLUPF = 0x08, // Write: color-lum playfield (D7-1) + COLUBK = 0x09, // Write: color-lum background (D7-1) + CTRLPF = 0x0a, // Write: cntrl playfield ballsize & coll. (D5-4,D2-0) + REFP0 = 0x0b, // Write: reflect player 0 (D3) + REFP1 = 0x0c, // Write: reflect player 1 (D3) + PF0 = 0x0d, // Write: playfield register byte 0 (D7-4) + PF1 = 0x0e, // Write: playfield register byte 1 (D7-0) + PF2 = 0x0f, // Write: playfield register byte 2 (D7-0) + RESP0 = 0x10, // Write: reset player 0 (strobe) + RESP1 = 0x11, // Write: reset player 1 (strobe) + RESM0 = 0x12, // Write: reset missle 0 (strobe) + RESM1 = 0x13, // Write: reset missle 1 (strobe) + RESBL = 0x14, // Write: reset ball (strobe) + AUDC0 = 0x15, // Write: audio control 0 (D3-0) + AUDC1 = 0x16, // Write: audio control 1 (D4-0) + AUDF0 = 0x17, // Write: audio frequency 0 (D4-0) + AUDF1 = 0x18, // Write: audio frequency 1 (D3-0) + AUDV0 = 0x19, // Write: audio volume 0 (D3-0) + AUDV1 = 0x1a, // Write: audio volume 1 (D3-0) + GRP0 = 0x1b, // Write: graphics player 0 (D7-0) + GRP1 = 0x1c, // Write: graphics player 1 (D7-0) + ENAM0 = 0x1d, // Write: graphics (enable) missle 0 (D1) + ENAM1 = 0x1e, // Write: graphics (enable) missle 1 (D1) + ENABL = 0x1f, // Write: graphics (enable) ball (D1) + HMP0 = 0x20, // Write: horizontal motion player 0 (D7-4) + HMP1 = 0x21, // Write: horizontal motion player 1 (D7-4) + HMM0 = 0x22, // Write: horizontal motion missle 0 (D7-4) + HMM1 = 0x23, // Write: horizontal motion missle 1 (D7-4) + HMBL = 0x24, // Write: horizontal motion ball (D7-4) + VDELP0 = 0x25, // Write: vertical delay player 0 (D0) + VDELP1 = 0x26, // Write: vertical delay player 1 (D0) + VDELBL = 0x27, // Write: vertical delay ball (D0) + RESMP0 = 0x28, // Write: reset missle 0 to player 0 (D1) + RESMP1 = 0x29, // Write: reset missle 1 to player 1 (D1) + HMOVE = 0x2a, // Write: apply horizontal motion (strobe) + HMCLR = 0x2b, // Write: clear horizontal motion registers (strobe) + CXCLR = 0x2c, // Write: clear collision latches (strobe) + + CXM0P = 0x00, // Read collision: D7=(M0,P1); D6=(M0,P0) + CXM1P = 0x01, // Read collision: D7=(M1,P0); D6=(M1,P1) + CXP0FB = 0x02, // Read collision: D7=(P0,PF); D6=(P0,BL) + CXP1FB = 0x03, // Read collision: D7=(P1,PF); D6=(P1,BL) + CXM0FB = 0x04, // Read collision: D7=(M0,PF); D6=(M0,BL) + CXM1FB = 0x05, // Read collision: D7=(M1,PF); D6=(M1,BL) + CXBLPF = 0x06, // Read collision: D7=(BL,PF); D6=(unused) + CXPPMM = 0x07, // Read collision: D7=(P0,P1); D6=(M0,M1) + INPT0 = 0x08, // Read pot port: D7 + INPT1 = 0x09, // Read pot port: D7 + INPT2 = 0x0a, // Read pot port: D7 + INPT3 = 0x0b, // Read pot port: D7 + INPT4 = 0x0c, // Read P1 joystick trigger: D7 + INPT5 = 0x0d // Read P2 joystick trigger: D7 +}; + +/** + The TIA class uses some static tables that aren't dependent on the actual + TIA state. For code organization, it's better to place that functionality + here. + + @author Stephen Anthony + @version $Id: TIATables.hxx 3239 2015-12-29 19:22:46Z stephena $ +*/ +class TIATables +{ + public: + /** + Compute all static tables used by the TIA + */ + static void computeAllTables(); + + // Player mask table + // [suppress mode][nusiz][pixel] + static unsigned char PxMask[2][8][320]; + + // Missle mask table (entries are true or false) + // [number][size][pixel] + // There are actually only 4 possible size combinations on a real system + // The fifth size is used for simulating the starfield effect in + // Cosmic Ark and Stay Frosty + static unsigned char MxMask[8][5][320]; + + // Ball mask table (entries are true or false) + // [size][pixel] + static unsigned char BLMask[4][320]; + + // Playfield mask table for reflected and non-reflected playfields + // [reflect, pixel] + static unsigned int PFMask[2][160]; + + // A mask table which can be used when an object is disabled + static unsigned char DisabledMask[640]; + + // Used to set the collision register to the correct value + static unsigned short CollisionMask[64]; + + // Indicates the update delay associated with poking at a TIA address + static const short PokeDelay[64]; + +#if 0 + // Used to convert value written in a motion register into + // its internal representation + static const int CompleteMotion[76][16]; +#endif + + // Indicates if HMOVE blanks should occur for the corresponding cycle + static const bool HMOVEBlankEnableCycles[76]; + + // Used to reflect a players graphics + static unsigned char GRPReflect[256]; + + // Indicates if player is being reset during delay, display or other times + // [nusiz][old pixel][new pixel] + static signed char PxPosResetWhen[8][160][160]; + + private: + // Compute the collision decode table + static void buildCollisionMaskTable(); + + // Compute the player mask table + static void buildPxMaskTable(); + + // Compute the missle mask table + static void buildMxMaskTable(); + + // Compute the ball mask table + static void buildBLMaskTable(); + + // Compute playfield mask table + static void buildPFMaskTable(); + + // Compute the player reflect table + static void buildGRPReflectTable(); + + // Compute the player position reset when table + static void buildPxPosResetWhenTable(); + + private: + // Following constructors and assignment operators not supported + TIATables() = delete; + TIATables(const TIATables&) = delete; + TIATables(TIATables&&) = delete; + TIATables& operator=(const TIATables&) = delete; + TIATables& operator=(TIATables&&) = delete; +}; + +#endif diff --git a/src/engine/platform/tia.cpp b/src/engine/platform/tia.cpp new file mode 100644 index 000000000..c5707cda2 --- /dev/null +++ b/src/engine/platform/tia.cpp @@ -0,0 +1,260 @@ +#include "tia.h" +#include "../engine.h" +#include +#include + +#define rWrite(a,v) if (!skipRegisterWrites) {tia.set(a,v);} + +void DivPlatformTIA::acquire(short* bufL, short* bufR, size_t start, size_t len) { + tia.process(bufL+start,len); +} + +unsigned char dealWithFreq(unsigned char shape, int base, int pitch) { + int bp=base+pitch; + double mult=0.25*27.5*pow(2.0,double(768+bp)/(256.0*12.0)); + switch (shape) { + case 1: // buzzy + return ceil(31400/(30.6*mult))-1; + break; + case 2: // low buzzy + return ceil(31400/(480*mult))-1; + break; + case 3: // flangy + return ceil(31400/(60*mult))-1; + break; + case 4: case 5: // square + return ceil(31400/(4.05*mult))-1; + break; + case 6: case 7: case 8: case 9: case 10: // pure buzzy/reedy/noise + return ceil(31400/(63*mult))-1; + break; + case 12: case 13: // low square + return round(31400/(4.0*3*mult))-1; + break; + case 14: case 15: // low pure buzzy/reedy + return ceil(31400/(3*63*mult))-1; + break; + } + return 0; +} + +void DivPlatformTIA::tick() { + for (int i=0; i<2; i++) { + chan[i].std.next(); + if (chan[i].std.hadVol) { + chan[i].outVol=chan[i].std.vol-(15-chan[i].vol); + if (chan[i].outVol<0) chan[i].outVol=0; + if (isMuted[i]) { + rWrite(0x19+i,0); + } else { + rWrite(0x19+i,chan[i].outVol&15); + } + } + if (chan[i].std.hadArp) { + if (!chan[i].inPorta) { + if (chan[i].std.arpMode) { + chan[i].baseFreq=chan[i].std.arp<<8; + } else { + chan[i].baseFreq=(chan[i].note+chan[i].std.arp-12)<<8; + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arpMode && chan[i].std.finishedArp) { + chan[i].baseFreq=chan[i].note<<8; + chan[i].freqChanged=true; + } + } + if (chan[i].std.hadWave) { + chan[i].shape=chan[i].std.wave&15; + rWrite(0x15+i,chan[i].shape); + chan[i].freqChanged=true; + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + if (chan[i].insChanged) { + if (!chan[i].std.willWave) { + chan[i].shape=4; + rWrite(0x15+i,chan[i].shape); + } + chan[i].insChanged=false; + } + chan[i].freq=dealWithFreq(chan[i].shape,chan[i].baseFreq,chan[i].pitch); + if (chan[i].shape==4 || chan[i].shape==5) { + if (chan[i].baseFreq<39*256) { + rWrite(0x15+i,6); + chan[i].freq=dealWithFreq(6,chan[i].baseFreq,chan[i].pitch); + } else if (chan[i].baseFreq<59*256) { + rWrite(0x15+i,12); + chan[i].freq=dealWithFreq(12,chan[i].baseFreq,chan[i].pitch); + } else { + rWrite(0x15+i,chan[i].shape); + } + } + if (chan[i].freq>31) chan[i].freq=31; + if (chan[i].keyOff) { + rWrite(0x19+i,0); + } + rWrite(0x17+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 DivPlatformTIA::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + chan[c.chan].baseFreq=c.value<<8; + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + rWrite(0x15+c.chan,chan[c.chan].shape); + chan[c.chan].std.init(ins); + if (isMuted[c.chan]) { + rWrite(0x19+c.chan,0); + } else { + rWrite(0x19+c.chan,chan[c.chan].vol&15); + } + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].keyOff=true; + chan[c.chan].active=false; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_VOLUME: { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.hasVol) { + chan[c.chan].outVol=c.value; + } + if (isMuted[c.chan]) { + rWrite(0x19+c.chan,0); + } else { + rWrite(0x19+c.chan,chan[c.chan].vol&15); + } + break; + } + case DIV_CMD_GET_VOLUME: { + return chan[c.chan].vol; + break; + } + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value) { + chan[c.chan].insChanged=true; + } + chan[c.chan].ins=c.value; + break; + case DIV_CMD_PITCH: { + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_NOTE_PORTA: { + int destFreq=c.value2<<8; + 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: { + chan[c.chan].baseFreq=c.value<<8; + chan[c.chan].freqChanged=true; + break; + } + case DIV_ALWAYS_SET_VOLUME: + return 0; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_CMD_PRE_PORTA: + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_PRE_NOTE: + break; + default: + //printf("WARNING: unimplemented command %d\n",c.cmd); + break; + } + return 1; +} + +void DivPlatformTIA::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + if (isMuted[ch]) { + rWrite(0x19+ch,0); + } else { + rWrite(0x19+ch,chan[ch].outVol&15); + } +} + +void DivPlatformTIA::forceIns() { + for (int i=0; i<2; i++) { + chan[i].insChanged=true; + } +} + +void DivPlatformTIA::reset() { + tia.reset(); + for (int i=0; i<2; i++) { + chan[i]=DivPlatformTIA::Channel(); + chan[i].vol=0x0f; + } +} + +bool DivPlatformTIA::isStereo() { + return false; +} + +bool DivPlatformTIA::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformTIA::notifyInsDeletion(void* ins) { + for (int i=0; i<2; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformTIA::setPAL(bool pal) { + if (pal) { + rate=31250; + } else { + rate=31400; + } +} + +int DivPlatformTIA::init(DivEngine* p, int channels, int sugRate, bool pal) { + parent=p; + skipRegisterWrites=false; + for (int i=0; i<2; i++) { + isMuted[i]=false; + } + tia.channels(1,false); + setPAL(pal); + reset(); + return 2; +} + +void DivPlatformTIA::quit() { +} \ No newline at end of file diff --git a/src/engine/platform/tia.h b/src/engine/platform/tia.h new file mode 100644 index 000000000..893549077 --- /dev/null +++ b/src/engine/platform/tia.h @@ -0,0 +1,37 @@ +#ifndef _TIA_H +#define _TIA_H +#include "../dispatch.h" +#include "../macroInt.h" +#include +#include "sound/tia/TIASnd.h" + +class DivPlatformTIA: public DivDispatch { + protected: + struct Channel { + int freq, baseFreq, pitch; + unsigned char ins, note, shape; + signed char konCycles; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta; + int vol, outVol; + DivMacroInt std; + Channel(): freq(0), baseFreq(0), pitch(0), ins(-1), note(0), shape(4), active(false), insChanged(true), freqChanged(false), keyOn(false), keyOff(false), portaPause(false), inPorta(false), vol(0), outVol(15) {} + }; + Channel chan[2]; + bool isMuted[2]; + TIASound tia; + + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + void setPAL(bool pal); + bool isStereo(); + bool keyOffAffectsArp(int ch); + void notifyInsDeletion(void* ins); + int init(DivEngine* parent, int channels, int sugRate, bool pal); + void quit(); +}; +#endif diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index e497b2aab..508f4f7d1 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -933,7 +933,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // 2. check whether we gonna tick if (cycles<=0) { // we have to tick - unsigned int realPos=(runPos[0]*size)/runtotal[0]; + unsigned int realPos=size-(runLeftG>>8); if (realPos>=size) realPos=size-1; if (song.hilightA>0) { if ((curRow%song.hilightA)==0 && ticks==1) metroTick[realPos]=1; @@ -951,7 +951,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // 3. tick the clock and fill buffers as needed if (cyclesscrH*dpiScale) continue; @@ -1672,7 +1673,7 @@ void FurnaceGUI::drawAbout() { ImGui::PopFont(); aboutScroll+=2; if (++aboutSin>=2400) aboutSin=0; - if (aboutScroll>(42*54+scrH)) aboutScroll=-20; + if (aboutScroll>(42*55+scrH)) aboutScroll=-20; } ImGui::End(); } diff --git a/src/main.cpp b/src/main.cpp index ed6c921a9..9d47eb1fb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -115,6 +115,7 @@ bool pVersion(String) { printf("- Mednafen PCE by Mednafen Team (GPLv2)\n"); printf("- puNES by FHorse (GPLv2)\n"); printf("- reSID by Dag Lem (GPLv2)\n"); + printf("- Stella by Stella Team (GPLv2)\n"); return false; }