mirror of
https://github.com/SourMesen/Mesen2.git
synced 2025-04-02 10:21:44 -04:00
575 lines
15 KiB
C++
575 lines
15 KiB
C++
#include "pch.h"
|
|
#include "GBA/APU/GbaApu.h"
|
|
#include "GBA/APU/GbaSquareChannel.h"
|
|
#include "GBA/APU/GbaNoiseChannel.h"
|
|
#include "GBA/APU/GbaWaveChannel.h"
|
|
#include "GBA/GbaConsole.h"
|
|
#include "GBA/GbaDmaController.h"
|
|
#include "GBA/GbaMemoryManager.h"
|
|
#include "Shared/MessageManager.h"
|
|
#include "Shared/Emulator.h"
|
|
#include "Shared/EmuSettings.h"
|
|
#include "Shared/Audio/SoundMixer.h"
|
|
#include "Utilities/HexUtilities.h"
|
|
#include "Utilities/BitUtilities.h"
|
|
#include "Utilities/Serializer.h"
|
|
#include "Utilities/StaticFor.h"
|
|
|
|
//TODOGBA APU todo list
|
|
//-do channels output 0 to 15, or -7 to 7? If -7 to 7, what does "0" correspond to?
|
|
//-GBA vs GB zombie mode (currently identical to the GB core)
|
|
//-a lot of gbc/gb behavior is implemented as-is (same as the GB core) - unsure how much of it is accurate or incorrect for GBA
|
|
//-does the length counter extra clock glitch exist on gba?
|
|
//-when is wave ram access possible?
|
|
|
|
void GbaApu::Init(Emulator* emu, GbaConsole* console, GbaDmaController* dmaController, GbaMemoryManager* memoryManager)
|
|
{
|
|
_soundBuffer = new int16_t[GbaApu::MaxSamples];
|
|
memset(_soundBuffer, 0, GbaApu::MaxSamples * sizeof(int16_t));
|
|
|
|
_dmaController = dmaController;
|
|
_memoryManager = memoryManager;
|
|
|
|
_square1.reset(new GbaSquareChannel(this));
|
|
_square2.reset(new GbaSquareChannel(this));
|
|
_wave.reset(new GbaWaveChannel(this, console));
|
|
_noise.reset(new GbaNoiseChannel(this));
|
|
|
|
_sampleCount = 0;
|
|
_prevClockCount = 0;
|
|
|
|
_state = {};
|
|
_state.Bias = 0x200;
|
|
|
|
UpdateSampleRate();
|
|
|
|
_emu = emu;
|
|
_console = console;
|
|
_settings = emu->GetSettings();
|
|
_soundMixer = emu->GetSoundMixer();
|
|
|
|
StaticFor<0, 16>::Apply([=](auto i) {
|
|
_runFunc[i] = &GbaApu::InternalRun<(bool)(i & 0x01), (bool)(i & 0x02), (bool)(i & 0x04), (bool)(i & 0x08)>;
|
|
});
|
|
}
|
|
|
|
GbaApu::GbaApu()
|
|
{
|
|
}
|
|
|
|
GbaApu::~GbaApu()
|
|
{
|
|
delete[] _soundBuffer;
|
|
}
|
|
|
|
template<bool sq1Enabled, bool sq2Enabled, bool waveEnabled, bool noiseEnabled>
|
|
void GbaApu::InternalRun()
|
|
{
|
|
uint64_t clockCount = _console->GetMasterClock() / 4;
|
|
if(clockCount == _prevClockCount) {
|
|
return;
|
|
}
|
|
|
|
uint32_t clocksToRun = (uint32_t)(clockCount - _prevClockCount);
|
|
|
|
GbaConfig& cfg = _settings->GetGbaConfig();
|
|
|
|
uint64_t samplingRate = 0x10ll << (3 - _state.SamplingRate);
|
|
int16_t bitRateMask = ~0;
|
|
switch(_state.SamplingRate) {
|
|
case 0: break;
|
|
case 1: bitRateMask = ~1; break;
|
|
case 2: bitRateMask = ~3; break;
|
|
case 3: bitRateMask = ~7; break;
|
|
}
|
|
|
|
bool enabled = _state.ApuEnabled && !_memoryManager->IsSystemStopped();
|
|
|
|
bool changed = true;
|
|
while(clocksToRun > 0) {
|
|
uint32_t minTimer = samplingRate - (_prevClockCount & (samplingRate - 1));
|
|
if(minTimer > clocksToRun) {
|
|
//Only sample audio every X clocks, based on the sampling rate selected
|
|
break;
|
|
}
|
|
|
|
if(enabled) {
|
|
if constexpr(sq1Enabled) {
|
|
uint8_t output = _square1->GetRawOutput();
|
|
_square1->Exec(minTimer);
|
|
changed |= output != _square1->GetRawOutput();
|
|
}
|
|
|
|
if constexpr(sq2Enabled) {
|
|
uint8_t output = _square2->GetRawOutput();
|
|
_square2->Exec(minTimer);
|
|
changed |= output != _square2->GetRawOutput();
|
|
}
|
|
|
|
if constexpr(waveEnabled) {
|
|
uint8_t output = _wave->GetRawOutput();
|
|
_wave->Exec(minTimer);
|
|
changed |= output != _wave->GetRawOutput();
|
|
}
|
|
|
|
if constexpr(noiseEnabled) {
|
|
uint8_t output = _noise->GetRawOutput();
|
|
_noise->Exec(minTimer);
|
|
changed |= output != _noise->GetRawOutput();
|
|
}
|
|
|
|
if((_prevClockCount & 0x1000) && !((_prevClockCount + minTimer) & 0x1000)) {
|
|
ClockFrameSequencer();
|
|
}
|
|
}
|
|
|
|
_prevClockCount += minTimer;
|
|
clocksToRun -= minTimer;
|
|
|
|
if(changed) {
|
|
changed = false;
|
|
|
|
double gbVolume = _state.GbVolume ? _state.GbVolume : 0.5;
|
|
|
|
int16_t gbLeftOutput = (
|
|
(_square1->GetOutput() * (int32_t)(cfg.Square1Vol & _state.EnableLeftSq1) / 100) +
|
|
(_square2->GetOutput() * (int32_t)(cfg.Square2Vol & _state.EnableLeftSq2) / 100) +
|
|
(_wave->GetOutput() * (int32_t)(cfg.WaveVol & _state.EnableLeftWave) / 100) +
|
|
(_noise->GetOutput() * (int32_t)(cfg.NoiseVol & _state.EnableLeftNoise) / 100)
|
|
) * (_state.LeftVolume + 1) * gbVolume;
|
|
|
|
_leftSample = ((std::clamp(
|
|
_state.Bias +
|
|
gbLeftOutput +
|
|
((_state.EnableLeftA ? (_state.DmaSampleA * (int32_t)cfg.ChannelAVol / 100) : 0) << (_state.VolumeA + 1)) +
|
|
((_state.EnableLeftB ? (_state.DmaSampleB * (int32_t)cfg.ChannelBVol / 100) : 0) << (_state.VolumeB + 1))
|
|
, 0, 0x3FF) & bitRateMask) - _state.Bias) * 32;
|
|
|
|
int16_t gbRightOutput = (
|
|
(_square1->GetOutput() * (int32_t)(cfg.Square1Vol & _state.EnableRightSq1) / 100) +
|
|
(_square2->GetOutput() * (int32_t)(cfg.Square2Vol & _state.EnableRightSq2) / 100) +
|
|
(_wave->GetOutput() * (int32_t)(cfg.WaveVol & _state.EnableRightWave) / 100) +
|
|
(_noise->GetOutput() * (int32_t)(cfg.NoiseVol & _state.EnableRightNoise) / 100)
|
|
) * (_state.RightVolume + 1) * gbVolume;
|
|
|
|
_rightSample = ((std::clamp(
|
|
_state.Bias +
|
|
gbRightOutput +
|
|
((_state.EnableRightA ? (_state.DmaSampleA * (int32_t)cfg.ChannelAVol / 100) : 0) << (_state.VolumeA + 1)) +
|
|
((_state.EnableRightB ? (_state.DmaSampleB * (int32_t)cfg.ChannelBVol / 100) : 0) << (_state.VolumeB + 1))
|
|
, 0, 0x3FF) & bitRateMask) - _state.Bias) * 32;
|
|
}
|
|
|
|
//Use low pass filter and subtract the result to filter out DC offset
|
|
_soundBuffer[_sampleCount] = _leftSample - _filterL.Process(_leftSample);
|
|
_soundBuffer[_sampleCount + 1] = _rightSample - _filterR.Process(_rightSample);
|
|
_sampleCount += 2;
|
|
}
|
|
|
|
if(_sampleCount >= 2000) {
|
|
PlayQueuedAudio();
|
|
}
|
|
}
|
|
|
|
void GbaApu::PlayQueuedAudio()
|
|
{
|
|
_soundMixer->PlayAudioBuffer(_soundBuffer, _sampleCount / 2, _sampleRate);
|
|
_sampleCount = 0;
|
|
}
|
|
|
|
void GbaApu::ClockFrameSequencer()
|
|
{
|
|
if((_state.FrameSequenceStep & 0x01) == 0) {
|
|
_square1->ClockLengthCounter();
|
|
_square2->ClockLengthCounter();
|
|
_wave->ClockLengthCounter();
|
|
_noise->ClockLengthCounter();
|
|
|
|
if((_state.FrameSequenceStep & 0x03) == 2) {
|
|
_square1->ClockSweepUnit();
|
|
}
|
|
} else if(_state.FrameSequenceStep == 7) {
|
|
_square1->ClockEnvelope();
|
|
_square2->ClockEnvelope();
|
|
_noise->ClockEnvelope();
|
|
}
|
|
|
|
_state.FrameSequenceStep = (_state.FrameSequenceStep + 1) & 0x07;
|
|
}
|
|
|
|
|
|
uint8_t GbaApu::ReadRegister(uint32_t addr)
|
|
{
|
|
switch(addr) {
|
|
case 0x60: return _square1->Read(0);
|
|
case 0x61: return 0;
|
|
case 0x62: return _square1->Read(1);
|
|
case 0x63: return _square1->Read(2);
|
|
case 0x64: return _square1->Read(3);
|
|
case 0x65: return _square1->Read(4);
|
|
case 0x66: return 0;
|
|
case 0x67: return 0;
|
|
|
|
case 0x68: return _square2->Read(1);
|
|
case 0x69: return _square2->Read(2);
|
|
case 0x6A: return 0;
|
|
case 0x6B: return 0;
|
|
case 0x6C: return _square2->Read(3);
|
|
case 0x6D: return _square2->Read(4);
|
|
case 0x6E: return 0;
|
|
case 0x6F: return 0;
|
|
|
|
case 0x70: return _wave->Read(0);
|
|
case 0x71: return 0;
|
|
case 0x72: return _wave->Read(1);
|
|
case 0x73: return _wave->Read(2);
|
|
case 0x74: return _wave->Read(3);
|
|
case 0x75: return _wave->Read(4);
|
|
case 0x76: return 0;
|
|
case 0x77: return 0;
|
|
|
|
case 0x78: return _noise->Read(1);
|
|
case 0x79: return _noise->Read(2);
|
|
case 0x7A: return 0;
|
|
case 0x7B: return 0;
|
|
case 0x7C: return _noise->Read(3);
|
|
case 0x7D: return _noise->Read(4);
|
|
case 0x7E: return 0;
|
|
case 0x7F: return 0;
|
|
|
|
case 0x80: return _state.RightVolume | (_state.LeftVolume << 4);
|
|
case 0x81: return _state.EnabledGb;
|
|
|
|
case 0x82: return _state.VolumeControl;
|
|
case 0x83: return _state.DmaSoundControl;
|
|
|
|
case 0x84:
|
|
return (
|
|
(_state.ApuEnabled ? 0x80 : 0) |
|
|
((_state.ApuEnabled && _noise->Enabled()) ? 0x08 : 0) |
|
|
((_state.ApuEnabled && _wave->Enabled()) ? 0x04 : 0) |
|
|
((_state.ApuEnabled && _square2->Enabled()) ? 0x02 : 0) |
|
|
((_state.ApuEnabled && _square1->Enabled()) ? 0x01 : 0)
|
|
);
|
|
|
|
case 0x85: return 0;
|
|
case 0x86: return 0;
|
|
case 0x87: return 0;
|
|
|
|
case 0x88: return (uint8_t)_state.Bias;
|
|
case 0x89: return (uint8_t)(_state.Bias >> 8) | (_state.SamplingRate << 6);
|
|
case 0x8A: return 0;
|
|
case 0x8B: return 0;
|
|
|
|
case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97:
|
|
case 0x98: case 0x99: case 0x9A: case 0x9B: case 0x9C: case 0x9D: case 0x9E: case 0x9F:
|
|
return _wave->ReadRam(addr);
|
|
|
|
default:
|
|
LogDebug("Read unknown sound register: " + HexUtilities::ToHex32(addr));
|
|
return _memoryManager->GetOpenBus(addr);
|
|
}
|
|
}
|
|
|
|
void GbaApu::WriteRegister(GbaAccessModeVal mode, uint32_t addr, uint8_t value)
|
|
{
|
|
Run();
|
|
|
|
if(!_state.ApuEnabled && addr <= 0x81) {
|
|
//Ignore all writes to these registers when APU is disabled
|
|
return;
|
|
}
|
|
|
|
switch(addr) {
|
|
case 0x60: _square1->Write(0, value); break;
|
|
case 0x61: break;
|
|
case 0x62: _square1->Write(1, value); break;
|
|
case 0x63: _square1->Write(2, value); break;
|
|
case 0x64: _square1->Write(3, value); break;
|
|
case 0x65: _square1->Write(4, value); break;
|
|
case 0x66: break;
|
|
case 0x67: break;
|
|
|
|
case 0x68: _square2->Write(1, value); break;
|
|
case 0x69: _square2->Write(2, value); break;
|
|
case 0x6A: break;
|
|
case 0x6B: break;
|
|
case 0x6C: _square2->Write(3, value); break;
|
|
case 0x6D: _square2->Write(4, value); break;
|
|
case 0x6E: break;
|
|
case 0x6F: break;
|
|
|
|
case 0x70: _wave->Write(0, value); break;
|
|
case 0x71: break;
|
|
case 0x72: _wave->Write(1, value); break;
|
|
case 0x73: _wave->Write(2, value); break;
|
|
case 0x74: _wave->Write(3, value); break;
|
|
case 0x75: _wave->Write(4, value); break;
|
|
case 0x76: break;
|
|
case 0x77: break;
|
|
|
|
case 0x78: _noise->Write(1, value); break;
|
|
case 0x79: _noise->Write(2, value); break;
|
|
case 0x7A: break;
|
|
case 0x7B: break;
|
|
case 0x7C: _noise->Write(3, value); break;
|
|
case 0x7D: _noise->Write(4, value); break;
|
|
case 0x7E: break;
|
|
case 0x7F: break;
|
|
|
|
case 0x80:
|
|
_state.LeftVolume = (value & 0x70) >> 4;
|
|
_state.RightVolume = (value & 0x07);
|
|
break;
|
|
|
|
case 0x81:
|
|
_state.EnabledGb = value;
|
|
_state.EnableLeftNoise = (value & 0x80) ? 0xFF : 0;
|
|
_state.EnableLeftWave = (value & 0x40) ? 0xFF : 0;
|
|
_state.EnableLeftSq2 = (value & 0x20) ? 0xFF : 0;
|
|
_state.EnableLeftSq1 = (value & 0x10) ? 0xFF : 0;
|
|
|
|
_state.EnableRightNoise = (value & 0x08) ? 0xFF : 0;
|
|
_state.EnableRightWave = (value & 0x04) ? 0xFF : 0;
|
|
_state.EnableRightSq2 = (value & 0x02) ? 0xFF : 0;
|
|
_state.EnableRightSq1 = (value & 0x01) ? 0xFF : 0;
|
|
break;
|
|
|
|
case 0x82:
|
|
_state.VolumeControl = value & 0x0F;
|
|
_state.GbVolume = (value & 0x03);
|
|
_state.VolumeA = (value & 0x04) >> 2;
|
|
_state.VolumeB = (value & 0x08) >> 3;
|
|
break;
|
|
|
|
case 0x83:
|
|
_state.EnableRightA = value & 0x01;
|
|
_state.EnableLeftA = value & 0x02;
|
|
_state.TimerA = (value & 0x04) >> 2;
|
|
if(value & 0x08) {
|
|
_fifo[0].Clear();
|
|
}
|
|
|
|
_state.EnableRightB = value & 0x10;
|
|
_state.EnableLeftB = value & 0x20;
|
|
_state.TimerB = (value & 0x40) >> 6;
|
|
if(value & 0x80) {
|
|
_fifo[1].Clear();
|
|
}
|
|
|
|
_state.DmaSoundControl = value & 0x77;
|
|
break;
|
|
|
|
case 0x84: {
|
|
bool apuEnabled = value & 0x80;
|
|
if(_state.ApuEnabled != apuEnabled) {
|
|
_state.ApuEnabled = apuEnabled;
|
|
if(!_state.ApuEnabled) {
|
|
_square1->Disable();
|
|
_square2->Disable();
|
|
_wave->Disable();
|
|
_noise->Disable();
|
|
WriteRegister({}, 0x80, 0);
|
|
WriteRegister({}, 0x81, 0);
|
|
_fifo[0].Clear();
|
|
_fifo[1].Clear();
|
|
} else {
|
|
_square1->Disable();
|
|
_square2->Disable();
|
|
_noise->Disable();
|
|
_wave->Disable();
|
|
_square1->ResetLengthCounter();
|
|
_square2->ResetLengthCounter();
|
|
_wave->ResetLengthCounter();
|
|
_noise->ResetLengthCounter();
|
|
_powerOnCycle = _memoryManager->GetMasterClock() / 4;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 0x85:
|
|
case 0x86:
|
|
case 0x87:
|
|
break;
|
|
|
|
case 0x88:
|
|
_state.Bias = (_state.Bias & 0x300) | (value & 0xFE);
|
|
break;
|
|
|
|
case 0x89:
|
|
_state.Bias = (_state.Bias & 0xFE) | ((value & 0x03) << 8);
|
|
|
|
if(_sampleCount) {
|
|
_soundMixer->PlayAudioBuffer(_soundBuffer, _sampleCount / 2, _sampleRate);
|
|
_sampleCount = 0;
|
|
}
|
|
|
|
_state.SamplingRate = (value >> 6) & 0x03;
|
|
UpdateSampleRate();
|
|
break;
|
|
|
|
case 0x8A: break;
|
|
case 0x8B: break;
|
|
|
|
case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97:
|
|
case 0x98: case 0x99: case 0x9A: case 0x9B: case 0x9C: case 0x9D: case 0x9E: case 0x9F:
|
|
_wave->WriteRam(addr, value);
|
|
break;
|
|
|
|
case 0xA0: case 0xA1: case 0xA2: case 0xA3:
|
|
if(_state.ApuEnabled) {
|
|
bool commit = (addr & 0x03) == 0x03 || (mode & GbaAccessMode::Byte) || ((mode & GbaAccessMode::HalfWord) && (addr & 0x01));
|
|
_fifo[0].Push(value, addr & 0x03, commit);
|
|
}
|
|
break;
|
|
|
|
case 0xA4: case 0xA5: case 0xA6: case 0xA7:
|
|
if(_state.ApuEnabled) {
|
|
bool commit = (addr & 0x03) == 0x03 || (mode & GbaAccessMode::Byte) || ((mode & GbaAccessMode::HalfWord) && (addr & 0x01));
|
|
_fifo[1].Push(value, addr & 0x03, commit);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
LogDebug("Write unknown sound register: " + HexUtilities::ToHex32(addr) + " = " + HexUtilities::ToHex(value));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GbaApu::ClockFifo(uint8_t timerIndex)
|
|
{
|
|
if(!_state.ApuEnabled) {
|
|
return;
|
|
}
|
|
|
|
Run();
|
|
|
|
if(_state.TimerA == timerIndex) {
|
|
if(_fifo[0].Size() <= 3) {
|
|
_dmaController->TriggerDmaChannel(GbaDmaTrigger::Special, 1);
|
|
}
|
|
|
|
if(!_fifo[0].Empty()) {
|
|
_state.DmaSampleA = (int8_t)_fifo[0].Pop();
|
|
}
|
|
}
|
|
|
|
if(_state.TimerB == timerIndex) {
|
|
if(_fifo[1].Size() <= 3) {
|
|
_dmaController->TriggerDmaChannel(GbaDmaTrigger::Special, 2);
|
|
}
|
|
|
|
if(!_fifo[1].Empty()) {
|
|
_state.DmaSampleB = (int8_t)_fifo[1].Pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GbaApu::UpdateEnabledChannels()
|
|
{
|
|
_enabledChannels = (
|
|
(_noise->Enabled() ? 0x08 : 0) |
|
|
(_wave->Enabled() ? 0x04 : 0) |
|
|
(_square2->Enabled() ? 0x02 : 0) |
|
|
(_square1->Enabled() ? 0x01 : 0)
|
|
);
|
|
}
|
|
|
|
void GbaApu::UpdateSampleRate()
|
|
{
|
|
_sampleRate = (32 * 1024) << _state.SamplingRate;
|
|
|
|
//Used to remove DC offset from audio signal
|
|
_filterL.SetCutoffFrequency(20, _sampleRate);
|
|
_filterR.SetCutoffFrequency(20, _sampleRate);
|
|
}
|
|
|
|
bool GbaApu::IsOddApuCycle()
|
|
{
|
|
return (((_memoryManager->GetMasterClock() / 4) - _powerOnCycle) & 0x02) != 0;
|
|
}
|
|
|
|
uint64_t GbaApu::GetElapsedApuCycles()
|
|
{
|
|
return (_memoryManager->GetMasterClock() / 4) - _powerOnCycle;
|
|
}
|
|
|
|
GbaApuDebugState GbaApu::GetState()
|
|
{
|
|
GbaApuDebugState state;
|
|
state.Common = _state;
|
|
state.Square1 = _square1->GetState();
|
|
state.Square2 = _square2->GetState();
|
|
state.Wave = _wave->GetState();
|
|
state.Noise = _noise->GetState();
|
|
return state;
|
|
}
|
|
|
|
void GbaApu::Serialize(Serializer& s)
|
|
{
|
|
if(s.IsSaving()) {
|
|
Run();
|
|
PlayQueuedAudio();
|
|
} else {
|
|
_sampleCount = 0;
|
|
}
|
|
|
|
SV(_state.DmaSampleA);
|
|
SV(_state.DmaSampleB);
|
|
|
|
SV(_state.VolumeControl);
|
|
SV(_state.GbVolume);
|
|
SV(_state.VolumeA);
|
|
SV(_state.VolumeB);
|
|
|
|
SV(_state.DmaSoundControl);
|
|
SV(_state.EnableRightA);
|
|
SV(_state.EnableLeftA);
|
|
SV(_state.TimerA);
|
|
SV(_state.EnableRightB);
|
|
SV(_state.EnableLeftB);
|
|
SV(_state.TimerB);
|
|
|
|
SV(_state.EnabledGb);
|
|
SV(_state.EnableLeftSq1);
|
|
SV(_state.EnableLeftSq2);
|
|
SV(_state.EnableLeftWave);
|
|
SV(_state.EnableLeftNoise);
|
|
|
|
SV(_state.EnableRightSq1);
|
|
SV(_state.EnableRightSq2);
|
|
SV(_state.EnableRightWave);
|
|
SV(_state.EnableRightNoise);
|
|
|
|
SV(_state.LeftVolume);
|
|
SV(_state.RightVolume);
|
|
|
|
SV(_state.FrameSequenceStep);
|
|
|
|
SV(_state.ApuEnabled);
|
|
|
|
SV(_state.Bias);
|
|
SV(_state.SamplingRate);
|
|
|
|
SV(_prevClockCount);
|
|
SV(_enabledChannels);
|
|
SV(_leftSample);
|
|
SV(_rightSample);
|
|
|
|
SV(_fifo[0]);
|
|
SV(_fifo[1]);
|
|
SV(_filterL);
|
|
SV(_filterR);
|
|
|
|
SV(_powerOnCycle);
|
|
|
|
SV(_square1);
|
|
SV(_square2);
|
|
SV(_wave);
|
|
SV(_noise);
|
|
|
|
if(!s.IsSaving()) {
|
|
UpdateSampleRate();
|
|
}
|
|
}
|