2020-05-18 16:10:53 -04:00
|
|
|
#include "stdafx.h"
|
|
|
|
#include "GbApu.h"
|
|
|
|
#include "Console.h"
|
|
|
|
#include "Gameboy.h"
|
|
|
|
#include "SoundMixer.h"
|
|
|
|
#include "EmuSettings.h"
|
2020-06-18 22:22:46 -04:00
|
|
|
#include "SuperGameboy.h"
|
2020-05-18 16:10:53 -04:00
|
|
|
#include "../Utilities/Serializer.h"
|
|
|
|
|
2020-06-18 00:58:22 -04:00
|
|
|
GbApu::GbApu()
|
2020-05-18 16:10:53 -04:00
|
|
|
{
|
2020-06-18 00:58:22 -04:00
|
|
|
_soundBuffer = new int16_t[GbApu::MaxSamples * 2];
|
|
|
|
memset(_soundBuffer, 0, GbApu::MaxSamples * 2 * sizeof(int16_t));
|
|
|
|
|
|
|
|
_leftChannel = blip_new(GbApu::MaxSamples);
|
|
|
|
_rightChannel = blip_new(GbApu::MaxSamples);
|
|
|
|
}
|
|
|
|
|
|
|
|
void GbApu::Init(Console* console, Gameboy* gameboy)
|
|
|
|
{
|
|
|
|
_square1.reset(new GbSquareChannel(this));
|
|
|
|
_square2.reset(new GbSquareChannel(this));
|
|
|
|
_wave.reset(new GbWaveChannel(this));
|
|
|
|
_noise.reset(new GbNoiseChannel(this));
|
|
|
|
|
|
|
|
_prevLeftOutput = 0;
|
|
|
|
_prevRightOutput = 0;
|
|
|
|
_clockCounter = 0;
|
|
|
|
_prevClockCount = 0;
|
|
|
|
|
2020-05-18 16:10:53 -04:00
|
|
|
_console = console;
|
2020-06-22 22:28:37 -04:00
|
|
|
_settings = console->GetSettings().get();
|
2020-05-18 16:10:53 -04:00
|
|
|
_soundMixer = console->GetSoundMixer().get();
|
|
|
|
_gameboy = gameboy;
|
2020-05-26 00:05:35 -04:00
|
|
|
_state = {};
|
2020-05-18 16:10:53 -04:00
|
|
|
|
2020-06-18 00:58:22 -04:00
|
|
|
blip_clear(_leftChannel);
|
|
|
|
blip_clear(_rightChannel);
|
2020-05-18 16:10:53 -04:00
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if(_gameboy->IsSgb()) {
|
2020-06-18 22:22:46 -04:00
|
|
|
blip_set_rates(_leftChannel, _gameboy->GetSgb()->GetClockRate(), GbApu::SampleRate);
|
|
|
|
blip_set_rates(_rightChannel, _gameboy->GetSgb()->GetClockRate(), GbApu::SampleRate);
|
2021-03-10 11:13:28 -05:00
|
|
|
} else {
|
2020-06-18 00:58:22 -04:00
|
|
|
blip_set_rates(_leftChannel, GbApu::ApuFrequency, GbApu::SampleRate);
|
|
|
|
blip_set_rates(_rightChannel, GbApu::ApuFrequency, GbApu::SampleRate);
|
|
|
|
}
|
2020-05-18 16:10:53 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
GbApu::~GbApu()
|
|
|
|
{
|
|
|
|
blip_delete(_leftChannel);
|
|
|
|
blip_delete(_rightChannel);
|
|
|
|
delete[] _soundBuffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
GbApuDebugState GbApu::GetState()
|
|
|
|
{
|
|
|
|
GbApuDebugState state;
|
|
|
|
state.Common = _state;
|
2020-06-18 00:58:22 -04:00
|
|
|
state.Square1 = _square1->GetState();
|
|
|
|
state.Square2 = _square2->GetState();
|
|
|
|
state.Wave = _wave->GetState();
|
|
|
|
state.Noise = _noise->GetState();
|
2020-05-18 16:10:53 -04:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
void GbApu::Run()
|
|
|
|
{
|
2020-05-19 21:31:33 -04:00
|
|
|
uint64_t clockCount = _gameboy->GetApuCycleCount();
|
2020-05-18 16:10:53 -04:00
|
|
|
uint32_t clocksToRun = (uint32_t)(clockCount - _prevClockCount);
|
|
|
|
_prevClockCount = clockCount;
|
|
|
|
|
2020-06-22 22:28:37 -04:00
|
|
|
GameboyConfig cfg = _settings->GetGameboyConfig();
|
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if(!_state.ApuEnabled) {
|
2020-05-19 21:31:33 -04:00
|
|
|
_clockCounter += clocksToRun;
|
2021-03-10 11:13:28 -05:00
|
|
|
} else {
|
|
|
|
while(clocksToRun > 0) {
|
|
|
|
uint32_t minTimer = std::min<uint32_t>({ clocksToRun, _square1->GetState().Timer, _square2->GetState().Timer, _wave->GetState().Timer, _noise->GetState().Timer });
|
2020-05-19 21:31:33 -04:00
|
|
|
|
|
|
|
clocksToRun -= minTimer;
|
2020-06-18 00:58:22 -04:00
|
|
|
_square1->Exec(minTimer);
|
|
|
|
_square2->Exec(minTimer);
|
|
|
|
_wave->Exec(minTimer);
|
|
|
|
_noise->Exec(minTimer);
|
2020-05-19 21:31:33 -04:00
|
|
|
|
|
|
|
int16_t leftOutput = (
|
2020-06-22 22:28:37 -04:00
|
|
|
(_square1->GetOutput() & _state.EnableLeftSq1) * cfg.Square1Vol / 100 +
|
|
|
|
(_square2->GetOutput() & _state.EnableLeftSq2) * cfg.Square2Vol / 100 +
|
|
|
|
(_wave->GetOutput() & _state.EnableLeftWave) * cfg.WaveVol / 100 +
|
|
|
|
(_noise->GetOutput() & _state.EnableLeftNoise) * cfg.NoiseVol / 100
|
2021-03-10 11:13:28 -05:00
|
|
|
) * (_state.LeftVolume + 1) * 40;
|
2020-05-19 21:31:33 -04:00
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if(_prevLeftOutput != leftOutput) {
|
2020-05-19 21:31:33 -04:00
|
|
|
blip_add_delta(_leftChannel, _clockCounter, leftOutput - _prevLeftOutput);
|
|
|
|
_prevLeftOutput = leftOutput;
|
|
|
|
}
|
|
|
|
|
|
|
|
int16_t rightOutput = (
|
2020-06-22 22:28:37 -04:00
|
|
|
(_square1->GetOutput() & _state.EnableRightSq1) * cfg.Square1Vol / 100 +
|
|
|
|
(_square2->GetOutput() & _state.EnableRightSq2) * cfg.Square2Vol / 100 +
|
|
|
|
(_wave->GetOutput() & _state.EnableRightWave) * cfg.WaveVol / 100 +
|
|
|
|
(_noise->GetOutput() & _state.EnableRightNoise) * cfg.NoiseVol / 100
|
2021-03-10 11:13:28 -05:00
|
|
|
) * (_state.RightVolume + 1) * 40;
|
2020-05-19 21:31:33 -04:00
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if(_prevRightOutput != rightOutput) {
|
2020-05-19 21:31:33 -04:00
|
|
|
blip_add_delta(_rightChannel, _clockCounter, rightOutput - _prevRightOutput);
|
|
|
|
_prevRightOutput = rightOutput;
|
|
|
|
}
|
|
|
|
|
|
|
|
_clockCounter += minTimer;
|
2020-05-18 16:10:53 -04:00
|
|
|
}
|
2020-05-19 21:31:33 -04:00
|
|
|
}
|
2020-05-18 16:10:53 -04:00
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if(!_gameboy->IsSgb() && _clockCounter >= 20000) {
|
2020-05-19 21:31:33 -04:00
|
|
|
blip_end_frame(_leftChannel, _clockCounter);
|
|
|
|
blip_end_frame(_rightChannel, _clockCounter);
|
2020-05-18 16:10:53 -04:00
|
|
|
|
2020-05-19 21:31:33 -04:00
|
|
|
uint32_t sampleCount = (uint32_t)blip_read_samples(_leftChannel, _soundBuffer, GbApu::MaxSamples, 1);
|
|
|
|
blip_read_samples(_rightChannel, _soundBuffer + 1, GbApu::MaxSamples, 1);
|
2020-06-18 00:58:22 -04:00
|
|
|
_soundMixer->PlayAudioBuffer(_soundBuffer, sampleCount, GbApu::SampleRate);
|
2020-05-19 21:31:33 -04:00
|
|
|
_clockCounter = 0;
|
2020-05-18 16:10:53 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
void GbApu::GetSoundSamples(int16_t* &samples, uint32_t& sampleCount)
|
2020-06-18 00:58:22 -04:00
|
|
|
{
|
|
|
|
Run();
|
|
|
|
blip_end_frame(_leftChannel, _clockCounter);
|
|
|
|
blip_end_frame(_rightChannel, _clockCounter);
|
|
|
|
|
|
|
|
sampleCount = (uint32_t)blip_read_samples(_leftChannel, _soundBuffer, GbApu::MaxSamples, 1);
|
|
|
|
blip_read_samples(_rightChannel, _soundBuffer + 1, GbApu::MaxSamples, 1);
|
|
|
|
samples = _soundBuffer;
|
|
|
|
_clockCounter = 0;
|
|
|
|
}
|
|
|
|
|
2020-05-18 16:10:53 -04:00
|
|
|
void GbApu::ClockFrameSequencer()
|
|
|
|
{
|
|
|
|
Run();
|
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if(!_state.ApuEnabled) {
|
2020-05-18 16:10:53 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if((_state.FrameSequenceStep & 0x01) == 0) {
|
2020-06-18 00:58:22 -04:00
|
|
|
_square1->ClockLengthCounter();
|
|
|
|
_square2->ClockLengthCounter();
|
|
|
|
_wave->ClockLengthCounter();
|
|
|
|
_noise->ClockLengthCounter();
|
2020-05-18 16:10:53 -04:00
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if((_state.FrameSequenceStep & 0x03) == 2) {
|
2020-06-18 00:58:22 -04:00
|
|
|
_square1->ClockSweepUnit();
|
2020-05-18 16:10:53 -04:00
|
|
|
}
|
2021-03-10 11:13:28 -05:00
|
|
|
} else if(_state.FrameSequenceStep == 7) {
|
2020-06-18 00:58:22 -04:00
|
|
|
_square1->ClockEnvelope();
|
|
|
|
_square2->ClockEnvelope();
|
|
|
|
_noise->ClockEnvelope();
|
2020-05-18 16:10:53 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
_state.FrameSequenceStep = (_state.FrameSequenceStep + 1) & 0x07;
|
|
|
|
}
|
|
|
|
|
2020-07-03 14:06:02 -04:00
|
|
|
uint8_t GbApu::Peek(uint16_t addr)
|
|
|
|
{
|
|
|
|
return InternalRead(addr);
|
|
|
|
}
|
|
|
|
|
2020-05-18 16:10:53 -04:00
|
|
|
uint8_t GbApu::Read(uint16_t addr)
|
|
|
|
{
|
|
|
|
Run();
|
2020-07-03 14:06:02 -04:00
|
|
|
return InternalRead(addr);
|
|
|
|
}
|
2020-05-18 16:10:53 -04:00
|
|
|
|
2020-07-03 14:06:02 -04:00
|
|
|
uint8_t GbApu::InternalRead(uint16_t addr)
|
|
|
|
{
|
2021-03-10 11:13:28 -05:00
|
|
|
switch(addr) {
|
|
|
|
case 0xFF10: case 0xFF11: case 0xFF12: case 0xFF13: case 0xFF14:
|
|
|
|
return _square1->Read(addr - 0xFF10);
|
|
|
|
|
|
|
|
case 0xFF16: case 0xFF17: case 0xFF18: case 0xFF19:
|
|
|
|
return _square2->Read(addr - 0xFF15);
|
|
|
|
|
|
|
|
case 0xFF1A: case 0xFF1B: case 0xFF1C: case 0xFF1D: case 0xFF1E:
|
|
|
|
return _wave->Read(addr - 0xFF1A);
|
|
|
|
|
|
|
|
case 0xFF20: case 0xFF21: case 0xFF22: case 0xFF23:
|
|
|
|
return _noise->Read(addr - 0xFF1F);
|
|
|
|
|
|
|
|
case 0xFF24:
|
|
|
|
return (
|
|
|
|
(_state.ExtAudioLeftEnabled ? 0x80 : 0) |
|
|
|
|
(_state.LeftVolume << 4) |
|
|
|
|
(_state.ExtAudioRightEnabled ? 0x08 : 0) |
|
|
|
|
_state.RightVolume
|
|
|
|
);
|
|
|
|
|
|
|
|
case 0xFF25:
|
|
|
|
return (
|
|
|
|
(_state.EnableLeftNoise ? 0x80 : 0) |
|
|
|
|
(_state.EnableLeftWave ? 0x40 : 0) |
|
|
|
|
(_state.EnableLeftSq2 ? 0x20 : 0) |
|
|
|
|
(_state.EnableLeftSq1 ? 0x10 : 0) |
|
|
|
|
(_state.EnableRightNoise ? 0x08 : 0) |
|
|
|
|
(_state.EnableRightWave ? 0x04 : 0) |
|
|
|
|
(_state.EnableRightSq2 ? 0x02 : 0) |
|
|
|
|
(_state.EnableRightSq1 ? 0x01 : 0)
|
|
|
|
);
|
|
|
|
|
|
|
|
case 0xFF26:
|
|
|
|
return (
|
|
|
|
(_state.ApuEnabled ? 0x80 : 0) |
|
|
|
|
0x70 | //open bus
|
|
|
|
((_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 0xFF30: case 0xFF31: case 0xFF32: case 0xFF33: case 0xFF34: case 0xFF35: case 0xFF36: case 0xFF37:
|
|
|
|
case 0xFF38: case 0xFF39: case 0xFF3A: case 0xFF3B: case 0xFF3C: case 0xFF3D: case 0xFF3E: case 0xFF3F:
|
|
|
|
return _wave->ReadRam(addr);
|
2020-05-18 16:10:53 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
//Open bus
|
|
|
|
return 0xFF;
|
|
|
|
}
|
|
|
|
|
|
|
|
void GbApu::Write(uint16_t addr, uint8_t value)
|
|
|
|
{
|
|
|
|
Run();
|
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
if(!_state.ApuEnabled) {
|
|
|
|
if(addr == 0xFF11 || addr == 0xFF16 || addr == 0xFF20) {
|
2020-05-18 16:10:53 -04:00
|
|
|
//Allow writes to length counter, but not the upper 2 bits (square duty)
|
|
|
|
value &= 0x3F;
|
2021-03-10 11:13:28 -05:00
|
|
|
} else if(addr < 0xFF26 && addr != 0xFF1B) {
|
2020-05-18 16:10:53 -04:00
|
|
|
//Ignore all writes to these registers when APU is disabled
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
switch(addr) {
|
|
|
|
case 0xFF10: case 0xFF11: case 0xFF12: case 0xFF13: case 0xFF14:
|
|
|
|
_square1->Write(addr - 0xFF10, value);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xFF16: case 0xFF17: case 0xFF18: case 0xFF19:
|
|
|
|
_square2->Write(addr - 0xFF15, value); //Same as square1, but without a sweep unit
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xFF1A: case 0xFF1B: case 0xFF1C: case 0xFF1D: case 0xFF1E:
|
|
|
|
_wave->Write(addr - 0xFF1A, value);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xFF20: case 0xFF21: case 0xFF22: case 0xFF23:
|
|
|
|
_noise->Write(addr - 0xFF1F, value);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xFF24:
|
|
|
|
_state.ExtAudioLeftEnabled = (value & 0x80) != 0;
|
|
|
|
_state.LeftVolume = (value & 0x70) >> 4;
|
|
|
|
_state.ExtAudioRightEnabled = (value & 0x08) != 0;
|
|
|
|
_state.RightVolume = (value & 0x07);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xFF25:
|
|
|
|
_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 0xFF26: {
|
2020-05-18 16:10:53 -04:00
|
|
|
bool apuEnabled = (value & 0x80) != 0;
|
2021-03-10 11:13:28 -05:00
|
|
|
if(_state.ApuEnabled != apuEnabled) {
|
|
|
|
if(!apuEnabled) {
|
2020-06-18 00:58:22 -04:00
|
|
|
_square1->Disable();
|
|
|
|
_square2->Disable();
|
|
|
|
_wave->Disable();
|
|
|
|
_noise->Disable();
|
2020-05-18 16:10:53 -04:00
|
|
|
Write(0xFF24, 0);
|
|
|
|
Write(0xFF25, 0);
|
2021-03-10 11:13:28 -05:00
|
|
|
} else {
|
2020-05-18 16:10:53 -04:00
|
|
|
//When powered on, the frame sequencer is reset so that the next step will be 0,
|
|
|
|
//the square duty units are reset to the first step of the waveform, and the wave channel's sample buffer is reset to 0.
|
|
|
|
_state.FrameSequenceStep = 0;
|
|
|
|
}
|
|
|
|
_state.ApuEnabled = apuEnabled;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2021-03-10 11:13:28 -05:00
|
|
|
case 0xFF30: case 0xFF31: case 0xFF32: case 0xFF33: case 0xFF34: case 0xFF35: case 0xFF36: case 0xFF37:
|
|
|
|
case 0xFF38: case 0xFF39: case 0xFF3A: case 0xFF3B: case 0xFF3C: case 0xFF3D: case 0xFF3E: case 0xFF3F:
|
|
|
|
_wave->WriteRam(addr, value);
|
|
|
|
break;
|
2020-05-18 16:10:53 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-03 00:38:55 -04:00
|
|
|
uint8_t GbApu::ReadCgbRegister(uint16_t addr)
|
|
|
|
{
|
2021-03-10 11:13:28 -05:00
|
|
|
switch(addr) {
|
|
|
|
case 0xFF76: return _square1->GetOutput() | (_square2->GetOutput() << 4);
|
|
|
|
case 0xFF77: return _noise->GetOutput() | (_wave->GetOutput() << 4);
|
2020-07-03 00:38:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
//Should not be called
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
template<typename T>
|
|
|
|
void GbApu::ProcessLengthEnableFlag(uint8_t value, T &length, bool &lengthEnabled, bool &enabled)
|
2020-06-09 18:34:10 -04:00
|
|
|
{
|
|
|
|
bool newLengthEnabled = (value & 0x40) != 0;
|
2021-03-10 11:13:28 -05:00
|
|
|
if(newLengthEnabled && !lengthEnabled && (_state.FrameSequenceStep & 0x01) == 1) {
|
2020-06-09 18:34:10 -04:00
|
|
|
//"Extra length clocking occurs when writing to NRx4 when the frame sequencer's next step is one that doesn't clock
|
|
|
|
//the length counter. In this case, if the length counter was PREVIOUSLY disabled and now enabled and the length counter
|
|
|
|
//is not zero, it is decremented. If this decrement makes it zero and trigger is clear, the channel is disabled."
|
2021-03-10 11:13:28 -05:00
|
|
|
if(length > 0) {
|
2020-06-09 18:34:10 -04:00
|
|
|
length--;
|
2021-03-10 11:13:28 -05:00
|
|
|
if(length == 0) {
|
|
|
|
if(value & 0x80) {
|
2020-06-09 18:34:10 -04:00
|
|
|
length = sizeof(T) == 1 ? 0x3F : 0xFF;
|
2021-03-10 11:13:28 -05:00
|
|
|
} else {
|
2020-06-09 18:34:10 -04:00
|
|
|
enabled = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
lengthEnabled = newLengthEnabled;
|
|
|
|
}
|
|
|
|
|
2020-05-18 16:10:53 -04:00
|
|
|
void GbApu::Serialize(Serializer& s)
|
|
|
|
{
|
|
|
|
s.Stream(
|
|
|
|
_state.ApuEnabled, _state.FrameSequenceStep,
|
|
|
|
_state.EnableLeftSq1, _state.EnableLeftSq2, _state.EnableLeftWave, _state.EnableLeftNoise,
|
|
|
|
_state.EnableRightSq1, _state.EnableRightSq2, _state.EnableRightWave, _state.EnableRightNoise,
|
|
|
|
_state.LeftVolume, _state.RightVolume, _state.ExtAudioLeftEnabled, _state.ExtAudioRightEnabled,
|
|
|
|
_prevLeftOutput, _prevRightOutput, _clockCounter, _prevClockCount
|
|
|
|
);
|
|
|
|
|
2020-06-18 00:58:22 -04:00
|
|
|
s.Stream(_square1.get());
|
|
|
|
s.Stream(_square2.get());
|
|
|
|
s.Stream(_wave.get());
|
|
|
|
s.Stream(_noise.get());
|
2020-05-18 16:10:53 -04:00
|
|
|
}
|
2020-06-09 18:34:10 -04:00
|
|
|
|
2021-03-10 11:13:28 -05:00
|
|
|
template void GbApu::ProcessLengthEnableFlag<uint8_t>(uint8_t value, uint8_t& length, bool& lengthEnabled, bool& enabled);
|
|
|
|
template void GbApu::ProcessLengthEnableFlag<uint16_t>(uint8_t value, uint16_t& length, bool& lengthEnabled, bool& enabled);
|