From b5b9a1ca532a7406cae9ae777114c81b62ae0aff Mon Sep 17 00:00:00 2001 From: Souryo Date: Mon, 23 Jun 2014 13:52:53 -0400 Subject: [PATCH] Sound improvements (sync, etc.), added pause/resume/stop/reset in GUI --- Core/APU.cpp | 42 ++++++----- Core/APU.h | 16 ++++- Core/CPU.h | 2 + Core/Console.cpp | 24 ++++--- Core/Console.h | 1 + Core/PPU.cpp | 23 ++++-- Core/PPU.h | 2 + GUI/GUI.rc | Bin 8528 -> 9432 bytes GUI/MainWindow.cpp | 163 +++++++++++++++++++++++++++++++------------ GUI/MainWindow.h | 18 ++++- GUI/SoundManager.cpp | 70 ++++++++++++++++--- GUI/SoundManager.h | 7 +- GUI/resource.h | Bin 2348 -> 2724 bytes 13 files changed, 277 insertions(+), 91 deletions(-) diff --git a/Core/APU.cpp b/Core/APU.cpp index ecd75a64..08560a65 100644 --- a/Core/APU.cpp +++ b/Core/APU.cpp @@ -4,8 +4,6 @@ #include "APU.h" #include "CPU.h" -void output_samples(const blip_sample_t*, size_t count); - APU* APU::Instance = nullptr; IAudioDevice* APU::AudioDevice = nullptr; @@ -13,16 +11,24 @@ APU::APU() { APU::Instance = this; - blargg_err_t error = _buf.sample_rate(44100); - if(error) { - //report_error(error); - } - - _buf.clock_rate(1789773); + _buf.sample_rate(APU::SampleRate); + _buf.clock_rate(CPU::ClockRate); _apu.output(&_buf); _apu.dmc_reader(&APU::DMCRead); //_apu.irq_notifier(irq_changed); + + _outputBuffer = new int16_t[APU::SamplesPerFrame]; +} + +APU::~APU() +{ + delete[] _outputBuffer; +} + +void APU::Reset() +{ + _apu.reset(); } int APU::DMCRead(void*, cpu_addr_t addr) @@ -34,7 +40,7 @@ uint8_t APU::ReadRAM(uint16_t addr) { switch(addr) { case 0x4015: - return _apu.read_status(0); + return _apu.read_status(5); break; } @@ -43,22 +49,22 @@ uint8_t APU::ReadRAM(uint16_t addr) void APU::WriteRAM(uint16_t addr, uint8_t value) { - _apu.write_register(0, addr, value); + _apu.write_register(5, addr, value); } -void APU::Exec(uint32_t executedCycles) +bool APU::Exec(uint32_t executedCycles) { _apu.end_frame(executedCycles); _buf.end_frame(executedCycles); - // Read some samples out of Blip_Buffer if there are enough to - // fill our output buffer - const size_t out_size = 4096; - blip_sample_t out_buf[out_size]; + // Read some samples out of Blip_Buffer if there are enough to fill our output buffer + uint32_t availableSampleCount = _buf.samples_avail(); + if(availableSampleCount >= APU::SamplesPerFrame) { + size_t sampleCount = _buf.read_samples(_outputBuffer, APU::SamplesPerFrame); + APU::AudioDevice->PlayBuffer(_outputBuffer, sampleCount * BitsPerSample / 8); - if(_buf.samples_avail() >= out_size) { - size_t count = _buf.read_samples(out_buf, out_size); - APU::AudioDevice->PlayBuffer(out_buf, count * sizeof(blip_sample_t)); + return true; } + return false; } diff --git a/Core/APU.h b/Core/APU.h index 8c5f8887..5f89b363 100644 --- a/Core/APU.h +++ b/Core/APU.h @@ -7,16 +7,26 @@ class APU : public IMemoryHandler { - Nes_Apu _apu; - Blip_Buffer _buf; + private: + Nes_Apu _apu; + Blip_Buffer _buf; + int16_t* _outputBuffer; private: static IAudioDevice* AudioDevice; static int DMCRead(void*, cpu_addr_t addr); static APU* Instance; + public: + static const uint32_t SampleRate = 44100; + static const uint32_t SamplesPerFrame = 44100 / 60; + static const uint32_t BitsPerSample = 16; + public: APU(); + ~APU(); + + void Reset(); vector> GetRAMAddresses() { @@ -31,5 +41,5 @@ class APU : public IMemoryHandler uint8_t ReadRAM(uint16_t addr); void WriteRAM(uint16_t addr, uint8_t value); - void Exec(uint32_t executedCycles); + bool Exec(uint32_t executedCycles); }; \ No newline at end of file diff --git a/Core/CPU.h b/Core/CPU.h index 2c58213c..96df8577 100644 --- a/Core/CPU.h +++ b/Core/CPU.h @@ -620,6 +620,8 @@ private: #pragma endregion public: + static const uint32_t ClockRate = 1789773; + CPU(MemoryManager *memoryManager); static uint64_t GetCycleCount() { return CPU::CycleCount; } static void IncCycleCount(uint32_t cycles) { diff --git a/Core/Console.cpp b/Core/Console.cpp index 4cd42c99..a070611e 100644 --- a/Core/Console.cpp +++ b/Core/Console.cpp @@ -30,7 +30,7 @@ Console::~Console() void Console::Reset() { - _cpu->Reset(); + _reset = true; } void Console::Stop() @@ -59,16 +59,13 @@ void Console::Run() Timer fpsTimer; uint32_t lastFrameCount = 0; double elapsedTime = 0; - uint32_t cycleCount = 0; + double targetTime = 16.6666666666666666; while(true) { uint32_t executedCycles = _cpu->Exec(); _ppu->Exec(); - _apu->Exec(executedCycles); + bool frameDone = _apu->Exec(executedCycles); - cycleCount += executedCycles; - - if(CheckFlag(EmulationFlags::LimitFPS) && cycleCount >= 29780) { - double targetTime = 16.638935108153078202995008319468; + if(CheckFlag(EmulationFlags::LimitFPS) && frameDone) { elapsedTime = clockTimer.GetElapsedMS(); while(targetTime > elapsedTime) { if(targetTime - elapsedTime > 2) { @@ -76,7 +73,6 @@ void Console::Run() } elapsedTime = clockTimer.GetElapsedMS(); } - cycleCount = 0; clockTimer.Reset(); } @@ -88,8 +84,20 @@ void Console::Run() } if(_stop) { + _stop = false; break; } + + if(_reset) { + clockTimer.Reset(); + fpsTimer.Reset(); + lastFrameCount = 0; + elapsedTime = 0; + _cpu->Reset(); + _ppu->Reset(); + _apu->Reset(); + _reset = false; + } } } diff --git a/Core/Console.h b/Core/Console.h index 78f08ff7..c54993ac 100644 --- a/Core/Console.h +++ b/Core/Console.h @@ -28,6 +28,7 @@ class Console wstring _romFilename; bool _stop = false; + bool _reset = false; public: Console(wstring filename); diff --git a/Core/PPU.cpp b/Core/PPU.cpp index e29cdf59..1bb9e238 100644 --- a/Core/PPU.cpp +++ b/Core/PPU.cpp @@ -23,13 +23,9 @@ uint32_t PPU_PALETTE_RGB[] = { PPU::PPU(MemoryManager *memoryManager) { _memoryManager = memoryManager; - _state = {}; - _flags = {}; - _statusFlags = {}; - - memset(_spriteRAM, 0xFF, 0x100); - _outputBuffer = new uint8_t[256 * 240 * 4]; + + Reset(); } PPU::~PPU() @@ -37,6 +33,21 @@ PPU::~PPU() delete[] _outputBuffer; } +void PPU::Reset() +{ + _state = {}; + _flags = {}; + _statusFlags = {}; + + _scanline = 0; + _cycle = 0; + _frameCount = 0; + _cycleCount = 0; + _memoryReadBuffer = 0; + + memset(_spriteRAM, 0xFF, 0x100); +} + bool PPU::CheckFlag(PPUControlFlags flag) { return false; diff --git a/Core/PPU.h b/Core/PPU.h index 75a7dfa9..f4a28b8d 100644 --- a/Core/PPU.h +++ b/Core/PPU.h @@ -160,6 +160,8 @@ class PPU : public IMemoryHandler PPU(MemoryManager *memoryManager); ~PPU(); + void Reset(); + vector> GetRAMAddresses() { return{ { { 0x2000, 0x3FFF } }, { {0x4014, 0x4014 } } }; diff --git a/GUI/GUI.rc b/GUI/GUI.rc index 599f950984fab9818c48c8d049085104d48c9f8f..f26022f6b0ed3c607751ef7aa10dde96fc979345 100644 GIT binary patch delta 408 zcmZ{gy-LGS6vux{V@-tHYhuztsKH5a6DOw_;|E0zO)CgO>(EV6Xgm81_AGq}2et3e z+0h5^1$==bH!($!uIK!}KMp_7UMF_tf`m+-oLKi6Fyfv*Hw-yuUqX9F=E~a6F?4Ae zEFI2C*ky|oT0~rva3pWRhP*hQn{vyD!LN$J$E8kK>ZC+;$d;=36_cee4f|lhDZCTf z%bZs-hCJ{np}jZO)kbNS(dLr$A5GT-={dek?00R-#GH~4)6DDWdNA!dYnG&{>R!>L zTXZ2JHM!+mxw^Di^pR&3h_5`E)izfPmf6u<=gM+R{GT23UtO)stmaMc^q2Yp9Pm{~ delta 25 hcmccNdBJJJ46eySyqhNHaOrGj;`_$B`HRRCegKWt3X1>$ diff --git a/GUI/MainWindow.cpp b/GUI/MainWindow.cpp index ad3a524b..08dce55a 100644 --- a/GUI/MainWindow.cpp +++ b/GUI/MainWindow.cpp @@ -7,8 +7,7 @@ using namespace DirectX; -namespace NES -{ +namespace NES { MainWindow* MainWindow::Instance = nullptr; bool MainWindow::Initialize() @@ -69,15 +68,16 @@ namespace NES Initialize(); + InitializeOptions(); InputManager inputManager; ControlManager::RegisterControlDevice(&inputManager, 0); - - HACCEL hAccel = LoadAccelerators(_hInstance, MAKEINTRESOURCE(IDC_Accelerator)); + + HACCEL hAccel = LoadAccelerators(_hInstance, MAKEINTRESOURCE(IDC_Accelerator)); if(hAccel == nullptr) { //error std::cout << "error"; } - + MSG msg = { 0 }; while(WM_QUIT != msg.message) { if(PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { @@ -135,28 +135,31 @@ namespace NES INT_PTR CALLBACK MainWindow::About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); - switch (message) - { - case WM_INITDIALOG: - return (INT_PTR)TRUE; - - case WM_COMMAND: - if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) - { - EndDialog(hDlg, LOWORD(wParam)); + switch(message) { + case WM_INITDIALOG: return (INT_PTR)TRUE; - } - break; + + case WM_COMMAND: + if(LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { + EndDialog(hDlg, LOWORD(wParam)); + return (INT_PTR)TRUE; + } + break; } return (INT_PTR)FALSE; } - void MainWindow::OpenROM() + void MainWindow::InitializeOptions() + { + Console::SetFlags(EmulationFlags::LimitFPS); + } + + wstring MainWindow::SelectROM() { wchar_t buffer[2000]; OPENFILENAME ofn; - ZeroMemory(&ofn , sizeof(ofn)); + ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = nullptr; ofn.lpstrFile = buffer; @@ -165,38 +168,90 @@ namespace NES ofn.lpstrFilter = L"NES Roms\0*.NES\0All\0*.*"; ofn.nFilterIndex = 1; ofn.lpstrFileTitle = nullptr; - ofn.nMaxFileTitle = 0 ; - ofn.lpstrInitialDir= nullptr; - ofn.Flags = OFN_PATHMUSTEXIST|OFN_FILEMUSTEXIST ; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = nullptr; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; GetOpenFileName(&ofn); - - wstring filename = wstring(buffer); - if(filename.length() > 0) { - Stop(); + return wstring(buffer); + } - _console.reset(new Console(filename)); + void MainWindow::Start(wstring romFilename = L"") + { + if(_emuThread) { + Stop(false); + } + + if(romFilename.length() > 0) { + _currentROM = romFilename; + _console.reset(new Console(_currentROM)); + } + + if(!_console) { + _console.reset(new Console(_currentROM)); + } + + if(_console) { _emuThread.reset(new thread(&Console::Run, _console.get())); + + SetMenuEnabled(ID_NES_PAUSE, true); + SetMenuEnabled(ID_NES_RESET, true); + SetMenuEnabled(ID_NES_STOP, true); + SetMenuEnabled(ID_NES_RESUME, false); } } - void MainWindow::Stop() + void MainWindow::Stop(bool powerOff) { + _soundManager.Reset(); if(_console) { _console->Stop(); - _emuThread->join(); - - _console.release(); + if(powerOff) { + _console.release(); + } } + if(_emuThread) { + _emuThread->join(); + _emuThread.release(); + } + + SetMenuEnabled(ID_NES_PAUSE, false); + SetMenuEnabled(ID_NES_RESET, !powerOff); + SetMenuEnabled(ID_NES_STOP, !powerOff); + SetMenuEnabled(ID_NES_RESUME, true); + } + + void MainWindow::Reset() + { + if(_console) { + _soundManager.Reset(); + _console->Reset(); + } + } + + void MainWindow::SetMenuEnabled(int resourceID, bool enabled) + { + HMENU hMenu = GetMenu(_hWnd); + EnableMenuItem(hMenu, resourceID, enabled ? MF_ENABLED : MF_GRAYED); + } + + bool MainWindow::IsMenuChecked(int resourceID) + { + HMENU hMenu = GetMenu(_hWnd); + return (GetMenuState(hMenu, resourceID, MF_BYCOMMAND) & MF_CHECKED) == MF_CHECKED; + } + + bool MainWindow::SetMenuCheck(int resourceID, bool checked) + { + HMENU hMenu = GetMenu(_hWnd); + CheckMenuItem(hMenu, resourceID, MF_BYCOMMAND | (checked ? MF_CHECKED : MF_UNCHECKED)); + return checked; } bool MainWindow::ToggleMenuCheck(int resourceID) { - HMENU hMenu = GetMenu(_hWnd); - bool checked = (GetMenuState(hMenu, resourceID, MF_BYCOMMAND) & MF_CHECKED) == MF_CHECKED; - CheckMenuItem(hMenu, resourceID, MF_BYCOMMAND | (checked ? MF_UNCHECKED : MF_CHECKED)); - return !checked; + return SetMenuCheck(resourceID, !IsMenuChecked(resourceID)); } void MainWindow::LimitFPS_Click() @@ -235,7 +290,7 @@ namespace NES void MainWindow::RunTests() { - Stop(); + Stop(true); int passCount = 0; int failCount = 0; int totalCount = 0; @@ -264,6 +319,7 @@ namespace NES } totalCount++; } + Stop(true); std::cout << "------------------------" << std::endl; std::cout << passCount << " / " << totalCount << " + " << failCount << " FAILED" << std::endl; @@ -273,6 +329,7 @@ namespace NES LRESULT CALLBACK MainWindow::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { static MainWindow *mainWindow = MainWindow::GetInstance(); + wstring filename; PAINTSTRUCT ps; int wmId, wmEvent; HDC hdc; @@ -284,23 +341,43 @@ namespace NES // Parse the menu selections: switch (wmId) { case ID_FILE_OPEN: - mainWindow->OpenROM(); + filename = mainWindow->SelectROM(); + if(filename.length() > 0) { + mainWindow->Start(filename); + } break; + case ID_FILE_EXIT: + DestroyWindow(hWnd); + break; + + case ID_NES_RESUME: + mainWindow->Start(); + break; + case ID_NES_PAUSE: + mainWindow->Stop(false); + break; + case ID_NES_STOP: + mainWindow->Stop(true); + break; + case ID_NES_RESET: + mainWindow->Reset(); + break; + + case ID_OPTIONS_LIMITFPS: + mainWindow->LimitFPS_Click(); + break; + case ID_TESTS_RUNTESTS: mainWindow->RunTests(); break; case ID_TESTS_SAVETESTRESULT: mainWindow->SaveTestResult(); break; - case ID_OPTIONS_LIMITFPS: - mainWindow->LimitFPS_Click(); - break; + case ID_HELP_ABOUT: DialogBox(nullptr, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break; - case ID_FILE_EXIT: - DestroyWindow(hWnd); - break; + default: return DefWindowProc(hWnd, message, wParam, lParam); } @@ -328,7 +405,7 @@ namespace NES break; case WM_DESTROY: - mainWindow->Stop(); + mainWindow->Stop(true); PostQuitMessage(0); break; diff --git a/GUI/MainWindow.h b/GUI/MainWindow.h index 26c48f01..acb7e0d3 100644 --- a/GUI/MainWindow.h +++ b/GUI/MainWindow.h @@ -15,7 +15,9 @@ namespace NES { SoundManager _soundManager; unique_ptr _console; unique_ptr _emuThread; + wstring _currentROM; + private: bool Initialize(); HRESULT InitWindow(); static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); @@ -30,15 +32,27 @@ namespace NES { void LimitFPS_Click(); + void SetMenuEnabled(int resourceID, bool enabled); + + bool IsMenuChecked(int resourceID); + bool SetMenuCheck(int resourceID, bool checked); bool ToggleMenuCheck(int resourceID); + wstring SelectROM(); + void Start(wstring romFilename); + void Reset(); + void Stop(bool powerOff); + + void InitializeOptions(); + + + public: MainWindow(HINSTANCE hInstance, int nCmdShow) : _hInstance(hInstance), _nCmdShow(nCmdShow) { MainWindow::Instance = this; } + int Run(); - void OpenROM(); - void Stop(); }; } \ No newline at end of file diff --git a/GUI/SoundManager.cpp b/GUI/SoundManager.cpp index d9893c5b..a09deaf2 100644 --- a/GUI/SoundManager.cpp +++ b/GUI/SoundManager.cpp @@ -76,8 +76,8 @@ bool SoundManager::InitializeDirectSound(HWND hwnd) // Set the buffer description of the secondary sound buffer that the wave file will be loaded onto. bufferDesc.dwSize = sizeof(DSBUFFERDESC); - bufferDesc.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS | DSBCAPS_LOCSOFTWARE | DSBCAPS_CTRLVOLUME; - bufferDesc.dwBufferBytes = 0xFFFFF; + bufferDesc.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS | DSBCAPS_LOCSOFTWARE | DSBCAPS_CTRLVOLUME | DSBCAPS_CTRLFREQUENCY; + bufferDesc.dwBufferBytes = 0xFFFF; bufferDesc.dwReserved = 0; bufferDesc.lpwfxFormat = &waveFormat; bufferDesc.guid3DAlgorithm = GUID_NULL; @@ -133,36 +133,86 @@ void SoundManager::ClearSecondaryBuffer() _secondaryBuffer->Lock(0, 0, (void**)&bufferPtr, (DWORD*)&bufferSize, nullptr, 0, DSBLOCK_ENTIREBUFFER); memset(bufferPtr, 0, bufferSize); _secondaryBuffer->Unlock((void*)bufferPtr, bufferSize, nullptr, 0); + + _secondaryBuffer->SetCurrentPosition(0); + _lastWriteOffset = 0; } void SoundManager::CopyToSecondaryBuffer(uint8_t *data, uint32_t size) { - unsigned char* bufferPtrA; - unsigned char* bufferPtrB; + uint8_t* bufferPtrA; + uint8_t* bufferPtrB; DWORD bufferASize; DWORD bufferBSize; - _secondaryBuffer->Lock(0, size, (void**)&bufferPtrA, (DWORD*)&bufferASize, (void**)&bufferPtrB, (DWORD*)&bufferBSize, DSBLOCK_FROMWRITECURSOR); + _secondaryBuffer->Lock(_lastWriteOffset, size, (void**)&bufferPtrA, (DWORD*)&bufferASize, (void**)&bufferPtrB, (DWORD*)&bufferBSize, 0); + _lastWriteOffset += size; - memcpy(bufferPtrA, data, min(bufferASize, size)); - if(bufferPtrB) { + memcpy(bufferPtrA, data, bufferASize); + if(bufferPtrB && bufferBSize > 0) { memcpy(bufferPtrB, data + bufferASize, bufferBSize); } _secondaryBuffer->Unlock((void*)bufferPtrA, bufferASize, (void*)bufferPtrB, bufferBSize); } +void SoundManager::Pause() +{ + _secondaryBuffer->Stop(); +} + +void SoundManager::Play() +{ + _secondaryBuffer->Play(0, 0, DSBPLAY_LOOPING); +} + +void SoundManager::Reset() +{ + _secondaryBuffer->Stop(); + ClearSecondaryBuffer(); +} + void SoundManager::PlayBuffer(int16_t *soundBuffer, uint32_t soundBufferSize) { + static int32_t byteLatency = _latency * (APU::BitsPerSample / 8); DWORD status; _secondaryBuffer->GetStatus(&status); if(!(status & DSBSTATUS_PLAYING)) { - ClearSecondaryBuffer(); CopyToSecondaryBuffer((uint8_t*)soundBuffer, soundBufferSize); - _secondaryBuffer->SetCurrentPosition(0); - _secondaryBuffer->Play(0, 0, DSBPLAY_LOOPING); + if(_lastWriteOffset >= byteLatency) { + Play(); + } } else { CopyToSecondaryBuffer((uint8_t*)soundBuffer, soundBufferSize); + DWORD currentPlayCursor; + _secondaryBuffer->GetCurrentPosition(¤tPlayCursor, nullptr); + + int32_t playWriteByteLatency = (_lastWriteOffset - currentPlayCursor); + if(playWriteByteLatency < -byteLatency * 2) { + playWriteByteLatency = 0xFFFF - currentPlayCursor + _lastWriteOffset; + } + + int32_t latencyGap = playWriteByteLatency - byteLatency; + if(abs(latencyGap) > 3000) { + //Out of sync, move back to where we should be (start of the latency buffer) + _secondaryBuffer->SetFrequency(44100); + _secondaryBuffer->SetCurrentPosition(_lastWriteOffset - byteLatency); + } else if(latencyGap < -200) { + //Playing too fast, slow down playing + _secondaryBuffer->SetFrequency(43900); + } else if(latencyGap > 200) { + //Playing too slow, speed up + _secondaryBuffer->SetFrequency(44300); + } else { + //Normal playback + _secondaryBuffer->SetFrequency(44100); + } + + static int counter = 0; + counter++; + if(counter % 5 == 0) { + std::cout << latencyGap << std::endl; + } } } \ No newline at end of file diff --git a/GUI/SoundManager.h b/GUI/SoundManager.h index 1b87a9fc..b20f2811 100644 --- a/GUI/SoundManager.h +++ b/GUI/SoundManager.h @@ -12,6 +12,9 @@ public: bool Initialize(HWND hWnd); void Release(); void PlayBuffer(int16_t *soundBuffer, uint32_t bufferSize); + void Play(); + void Pause(); + void Reset(); private: bool InitializeDirectSound(HWND); @@ -20,7 +23,9 @@ private: void CopyToSecondaryBuffer(uint8_t *data, uint32_t size); private: - vector _buffer; + uint16_t _lastWriteOffset = 0; + const uint16_t _latency = APU::SampleRate / (1000 / 150); // == 150ms latency + IDirectSound8* _directSound; IDirectSoundBuffer* _primaryBuffer; IDirectSoundBuffer8* _secondaryBuffer; diff --git a/GUI/resource.h b/GUI/resource.h index 8cacf5bc3d6a0e5f00fe27e013db4aa12fa528b7..b842d3686c2275b7757d9a8c6a1717a7f6f13f08 100644 GIT binary patch delta 160 zcmZ1@v_y1+9-Cl1gCBz{Loh=;LjZ##LnuS=;M1& delta 24 gcmZ1?x<+V&9^2++>`F|Ni`c{_H*iW!{>Hfu0B2PQbpQYW