/* SPU2-X, A plugin for Emulating the Sound Processing Unit of the Playstation 2 * Developed and maintained by the Pcsx2 Development Team. * * Original portions from SPU2ghz are (c) 2008 by David Quintana [gigaherz] * * SPU2-X is free software: you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * SPU2-X 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with SPU2-X. If not, see . */ // ====================================================================================== // spu2sys.cpp -- Emulation module for the SPU2 'virtual machine' // ====================================================================================== // This module contains (most!) stuff which is directly related to SPU2 emulation. // Contents should be cross-platform compatible whenever possible. #include "Global.h" #include "dma.h" #include "PS2E-spu2.h" // needed until I figure out a nice solution for irqcallback dependencies. short *spu2regs; short *_spu2mem; V_CoreDebug DebugCores[2]; V_Core Cores[2]; V_SPDIF Spdif; s16 OutPos; s16 InputPos; u32 Cycles; int PlayMode; bool has_to_call_irq=false; void SetIrqCall() { has_to_call_irq=true; } __forceinline s16 * __fastcall GetMemPtr(u32 addr) { #ifndef DEBUG_FAST // In case you're wondering, this assert is the reason SPU2-X // runs so incrediously slow in Debug mode. :P jASSUME( addr < 0x100000 ); #endif return (_spu2mem+addr); } __forceinline s16 __fastcall spu2M_Read( u32 addr ) { return *GetMemPtr( addr & 0xfffff ); } // writes a signed value to the SPU2 ram // Invalidates the ADPCM cache in the process. __forceinline void __fastcall spu2M_Write( u32 addr, s16 value ) { // Make sure the cache is invalidated: // (note to self : addr address WORDs, not bytes) addr &= 0xfffff; if( addr >= SPU2_DYN_MEMLINE ) { const int cacheIdx = addr / pcm_WordsPerBlock; pcm_cache_data[cacheIdx].Validated = false; ConLog( " * SPU2 : PcmCache Block Clear at 0x%x (cacheIdx=0x%x)\n", addr, cacheIdx); } *GetMemPtr( addr ) = value; } // writes an unsigned value to the SPU2 ram __inline void __fastcall spu2M_Write( u32 addr, u16 value ) { spu2M_Write( addr, (s16)value ); } V_VolumeLR V_VolumeLR::Max( 0x7FFFFFFF ); V_VolumeSlideLR V_VolumeSlideLR::Max( 0x3FFF, 0x7FFFFFFF ); V_Core::V_Core( int coreidx ) : Index( coreidx ) //LogFile_AutoDMA( NULL ) { /*char fname[128]; sprintf( fname, "logs/adma%d.raw", GetDmaIndex() ); LogFile_AutoDMA = fopen( fname, "wb" );*/ } V_Core::~V_Core() throw() { // Can't use this yet because we dumb V_Core into savestates >_< /*if( LogFile_AutoDMA != NULL ) { fclose( LogFile_AutoDMA ); LogFile_AutoDMA = NULL; }*/ } void V_Core::Reset( int index ) { ConLog( " * SPU2: RESET SPU2 core%d \n", index ); memset( this, 0, sizeof(V_Core) ); const int c = Index = index; Regs.STATX = 0; Regs.ATTR = 0; ExtVol = V_VolumeLR::Max; InpVol = V_VolumeLR::Max; FxVol = V_VolumeLR::Max; MasterVol = V_VolumeSlideLR::Max; memset( &DryGate, -1, sizeof(DryGate) ); memset( &WetGate, -1, sizeof(WetGate) ); Regs.MMIX = 0xFFCF; Regs.VMIXL = 0xFFFFFF; Regs.VMIXR = 0xFFFFFF; Regs.VMIXEL = 0xFFFFFF; Regs.VMIXER = 0xFFFFFF; EffectsStartA = 0xEFFF8 + (0x10000*c); EffectsEndA = 0xEFFFF + (0x10000*c); FxEnable = 0; IRQA = 0xFFFF0; IRQEnable = 1; for( uint v=0; v EffectsEndA ) { pos = EffectsStartA + (offset % EffectsBufferSize); } else if( pos < EffectsStartA ) { pos = EffectsEndA+1 - (offset % EffectsBufferSize ); } return pos; } void V_Core::UpdateFeedbackBuffersA() { RevBuffers.FB_SRC_A0 = EffectsBufferIndexer( Revb.MIX_DEST_A0 - Revb.FB_SRC_A ); RevBuffers.FB_SRC_A1 = EffectsBufferIndexer( Revb.MIX_DEST_A1 - Revb.FB_SRC_A ); } void V_Core::UpdateFeedbackBuffersB() { RevBuffers.FB_SRC_B0 = EffectsBufferIndexer( Revb.MIX_DEST_B0 - Revb.FB_SRC_B ); RevBuffers.FB_SRC_B1 = EffectsBufferIndexer( Revb.MIX_DEST_B1 - Revb.FB_SRC_B ); } void V_Core::UpdateEffectsBufferSize() { const s32 newbufsize = EffectsEndA - EffectsStartA + 1; if( !RevBuffers.NeedsUpdated && (newbufsize == EffectsBufferSize) ) return; RevBuffers.NeedsUpdated = false; EffectsBufferSize = newbufsize; if( EffectsBufferSize <= 0 ) return; // Rebuild buffer indexers. RevBuffers.ACC_SRC_A0 = EffectsBufferIndexer( Revb.ACC_SRC_A0 ); RevBuffers.ACC_SRC_A1 = EffectsBufferIndexer( Revb.ACC_SRC_A1 ); RevBuffers.ACC_SRC_B0 = EffectsBufferIndexer( Revb.ACC_SRC_B0 ); RevBuffers.ACC_SRC_B1 = EffectsBufferIndexer( Revb.ACC_SRC_B1 ); RevBuffers.ACC_SRC_C0 = EffectsBufferIndexer( Revb.ACC_SRC_C0 ); RevBuffers.ACC_SRC_C1 = EffectsBufferIndexer( Revb.ACC_SRC_C1 ); RevBuffers.ACC_SRC_D0 = EffectsBufferIndexer( Revb.ACC_SRC_D0 ); RevBuffers.ACC_SRC_D1 = EffectsBufferIndexer( Revb.ACC_SRC_D1 ); UpdateFeedbackBuffersA(); UpdateFeedbackBuffersB(); RevBuffers.IIR_DEST_A0 = EffectsBufferIndexer( Revb.IIR_DEST_A0 ); RevBuffers.IIR_DEST_A1 = EffectsBufferIndexer( Revb.IIR_DEST_A1 ); RevBuffers.IIR_DEST_B0 = EffectsBufferIndexer( Revb.IIR_DEST_B0 ); RevBuffers.IIR_DEST_B1 = EffectsBufferIndexer( Revb.IIR_DEST_B1 ); RevBuffers.IIR_SRC_A0 = EffectsBufferIndexer( Revb.IIR_SRC_A0 ); RevBuffers.IIR_SRC_A1 = EffectsBufferIndexer( Revb.IIR_SRC_A1 ); RevBuffers.IIR_SRC_B0 = EffectsBufferIndexer( Revb.IIR_SRC_B0 ); RevBuffers.IIR_SRC_B1 = EffectsBufferIndexer( Revb.IIR_SRC_B1 ); RevBuffers.MIX_DEST_A0 = EffectsBufferIndexer( Revb.MIX_DEST_A0 ); RevBuffers.MIX_DEST_A1 = EffectsBufferIndexer( Revb.MIX_DEST_A1 ); RevBuffers.MIX_DEST_B0 = EffectsBufferIndexer( Revb.MIX_DEST_B0 ); RevBuffers.MIX_DEST_B1 = EffectsBufferIndexer( Revb.MIX_DEST_B1 ); } void V_Voice::Start() { if((Cycles-PlayCycle)>=4) { if(StartA&7) { fprintf( stderr, " *** Misaligned StartA %05x!\n",StartA); StartA=(StartA+0xFFFF8)+0x8; } ADSR.Releasing = false; ADSR.Value = 1; ADSR.Phase = 1; PlayCycle = Cycles; SCurrent = 28; LoopMode = 0; LoopFlags = 0; // Setting the loopstart to NextA breaks Squaresoft games (KH2 intro gets crackly) //LoopStartA = StartA; NextA = StartA; Prev1 = 0; Prev2 = 0; PV1 = PV2 = 0; PV3 = PV4 = 0; } else { printf(" *** KeyOn after less than 4 T disregarded.\n"); } } void V_Voice::Stop() { ADSR.Value = 0; ADSR.Phase = 0; } static const int TickInterval = 768; static const int SanityInterval = 4800; u32 TicksCore = 0; u32 TicksThread = 0; __forceinline void TimeUpdate(u32 cClocks) { u32 dClocks = cClocks - lClocks; // Sanity Checks: // It's not totally uncommon for the IOP's clock to jump backwards a cycle or two, and in // such cases we just want to ignore the TimeUpdate call. if( dClocks > (u32)-15 ) return; // But if for some reason our clock value seems way off base (typically due to bad dma // timings from PCSX2), just mix out a little bit, skip the rest, and hope the ship // "rights" itself later on. if( dClocks > TickInterval*SanityInterval ) { ConLog( " * SPU2 > TimeUpdate Sanity Check (Tick Delta: %d) (PS2 Ticks: %d)\n", dClocks/TickInterval, cClocks/TickInterval ); dClocks = TickInterval * SanityInterval; lClocks = cClocks - dClocks; } //UpdateDebugDialog(); //Update Mixing Progress while(dClocks>=TickInterval) { if(has_to_call_irq) { ConLog(" * SPU2: Irq Called (%04x).\n",Spdif.Info); has_to_call_irq=false; if(_irqcallback) _irqcallback(); } // Part of the no core resets hack. See fixme in RegWrite_Core. /*if(Cores[0].InitDelay>0) { Cores[0].InitDelay--; if(Cores[0].InitDelay==0) { Cores[0].Reset(0); } } if(Cores[1].InitDelay>0) { Cores[1].InitDelay--; if(Cores[1].InitDelay==0) { Cores[1].Reset(1); } }*/ #ifndef ENABLE_NEW_IOPDMA_SPU2 //Update DMA4 interrupt delay counter if(Cores[0].DMAICounter>0) { Cores[0].DMAICounter-=TickInterval; if(Cores[0].DMAICounter<=0) { Cores[0].MADR=Cores[0].TADR; Cores[0].DMAICounter=0; if(dma4callback) dma4callback(); } else { Cores[0].MADR+=TickInterval<<1; } } //Update DMA7 interrupt delay counter if(Cores[1].DMAICounter>0) { Cores[1].DMAICounter-=TickInterval; if(Cores[1].DMAICounter<=0) { Cores[1].MADR=Cores[1].TADR; Cores[1].DMAICounter=0; //ConLog( "* SPU2 > DMA 7 Callback! %d\n", Cycles ); if(dma7callback) dma7callback(); } else { Cores[1].MADR+=TickInterval<<1; } } #endif dClocks -= TickInterval; lClocks += TickInterval; Cycles++; // Note: IOP does not use MMX regs, so no need to save them. //SaveMMXRegs(); Mix(); //RestoreMMXRegs(); } } static u16 mask = 0xFFFF; __forceinline void UpdateSpdifMode() { int OPM=PlayMode; u16 last = 0; if(mask&Spdif.Out) { last = mask & Spdif.Out; mask=mask&(~Spdif.Out); } if(Spdif.Out&0x4) // use 24/32bit PCM data streaming { PlayMode=8; ConLog(" * SPU2: WARNING: Possibly CDDA mode set!\n"); return; } if(Spdif.Out&SPDIF_OUT_BYPASS) { PlayMode=2; if(Spdif.Mode&SPDIF_MODE_BYPASS_BITSTREAM) PlayMode=4; //bitstream bypass } else { PlayMode=0; //normal processing if(Spdif.Out&SPDIF_OUT_PCM) { PlayMode=1; } } if(OPM!=PlayMode) { ConLog(" * SPU2: Play Mode Set to %s (%d).\n", (PlayMode==0) ? "Normal" : ((PlayMode==1) ? "PCM Clone" : ((PlayMode==2) ? "PCM Bypass" : "BitStream Bypass")),PlayMode); } } // Converts an SPU2 register volume write into a 32 bit SPU2-X volume. The value is extended // properly into the lower 16 bits of the value to provide a full spectrum of volumes. static s32 GetVol32( u16 src ) { return (((s32)src) << 16 ) | ((src<<1) & 0xffff); } void V_VolumeSlide::RegSet( u16 src ) { Value = GetVol32( src ); } void V_Core::WriteRegPS1( u32 mem, u16 value ) { jASSUME( Index == 0 ); // Valid on Core 0 only! bool show = true; u32 reg = mem & 0xffff; if((reg>=0x1c00)&&(reg<0x1d80)) { //voice values u8 voice = ((reg-0x1c00)>>4); u8 vval = reg&0xf; switch(vval) { case 0: //VOLL (Volume L) Voices[voice].Volume.Left.Mode = 0; Voices[voice].Volume.Left.RegSet( value << 1 ); Voices[voice].Volume.Left.Reg_VOL = value; break; case 1: //VOLR (Volume R) Voices[voice].Volume.Right.Mode = 0; Voices[voice].Volume.Right.RegSet( value << 1 ); Voices[voice].Volume.Right.Reg_VOL = value; break; case 2: Voices[voice].Pitch = value; break; case 3: Voices[voice].StartA = (u32)value<<8; break; case 4: // ADSR1 (Envelope) Voices[voice].ADSR.regADSR1 = value; break; case 5: // ADSR2 (Envelope) Voices[voice].ADSR.regADSR2 = value; break; case 6: Voices[voice].ADSR.Value = ((s32)value<<16) | value; ConLog( "* SPU2: Mysterious ADSR Volume Set to 0x%x", value ); break; case 7: Voices[voice].LoopStartA = (u32)value <<8; break; jNO_DEFAULT; } } else switch(reg) { case 0x1d80:// Mainvolume left MasterVol.Left.Mode = 0; MasterVol.Left.RegSet( value ); break; case 0x1d82:// Mainvolume right MasterVol.Right.Mode = 0; MasterVol.Right.RegSet( value ); break; case 0x1d84:// Reverberation depth left FxVol.Left = GetVol32( value ); break; case 0x1d86:// Reverberation depth right FxVol.Right = GetVol32( value ); break; case 0x1d88:// Voice ON (0-15) SPU2_FastWrite(REG_S_KON,value); break; case 0x1d8a:// Voice ON (16-23) SPU2_FastWrite(REG_S_KON+2,value); break; case 0x1d8c:// Voice OFF (0-15) SPU2_FastWrite(REG_S_KOFF,value); break; case 0x1d8e:// Voice OFF (16-23) SPU2_FastWrite(REG_S_KOFF+2,value); break; case 0x1d90:// Channel FM (pitch lfo) mode (0-15) SPU2_FastWrite(REG_S_PMON,value); break; case 0x1d92:// Channel FM (pitch lfo) mode (16-23) SPU2_FastWrite(REG_S_PMON+2,value); break; case 0x1d94:// Channel Noise mode (0-15) SPU2_FastWrite(REG_S_NON,value); break; case 0x1d96:// Channel Noise mode (16-23) SPU2_FastWrite(REG_S_NON+2,value); break; case 0x1d98:// Channel Reverb mode (0-15) SPU2_FastWrite(REG_S_VMIXEL,value); SPU2_FastWrite(REG_S_VMIXER,value); break; case 0x1d9a:// Channel Reverb mode (16-23) SPU2_FastWrite(REG_S_VMIXEL+2,value); SPU2_FastWrite(REG_S_VMIXER+2,value); break; case 0x1d9c:// Channel Reverb mode (0-15) SPU2_FastWrite(REG_S_VMIXL,value); SPU2_FastWrite(REG_S_VMIXR,value); break; case 0x1d9e:// Channel Reverb mode (16-23) SPU2_FastWrite(REG_S_VMIXL+2,value); SPU2_FastWrite(REG_S_VMIXR+2,value); break; case 0x1da2:// Reverb work area start { u32 val = (u32)value << 8; SPU2_FastWrite(REG_A_ESA, val&0xFFFF); SPU2_FastWrite(REG_A_ESA+2,val>>16); } break; case 0x1da4: IRQA = (u32)value<<8; break; case 0x1da6: TSA = (u32)value<<8; break; case 0x1daa: SPU2_FastWrite(REG_C_ATTR,value); break; case 0x1dae: SPU2_FastWrite(REG_P_STATX,value); break; case 0x1da8:// Spu Write to Memory DmaWrite(value); show=false; break; } if(show) FileLog("[%10d] (!) SPU write mem %08x value %04x\n",Cycles,mem,value); spu2Ru16(mem)=value; } u16 V_Core::ReadRegPS1(u32 mem) { jASSUME( Index == 0 ); // Valid on Core 0 only! bool show=true; u16 value = spu2Ru16(mem); u32 reg = mem&0xffff; if((reg>=0x1c00)&&(reg<0x1d80)) { //voice values u8 voice = ((reg-0x1c00)>>4); u8 vval = reg&0xf; switch(vval) { case 0: //VOLL (Volume L) //value=Voices[voice].VolumeL.Mode; //value=Voices[voice].VolumeL.Value; value = Voices[voice].Volume.Left.Reg_VOL; break; case 1: //VOLR (Volume R) //value=Voices[voice].VolumeR.Mode; //value=Voices[voice].VolumeR.Value; value = Voices[voice].Volume.Right.Reg_VOL; break; case 2: value = Voices[voice].Pitch; break; case 3: value = Voices[voice].StartA; break; case 4: value = Voices[voice].ADSR.regADSR1; break; case 5: value = Voices[voice].ADSR.regADSR2; break; case 6: value = Voices[voice].ADSR.Value >> 16; break; case 7: value = Voices[voice].LoopStartA; break; jNO_DEFAULT; } } else switch(reg) { case 0x1d80: value = MasterVol.Left.Value >> 16; break; case 0x1d82: value = MasterVol.Right.Value >> 16; break; case 0x1d84: value = FxVol.Left >> 16; break; case 0x1d86: value = FxVol.Right >> 16; break; case 0x1d88: value = 0; break; case 0x1d8a: value = 0; break; case 0x1d8c: value = 0; break; case 0x1d8e: value = 0; break; case 0x1d90: value = Regs.PMON&0xFFFF; break; case 0x1d92: value = Regs.PMON>>16; break; case 0x1d94: value = Regs.NON&0xFFFF; break; case 0x1d96: value = Regs.NON>>16; break; case 0x1d98: value = Regs.VMIXEL&0xFFFF; break; case 0x1d9a: value = Regs.VMIXEL>>16; break; case 0x1d9c: value = Regs.VMIXL&0xFFFF; break; case 0x1d9e: value = Regs.VMIXL>>16; break; case 0x1da2: if( value != EffectsStartA>>3 ) { value = EffectsStartA>>3; UpdateEffectsBufferSize(); ReverbX = 0; } break; case 0x1da4: value = IRQA>>3; break; case 0x1da6: value = TSA>>3; break; case 0x1daa: value = SPU2read(REG_C_ATTR); break; case 0x1dae: value = 0; //SPU2read(REG_P_STATX)<<3; break; case 0x1da8: value = DmaRead(); show=false; break; } if(show) FileLog("[%10d] (!) SPU read mem %08x value %04x\n",Cycles,mem,value); return value; } // Ah the joys of endian-specific code! :D static __forceinline void SetHiWord( u32& src, u16 value ) { ((u16*)&src)[1] = value; } static __forceinline void SetLoWord( u32& src, u16 value ) { ((u16*)&src)[0] = value; } static __forceinline void SetHiWord( s32& src, u16 value ) { ((u16*)&src)[1] = value; } static __forceinline void SetLoWord( s32& src, u16 value ) { ((u16*)&src)[0] = value; } static __forceinline u16 GetHiWord( u32& src ) { return ((u16*)&src)[1]; } static __forceinline u16 GetLoWord( u32& src ) { return ((u16*)&src)[0]; } static __forceinline u16 GetHiWord( s32& src ) { return ((u16*)&src)[1]; } static __forceinline u16 GetLoWord( s32& src ) { return ((u16*)&src)[0]; } template< int CoreIdx, int VoiceIdx, int param > static void __fastcall RegWrite_VoiceParams( u16 value ) { const int core = CoreIdx; const int voice = VoiceIdx; V_Voice& thisvoice = Cores[core].Voices[voice]; switch (param) { case 0: //VOLL (Volume L) case 1: //VOLR (Volume R) { V_VolumeSlide& thisvol = (param==0) ? thisvoice.Volume.Left : thisvoice.Volume.Right; thisvol.Reg_VOL = value; if (value & 0x8000) // +Lin/-Lin/+Exp/-Exp { thisvol.Mode = (value & 0xF000)>>12; thisvol.Increment = (value & 0x3F); } else { // Constant Volume mode (no slides or envelopes) // Volumes range from 0x3fff to 0x7fff, with 0x4000 serving as // the "sign" bit, so a simple bitwise extension will do the trick: thisvol.RegSet( value<<1 ); thisvol.Mode = 0; thisvol.Increment = 0; } } break; case 2: thisvoice.Pitch = value; break; case 3: // ADSR1 (Envelope) thisvoice.ADSR.regADSR1 = value; break; case 4: // ADSR2 (Envelope) thisvoice.ADSR.regADSR2 = value; break; case 5: // [Air] : Mysterious ADSR set code. Too bad none of my games ever use it. // (as usual... ) thisvoice.ADSR.Value = (value << 16) | value; ConLog( "* SPU2: Mysterious ADSR Volume Set to 0x%x", value ); break; case 6: thisvoice.Volume.Left.RegSet( value ); break; case 7: thisvoice.Volume.Right.RegSet( value ); break; jNO_DEFAULT; } } template< int CoreIdx, int VoiceIdx, int address > static void __fastcall RegWrite_VoiceAddr( u16 value ) { const int core = CoreIdx; const int voice = VoiceIdx; V_Voice& thisvoice = Cores[core].Voices[voice]; switch (address) { case 0: // SSA (Waveform Start Addr) (hiword, 4 bits only) thisvoice.StartA = ((value & 0x0F) << 16) | (thisvoice.StartA & 0xFFF8); if( IsDevBuild ) DebugCores[core].Voices[voice].lastSetStartA = thisvoice.StartA; break; case 1: // SSA (loword) thisvoice.StartA = (thisvoice.StartA & 0x0F0000) | (value & 0xFFF8); if( IsDevBuild ) DebugCores[core].Voices[voice].lastSetStartA = thisvoice.StartA; break; case 2: thisvoice.LoopStartA = ((value & 0x0F) << 16) | (thisvoice.LoopStartA & 0xFFF8); thisvoice.LoopMode = 3; break; case 3: thisvoice.LoopStartA = (thisvoice.LoopStartA & 0x0F0000) | (value & 0xFFF8); thisvoice.LoopMode = 3; break; case 4: thisvoice.NextA = ((value & 0x0F) << 16) | (thisvoice.NextA & 0xFFF8); break; case 5: thisvoice.NextA = (thisvoice.NextA & 0x0F0000) | (value & 0xFFF8); break; } } template< int CoreIdx, int cAddr > static void __fastcall RegWrite_Core( u16 value ) { const int omem = cAddr; const int core = CoreIdx; V_Core& thiscore = Cores[core]; switch(omem) { case REG__1AC: // ---------------------------------------------------------------------------- // 0x1ac / 0x5ac : direct-write to DMA address : special register (undocumented) // ---------------------------------------------------------------------------- // On the GS, DMAs are actually pushed through a hardware register. Chances are the // SPU works the same way, and "technically" *all* DMA data actually passes through // the HW registers at 0x1ac (core0) and 0x5ac (core1). We handle normal DMAs in // optimized block copy fashion elsewhere, but some games will write this register // directly, so handle those here: // Performance Note: The PS2 Bios uses this extensively right before booting games, // causing massive slowdown if we don't shortcut it here. for( int i=0; i<2; i++ ) { if(Cores[i].IRQEnable && (Cores[i].IRQA == thiscore.TSA)) { Spdif.Info |= 4 << i; SetIrqCall(); } } thiscore.DmaWrite( value ); break; case REG_C_ATTR: { bool irqe = thiscore.IRQEnable; int bit0 = thiscore.AttrBit0; //int bit4 = thiscore.AttrBit4; if( ((value>>15)&1) && (!thiscore.CoreEnabled) && (thiscore.InitDelay==0) ) // on init/reset { // When we have exact cycle update info from the Pcsx2 IOP unit, then use // the more accurate delayed initialization system. ConLog( " * SPU2: Runtime core%d reset requested, (but ignored. Hack.). \n", core ); // Fixme: // Not initializing a core reset here fixes SH Shattered Memories and Silver Surfer audio. // This is a hack, but better than clearing the wrong bits. // Also check the commented out code in TimeUpdate() above. /*if(cyclePtr != NULL) { thiscore.InitDelay = 1; thiscore.Regs.STATX = 0; } else { thiscore.Reset(thiscore.Index); }*/ } thiscore.AttrBit0 =(value>> 0) & 0x01; //1 bit thiscore.DMABits =(value>> 1) & 0x07; //3 bits thiscore.AttrBit4 =(value>> 4) & 0x01; //1 bit thiscore.AttrBit5 =(value>> 5) & 0x01; //1 bit thiscore.IRQEnable =(value>> 6) & 0x01; //1 bit thiscore.FxEnable =(value>> 7) & 0x01; //1 bit thiscore.NoiseClk =(value>> 8) & 0x3f; //6 bits //thiscore.Mute =(value>>14) & 0x01; //1 bit thiscore.Mute=0; thiscore.CoreEnabled=(value>>15) & 0x01; //1 bit thiscore.Regs.ATTR =value&0x7fff; if(value&0x000E) { ConLog(" * SPU2: Core %d ATTR unknown bits SET! value=%04x\n",core,value); } if(thiscore.AttrBit0!=bit0) { ConLog(" * SPU2: ATTR bit 0 set to %d\n",thiscore.AttrBit0); } if(thiscore.IRQEnable!=irqe) { ConLog(" * SPU2: IRQ %s\n",((thiscore.IRQEnable==0)?"disabled":"enabled")); if(!thiscore.IRQEnable) Spdif.Info &= ~(4 << thiscore.Index); } } break; case REG_S_PMON: for( int vc=1; vc<16; ++vc ) thiscore.Voices[vc].Modulated = (value >> vc) & 1; SetLoWord( thiscore.Regs.PMON, value ); break; case (REG_S_PMON + 2): for( int vc=0; vc<8; ++vc ) thiscore.Voices[vc+16].Modulated = (value >> vc) & 1; SetHiWord( thiscore.Regs.PMON, value ); break; case REG_S_NON: for( int vc=0; vc<16; ++vc) thiscore.Voices[vc].Noise = (value >> vc) & 1; SetLoWord( thiscore.Regs.NON, value ); break; case (REG_S_NON + 2): for( int vc=0; vc<8; ++vc ) thiscore.Voices[vc+16].Noise = (value >> vc) & 1; SetHiWord( thiscore.Regs.NON, value ); break; // Games like to repeatedly write these regs over and over with the same value, hence // the shortcut that skips the bitloop if the values are equal. #define vx_SetSomeBits( reg_out, mask_out, hiword ) \ { \ const u32 result = thiscore.Regs.reg_out; \ if( hiword ) \ SetHiWord( thiscore.Regs.reg_out, value ); \ else \ SetLoWord( thiscore.Regs.reg_out, value ); \ if( result == thiscore.Regs.reg_out ) break; \ \ const uint start_bit = hiword ? 16 : 0; \ const uint end_bit = hiword ? 24 : 16; \ for (uint vc=start_bit, vx=1; vc>1]) = value; } break; } } template< int CoreIdx, int addr > static void __fastcall RegWrite_CoreExt( u16 value ) { V_Core& thiscore = Cores[CoreIdx]; const int core = CoreIdx; switch(addr) { // Master Volume Address Write! case REG_P_MVOLL: case REG_P_MVOLR: { V_VolumeSlide& thisvol = (addr==REG_P_MVOLL) ? thiscore.MasterVol.Left : thiscore.MasterVol.Right; if( value & 0x8000 ) // +Lin/-Lin/+Exp/-Exp { thisvol.Mode = (value & 0xE000) / 0x2000; thisvol.Increment = (value & 0x7F); // | ((value & 0x800)/0x10); } else { // Constant Volume mode (no slides or envelopes) // Volumes range from 0x3fff to 0x7fff, with 0x4000 serving as // the "sign" bit, so a simple bitwise extension will do the trick: thisvol.Value = GetVol32( value<<1 ); thisvol.Mode = 0; thisvol.Increment = 0; } thisvol.Reg_VOL = value; } break; case REG_P_EVOLL: thiscore.FxVol.Left = GetVol32( value ); break; case REG_P_EVOLR: thiscore.FxVol.Right = GetVol32( value ); break; case REG_P_AVOLL: thiscore.ExtVol.Left = GetVol32( value ); break; case REG_P_AVOLR: thiscore.ExtVol.Right = GetVol32( value ); break; case REG_P_BVOLL: thiscore.InpVol.Left = GetVol32( value ); break; case REG_P_BVOLR: thiscore.InpVol.Right = GetVol32( value ); break; default: { const int raddr = addr + ((core==1) ? 0x28 : 0 ); *(regtable[raddr>>1]) = value; } break; } } template< int core, int addr > static void __fastcall RegWrite_Reverb( u16 value ) { // Signal to the Reverb code that the effects buffers need to be re-aligned. // This is both simple, efficient, and safe, since we only want to re-align // buffers after both hi and lo words have been written. *(regtable[addr>>1]) = value; Cores[core].RevBuffers.NeedsUpdated = true; } template< int addr > static void __fastcall RegWrite_SPDIF( u16 value ) { *(regtable[addr>>1]) = value; UpdateSpdifMode(); } template< int addr > static void __fastcall RegWrite_Raw( u16 value ) { *(regtable[addr>>1]) = value; } // -------------------------------------------------------------------------------------- // Macros for tbl_reg_writes // -------------------------------------------------------------------------------------- #define VoiceParamsSet( core, voice ) \ RegWrite_VoiceParams, RegWrite_VoiceParams, \ RegWrite_VoiceParams, RegWrite_VoiceParams, \ RegWrite_VoiceParams, RegWrite_VoiceParams, \ RegWrite_VoiceParams, RegWrite_VoiceParams #define VoiceParamsCore( core ) \ VoiceParamsSet(core, 0), VoiceParamsSet(core, 1), VoiceParamsSet(core, 2), VoiceParamsSet(core, 3 ), \ VoiceParamsSet(core, 4), VoiceParamsSet(core, 5), VoiceParamsSet(core, 6), VoiceParamsSet(core, 7 ), \ VoiceParamsSet(core, 8), VoiceParamsSet(core, 9), VoiceParamsSet(core, 10),VoiceParamsSet(core, 11 ), \ VoiceParamsSet(core, 12),VoiceParamsSet(core, 13),VoiceParamsSet(core, 14),VoiceParamsSet(core, 15 ), \ VoiceParamsSet(core, 16),VoiceParamsSet(core, 17),VoiceParamsSet(core, 18),VoiceParamsSet(core, 19 ), \ VoiceParamsSet(core, 20),VoiceParamsSet(core, 21),VoiceParamsSet(core, 22),VoiceParamsSet(core, 23 ) #define VoiceAddrSet( core, voice ) \ RegWrite_VoiceAddr, RegWrite_VoiceAddr, \ RegWrite_VoiceAddr, RegWrite_VoiceAddr, \ RegWrite_VoiceAddr, RegWrite_VoiceAddr #define CoreParamsPair( core, omem ) \ RegWrite_Core, RegWrite_Core #define ReverbPair( core, mem ) \ RegWrite_Reverb, RegWrite_Core #define REGRAW(addr) RegWrite_Raw // -------------------------------------------------------------------------------------- // tbl_reg_writes - Register Write Function Invocation LUT // -------------------------------------------------------------------------------------- typedef void __fastcall RegWriteHandler( u16 value ); static RegWriteHandler * const tbl_reg_writes[0x401] = { VoiceParamsCore(0), // 0x000 -> 0x180 CoreParamsPair(0,REG_S_PMON), CoreParamsPair(0,REG_S_NON), CoreParamsPair(0,REG_S_VMIXL), CoreParamsPair(0,REG_S_VMIXEL), CoreParamsPair(0,REG_S_VMIXR), CoreParamsPair(0,REG_S_VMIXER), RegWrite_Core<0,REG_P_MMIX>, RegWrite_Core<0,REG_C_ATTR>, CoreParamsPair(0,REG_A_IRQA), CoreParamsPair(0,REG_S_KON), CoreParamsPair(0,REG_S_KOFF), CoreParamsPair(0,REG_A_TSA), CoreParamsPair(0,REG__1AC), RegWrite_Core<0,REG_S_ADMAS>, REGRAW(0x1b2), REGRAW(0x1b4), REGRAW(0x1b6), REGRAW(0x1b8), REGRAW(0x1ba), REGRAW(0x1bc), REGRAW(0x1be), // 0x1c0! VoiceAddrSet(0, 0),VoiceAddrSet(0, 1),VoiceAddrSet(0, 2),VoiceAddrSet(0, 3),VoiceAddrSet(0, 4),VoiceAddrSet(0, 5), VoiceAddrSet(0, 6),VoiceAddrSet(0, 7),VoiceAddrSet(0, 8),VoiceAddrSet(0, 9),VoiceAddrSet(0,10),VoiceAddrSet(0,11), VoiceAddrSet(0,12),VoiceAddrSet(0,13),VoiceAddrSet(0,14),VoiceAddrSet(0,15),VoiceAddrSet(0,16),VoiceAddrSet(0,17), VoiceAddrSet(0,18),VoiceAddrSet(0,19),VoiceAddrSet(0,20),VoiceAddrSet(0,21),VoiceAddrSet(0,22),VoiceAddrSet(0,23), CoreParamsPair(0,REG_A_ESA), ReverbPair(0,R_FB_SRC_A), // 0x02E4 // Feedback Source A ReverbPair(0,R_FB_SRC_B), // 0x02E8 // Feedback Source B ReverbPair(0,R_IIR_DEST_A0), // 0x02EC ReverbPair(0,R_IIR_DEST_A1), // 0x02F0 ReverbPair(0,R_ACC_SRC_A0), // 0x02F4 ReverbPair(0,R_ACC_SRC_A1), // 0x02F8 ReverbPair(0,R_ACC_SRC_B0), // 0x02FC ReverbPair(0,R_ACC_SRC_B1), // 0x0300 ReverbPair(0,R_IIR_SRC_A0), // 0x0304 ReverbPair(0,R_IIR_SRC_A1), // 0x0308 ReverbPair(0,R_IIR_DEST_B0), // 0x030C ReverbPair(0,R_IIR_DEST_B1), // 0x0310 ReverbPair(0,R_ACC_SRC_C0), // 0x0314 ReverbPair(0,R_ACC_SRC_C1), // 0x0318 ReverbPair(0,R_ACC_SRC_D0), // 0x031C ReverbPair(0,R_ACC_SRC_D1), // 0x0320 ReverbPair(0,R_IIR_SRC_B1), // 0x0324 ReverbPair(0,R_IIR_SRC_B0), // 0x0328 ReverbPair(0,R_MIX_DEST_A0), // 0x032C ReverbPair(0,R_MIX_DEST_A1), // 0x0330 ReverbPair(0,R_MIX_DEST_B0), // 0x0334 ReverbPair(0,R_MIX_DEST_B1), // 0x0338 RegWrite_Core<0,REG_A_EEA>, NULL, CoreParamsPair(0,REG_S_ENDX), // 0x0340 // End Point passed flag RegWrite_Core<0,REG_P_STATX>, // 0x0344 // Status register? //0x346 here REGRAW(0x346), REGRAW(0x348),REGRAW(0x34A),REGRAW(0x34C),REGRAW(0x34E), REGRAW(0x350),REGRAW(0x352),REGRAW(0x354),REGRAW(0x356), REGRAW(0x358),REGRAW(0x35A),REGRAW(0x35C),REGRAW(0x35E), REGRAW(0x360),REGRAW(0x362),REGRAW(0x364),REGRAW(0x366), REGRAW(0x368),REGRAW(0x36A),REGRAW(0x36C),REGRAW(0x36E), REGRAW(0x370),REGRAW(0x372),REGRAW(0x374),REGRAW(0x376), REGRAW(0x378),REGRAW(0x37A),REGRAW(0x37C),REGRAW(0x37E), REGRAW(0x380),REGRAW(0x382),REGRAW(0x384),REGRAW(0x386), REGRAW(0x388),REGRAW(0x38A),REGRAW(0x38C),REGRAW(0x38E), REGRAW(0x390),REGRAW(0x392),REGRAW(0x394),REGRAW(0x396), REGRAW(0x398),REGRAW(0x39A),REGRAW(0x39C),REGRAW(0x39E), REGRAW(0x3A0),REGRAW(0x3A2),REGRAW(0x3A4),REGRAW(0x3A6), REGRAW(0x3A8),REGRAW(0x3AA),REGRAW(0x3AC),REGRAW(0x3AE), REGRAW(0x3B0),REGRAW(0x3B2),REGRAW(0x3B4),REGRAW(0x3B6), REGRAW(0x3B8),REGRAW(0x3BA),REGRAW(0x3BC),REGRAW(0x3BE), REGRAW(0x3C0),REGRAW(0x3C2),REGRAW(0x3C4),REGRAW(0x3C6), REGRAW(0x3C8),REGRAW(0x3CA),REGRAW(0x3CC),REGRAW(0x3CE), REGRAW(0x3D0),REGRAW(0x3D2),REGRAW(0x3D4),REGRAW(0x3D6), REGRAW(0x3D8),REGRAW(0x3DA),REGRAW(0x3DC),REGRAW(0x3DE), REGRAW(0x3E0),REGRAW(0x3E2),REGRAW(0x3E4),REGRAW(0x3E6), REGRAW(0x3E8),REGRAW(0x3EA),REGRAW(0x3EC),REGRAW(0x3EE), REGRAW(0x3F0),REGRAW(0x3F2),REGRAW(0x3F4),REGRAW(0x3F6), REGRAW(0x3F8),REGRAW(0x3FA),REGRAW(0x3FC),REGRAW(0x3FE), // AND... we reached 0x400! // Last verse, same as the first: VoiceParamsCore(1), // 0x000 -> 0x180 CoreParamsPair(1,REG_S_PMON), CoreParamsPair(1,REG_S_NON), CoreParamsPair(1,REG_S_VMIXL), CoreParamsPair(1,REG_S_VMIXEL), CoreParamsPair(1,REG_S_VMIXR), CoreParamsPair(1,REG_S_VMIXER), RegWrite_Core<1,REG_P_MMIX>, RegWrite_Core<1,REG_C_ATTR>, CoreParamsPair(1,REG_A_IRQA), CoreParamsPair(1,REG_S_KON), CoreParamsPair(1,REG_S_KOFF), CoreParamsPair(1,REG_A_TSA), CoreParamsPair(1,REG__1AC), RegWrite_Core<1,REG_S_ADMAS>, REGRAW(0x5b2), REGRAW(0x5b4), REGRAW(0x5b6), REGRAW(0x5b8), REGRAW(0x5ba), REGRAW(0x5bc), REGRAW(0x5be), // 0x1c0! VoiceAddrSet(1, 0),VoiceAddrSet(1, 1),VoiceAddrSet(1, 2),VoiceAddrSet(1, 3),VoiceAddrSet(1, 4),VoiceAddrSet(1, 5), VoiceAddrSet(1, 6),VoiceAddrSet(1, 7),VoiceAddrSet(1, 8),VoiceAddrSet(1, 9),VoiceAddrSet(1,10),VoiceAddrSet(1,11), VoiceAddrSet(1,12),VoiceAddrSet(1,13),VoiceAddrSet(1,14),VoiceAddrSet(1,15),VoiceAddrSet(1,16),VoiceAddrSet(1,17), VoiceAddrSet(1,18),VoiceAddrSet(1,19),VoiceAddrSet(1,20),VoiceAddrSet(1,21),VoiceAddrSet(1,22),VoiceAddrSet(1,23), CoreParamsPair(1,REG_A_ESA), ReverbPair(1,R_FB_SRC_A), // 0x02E4 // Feedback Source A ReverbPair(1,R_FB_SRC_B), // 0x02E8 // Feedback Source B ReverbPair(1,R_IIR_DEST_A0), // 0x02EC ReverbPair(1,R_IIR_DEST_A1), // 0x02F0 ReverbPair(1,R_ACC_SRC_A0), // 0x02F4 ReverbPair(1,R_ACC_SRC_A1), // 0x02F8 ReverbPair(1,R_ACC_SRC_B0), // 0x02FC ReverbPair(1,R_ACC_SRC_B1), // 0x0300 ReverbPair(1,R_IIR_SRC_A0), // 0x0304 ReverbPair(1,R_IIR_SRC_A1), // 0x0308 ReverbPair(1,R_IIR_DEST_B0), // 0x030C ReverbPair(1,R_IIR_DEST_B1), // 0x0310 ReverbPair(1,R_ACC_SRC_C0), // 0x0314 ReverbPair(1,R_ACC_SRC_C1), // 0x0318 ReverbPair(1,R_ACC_SRC_D0), // 0x031C ReverbPair(1,R_ACC_SRC_D1), // 0x0320 ReverbPair(1,R_IIR_SRC_B1), // 0x0324 ReverbPair(1,R_IIR_SRC_B0), // 0x0328 ReverbPair(1,R_MIX_DEST_A0), // 0x032C ReverbPair(1,R_MIX_DEST_A1), // 0x0330 ReverbPair(1,R_MIX_DEST_B0), // 0x0334 ReverbPair(1,R_MIX_DEST_B1), // 0x0338 RegWrite_Core<1,REG_A_EEA>, NULL, CoreParamsPair(1,REG_S_ENDX), // 0x0340 // End Point passed flag RegWrite_Core<1,REG_P_STATX>, // 0x0344 // Status register? REGRAW(0x746), REGRAW(0x748),REGRAW(0x74A),REGRAW(0x74C),REGRAW(0x74E), REGRAW(0x750),REGRAW(0x752),REGRAW(0x754),REGRAW(0x756), REGRAW(0x758),REGRAW(0x75A),REGRAW(0x75C),REGRAW(0x75E), // ------ ------- RegWrite_CoreExt<0,REG_P_MVOLL>, // 0x0760 // Master Volume Left RegWrite_CoreExt<0,REG_P_MVOLR>, // 0x0762 // Master Volume Right RegWrite_CoreExt<0,REG_P_EVOLL>, // 0x0764 // Effect Volume Left RegWrite_CoreExt<0,REG_P_EVOLR>, // 0x0766 // Effect Volume Right RegWrite_CoreExt<0,REG_P_AVOLL>, // 0x0768 // Core External Input Volume Left (Only Core 1) RegWrite_CoreExt<0,REG_P_AVOLR>, // 0x076A // Core External Input Volume Right (Only Core 1) RegWrite_CoreExt<0,REG_P_BVOLL>, // 0x076C // Sound Data Volume Left RegWrite_CoreExt<0,REG_P_BVOLR>, // 0x076E // Sound Data Volume Right RegWrite_CoreExt<0,REG_P_MVOLXL>, // 0x0770 // Current Master Volume Left RegWrite_CoreExt<0,REG_P_MVOLXR>, // 0x0772 // Current Master Volume Right RegWrite_CoreExt<0,R_IIR_ALPHA>, // 0x0774 //IIR alpha (% used) RegWrite_CoreExt<0,R_ACC_COEF_A>, // 0x0776 RegWrite_CoreExt<0,R_ACC_COEF_B>, // 0x0778 RegWrite_CoreExt<0,R_ACC_COEF_C>, // 0x077A RegWrite_CoreExt<0,R_ACC_COEF_D>, // 0x077C RegWrite_CoreExt<0,R_IIR_COEF>, // 0x077E RegWrite_CoreExt<0,R_FB_ALPHA>, // 0x0780 //feedback alpha (% used) RegWrite_CoreExt<0,R_FB_X>, // 0x0782 //feedback RegWrite_CoreExt<0,R_IN_COEF_L>, // 0x0784 RegWrite_CoreExt<0,R_IN_COEF_R>, // 0x0786 // ------ ------- RegWrite_CoreExt<1,REG_P_MVOLL>, // 0x0788 // Master Volume Left RegWrite_CoreExt<1,REG_P_MVOLR>, // 0x078A // Master Volume Right RegWrite_CoreExt<1,REG_P_EVOLL>, // 0x0764 // Effect Volume Left RegWrite_CoreExt<1,REG_P_EVOLR>, // 0x0766 // Effect Volume Right RegWrite_CoreExt<1,REG_P_AVOLL>, // 0x0768 // Core External Input Volume Left (Only Core 1) RegWrite_CoreExt<1,REG_P_AVOLR>, // 0x076A // Core External Input Volume Right (Only Core 1) RegWrite_CoreExt<1,REG_P_BVOLL>, // 0x076C // Sound Data Volume Left RegWrite_CoreExt<1,REG_P_BVOLR>, // 0x076E // Sound Data Volume Right RegWrite_CoreExt<1,REG_P_MVOLXL>, // 0x0770 // Current Master Volume Left RegWrite_CoreExt<1,REG_P_MVOLXR>, // 0x0772 // Current Master Volume Right RegWrite_CoreExt<1,R_IIR_ALPHA>, // 0x0774 //IIR alpha (% used) RegWrite_CoreExt<1,R_ACC_COEF_A>, // 0x0776 RegWrite_CoreExt<1,R_ACC_COEF_B>, // 0x0778 RegWrite_CoreExt<1,R_ACC_COEF_C>, // 0x077A RegWrite_CoreExt<1,R_ACC_COEF_D>, // 0x077C RegWrite_CoreExt<1,R_IIR_COEF>, // 0x077E RegWrite_CoreExt<1,R_FB_ALPHA>, // 0x0780 //feedback alpha (% used) RegWrite_CoreExt<1,R_FB_X>, // 0x0782 //feedback RegWrite_CoreExt<1,R_IN_COEF_L>, // 0x0784 RegWrite_CoreExt<1,R_IN_COEF_R>, // 0x0786 REGRAW(0x7B0),REGRAW(0x7B2),REGRAW(0x7B4),REGRAW(0x7B6), REGRAW(0x7B8),REGRAW(0x7BA),REGRAW(0x7BC),REGRAW(0x7BE), // SPDIF interface RegWrite_SPDIF, // 0x07C0 // SPDIF Out: OFF/'PCM'/Bitstream/Bypass RegWrite_SPDIF, // 0x07C2 REGRAW(0x7C4), RegWrite_SPDIF, // 0x07C6 RegWrite_SPDIF, // 0x07C8 // SPDIF Media: 'CD'/DVD REGRAW(0x7CA), RegWrite_SPDIF, // 0x07CC // SPDIF Copy Protection REGRAW(0x7CE), REGRAW(0x7D0),REGRAW(0x7D2),REGRAW(0x7D4),REGRAW(0x7D6), REGRAW(0x7D8),REGRAW(0x7DA),REGRAW(0x7DC),REGRAW(0x7DE), REGRAW(0x7E0),REGRAW(0x7E2),REGRAW(0x7E4),REGRAW(0x7E6), REGRAW(0x7E8),REGRAW(0x7EA),REGRAW(0x7EC),REGRAW(0x7EE), REGRAW(0x7F0),REGRAW(0x7F2),REGRAW(0x7F4),REGRAW(0x7F6), REGRAW(0x7F8),REGRAW(0x7FA),REGRAW(0x7FC),REGRAW(0x7FE), NULL // should be at 0x400! (we assert check it on startup) }; __forceinline void SPU2_FastWrite( u32 rmem, u16 value ) { tbl_reg_writes[(rmem&0x7ff)/2]( value ); } void StartVoices(int core, u32 value) { // Optimization: Games like to write zero to the KeyOn reg a lot, so shortcut // this loop if value is zero. if( value == 0 ) return; Cores[core].Regs.ENDX &= ~value; for( u8 vc=0; vc>vc) & 1) { Cores[core].Voices[vc].Start(); if( IsDevBuild ) { V_Voice& thisvc( Cores[core].Voices[vc] ); if(MsgKeyOnOff()) ConLog(" * SPU2: KeyOn: C%dV%02d: SSA: %8x; M: %s%s%s%s; H: %02x%02x; P: %04x V: %04x/%04x; ADSR: %04x%04x\n", core,vc,thisvc.StartA, (Cores[core].VoiceGates[vc].DryL)?"+":"-",(Cores[core].VoiceGates[vc].DryR)?"+":"-", (Cores[core].VoiceGates[vc].WetL)?"+":"-",(Cores[core].VoiceGates[vc].WetR)?"+":"-", *(u8*)GetMemPtr(thisvc.StartA),*(u8 *)GetMemPtr((thisvc.StartA)+1), thisvc.Pitch, thisvc.Volume.Left.Value>>16,thisvc.Volume.Right.Value>>16, thisvc.ADSR.regADSR1,thisvc.ADSR.regADSR2); } } } } void StopVoices(int core, u32 value) { if( value == 0 ) return; for( u8 vc=0; vc>vc) & 1) { Cores[core].Voices[vc].ADSR.Releasing = true; //if(MsgKeyOnOff()) ConLog(" * SPU2: KeyOff: Core %d; Voice %d.\n",core,vc); } } }