From 6a56b467d78d8c97ab7a6c81db592287db986bf2 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Thu, 26 Oct 2023 12:50:31 -0700 Subject: [PATCH] Match and link CMidiManager Former-commit-id: 6544d347a623a29d670c4f923da1aea09e16bf3a --- config/GM8E01_00/symbols.txt | 12 +-- config/GM8E01_01/symbols.txt | 10 +-- configure.py | 2 +- include/Kyoto/Audio/CAudioSys.hpp | 4 + include/Kyoto/Audio/CMidiManager.hpp | 47 +++++++++- include/Kyoto/Audio/CSfxHandle.hpp | 6 +- include/Kyoto/Audio/CSfxManager.hpp | 3 + src/Kyoto/Audio/CMidiManager.cpp | 128 +++++++++++++++++++++++++++ src/Kyoto/Audio/CSfxManager.cpp | 64 +++++++++++++- 9 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 src/Kyoto/Audio/CMidiManager.cpp diff --git a/config/GM8E01_00/symbols.txt b/config/GM8E01_00/symbols.txt index aed739cf..41a61a01 100644 --- a/config/GM8E01_00/symbols.txt +++ b/config/GM8E01_00/symbols.txt @@ -12772,12 +12772,12 @@ LocateHandle__11CSfxManagerFs = .text:0x802E9660; // type:function size:0xAC sco TurnOnChannel__11CSfxManagerFQ211CSfxManager12ESfxChannels = .text:0x802E970C; // type:function size:0x98 scope:global TurnOffChannel__11CSfxManagerFi = .text:0x802E97A4; // type:function size:0x110 scope:global SetChannel__11CSfxManagerFQ211CSfxManager12ESfxChannels = .text:0x802E98B4; // type:function size:0x50 scope:global -SetDuration__10CSfxHandleFf = .text:0x802E9904; // type:function size:0xB8 scope:global -StopSound__11CSfxManagerFRC10CSfxHandle = .text:0x802E99BC; // type:function size:0xF8 scope:global +SetDuration__11CSfxManagerFRC10CSfxHandlef = .text:0x802E9904; // type:function size:0xB8 scope:global +StopSound__11CSfxManagerF10CSfxHandle = .text:0x802E99BC; // type:function size:0xF8 scope:global KillAll__11CSfxManagerFQ211CSfxManager12ESfxChannels = .text:0x802E9AB4; // type:function size:0xC0 scope:global SfxSpan__11CSfxManagerF10CSfxHandleUc = .text:0x802E9B74; // type:function size:0xE4 scope:global SfxVolume__11CSfxManagerF10CSfxHandleUc = .text:0x802E9C58; // type:function size:0xF0 scope:global -SfxStop__11CSfxManagerFRC10CSfxHandle = .text:0x802E9D48; // type:function size:0x2C scope:global +SfxStop__11CSfxManagerF10CSfxHandle = .text:0x802E9D48; // type:function size:0x2C scope:global SfxStart__11CSfxManagerFUsssbsbi = .text:0x802E9D74; // type:function size:0x110 scope:global RemoveEmitter__11CSfxManagerF10CSfxHandle = .text:0x802E9E84; // type:function size:0x2C scope:global UpdateEmitter__11CSfxManagerF10CSfxHandleRC9CVector3fRC9CVector3fUc = .text:0x802E9EB0; // type:function size:0x130 scope:global @@ -14968,7 +14968,7 @@ __ct__10CARAMTokenFPvUii = .text:0x80358A4C; // type:function size:0xB0 scope:gl __ct__10CARAMTokenFv = .text:0x80358AFC; // type:function size:0x68 scope:global FMidiDataFactory__FRC10SObjectTagR12CInputStreamRC15CVParamTransfer = .text:0x80358B64; // type:function size:0x64 scope:global __ct__16CFactoryFnReturnFPQ212CMidiManager9CMidiData = .text:0x80358BC8; // type:function size:0xDC scope:global -fn_80358CA4 = .text:0x80358CA4; // type:function size:0xB8 +__dt__52TObjOwnerDerivedFromIObjFv = .text:0x80358CA4; // type:function size:0xB8 GetIObjObjectFor__34TTokenFRCQ24rstl36auto_ptr = .text:0x80358D5C; // type:function size:0x2C scope:global GetNewDerivedObject__52TObjOwnerDerivedFromIObjFRCQ24rstl36auto_ptr = .text:0x80358D88; // type:function size:0x9C scope:global __ct__Q212CMidiManager9CMidiDataFR12CInputStream = .text:0x80358E24; // type:function size:0x118 scope:global @@ -19530,8 +19530,8 @@ mpDefaultInvalidString__9CAudioSys = .bss:0x805A67CC; // type:object size:0x10 s lbl_805A67DC = .bss:0x805A67DC; // type:object size:0x10 lbl_805A67FC = .bss:0x805A67FC; // type:object size:0x10 sLists__10CARAMToken = .bss:0x805A680C; // type:object size:0x1C scope:global -lbl_805A6828 = .bss:0x805A6828; // type:object size:0x38 data:4byte -lbl_805A6860 = .bss:0x805A6860; // type:object size:0x300 data:4byte +mMidiWrappers__12CMidiManager = .bss:0x805A6828; // type:object size:0x28 data:4byte +lbl_805A6860 = .bss:0x805A6860; // type:object size:0x300 align:32 data:4byte lbl_805A6B60 = .bss:0x805A6B60; // type:object size:0x10 data:4byte lbl_805A6B90 = .bss:0x805A6B90; // type:object size:0x10 data:4byte lbl_805A6BA0 = .bss:0x805A6BA0; // type:object size:0x10 data:4byte diff --git a/config/GM8E01_01/symbols.txt b/config/GM8E01_01/symbols.txt index 955ca340..ac925a66 100644 --- a/config/GM8E01_01/symbols.txt +++ b/config/GM8E01_01/symbols.txt @@ -12787,12 +12787,12 @@ LocateHandle__11CSfxManagerFs = .text:0x802E9740; // type:function size:0xAC sco TurnOnChannel__11CSfxManagerFQ211CSfxManager12ESfxChannels = .text:0x802E97EC; // type:function size:0x98 scope:global TurnOffChannel__11CSfxManagerFi = .text:0x802E9884; // type:function size:0x110 scope:global SetChannel__11CSfxManagerFQ211CSfxManager12ESfxChannels = .text:0x802E9994; // type:function size:0x50 scope:global -SetDuration__10CSfxHandleFf = .text:0x802E99E4; // type:function size:0xB8 scope:global -StopSound__11CSfxManagerFRC10CSfxHandle = .text:0x802E9A9C; // type:function size:0xF8 scope:global +SetDuration__11CSfxManagerFRC10CSfxHandlef = .text:0x802E99E4; // type:function size:0xB8 scope:global +StopSound__11CSfxManagerF10CSfxHandle = .text:0x802E9A9C; // type:function size:0xF8 scope:global KillAll__11CSfxManagerFQ211CSfxManager12ESfxChannels = .text:0x802E9B94; // type:function size:0xC0 scope:global SfxSpan__11CSfxManagerF10CSfxHandleUc = .text:0x802E9C54; // type:function size:0xE4 scope:global SfxVolume__11CSfxManagerF10CSfxHandleUc = .text:0x802E9D38; // type:function size:0xF0 scope:global -SfxStop__11CSfxManagerFRC10CSfxHandle = .text:0x802E9E28; // type:function size:0x2C scope:global +SfxStop__11CSfxManagerF10CSfxHandle = .text:0x802E9E28; // type:function size:0x2C scope:global SfxStart__11CSfxManagerFUsssbsbi = .text:0x802E9E54; // type:function size:0x110 scope:global RemoveEmitter__11CSfxManagerF10CSfxHandle = .text:0x802E9F64; // type:function size:0x2C scope:global UpdateEmitter__11CSfxManagerF10CSfxHandleRC9CVector3fRC9CVector3fUc = .text:0x802E9F90; // type:function size:0x130 scope:global @@ -14983,7 +14983,7 @@ __ct__10CARAMTokenFPvUii = .text:0x80358B50; // type:function size:0xB0 scope:gl __ct__10CARAMTokenFv = .text:0x80358C00; // type:function size:0x68 scope:global FMidiDataFactory__FRC10SObjectTagR12CInputStreamRC15CVParamTransfer = .text:0x80358C68; // type:function size:0x64 scope:global __ct__16CFactoryFnReturnFPQ212CMidiManager9CMidiData = .text:0x80358CCC; // type:function size:0xDC scope:global -fn_80358CA4 = .text:0x80358DA8; // type:function size:0xB8 scope:global +__dt__52TObjOwnerDerivedFromIObjFv = .text:0x80358DA8; // type:function size:0xB8 scope:global GetIObjObjectFor__34TTokenFRCQ24rstl36auto_ptr = .text:0x80358E60; // type:function size:0x2C scope:global GetNewDerivedObject__52TObjOwnerDerivedFromIObjFRCQ24rstl36auto_ptr = .text:0x80358E8C; // type:function size:0x9C scope:global __ct__Q212CMidiManager9CMidiDataFR12CInputStream = .text:0x80358F28; // type:function size:0x118 scope:global @@ -19573,7 +19573,7 @@ lbl_805A67DC = .bss:0x805A69BC; // type:object size:0x30 scope:global lbl_805A67EC = .bss:0x805A69CC; // type:object size:0x10 scope:global lbl_805A67FC = .bss:0x805A69DC; // type:object size:0x10 scope:global sLists__10CARAMToken = .bss:0x805A69EC; // type:object size:0x1C scope:global -lbl_805A6828 = .bss:0x805A6A08; // type:object size:0x38 scope:global data:4byte +mMidiWrappers__12CMidiManager = .bss:0x805A6A08; // type:object size:0x38 scope:global data:4byte lbl_805A6860 = .bss:0x805A6A40; // type:object size:0x300 scope:global data:4byte lbl_805A6B60 = .bss:0x805A6D40; // type:object size:0x50 scope:global data:4byte lbl_805A6B90 = .bss:0x805A6D70; // type:object size:0x10 scope:global data:4byte diff --git a/configure.py b/configure.py index 2fadf270..03855f0a 100755 --- a/configure.py +++ b/configure.py @@ -953,7 +953,7 @@ config.libs = [ Object(NonMatching, "Kyoto/Animation/CSkinnedModelWithAvgNormals.cpp"), Object(Matching, "Kyoto/CTimeProvider.cpp"), Object(Matching, "Kyoto/CARAMToken.cpp"), - Object(NonMatching, "Kyoto/Audio/CMidiManager.cpp"), + Object(Matching, "Kyoto/Audio/CMidiManager.cpp"), Object(Matching, "Kyoto/Text/CFontImageDef.cpp"), Object(NonMatching, "Kyoto/Text/CImageInstruction.cpp"), Object(NonMatching, "Kyoto/Text/CTextRenderBuffer.cpp"), diff --git a/include/Kyoto/Audio/CAudioSys.hpp b/include/Kyoto/Audio/CAudioSys.hpp index 075be56a..b2e2e00d 100644 --- a/include/Kyoto/Audio/CAudioSys.hpp +++ b/include/Kyoto/Audio/CAudioSys.hpp @@ -100,6 +100,10 @@ public: static void S3dAddEmitter(SND_FXID fxid, const CVector3f& pos, const CVector3f& dir, const bool b1, const bool b2, short, int); + + static u32 SeqPlayEx(unsigned short, unsigned short, void*, SND_PLAYPARA*, unsigned char); + static void SeqStop(u32); + static void SeqVolume(u8, u16, u32, u8); static bool mInitialized; static bool mIsListenerActive; static bool mVerbose; diff --git a/include/Kyoto/Audio/CMidiManager.hpp b/include/Kyoto/Audio/CMidiManager.hpp index 4168ffcc..4440eb73 100644 --- a/include/Kyoto/Audio/CMidiManager.hpp +++ b/include/Kyoto/Audio/CMidiManager.hpp @@ -3,13 +3,54 @@ #include "Kyoto/Audio/CSfxHandle.hpp" +#include +#include + +class CInputStream; class CMidiManager { public: - class CMidiWrapper {}; - class CMidiData {}; + class CMidiWrapper { + public: + CMidiWrapper(); + const CSfxHandle& GetManagerHandle() const; + const u32 GetAudioSysHandle() const; + const bool IsAvailable() const; - static CSfxHandle Play(const CMidiData&, unsigned short fadeTime, bool stopExisting, short volume); + void SetAvailable(const bool v); + void SetAudioSysHandle(const u32 handle); + const short GetSongId() const; + void SetMidiHandle(const CSfxHandle& handle); + void SetSongId(const short id); + + private: + u32 x0_; + CSfxHandle x4_; + short x8_; + bool xa_; + }; + class CMidiData { + public: + CMidiData(CInputStream& in); + + const short GetSongId() const { return x0_songId; } + const short GetGroupId() const { return x2_groupId; } + const int GetSetupId() const { return x4_setupId; } + uchar* GetData() const { return x8_data.get(); } + private: + short x0_songId; + short x2_groupId; + int x4_setupId; + rstl::auto_ptr< uchar > x8_data; + }; + + static CSfxHandle Play(const CMidiData&, unsigned short fadeTime, bool stopExisting, + short volume); static void Stop(const CSfxHandle&, unsigned short); + static void StopAll(); + + static CSfxHandle LocateHandle(); + + static rstl::reserved_vector< CMidiWrapper, 3 > mMidiWrappers; }; #endif // _CMIDIMANAGER diff --git a/include/Kyoto/Audio/CSfxHandle.hpp b/include/Kyoto/Audio/CSfxHandle.hpp index aeebb27c..b1b72f34 100644 --- a/include/Kyoto/Audio/CSfxHandle.hpp +++ b/include/Kyoto/Audio/CSfxHandle.hpp @@ -8,11 +8,11 @@ public: CSfxHandle() : mID(0) {} CSfxHandle(uint value); - uint GetIndex() const { return mID & 0xFF; } + int GetIndex() const { return mID & 0xFFF; } static CSfxHandle NullHandle() { return CSfxHandle(); } void operator=(const CSfxHandle& other) { mID = other.mID; } - bool operator==(const CSfxHandle& other) { return mID == other.mID; } - bool operator!=(const CSfxHandle& other) { return mID != other.mID; } + const bool operator==(const CSfxHandle& other) const { return mID == other.mID; } + const bool operator!=(const CSfxHandle& other) const { return mID != other.mID; } operator bool() const { return mID != 0; } void Clear() { mID = 0; } diff --git a/include/Kyoto/Audio/CSfxManager.hpp b/include/Kyoto/Audio/CSfxManager.hpp index f8b8da8e..32f7c957 100644 --- a/include/Kyoto/Audio/CSfxManager.hpp +++ b/include/Kyoto/Audio/CSfxManager.hpp @@ -211,9 +211,12 @@ public: static CSfxHandle SfxStart(ushort id, short vol, short pan, bool useAcoustics, short prio = kMaxPriority, bool looped = false, int areaId = kAllAreas); + static void SfxStop(CSfxHandle handle); + static void SfxVolume(CSfxHandle handle, uchar volume); static void SfxSpan(CSfxHandle, uchar); static bool IsPlaying(const CSfxHandle& handle); + static void StopSound(CSfxHandle handle); static void SetChannel(ESfxChannels); static void KillAll(ESfxChannels); diff --git a/src/Kyoto/Audio/CMidiManager.cpp b/src/Kyoto/Audio/CMidiManager.cpp new file mode 100644 index 00000000..cf93906a --- /dev/null +++ b/src/Kyoto/Audio/CMidiManager.cpp @@ -0,0 +1,128 @@ +#include "Kyoto/Audio/CSfxHandle.hpp" +#include + +#include "Kyoto/Audio/CAudioSys.hpp" + +#include +#include + +rstl::reserved_vector< CMidiManager::CMidiWrapper, 3 > CMidiManager::mMidiWrappers; + +CMidiManager::CMidiWrapper::CMidiWrapper() : x0_(0), xa_(1) {} + +const CSfxHandle& CMidiManager::CMidiWrapper::GetManagerHandle() const { return x4_; } + +const u32 CMidiManager::CMidiWrapper::GetAudioSysHandle() const { return x0_; } + +const bool CMidiManager::CMidiWrapper::IsAvailable() const { return xa_; } + +const short CMidiManager::CMidiWrapper::GetSongId() const { return x8_; } + +void CMidiManager::CMidiWrapper::SetAvailable(const bool v) { xa_ = v; } + +void CMidiManager::CMidiWrapper::SetAudioSysHandle(const u32 handle) { x0_ = handle; } + +void CMidiManager::CMidiWrapper::SetMidiHandle(const CSfxHandle& handle) { x4_ = handle; } + +void CMidiManager::CMidiWrapper::SetSongId(const short id) { x8_ = id; } + +CSfxHandle CMidiManager::Play(const CMidiData& data, unsigned short fadeTime, bool stopExisting, + short volume) { + bool bVar2 = false; + u32 uVar6 = 0; + CSfxHandle handle = LocateHandle(); + if (!handle) { + return CSfxHandle(); + } + CMidiWrapper& wrapper = mMidiWrappers[handle.GetIndex()]; + wrapper.SetAvailable(false); + wrapper.SetMidiHandle(handle); + if (stopExisting) { + for (int i = 0; i < mMidiWrappers.size(); ++i) { + if (mMidiWrappers[i].IsAvailable()) { + continue; + } + + if (data.GetSongId() == mMidiWrappers[i].GetSongId()) { + bVar2 = true; + uVar6 = mMidiWrappers[i].GetAudioSysHandle(); + mMidiWrappers[i].SetAvailable(true); + } else { + Stop(mMidiWrappers[i].GetManagerHandle(), fadeTime); + } + } + } + + if (bVar2) { + wrapper.SetAudioSysHandle(uVar6); + wrapper.SetSongId(data.GetSongId()); + } else { + u32 tmp = CAudioSys::SeqPlayEx(data.GetGroupId(), data.GetSongId(), data.GetData(), nullptr, 0); + if (fadeTime != 0) { + CAudioSys::SeqVolume(0, 0, tmp, 0); + } + CAudioSys::SeqVolume(volume, fadeTime, tmp, 0); + wrapper.SetAudioSysHandle(tmp); + wrapper.SetSongId(data.GetSongId()); + } + + return handle; +} + +void CMidiManager::Stop(const CSfxHandle& handle, ushort fadeTime) { + if (!handle) { + return; + } + + if (handle != mMidiWrappers[handle.GetIndex()].GetManagerHandle()) { + return; + } + + u32 sysHandle = mMidiWrappers[handle.GetIndex()].GetAudioSysHandle(); + if (fadeTime == 0) { + CAudioSys::SeqStop(sysHandle); + } else { + CAudioSys::SeqVolume(0, fadeTime, sysHandle, 1); + } + + mMidiWrappers[handle.GetIndex()].SetAvailable(true); +} + +void CMidiManager::StopAll() { + for (int i = 0; i < mMidiWrappers.size(); ++i) { + if (!mMidiWrappers[i].IsAvailable()) { + Stop(mMidiWrappers[i].GetManagerHandle(), 0); + } + } +} + +CSfxHandle CMidiManager::LocateHandle() { + for (int i = 0; i < mMidiWrappers.size(); ++i) { + if (mMidiWrappers[i].IsAvailable()) { + return CSfxHandle(i); + } + } + + if (mMidiWrappers.size() == mMidiWrappers.capacity()) { + return CSfxHandle(); + } + + mMidiWrappers.push_back(CMidiWrapper()); + return CSfxHandle(mMidiWrappers.size() - 1); +} + +CMidiManager::CMidiData::CMidiData(CInputStream& in) +: x0_songId(-1), x2_groupId(-1), x4_setupId(-1) { + in.ReadLong(); + x0_songId = in.ReadLong(); + x2_groupId = in.ReadLong(); + x4_setupId = in.ReadLong(); + int len = in.ReadInt32(); + x8_data = rs_new uchar[len]; + in.Get(x8_data.get(), len); +} + +#pragma inline_max_size(250) +CFactoryFnReturn FMidiDataFactory(const SObjectTag& tag, CInputStream& in, const CVParamTransfer&) { + return rs_new CMidiManager::CMidiData(in); +} diff --git a/src/Kyoto/Audio/CSfxManager.cpp b/src/Kyoto/Audio/CSfxManager.cpp index 17eb7e76..5920e028 100644 --- a/src/Kyoto/Audio/CSfxManager.cpp +++ b/src/Kyoto/Audio/CSfxManager.cpp @@ -1,6 +1,7 @@ #include "Kyoto/Alloc/CMemory.hpp" #include "Kyoto/Audio/CAudioSys.hpp" #include "Kyoto/Audio/CSfxHandle.hpp" +#include "dolphin/types.h" #include "musyx/musyx.h" #include "rstl/vector.hpp" #include @@ -208,7 +209,7 @@ short CSfxManager::CSfxWrapper::GetAudible(const CVector3f&) { return kSA_Aud3; const SND_VOICEID CSfxManager::CSfxWrapper::GetVoice() const { return x1c_voiceHandle; } -void CSfxManager::CSfxWrapper::SetVolume(short vol) { x20_vol = vol; } +void CSfxManager::CSfxWrapper::SetVolume(const short vol) { x20_vol = vol; } void CSfxManager::CSfxWrapper::UpdateEmitterSilent() { CAudioSys::SfxVolume(x1c_voiceHandle, 1); } @@ -264,7 +265,8 @@ void CSfxManager::UpdateListener(const CVector3f& pos, const CVector3f& dir, con } CSfxHandle CSfxManager::AddEmitter(const SND_FXID id, const CVector3f& pos, const CVector3f& dir, - const bool useAcoustics, const bool looped, const short prio, const int areaId) { + const bool useAcoustics, const bool looped, const short prio, + const int areaId) { CAudioSys::C3DEmitterParmData emitterParm; emitterParm.x24_sfxId = id; emitterParm.x29_prio = prio; @@ -274,8 +276,8 @@ CSfxHandle CSfxManager::AddEmitter(const SND_FXID id, const CVector3f& pos, cons } CSfxHandle CSfxManager::AddEmitter(const SND_FXID id, const CVector3f& pos, const CVector3f& dir, - const uchar vol, const bool useAcoustics, const bool looped, const short prio, - const int areaId) { + const uchar vol, const bool useAcoustics, const bool looped, + const short prio, const int areaId) { CAudioSys::C3DEmitterParmData emitterParm(150.f, 0.1f, 1, vol > 20 ? vol : 21); emitterParm.x0_pos = pos; emitterParm.xc_dir = dir; @@ -286,6 +288,60 @@ CSfxHandle CSfxManager::AddEmitter(const SND_FXID id, const CVector3f& pos, cons CSfxHandle CSfxManager::AddEmitter(CAudioSys::C3DEmitterParmData& parmData, const bool useAcoustics, const short prio, const bool looped, const int areaId) {} +void CSfxManager::UpdateEmitter(CSfxHandle handle, const CVector3f& pos, const CVector3f& dir, + uchar maxVol) {} + +void CSfxManager::RemoveEmitter(CSfxHandle handle) { StopSound(handle); } + +CSfxHandle CSfxManager::SfxStart(ushort id, short vol, short pan, bool useAcoustics, short prio, + bool looped, int areaId) {} + +void CSfxManager::SfxStop(CSfxHandle handle) { StopSound(handle); } + +void CSfxManager::SfxVolume(CSfxHandle handle, uchar volume) { + if (handle.GetIndex() > mChannels[mCurrentChannel].x48_.size()) { + + } else { + CSfxWrapper* wrapper = (CSfxWrapper*)mChannels[mCurrentChannel].x48_[handle.GetIndex()]; + if (wrapper == nullptr || wrapper->GetSfxHandle() != handle) { + return; + } + wrapper->SetVolume(volume); + if (wrapper->IsPlaying()) { + CAudioSys::SfxVolume(wrapper->GetVoice(), volume); + } + } +} + +void CSfxManager::SfxSpan(CSfxHandle handle, uchar span) {} + +void CSfxManager::KillAll(ESfxChannels channel) { + CSfxChannel& chan = mChannels[channel]; + + for (int i = 0; i < chan.x48_.size(); ++i) { + CBaseSfxWrapper* wrapper = chan.x48_[i]; + if (wrapper != NULL && wrapper->IsPlaying()) { + wrapper->Stop(); + } + + if (wrapper) { + wrapper->Release(); + } + chan.x48_[i] = nullptr; + } +} + +ushort CSfxManager::TranslateSFXID(ushort id) { + if (mTranslationTable == nullptr || id >= mTranslationTable->size()) { + return -1; + } + + short ret = (*mTranslationTable)[id]; + if (ret < 0) { + return -1; + } + return ret; +} #pragma inline_max_size(250) CFactoryFnReturn FAudioTranslationTableFactory(const SObjectTag& obj, CInputStream& in, const CVParamTransfer& xfer) {