|
|
@@ -0,0 +1,403 @@ |
|
|
// Copyright 2022 Dolphin Emulator Project |
|
|
// SPDX-License-Identifier: GPL-2.0-or-later |
|
|
|
|
|
// High-level emulation for the libaesnd ucode, used by homebrew |
|
|
// libaesnd is part of devkitPro's libogc, released under the Zlib license |
|
|
|
|
|
#include "Core/HW/DSPHLE/UCodes/AESnd.h" |
|
|
|
|
|
#include <algorithm> |
|
|
|
|
|
#include "Common/ChunkFile.h" |
|
|
#include "Common/Logging/Log.h" |
|
|
#include "Common/Swap.h" |
|
|
#include "Core/DSP/DSPAccelerator.h" |
|
|
#include "Core/HW/DSP.h" |
|
|
#include "Core/HW/DSPHLE/DSPHLE.h" |
|
|
#include "Core/HW/DSPHLE/MailHandler.h" |
|
|
#include "Core/HW/DSPHLE/UCodes/UCodes.h" |
|
|
|
|
|
namespace DSP::HLE |
|
|
{ |
|
|
constexpr u32 MAIL_PREFIX = 0xface'0000; |
|
|
constexpr u32 MAIL_PROCESS_FIRST_VOICE = MAIL_PREFIX | 0x0010; |
|
|
constexpr u32 MAIL_PROCESS_NEXT_VOICE = MAIL_PREFIX | 0x0020; |
|
|
constexpr u32 MAIL_GET_PB_ADDRESS = MAIL_PREFIX | 0x0080; |
|
|
constexpr u32 MAIL_SEND_SAMPLES = MAIL_PREFIX | 0x0100; |
|
|
constexpr u32 MAIL_TERMINATE = MAIL_PREFIX | 0xdead; |
|
|
|
|
|
// June 5, 2010 version (padded to 0x03e0 bytes) - initial release |
|
|
// First included with libogc 1.8.4 on October 3, 2010: https://devkitpro.org/viewtopic.php?t=2249 |
|
|
// https://github.com/devkitPro/libogc/blob/b5fdbdb069c45584aa4dfd950a136a8db9b1144c/libaesnd/dspcode/dspmixer.s |
|
|
constexpr u32 HASH_2010 = 0x008366af; |
|
|
// April 11, 2012 version (padded to 0x03e0 bytes) - swapped input channels |
|
|
// First included with libogc 1.8.11 on April 22, 2012: https://devkitpro.org/viewtopic.php?t=3094 |
|
|
// https://github.com/devkitPro/libogc/commit/8f188e12b6a3d8b5a0d49a109fe6a3e4e1702aab |
|
|
constexpr u32 HASH_2012 = 0x078066ab; |
|
|
// June 14, 2020 version (0x03e6 bytes) - added unsigned formats |
|
|
// First included with libogc 2.1.0 on June 15, 2020: https://devkitpro.org/viewtopic.php?t=9079 |
|
|
// https://github.com/devkitPro/libogc/commit/eac8fe2c29aa790d552dd6166a1fb195dfdcb825 |
|
|
constexpr u32 HASH_2020 = 0x84c680a9; |
|
|
|
|
|
constexpr u32 VOICE_MONO8 = 0x00000000; |
|
|
constexpr u32 VOICE_STEREO8 = 0x00000001; |
|
|
constexpr u32 VOICE_MONO16 = 0x00000002; |
|
|
constexpr u32 VOICE_STEREO16 = 0x00000003; |
|
|
// These are only present in the 2020 release |
|
|
constexpr u32 VOICE_MONO8_UNSIGNED = 0x00000004; |
|
|
constexpr u32 VOICE_STEREO8_UNSIGNED = 0x00000005; |
|
|
constexpr u32 VOICE_MONO16_UNSIGNED = 0x00000006; |
|
|
constexpr u32 VOICE_STEREO16_UNSIGNED = 0x00000007; |
|
|
|
|
|
constexpr u32 VOICE_FORMAT_MASK_OLD = 3; |
|
|
constexpr u32 VOICE_FORMAT_MASK_NEW = 7; |
|
|
// Note that the above formats have the 2-bit set for 16-bit formats and not set for 8-bit formats |
|
|
constexpr u32 VOICE_16_BIT_FLAG = 2; |
|
|
|
|
|
// These are used in the pre-2020 versions version |
|
|
constexpr u32 VOICE_PAUSE_OLD = 0x00000004; |
|
|
constexpr u32 VOICE_LOOP_OLD = 0x00000008; // not used by the DSP |
|
|
constexpr u32 VOICE_ONCE_OLD = 0x00000010; // not used by the DSP |
|
|
constexpr u32 VOICE_STREAM_OLD = 0x00000020; // not used by the DSP |
|
|
|
|
|
// These were changed in the 2020 version to account for the different flags |
|
|
constexpr u32 VOICE_PAUSE_NEW = 0x00000008; |
|
|
constexpr u32 VOICE_LOOP_NEW = 0x00000010; // not used by the DSP |
|
|
constexpr u32 VOICE_ONCE_NEW = 0x00000020; // not used by the DSP |
|
|
constexpr u32 VOICE_STREAM_NEW = 0x00000040; // not used by the DSP |
|
|
|
|
|
// These did not change between versions |
|
|
constexpr u32 VOICE_FINISHED = 0x00100000; |
|
|
constexpr u32 VOICE_STOPPED = 0x00200000; // not used by the DSP |
|
|
constexpr u32 VOICE_RUNNING = 0x40000000; |
|
|
constexpr u32 VOICE_USED = 0x80000000; // not used by the DSP |
|
|
|
|
|
// 1<<4 = scale gain by 1/1, 2<<2 = PCM decoding from ARAM, 1<<0 = 8-bit reads |
|
|
constexpr u32 ACCELERATOR_FORMAT_8_BIT = 0x0019; |
|
|
// 0<<4 = scale gain by 1/2048, 2<<2 = PCM decoding from ARAM, 2<<0 = 16-bit reads |
|
|
constexpr u32 ACCELERATOR_FORMAT_16_BIT = 0x000a; |
|
|
// Multiply samples by 0x100/1 = 0x100 (for ACCELERATOR_FORMAT_8_BIT) |
|
|
constexpr u32 ACCELERATOR_GAIN_8_BIT = 0x0100; |
|
|
// Multiply samples by 0x800/2048 = 1 (for ACCELERATOR_FORMAT_16_BIT) |
|
|
constexpr u32 ACCELERATOR_GAIN_16_BIT = 0x0800; |
|
|
|
|
|
AESndUCode::AESndUCode(DSPHLE* dsphle, u32 crc) : UCodeInterface(dsphle, crc) |
|
|
{ |
|
|
} |
|
|
|
|
|
void AESndUCode::Initialize() |
|
|
{ |
|
|
m_mail_handler.PushMail(DSP_INIT); |
|
|
} |
|
|
|
|
|
void AESndUCode::Update() |
|
|
{ |
|
|
// This is dubious in general, since we set the interrupt parameter on m_mail_handler.PushMail |
|
|
if (m_mail_handler.HasPending()) |
|
|
{ |
|
|
DSP::GenerateDSPInterruptFromDSPEmu(DSP::INT_DSP); |
|
|
} |
|
|
} |
|
|
|
|
|
void AESndUCode::HandleMail(u32 mail) |
|
|
{ |
|
|
if (m_upload_setup_in_progress) |
|
|
{ |
|
|
PrepareBootUCode(mail); |
|
|
} |
|
|
else if (m_next_mail_is_parameter_block_addr) |
|
|
{ |
|
|
// get_pb_address |
|
|
m_parameter_block_addr = mail; |
|
|
INFO_LOG_FMT(DSPHLE, "AESndUCode - Parameter block is at {:08x}", mail); |
|
|
m_next_mail_is_parameter_block_addr = false; |
|
|
// No mail is sent in response |
|
|
} |
|
|
else if ((mail & TASK_MAIL_MASK) == TASK_MAIL_TO_DSP) |
|
|
{ |
|
|
switch (mail) |
|
|
{ |
|
|
case MAIL_NEW_UCODE: |
|
|
m_upload_setup_in_progress = true; |
|
|
break; |
|
|
case MAIL_RESET: |
|
|
m_dsphle->SetUCode(UCODE_ROM); |
|
|
break; |
|
|
default: |
|
|
WARN_LOG_FMT(DSPHLE, "AESndUCode - unknown 0xcdd1 command: {:08x}", mail); |
|
|
break; |
|
|
} |
|
|
// No mail is sent in response to any of these. |
|
|
// The actual uCode halts on unknown cdd1 commands, which I have not implemented. |
|
|
} |
|
|
else |
|
|
{ |
|
|
// The uCode checks for a 0xface prefix, which we include in the constants above. |
|
|
switch (mail) |
|
|
{ |
|
|
case MAIL_PROCESS_FIRST_VOICE: |
|
|
DEBUG_LOG_FMT(DSPHLE, "ASndUCode - MAIL_PROCESS_FIRST_VOICE"); |
|
|
DMAInParameterBlock(); // dma_pb_block |
|
|
m_output_buffer.fill(0); |
|
|
DoMixing(); // fall through to dsp_mixer |
|
|
// Mail is handled by DoMixing() |
|
|
break; |
|
|
case MAIL_PROCESS_NEXT_VOICE: |
|
|
DEBUG_LOG_FMT(DSPHLE, "ASndUCode - MAIL_PROCESS_NEXT_VOICE"); |
|
|
DMAInParameterBlock(); // dma_pb_block |
|
|
DoMixing(); // jump to dsp_mixer |
|
|
// Mail is handled by DoMixing() |
|
|
break; |
|
|
case MAIL_GET_PB_ADDRESS: |
|
|
DEBUG_LOG_FMT(DSPHLE, "ASndUCode - MAIL_GET_PB_ADDRESS"); |
|
|
m_next_mail_is_parameter_block_addr = true; |
|
|
// No mail is sent in response |
|
|
break; |
|
|
case MAIL_SEND_SAMPLES: |
|
|
DEBUG_LOG_FMT(DSPHLE, "ASndUCode - MAIL_SEND_SAMPLES"); |
|
|
// send_samples |
|
|
for (u32 i = 0; i < NUM_OUTPUT_SAMPLES * 2; i++) |
|
|
{ |
|
|
HLEMemory_Write_U16(m_parameter_block.out_buf + i * sizeof(u16), m_output_buffer[i]); |
|
|
} |
|
|
m_mail_handler.PushMail(DSP_SYNC, true); |
|
|
break; |
|
|
case MAIL_TERMINATE: |
|
|
INFO_LOG_FMT(DSPHLE, "ASndUCode - MAIL_TERMINATE: {:08x}", mail); |
|
|
// This doesn't actually change the state of the system. |
|
|
m_mail_handler.PushMail(DSP_DONE, true); |
|
|
break; |
|
|
default: |
|
|
WARN_LOG_FMT(DSPHLE, "AESndUCode - unknown command: {:08x}", mail); |
|
|
// No mail is sent in this case |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
void AESndUCode::DMAInParameterBlock() |
|
|
{ |
|
|
m_parameter_block.out_buf = HLEMemory_Read_U32(m_parameter_block_addr + 0); |
|
|
m_parameter_block.buf_start = HLEMemory_Read_U32(m_parameter_block_addr + 4); |
|
|
m_parameter_block.buf_end = HLEMemory_Read_U32(m_parameter_block_addr + 8); |
|
|
m_parameter_block.buf_curr = HLEMemory_Read_U32(m_parameter_block_addr + 12); |
|
|
m_parameter_block.yn1 = HLEMemory_Read_U16(m_parameter_block_addr + 16); |
|
|
m_parameter_block.yn2 = HLEMemory_Read_U16(m_parameter_block_addr + 18); |
|
|
m_parameter_block.pds = HLEMemory_Read_U16(m_parameter_block_addr + 20); |
|
|
m_parameter_block.freq = HLEMemory_Read_U32(m_parameter_block_addr + 22); |
|
|
m_parameter_block.counter = HLEMemory_Read_U16(m_parameter_block_addr + 26); |
|
|
m_parameter_block.left = HLEMemory_Read_U16(m_parameter_block_addr + 28); |
|
|
m_parameter_block.right = HLEMemory_Read_U16(m_parameter_block_addr + 30); |
|
|
m_parameter_block.volume_l = HLEMemory_Read_U16(m_parameter_block_addr + 32); |
|
|
m_parameter_block.volume_r = HLEMemory_Read_U16(m_parameter_block_addr + 34); |
|
|
m_parameter_block.delay = HLEMemory_Read_U32(m_parameter_block_addr + 36); |
|
|
m_parameter_block.flags = HLEMemory_Read_U32(m_parameter_block_addr + 40); |
|
|
} |
|
|
|
|
|
void AESndUCode::DMAOutParameterBlock() |
|
|
{ |
|
|
HLEMemory_Write_U32(m_parameter_block_addr + 0, m_parameter_block.out_buf); |
|
|
HLEMemory_Write_U32(m_parameter_block_addr + 4, m_parameter_block.buf_start); |
|
|
HLEMemory_Write_U32(m_parameter_block_addr + 8, m_parameter_block.buf_end); |
|
|
HLEMemory_Write_U32(m_parameter_block_addr + 12, m_parameter_block.buf_curr); |
|
|
HLEMemory_Write_U16(m_parameter_block_addr + 16, m_parameter_block.yn1); |
|
|
HLEMemory_Write_U16(m_parameter_block_addr + 18, m_parameter_block.yn2); |
|
|
HLEMemory_Write_U16(m_parameter_block_addr + 20, m_parameter_block.pds); |
|
|
HLEMemory_Write_U32(m_parameter_block_addr + 22, m_parameter_block.freq); |
|
|
HLEMemory_Write_U16(m_parameter_block_addr + 26, m_parameter_block.counter); |
|
|
HLEMemory_Write_U16(m_parameter_block_addr + 28, m_parameter_block.left); |
|
|
HLEMemory_Write_U16(m_parameter_block_addr + 30, m_parameter_block.right); |
|
|
HLEMemory_Write_U16(m_parameter_block_addr + 32, m_parameter_block.volume_l); |
|
|
HLEMemory_Write_U16(m_parameter_block_addr + 34, m_parameter_block.volume_r); |
|
|
HLEMemory_Write_U32(m_parameter_block_addr + 36, m_parameter_block.delay); |
|
|
HLEMemory_Write_U32(m_parameter_block_addr + 40, m_parameter_block.flags); |
|
|
} |
|
|
|
|
|
class AESndAccelerator final : public Accelerator |
|
|
{ |
|
|
protected: |
|
|
void OnEndException() override |
|
|
{ |
|
|
// exception5 - this updates internal state |
|
|
SetYn1(GetYn1()); |
|
|
SetYn2(GetYn2()); |
|
|
SetPredScale(GetPredScale()); |
|
|
} |
|
|
|
|
|
u8 ReadMemory(u32 address) override { return ReadARAM(address); } |
|
|
void WriteMemory(u32 address, u8 value) override { WriteARAM(value, address); } |
|
|
}; |
|
|
|
|
|
static std::unique_ptr<Accelerator> s_accelerator = std::make_unique<AESndAccelerator>(); |
|
|
static constexpr std::array<s16, 16> ACCELERATOR_COEFS = {}; // all zeros |
|
|
|
|
|
void AESndUCode::SetUpAccelerator(u16 format, [[maybe_unused]] u16 gain) |
|
|
{ |
|
|
// setup_accl |
|
|
s_accelerator->SetSampleFormat(format); |
|
|
// not currently implemented, but it doesn't matter since the gain is configured to be a no-op |
|
|
// s_accelerator->SetGain(gain); |
|
|
s_accelerator->SetStartAddress(m_parameter_block.buf_start); |
|
|
s_accelerator->SetEndAddress(m_parameter_block.buf_end); |
|
|
s_accelerator->SetCurrentAddress(m_parameter_block.buf_curr); |
|
|
s_accelerator->SetYn1(m_parameter_block.yn1); |
|
|
s_accelerator->SetYn2(m_parameter_block.yn2); |
|
|
s_accelerator->SetPredScale(m_parameter_block.pds); |
|
|
// All of the coefficients (COEF_A1_0 at ffa0 - COEF_A2_7 at ffaf) are set to 0 |
|
|
} |
|
|
|
|
|
void AESndUCode::DoMixing() |
|
|
{ |
|
|
const u32 pause_flag = (m_crc == HASH_2020) ? VOICE_PAUSE_NEW : VOICE_PAUSE_OLD; |
|
|
const u32 format_mask = (m_crc == HASH_2020) ? VOICE_FORMAT_MASK_NEW : VOICE_FORMAT_MASK_OLD; |
|
|
// dsp_mixer |
|
|
const bool paused = (m_parameter_block.flags & pause_flag) != 0; |
|
|
const bool running = (m_parameter_block.flags & VOICE_RUNNING) != 0; |
|
|
const bool has_buf = m_parameter_block.buf_start != 0; |
|
|
if (!paused && running && has_buf) |
|
|
{ |
|
|
// no_change_buffer |
|
|
const u32 voice_format = m_parameter_block.flags & format_mask; |
|
|
const bool is_16_bit = (voice_format & VOICE_16_BIT_FLAG) != 0; |
|
|
// select_format table |
|
|
const u16 accelerator_format = is_16_bit ? ACCELERATOR_FORMAT_16_BIT : ACCELERATOR_FORMAT_8_BIT; |
|
|
const u16 accelerator_gain = is_16_bit ? ACCELERATOR_GAIN_16_BIT : ACCELERATOR_GAIN_8_BIT; |
|
|
SetUpAccelerator(accelerator_format, accelerator_gain); |
|
|
// read from select_mixer skipped |
|
|
|
|
|
// Note: We don't handle masking/sign extension from use of $ac0.h and $ac1.h, as this isn't |
|
|
// something that should happen if the PowerPC code is using the library normally. |
|
|
// This applies throughout. |
|
|
u32 delay = m_parameter_block.delay; |
|
|
u32 sample_index = 0; |
|
|
u32 remaining_samples = NUM_OUTPUT_SAMPLES; |
|
|
|
|
|
if (delay != 0) |
|
|
{ |
|
|
// delay_loop |
|
|
// This is implemented with a BLOOP and then a read from the loop counter stack register in |
|
|
// the actual uCode. I'm not 100% sure how it works/if it works correctly there. |
|
|
while (remaining_samples != 0) |
|
|
{ |
|
|
remaining_samples--; |
|
|
delay--; |
|
|
if (delay == 0) |
|
|
break; |
|
|
sample_index++; |
|
|
} |
|
|
// exit_delay |
|
|
m_parameter_block.delay = delay; |
|
|
} |
|
|
// no_delay - operates in set40 mode |
|
|
while (remaining_samples != 0) // BLOOP dspmixer_loop_end |
|
|
{ |
|
|
remaining_samples--; |
|
|
s32 right = m_output_buffer[sample_index * 2 + 0]; |
|
|
s32 left = m_output_buffer[sample_index * 2 + 1]; |
|
|
right += m_parameter_block.right; |
|
|
left += m_parameter_block.left; |
|
|
// Clamping from set40 mode |
|
|
right = std::clamp(right, -32768, 32767); |
|
|
left = std::clamp(left, -32768, 32767); |
|
|
m_output_buffer[sample_index * 2 + 0] = right; |
|
|
m_output_buffer[sample_index * 2 + 1] = left; |
|
|
sample_index++; |
|
|
|
|
|
const u32 counter = static_cast<u32>(m_parameter_block.counter) + m_parameter_block.freq; |
|
|
m_parameter_block.counter = static_cast<u16>(counter); |
|
|
u16 counter_h = counter >> 16; |
|
|
if (counter_h >= 1) |
|
|
{ |
|
|
s16 new_l = 0, new_r = 0; |
|
|
|
|
|
// jrge $ar3 (using select_mixer table) |
|
|
// We already masked voice_format with the right mask for the version, so nothing special |
|
|
// needs to be done for this switch statement to exclude unsupported formats |
|
|
switch (voice_format) |
|
|
{ |
|
|
case VOICE_MONO8: |
|
|
case VOICE_MONO16: |
|
|
// mono_mix |
|
|
while (counter_h >= 1) |
|
|
{ |
|
|
counter_h--; |
|
|
new_r = s_accelerator->Read(ACCELERATOR_COEFS.data()); |
|
|
new_l = new_r; |
|
|
} |
|
|
break; |
|
|
|
|
|
case VOICE_STEREO8: |
|
|
case VOICE_STEREO16: |
|
|
// stereo_mix |
|
|
while (counter_h >= 1) |
|
|
{ |
|
|
counter_h--; |
|
|
new_r = s_accelerator->Read(ACCELERATOR_COEFS.data()); |
|
|
new_l = s_accelerator->Read(ACCELERATOR_COEFS.data()); |
|
|
} |
|
|
break; // falls through to mix_samples normally |
|
|
|
|
|
case VOICE_MONO8_UNSIGNED: |
|
|
case VOICE_MONO16_UNSIGNED: |
|
|
// mono_unsigned_mix |
|
|
while (counter_h >= 1) |
|
|
{ |
|
|
counter_h--; |
|
|
new_r = s_accelerator->Read(ACCELERATOR_COEFS.data()); |
|
|
new_l = new_r; |
|
|
} |
|
|
new_r ^= 0x8000; |
|
|
new_l ^= 0x8000; |
|
|
break; |
|
|
|
|
|
case VOICE_STEREO8_UNSIGNED: |
|
|
case VOICE_STEREO16_UNSIGNED: |
|
|
// stereo_unsigned_mix |
|
|
while (counter_h >= 1) |
|
|
{ |
|
|
counter_h--; |
|
|
new_r = s_accelerator->Read(ACCELERATOR_COEFS.data()); |
|
|
new_l = s_accelerator->Read(ACCELERATOR_COEFS.data()); |
|
|
} |
|
|
new_r ^= 0x8000; |
|
|
new_l ^= 0x8000; |
|
|
break; |
|
|
} |
|
|
if (m_crc == HASH_2012 || m_crc == HASH_2020) |
|
|
{ |
|
|
// The 2012 version swapped the left and right input channels so that left comes first, |
|
|
// and then right. Before, right came before left. The 2012 version didn't update comments |
|
|
// for it, though. |
|
|
std::swap(new_r, new_l); |
|
|
} |
|
|
// mix_samples |
|
|
const s32 mixed_l = (static_cast<s32>(new_l) * m_parameter_block.volume_l) >> 8; |
|
|
const s32 mixed_r = (static_cast<s32>(new_r) * m_parameter_block.volume_r) >> 8; |
|
|
// Clamping from set40 mode |
|
|
m_parameter_block.left = std::clamp(mixed_l, -32768, 32767); |
|
|
m_parameter_block.right = std::clamp(mixed_r, -32768, 32767); |
|
|
} |
|
|
// no_mix - we don't need to do anything as we modify m_parameter_block.left/right in place |
|
|
} |
|
|
// mixer_end - back to set16 mode |
|
|
m_parameter_block.pds = s_accelerator->GetPredScale(); |
|
|
m_parameter_block.yn2 = s_accelerator->GetYn2(); |
|
|
m_parameter_block.yn1 = s_accelerator->GetYn1(); |
|
|
m_parameter_block.buf_curr = s_accelerator->GetCurrentAddress(); |
|
|
} |
|
|
// finish_voice |
|
|
m_parameter_block.flags |= VOICE_FINISHED; |
|
|
DMAOutParameterBlock(); // dma_pb_block |
|
|
m_mail_handler.PushMail(DSP_SYNC, true); |
|
|
} |
|
|
|
|
|
void AESndUCode::DoState(PointerWrap& p) |
|
|
{ |
|
|
DoStateShared(p); |
|
|
p.Do(m_next_mail_is_parameter_block_addr); |
|
|
p.Do(m_parameter_block_addr); |
|
|
p.Do(m_parameter_block); |
|
|
p.Do(m_output_buffer); |
|
|
s_accelerator->DoState(p); |
|
|
} |
|
|
} // namespace DSP::HLE |