#include "utils/soundsample.h" #include #include #include #ifndef PS2 #include #include #endif #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #else #include "utils/sdl2_backports.h" #endif #include "engine/assets.hpp" #include "options.h" #ifndef PS2 #include "utils/aulib.hpp" #endif #include "utils/log.hpp" #include "utils/math.h" #include "utils/stubs.h" namespace devilution { namespace { constexpr float LogBase = 10.0; /** * Scaling factor for attenuating volume. * Picked so that a volume change of -10 dB results in half perceived loudness. * VolumeScale = -1000 / log(0.5) */ constexpr float VolumeScale = 3321.9281; /** * Min and max volume range, in millibel. * -100 dB (muted) to 0 dB (max. loudness). */ constexpr float MillibelMin = -10000.F; constexpr float MillibelMax = 0.F; /** * Stereo separation factor for left/right speaker panning. Lower values increase separation, moving * sounds further left/right, while higher values will pull sounds more towards the middle, reducing separation. * Current value is tuned to have ~2:1 mix for sounds that happen on the edge of a 640x480 screen. */ constexpr float StereoSeparation = 6000.F; float PanLogToLinear(int logPan) { if (logPan == 0) return 0; auto factor = std::pow(LogBase, static_cast(-std::abs(logPan)) / StereoSeparation); return copysign(1.F - factor, static_cast(logPan)); } #ifndef PS2 std::unique_ptr CreateDecoder(bool isMp3) { if (isMp3) return std::make_unique(); return std::make_unique(); } std::unique_ptr CreateStream(SDL_RWops *handle, bool isMp3) { auto decoder = CreateDecoder(isMp3); if (!decoder->open(handle)) // open for `getRate` return nullptr; auto resampler = CreateAulibResampler(decoder->getRate()); return std::make_unique(handle, std::move(decoder), std::move(resampler), /*closeRw=*/true); } #endif /** * @brief Converts log volume passed in into linear volume. * @param logVolume Logarithmic volume in the range [logMin..logMax] * @param logMin Volume range minimum (usually ATTENUATION_MIN for game sounds and VOLUME_MIN for volume sliders) * @param logMax Volume range maximum (usually 0) * @return Linear volume in the range [0..1] */ float VolumeLogToLinear(int logVolume, int logMin, int logMax) { const auto logScaled = math::Remap(static_cast(logMin), static_cast(logMax), MillibelMin, MillibelMax, static_cast(logVolume)); return std::pow(LogBase, logScaled / VolumeScale); // linVolume } } // namespace ///// SoundSample ///// void SoundSample::Release() { #ifndef PS2 file_data_ = nullptr; file_data_size_ = 0; #else if (stream_ != nullptr) // We are the owner audsrv_free_adpcm(sampleId_); sampleId_ = nullptr; #endif stream_ = nullptr; } /** * @brief Check if a the sound is being played atm */ bool SoundSample::IsPlaying() { #ifndef PS2 return stream_ && stream_->isPlaying(); #else if (channel_ == -1) return false; return audsrv_is_adpcm_playing(channel_, sampleId_) != 0; #endif } bool SoundSample::Play(int numIterations) { #ifndef PS2 if (!stream_->play(numIterations)) { LogError(LogCategory::Audio, "Aulib::Stream::play (from SoundSample::Play): {}", SDL_GetError()); return false; } #else if (IsStreaming()) { return true; } int channel = audsrv_ch_play_adpcm(-1, sampleId_); printf("channel %d\n", channel); if (channel < 0) { LogError(LogCategory::Audio, "audsrv_ch_play_adpcm (from SoundSample::Play): {}", channel); return false; } channel_ = channel; audsrv_adpcm_set_volume_and_pan(channel_, volume_, pan_); #endif return true; } int SoundSample::SetChunkStream(std::string filePath, bool isMp3, bool logErrors) { SDL_RWops *handle = OpenAssetAsSdlRwOps(filePath.c_str(), /*threadsafe=*/true); if (handle == nullptr) { if (logErrors) LogError(LogCategory::Audio, "OpenAsset failed (from SoundSample::SetChunkStream) for {}: {}", filePath, SDL_GetError()); return -1; } file_path_ = std::move(filePath); #ifndef PS2 isMp3_ = isMp3; stream_ = CreateStream(handle, isMp3); if (!stream_->open()) { stream_ = nullptr; if (logErrors) LogError(LogCategory::Audio, "Aulib::Stream::open (from SoundSample::SetChunkStream) for {}: {}", file_path_, SDL_GetError()); return -1; } #endif return 0; } int SoundSample::SetChunk(ArraySharedPtr fileData, std::size_t dwBytes, bool isMp3) { #ifndef PS2 isMp3_ = isMp3; file_data_ = std::move(fileData); file_data_size_ = dwBytes; SDL_RWops *buf = SDL_RWFromConstMem(file_data_.get(), dwBytes); if (buf == nullptr) { return -1; } stream_ = CreateStream(buf, isMp3_); if (!stream_->open()) { stream_ = nullptr; file_data_ = nullptr; LogError(LogCategory::Audio, "Aulib::Stream::open (from SoundSample::SetChunk): {}", SDL_GetError()); return -1; } #else stream_ = std::make_unique(); int success = audsrv_load_adpcm(stream_.get(), fileData.get(), dwBytes); if (success != 0) { LogError(LogCategory::Audio, "audsrv_load_adpcm (from SoundSample::SetChunk): {}", success); return -1; } sampleId_ = stream_.get(); #endif return 0; } void SoundSample::SetVolume(int logVolume, int logMin, int logMax) { #ifndef PS2 stream_->setVolume(VolumeLogToLinear(logVolume, logMin, logMax)); #else volume_ = VolumeLogToLinear(logVolume, logMin, logMax) * MAX_VOLUME; if (channel_ == -1) return; audsrv_adpcm_set_volume_and_pan(channel_, volume_, pan_); #endif } void SoundSample::SetStereoPosition(int logPan) { #ifndef PS2 stream_->setStereoPosition(PanLogToLinear(logPan)); #else pan_ = PanLogToLinear(logPan) * 100; if (channel_ == -1) return; audsrv_adpcm_set_volume_and_pan(channel_, volume_, pan_); #endif } int SoundSample::GetLength() const { #ifndef PS2 if (!stream_) return 0; return std::chrono::duration_cast(stream_->duration()).count(); #else size_t size = 0; int pitch = 0; if (stream_ != nullptr) { size = stream_->size; pitch = stream_->pitch; } else if (!file_path_.empty()) { AssetHandle handle = OpenAsset(file_path_.c_str(), size); if (handle.ok()) { size -= 16; uint32_t buffer[3]; if (handle.read(buffer, sizeof(buffer))) { pitch = buffer[2]; } } } if (pitch == 0) return 0; uint64_t microSamples = size; microSamples *= 56 * 1000; return microSamples / (pitch * 375); #endif } } // namespace devilution