diff --git a/src/core/spu.cpp b/src/core/spu.cpp index e827a0394..99d59e784 100644 --- a/src/core/spu.cpp +++ b/src/core/spu.cpp @@ -283,7 +283,7 @@ void SPU::WriteVoiceRegister(u32 offset, u16 value) Assert(voice_index < 24); Voice& voice = m_voices[voice_index]; - if (voice.key_on) + if (voice.IsOn()) m_system->Synchronize(); switch (reg_index) @@ -318,14 +318,14 @@ void SPU::WriteVoiceRegister(u32 offset, u16 value) case 0x08: // adsr low { - Log_WarningPrintf("SPU voice %u ADSR low <- 0x%04X", voice_index, value); + Log_DebugPrintf("SPU voice %u ADSR low <- 0x%04X", voice_index, value); voice.regs.adsr.bits_low = value; } break; case 0x0A: // adsr high { - Log_WarningPrintf("SPU voice %u ADSR high <- 0x%04X", voice_index, value); + Log_DebugPrintf("SPU voice %u ADSR high <- 0x%04X", voice_index, value); voice.regs.adsr.bits_high = value; } break; @@ -404,14 +404,113 @@ void SPU::Execute(TickCount ticks) void SPU::Voice::KeyOn() { current_address = regs.adpcm_start_address; + regs.adsr_volume = 0; has_samples = false; - key_on = true; + SetADSRPhase(ADSRPhase::Attack); } void SPU::Voice::KeyOff() { - has_samples = false; - key_on = false; + if (adsr_phase == ADSRPhase::Off) + return; + + SetADSRPhase(ADSRPhase::Release); +} + +SPU::ADSRPhase SPU::GetNextADSRPhase(ADSRPhase phase) +{ + switch (phase) + { + case ADSRPhase::Attack: + // attack -> decay + return ADSRPhase::Decay; + + case ADSRPhase::Decay: + // decay -> sustain + return ADSRPhase::Sustain; + + case ADSRPhase::Sustain: + // sustain stays in sustain until key off + return ADSRPhase::Sustain; + + default: + case ADSRPhase::Release: + // end of release disables the voice + return ADSRPhase::Off; + } +} + +void SPU::Voice::SetADSRPhase(ADSRPhase phase) +{ + adsr_phase = phase; + switch (phase) + { + case ADSRPhase::Off: + adsr_target = {}; + break; + + case ADSRPhase::Attack: + adsr_target.level = 32767; // 0 -> max + adsr_target.step = regs.adsr.attack_step + 4; + adsr_target.shift = regs.adsr.attack_shift; + adsr_target.decreasing = false; + adsr_target.exponential = regs.adsr.attack_exponential; + break; + + case ADSRPhase::Decay: + adsr_target.level = (u32(regs.adsr.sustain_level.GetValue()) + 1) * 0x800; // max -> sustain level + adsr_target.step = 0; + adsr_target.shift = regs.adsr.decay_shift; + adsr_target.decreasing = true; + adsr_target.exponential = true; + break; + + case ADSRPhase::Sustain: + adsr_target.level = regs.adsr.sustain_direction_decrease ? -1 : 1; + adsr_target.step = 0; + adsr_target.shift = regs.adsr.sustain_shift; + adsr_target.decreasing = regs.adsr.sustain_direction_decrease; + adsr_target.exponential = regs.adsr.sustain_exponential; + break; + + case ADSRPhase::Release: + adsr_target.level = 0; + adsr_target.step = 0; + adsr_target.shift = regs.adsr.release_shift; + adsr_target.decreasing = true; + adsr_target.exponential = regs.adsr.release_exponential; + break; + + default: + break; + } + + const s32 step = adsr_target.decreasing ? (-8 + adsr_target.step) : (7 - adsr_target.step); + adsr_ticks = 1 << std::max(0, adsr_target.shift - 11); + adsr_ticks_remaining = adsr_ticks; + adsr_step = step << std::max(0, 11 - adsr_target.shift); +} + +void SPU::Voice::TickADSR() +{ + adsr_ticks_remaining--; + if (adsr_ticks_remaining <= 0) + { + const s32 new_volume = s32(regs.adsr_volume) + s32(adsr_step); + regs.adsr_volume = static_cast(std::clamp(new_volume, ADSR_MIN_VOLUME, ADSR_MAX_VOLUME)); + + const bool reached_target = + adsr_target.decreasing ? (new_volume <= adsr_target.level) : (new_volume >= adsr_target.level); + if (adsr_phase != ADSRPhase::Sustain && reached_target) + { + // next phase + SetADSRPhase(GetNextADSRPhase(adsr_phase)); + } + else + { + adsr_ticks_remaining = adsr_ticks; + } + } } void SPU::Voice::DecodeBlock() @@ -575,7 +674,7 @@ void SPU::DecodeADPCMBlock(const ADPCMBlock& block, SampleFormat out_samples[NUM std::tuple SPU::SampleVoice(u32 voice_index) { Voice& voice = m_voices[voice_index]; - if (!voice.key_on) + if (!voice.IsOn()) return std::make_tuple(0, 0); if (!voice.has_samples) @@ -624,10 +723,13 @@ std::tuple SPU::SampleVoice(u32 voice_inde } // TODO: Volume + const float adsr_volume = S16ToFloat(voice.regs.adsr_volume); + voice.TickADSR(); + const s32 sample = voice.Interpolate(); // s32 sample = voice.SampleBlock(voice.counter.sample_index); const s16 sample16 = Clamp16(sample); - const float samplef = S16ToFloat(sample16); + const float samplef = S16ToFloat(sample16) * adsr_volume; // apply volume const float volume_left = S16ToFloat(voice.regs.volume_left.GetVolume()); @@ -672,7 +774,7 @@ void SPU::DrawDebugWindow() if (!m_debug_window_open) return; - ImGui::SetNextWindowSize(ImVec2(700, 400), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(800, 400), ImGuiCond_FirstUseEver); if (!ImGui::Begin("SPU State", &m_debug_window_open)) { ImGui::End(); @@ -682,14 +784,15 @@ void SPU::DrawDebugWindow() // draw voice states if (ImGui::CollapsingHeader("Voice State", ImGuiTreeNodeFlags_DefaultOpen)) { - static constexpr u32 NUM_COLUMNS = 11; + static constexpr u32 NUM_COLUMNS = 12; ImGui::Columns(NUM_COLUMNS); // headers static constexpr std::array column_titles = { {"#", "InterpIndex", "SampleIndex", "CurAddr", "StartAddr", "RepeatAddr", "SampleRate", "VolLeft", "VolRight", - "ADSR", "ADSRVol"}}; + "ADSR", "ADSRPhase", "ADSRVol"}}; + static constexpr std::array adsr_phases = {{"Off", "Attack", "Decay", "Sustain", "Release"}}; for (u32 i = 0; i < NUM_COLUMNS; i++) { ImGui::TextUnformatted(column_titles[i]); @@ -700,7 +803,7 @@ void SPU::DrawDebugWindow() for (u32 voice_index = 0; voice_index < NUM_VOICES; voice_index++) { const Voice& v = m_voices[voice_index]; - ImVec4 color = v.key_on ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f); + ImVec4 color = v.IsOn() ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f); ImGui::TextColored(color, "%u", ZeroExtend32(voice_index)); ImGui::NextColumn(); ImGui::TextColored(color, "%u", ZeroExtend32(v.counter.interpolation_index.GetValue())); @@ -721,7 +824,9 @@ void SPU::DrawDebugWindow() ImGui::NextColumn(); ImGui::TextColored(color, "%08X", v.regs.adsr.bits); ImGui::NextColumn(); - ImGui::TextColored(color, "%04X", ZeroExtend32(v.regs.adsr_volume)); + ImGui::TextColored(color, adsr_phases[static_cast(v.adsr_phase)]); + ImGui::NextColumn(); + ImGui::TextColored(color, "%d", ZeroExtend32(v.regs.adsr_volume)); ImGui::NextColumn(); } diff --git a/src/core/spu.h b/src/core/spu.h index b98ed086f..87f762c8a 100644 --- a/src/core/spu.h +++ b/src/core/spu.h @@ -46,6 +46,8 @@ private: static constexpr u32 NUM_SAMPLES_PER_ADPCM_BLOCK = 28; static constexpr u32 SAMPLE_RATE = 44100; static constexpr u32 SYSCLK_TICKS_PER_SPU_TICK = MASTER_CLOCK / SAMPLE_RATE; // 0x300 + static constexpr s16 ADSR_MIN_VOLUME = 0; + static constexpr s16 ADSR_MAX_VOLUME = 0x7FFF; enum class RAMTransferMode : u8 { @@ -99,7 +101,7 @@ private: BitField sustain_level; BitField decay_shift; BitField attack_step; - BitField attack_shift; + BitField attack_shift; BitField attack_exponential; BitField release_shift; @@ -140,7 +142,7 @@ private: u16 adpcm_start_address; // multiply by 8 ADSRRegister adsr; - u16 adsr_volume; + s16 adsr_volume; u16 adpcm_repeat_address; // multiply by 8 }; @@ -187,6 +189,24 @@ private: u8 GetNibble(u32 index) const { return (data[index / 2] >> ((index % 2) * 4)) & 0x0F; } }; + enum class ADSRPhase : u8 + { + Off = 0, + Attack = 1, + Decay = 2, + Sustain = 3, + Release = 4 + }; + + struct ADSRTarget + { + s32 level; + s16 step; + u8 shift; + bool decreasing; + bool exponential; + }; + struct Voice { u16 current_address; @@ -197,8 +217,14 @@ private: std::array previous_block_last_samples; std::array adpcm_state; - bool has_samples; - bool key_on; + ADSRPhase adsr_phase; + ADSRTarget adsr_target; + TickCount adsr_ticks; + TickCount adsr_ticks_remaining; + s16 adsr_step; + bool has_samples; + + bool IsOn() const { return adsr_phase != ADSRPhase::Off; } void KeyOn(); void KeyOff(); @@ -206,8 +232,16 @@ private: void DecodeBlock(); SampleFormat SampleBlock(s32 index) const; s16 Interpolate() const; + + // Switches to the specified phase, filling in target. + void SetADSRPhase(ADSRPhase phase); + + // Updates the ADSR volume/phase. + void TickADSR(); }; + static ADSRPhase GetNextADSRPhase(ADSRPhase phase); + u16 ReadVoiceRegister(u32 offset); void WriteVoiceRegister(u32 offset, u16 value);