diff --git a/Core/IAudioDevice.h b/Core/IAudioDevice.h index 4fad5351..33e42908 100644 --- a/Core/IAudioDevice.h +++ b/Core/IAudioDevice.h @@ -18,4 +18,9 @@ class IAudioDevice virtual void Pause() = 0; virtual void ProcessEndOfFrame() = 0; virtual void UpdateSoundSettings() = 0; -}; + + virtual string GetAvailableDevices() = 0; + virtual void SetAudioDevice(string deviceName) = 0; + + virtual AudioStatistics GetStatistics() = 0; +}; \ No newline at end of file diff --git a/Core/SoundMixer.cpp b/Core/SoundMixer.cpp index 55847be5..b05899bb 100644 --- a/Core/SoundMixer.cpp +++ b/Core/SoundMixer.cpp @@ -5,6 +5,7 @@ #include "CPU.h" #include "VideoRenderer.h" #include "RewindManager.h" +#include "WaveRecorder.h" #include "OggMixer.h" #include "Console.h" #include "BaseMapper.h" @@ -132,7 +133,7 @@ void SoundMixer::PlayAudioBuffer(uint32_t time) shared_ptr rewindManager = _console->GetRewindManager(); - if(!_console->GetVideoRenderer()->IsRecording() && !_settings->CheckFlag(EmulationFlags::NsfPlayerEnabled)) { + if(!_console->GetVideoRenderer()->IsRecording() && !_waveRecorder && !_settings->CheckFlag(EmulationFlags::NsfPlayerEnabled)) { if((_settings->CheckFlag(EmulationFlags::Turbo) || (rewindManager && rewindManager->IsRewinding())) && _settings->CheckFlag(EmulationFlags::ReduceSoundInFastForward)) { //Reduce volume when fast forwarding or rewinding _lowPassFilter.ApplyFilter(_outputBuffer, sampleCount, 0, 1.0 - _settings->GetVolumeReduction()); @@ -167,6 +168,17 @@ void SoundMixer::PlayAudioBuffer(uint32_t time) } if(!_settings->IsRunAheadFrame() && rewindManager && rewindManager->SendAudio(_outputBuffer, (uint32_t)sampleCount, _sampleRate)) { + bool isRecording = _waveRecorder || _console->GetVideoRenderer()->IsRecording(); + if(isRecording) { + shared_ptr recorder = _waveRecorder; + if(recorder) { + if(!recorder->WriteSamples(_outputBuffer, (uint32_t)sampleCount, _sampleRate, true)) { + _waveRecorder.reset(); + } + } + _console->GetVideoRenderer()->AddRecordingSound(_outputBuffer, (uint32_t)sampleCount, _sampleRate); + } + if(_audioDevice && !_console->IsPaused()) { _audioDevice->PlayBuffer(_outputBuffer, (uint32_t)sampleCount, _sampleRate, true); } @@ -361,15 +373,17 @@ void SoundMixer::UpdateEqualizers(bool forceUpdate) void SoundMixer::StartRecording(string filepath) { + _waveRecorder.reset(new WaveRecorder(filepath, _settings->GetSampleRate(), true)); } void SoundMixer::StopRecording() { + _waveRecorder.reset(); } bool SoundMixer::IsRecording() { - return false; + return _waveRecorder.get() != nullptr; } void SoundMixer::SetFadeRatio(double fadeRatio) @@ -396,6 +410,15 @@ OggMixer* SoundMixer::GetOggMixer() return _oggMixer.get(); } +AudioStatistics SoundMixer::GetStatistics() +{ + if(_audioDevice) { + return _audioDevice->GetStatistics(); + } else { + return AudioStatistics(); + } +} + void SoundMixer::ProcessEndOfFrame() { if(_audioDevice) { @@ -405,12 +428,55 @@ void SoundMixer::ProcessEndOfFrame() double SoundMixer::GetRateAdjustment() { - return 1.0; + return _rateAdjustment; } double SoundMixer::GetTargetRateAdjustment() { - return 1.0; + bool isRecording = _waveRecorder || _console->GetVideoRenderer()->IsRecording(); + if(!isRecording && !_settings->CheckFlag(EmulationFlags::DisableDynamicSampleRate)) { + //Don't deviate from selected sample rate while recording + //TODO: Have 2 output streams (one for recording, one for the speakers) + AudioStatistics stats = GetStatistics(); + + if(stats.AverageLatency > 0 && _settings->GetEmulationSpeed() == 100) { + //Try to stay within +/- 3ms of requested latency + constexpr int32_t maxGap = 3; + constexpr int32_t maxSubAdjustment = 3600; + + int32_t requestedLatency = (int32_t)_settings->GetAudioLatency(); + double latencyGap = stats.AverageLatency - requestedLatency; + double adjustment = std::min(0.0025, (std::ceil((std::abs(latencyGap) - maxGap) * 8)) * 0.00003125); + + if(latencyGap < 0 && _underTarget < maxSubAdjustment) { + _underTarget++; + } else if(latencyGap > 0 && _underTarget > -maxSubAdjustment) { + _underTarget--; + } + + //For every ~1 second spent under/over target latency, further adjust rate (GetTargetRate is called approx. 3x per frame) + //This should slowly get us closer to the actual output rate of the sound card + double subAdjustment = 0.00003125 * _underTarget / 180; + + if(adjustment > 0) { + if(latencyGap > maxGap) { + _rateAdjustment = 1 - adjustment + subAdjustment; + } else if(latencyGap < -maxGap) { + _rateAdjustment = 1 + adjustment + subAdjustment; + } + } else if(std::abs(latencyGap) < 1) { + //Restore normal rate once we get within +/- 1ms + _rateAdjustment = 1.0 + subAdjustment; + } + } else { + _underTarget = 0; + _rateAdjustment = 1.0; + } + } else { + _underTarget = 0; + _rateAdjustment = 1.0; + } + return _rateAdjustment; } void SoundMixer::UpdateTargetSampleRate() @@ -421,4 +487,4 @@ void SoundMixer::UpdateTargetSampleRate() blip_set_rates(_blipBufRight, _clockRate, targetRate); _previousTargetRate = targetRate; } -} +} \ No newline at end of file diff --git a/Core/SoundMixer.h b/Core/SoundMixer.h index ecb451f5..2bf2135a 100644 --- a/Core/SoundMixer.h +++ b/Core/SoundMixer.h @@ -13,6 +13,7 @@ #include "CrossFeedFilter.h" class Console; +class WaveRecorder; class OggMixer; namespace orfanidis_eq { @@ -33,6 +34,7 @@ private: IAudioDevice* _audioDevice; EmulationSettings* _settings; + shared_ptr _waveRecorder; double _fadeRatio; uint32_t _muteFrameCount; unique_ptr _oggMixer; @@ -52,6 +54,9 @@ private: int16_t _previousOutputLeft = 0; int16_t _previousOutputRight = 0; + double _rateAdjustment = 1.0; + int32_t _underTarget = 0; + vector _timestamps; int16_t _channelOutput[MaxChannelCount][CycleLength]; int16_t _currentOutput[MaxChannelCount]; @@ -109,6 +114,7 @@ public: OggMixer* GetOggMixer(); + AudioStatistics GetStatistics(); void ProcessEndOfFrame(); double GetRateAdjustment(); }; diff --git a/Core/WaveRecorder.cpp b/Core/WaveRecorder.cpp new file mode 100644 index 00000000..7e68627d --- /dev/null +++ b/Core/WaveRecorder.cpp @@ -0,0 +1,87 @@ +#include "stdafx.h" +#include "WaveRecorder.h" +#include "MessageManager.h" + +WaveRecorder::WaveRecorder(string outputFile, uint32_t sampleRate, bool isStereo) +{ + _stream = ofstream(outputFile, ios::out | ios::binary); + _outputFile = outputFile; + _streamSize = 0; + _sampleRate = sampleRate; + _isStereo = isStereo; + + if(_stream) { + WriteHeader(); + MessageManager::DisplayMessage("SoundRecorder", "SoundRecorderStarted", _outputFile); + } +} + +WaveRecorder::~WaveRecorder() +{ + CloseFile(); +} + +void WaveRecorder::WriteHeader() +{ + _stream << "RIFF"; + uint32_t size = 0; + _stream.write((char*)&size, sizeof(size)); + + _stream << "WAVE"; + _stream << "fmt "; + + uint32_t chunkSize = 16; + _stream.write((char*)&chunkSize, sizeof(chunkSize)); + + uint16_t format = 1; //PCM + uint16_t channelCount = _isStereo ? 2 : 1; + uint16_t bytesPerSample = 2; + uint16_t blockAlign = channelCount * bytesPerSample; + uint32_t byteRate = _sampleRate * channelCount * bytesPerSample; + uint16_t bitsPerSample = bytesPerSample * 8; + + _stream.write((char*)&format, sizeof(format)); + _stream.write((char*)&channelCount, sizeof(channelCount)); + _stream.write((char*)&_sampleRate, sizeof(_sampleRate)); + _stream.write((char*)&byteRate, sizeof(byteRate)); + + _stream.write((char*)&blockAlign, sizeof(blockAlign)); + _stream.write((char*)&bitsPerSample, sizeof(bitsPerSample)); + + _stream << "data"; + _stream.write((char*)&size, sizeof(size)); +} + +bool WaveRecorder::WriteSamples(int16_t * samples, uint32_t sampleCount, uint32_t sampleRate, bool isStereo) +{ + if(_sampleRate != sampleRate || _isStereo != isStereo) { + //Format changed, stop recording + CloseFile(); + return false; + } else { + uint32_t sampleBytes = sampleCount * (isStereo ? 4 : 2); + _stream.write((char*)samples, sampleBytes); + _streamSize += sampleBytes; + return true; + } +} + +void WaveRecorder::UpdateSizeValues() +{ + _stream.seekp(4, ios::beg); + uint32_t fileSize = _streamSize + 36; + _stream.write((char*)&fileSize, sizeof(fileSize)); + + _stream.seekp(40, ios::beg); + _stream.write((char*)&_streamSize, sizeof(_streamSize)); +} + +void WaveRecorder::CloseFile() +{ + if(_stream && _stream.is_open()) { + UpdateSizeValues(); + _stream.close(); + + MessageManager::DisplayMessage("SoundRecorder", "SoundRecorderStopped", _outputFile); + } +} diff --git a/Core/WaveRecorder.h b/Core/WaveRecorder.h new file mode 100644 index 00000000..1cb3dabe --- /dev/null +++ b/Core/WaveRecorder.h @@ -0,0 +1,21 @@ +#include "stdafx.h" + +class WaveRecorder +{ +private: + std::ofstream _stream; + uint32_t _streamSize; + uint32_t _sampleRate; + bool _isStereo; + string _outputFile; + + void WriteHeader(); + void UpdateSizeValues(); + void CloseFile(); + +public: + WaveRecorder(string outputFile, uint32_t sampleRate, bool isStereo); + ~WaveRecorder(); + + bool WriteSamples(int16_t* samples, uint32_t sampleCount, uint32_t sampleRate, bool isStereo); +}; \ No newline at end of file diff --git a/Libretro/LibretroSoundManager.h b/Libretro/LibretroSoundManager.h index 8ac945c5..37e25f72 100644 --- a/Libretro/LibretroSoundManager.h +++ b/Libretro/LibretroSoundManager.h @@ -7,31 +7,35 @@ class LibretroSoundManager : public IAudioDevice { private: - retro_audio_sample_batch_t audio_batch_cb = nullptr; + retro_audio_sample_batch_t _sendAudioSample = nullptr; bool _skipMode = false; + shared_ptr _console; public: LibretroSoundManager(shared_ptr console) { + _console = console; + _console->GetSoundMixer()->RegisterAudioDevice(this); } ~LibretroSoundManager() { + _console->GetSoundMixer()->RegisterAudioDevice(nullptr); } // Inherited via IAudioDevice virtual void PlayBuffer(int16_t *soundBuffer, uint32_t sampleCount, uint32_t sampleRate, bool isStereo) override { - if(!_skipMode && audio_batch_cb) { + if(!_skipMode && _sendAudioSample) { for(uint32_t total = 0; total < sampleCount; ) { - total += (uint32_t)audio_batch_cb(soundBuffer + total*2, sampleCount - total); + total += (uint32_t)_sendAudioSample(soundBuffer + total*2, sampleCount - total); } } } void SetSendAudioSample(retro_audio_sample_batch_t sendAudioSample) { - audio_batch_cb = sendAudioSample; + _sendAudioSample = sendAudioSample; } void SetSkipMode(bool skip) @@ -47,6 +51,15 @@ public: { } + virtual string GetAvailableDevices() override + { + return string(); + } + + virtual void SetAudioDevice(string deviceName) override + { + } + virtual void UpdateSoundSettings() override { } @@ -54,4 +67,9 @@ public: virtual void ProcessEndOfFrame() override { } + + virtual AudioStatistics GetStatistics() override + { + return AudioStatistics(); + } }; diff --git a/Libretro/Makefile.common b/Libretro/Makefile.common index 27bc559b..1735dbf3 100644 --- a/Libretro/Makefile.common +++ b/Libretro/Makefile.common @@ -106,6 +106,7 @@ SOURCES_CXX := $(LIBRETRO_DIR)/libretro.cpp \ $(CORE_DIR)/VideoRenderer.cpp \ $(CORE_DIR)/VirtualFile.cpp \ $(CORE_DIR)/VsControlManager.cpp \ + $(CORE_DIR)/WaveRecorder.cpp \ $(UTIL_DIR)/ArchiveReader.cpp \ $(UTIL_DIR)/AutoResetEvent.cpp \ $(UTIL_DIR)/blip_buf.cpp \