diff --git a/deps/miles/miles.h b/deps/miles/miles.h index 11f4dfade..f56a4a4cc 100644 --- a/deps/miles/miles.h +++ b/deps/miles/miles.h @@ -133,6 +133,9 @@ int32_t __stdcall AIL_stream_loop_count(HSTREAM stream); int32_t __stdcall AIL_3D_sample_playback_rate(H3DSAMPLE sample); void __stdcall AIL_set_3D_sample_playback_rate(H3DSAMPLE sample, int32_t playback_rate); +#define WAVE_FORMAT_PCM 1 +#define WAVE_FORMAT_IMA_ADPCM 0x0011 + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4876b087e..29575d7cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -287,6 +287,7 @@ set(GAMEENGINE_SRC platform/win32gameengine.cpp platform/win32localfile.cpp platform/win32localfilesystem.cpp + platform/audio/audiofilecache.cpp platform/w3dengine/client/w3dbibbuffer.cpp platform/w3dengine/client/w3dbridgebuffer.cpp platform/w3dengine/client/w3ddebugdisplay.cpp diff --git a/src/platform/audio/audiofilecache.cpp b/src/platform/audio/audiofilecache.cpp new file mode 100644 index 000000000..c0f9c93ca --- /dev/null +++ b/src/platform/audio/audiofilecache.cpp @@ -0,0 +1,212 @@ +/** + * @file + * + * @author feliwir + * + * @brief Base class for caching loaded audio samples to reduce file IO. + * + * @copyright Thyme is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version + * 2 of the License, or (at your option) any later version. + * A full copy of the GNU General Public License can be found in + * LICENSE + */ +#include "audiofilecache.h" +#include "audioeventrts.h" +#include "audiomanager.h" +#include "filesystem.h" + +#include + +using namespace Thyme; + +/** + * Opens an audio file. Reads from the cache if available or loads from file if not. + */ +AudioDataHandle AudioFileCache::Open_File(const Utf8String &filename, const AudioEventInfo *event_info) +{ + ScopedMutexClass lock(&m_mutex); + + captainslog_trace("AudioFileCache: opening file %s", filename.Str()); + + // Try to find existing data for this file to avoid loading it if unneeded. + auto it = m_cacheMap.find(filename); + + if (it != m_cacheMap.end()) { + ++(it->second.ref_count); + + return static_cast(it->second.wave_data); + } + + // Load the file from disk + File *file = g_theFileSystem->Open_File(filename.Str(), File::READ | File::BINARY | File::BUFFERED); + + if (file == nullptr) { + if (filename.Is_Not_Empty()) { + captainslog_warn("Missing audio file '%s', could not cache.", filename.Str()); + } + + return nullptr; + } + + OpenAudioFile open_audio; + if (!Load_File(file, open_audio)) { + captainslog_warn("Failed to load audio file '%s', could not cache.", filename.Str()); + return nullptr; + } + + file->Close(); + + open_audio.audio_event_info = event_info; + open_audio.ref_count = 1; + m_currentSize += open_audio.data_size; + + // m_maxSize prevents using overly large amounts of memory, so if we are over it, unload some other samples. + if (m_currentSize > m_maxSize && !Free_Space_For_Sample(open_audio)) { + captainslog_warn("Cannot play audio file since cache is full: %s", filename.Str()); + m_currentSize -= open_audio.data_size; + Release_Open_Audio(&open_audio); + + return nullptr; + } + + m_cacheMap[filename] = open_audio; + + return static_cast(open_audio.wave_data); +} + +/** + * Opens an audio file for an event. Reads from the cache if available or loads from file if not. + */ +AudioDataHandle AudioFileCache::Open_File(AudioEventRTS *audio_event) +{ + Utf8String filename; + + // What part of an event are we playing? + switch (audio_event->Get_Next_Play_Portion()) { + case 0: + filename = audio_event->Get_Attack_Name(); + break; + case 1: + filename = audio_event->Get_File_Name(); + break; + case 2: + filename = audio_event->Get_Decay_Name(); + break; + case 3: + default: + return nullptr; + } + + return Open_File(filename, audio_event->Get_Event_Info()); +} + +/** + * Closes a file, reducing the references to it. Does not actually free the cache. + */ +void AudioFileCache::Close_File(AudioDataHandle file) +{ + if (file == nullptr) { + return; + } + + ScopedMutexClass lock(&m_mutex); + + for (auto it = m_cacheMap.begin(); it != m_cacheMap.end(); ++it) { + if (static_cast(it->second.wave_data) == file) { + --(it->second.ref_count); + + break; + } + } +} + +/** + * Sets the maximum amount of memory in bytes that the cache should use. + */ +void AudioFileCache::Set_Max_Size(unsigned size) +{ + ScopedMutexClass lock(&m_mutex); + m_maxSize = size; +} + +/** + * Attempts to free space by releasing files with no references + */ +unsigned AudioFileCache::Free_Space(unsigned required) +{ + std::list to_free; + unsigned freed = 0; + + // First check for samples that don't have any references. + for (const auto &cached : m_cacheMap) { + if (cached.second.ref_count == 0) { + to_free.push_back(cached.first); + freed += cached.second.data_size; + + // If required is "0" we free as much as possible + if (required && freed >= required) { + break; + } + } + } + + for (const auto &file : to_free) { + auto to_remove = m_cacheMap.find(file); + + if (to_remove != m_cacheMap.end()) { + Release_Open_Audio(&to_remove->second); + m_currentSize -= to_remove->second.data_size; + m_cacheMap.erase(to_remove); + } + } + + return freed; +} + +/** + * Attempts to free space for a file by releasing files with no references and lower priority sounds. + */ +bool AudioFileCache::Free_Space_For_Sample(const OpenAudioFile &file) +{ + captainslog_assert(m_currentSize >= m_maxSize); // Assumed to be called only when we need more than allowed. + std::list to_free; + unsigned required = m_currentSize - m_maxSize; + unsigned freed = 0; + + // First check for samples that don't have any references. + freed = Free_Space(required); + + // If we still don't have enough potential space freed up, look for lower priority sounds to remove. + if (freed < required) { + for (const auto &cached : m_cacheMap) { + if (cached.second.ref_count != 0 + && cached.second.audio_event_info->Get_Priority() < file.audio_event_info->Get_Priority()) { + to_free.push_back(cached.first); + freed += cached.second.data_size; + + if (freed >= required) { + break; + } + } + } + } + + // If we have enough space to free, do the actual freeing, otherwise we didn't succeed, no point bothering. + if (freed < required) { + return false; + } + + for (const auto &file : to_free) { + auto to_remove = m_cacheMap.find(file); + + if (to_remove != m_cacheMap.end()) { + Release_Open_Audio(&to_remove->second); + m_currentSize -= to_remove->second.data_size; + m_cacheMap.erase(to_remove); + } + } + + return true; +} diff --git a/src/platform/audio/audiofilecache.h b/src/platform/audio/audiofilecache.h new file mode 100644 index 000000000..0a3dec762 --- /dev/null +++ b/src/platform/audio/audiofilecache.h @@ -0,0 +1,80 @@ +/** + * @file + * + * @author feliwir + * + * @brief Base class for caching loaded audio samples to reduce file IO. + * + * @copyright Thyme is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version + * 2 of the License, or (at your option) any later version. + * A full copy of the GNU General Public License can be found in + * LICENSE + */ +#pragma once + +#include "always.h" +#include "asciistring.h" +#include "audiomanager.h" +#include "file.h" +#include "mutex.h" +#include "rtsutils.h" + +#ifdef THYME_USE_STLPORT +#include +#else +#include +#endif + +class AudioEventInfo; +class AudioEventRTS; + +struct OpenAudioFile +{ + AudioDataHandle wave_data = nullptr; + int ref_count = 0; + int data_size = 0; + const AudioEventInfo *audio_event_info = nullptr; + void *opaque = nullptr; +}; + +#ifdef THYME_USE_STLPORT +typedef std::hash_map, std::equal_to> audiocachemap_t; +#else +typedef std::unordered_map, std::equal_to> + audiocachemap_t; +#endif + +namespace Thyme +{ + +class AudioFileCache +{ +public: + AudioFileCache() : m_maxSize(0), m_currentSize(0), m_mutex("AudioFileCacheMutex") {} + AudioDataHandle Open_File(AudioEventRTS *file); + AudioDataHandle Open_File(const Utf8String &filename, const AudioEventInfo *event_info = nullptr); + + void Close_File(AudioDataHandle file); + void Set_Max_Size(unsigned size); + inline unsigned Get_Max_Size() const { return m_maxSize; } + inline unsigned Get_Current_Size() const { return m_currentSize; } + + // #FEATURE: We can maybe call this during loading to free any old sounds we won't need ingame and decrease computation + // ingame + unsigned Free_Space(unsigned required = 0); + +protected: + bool Free_Space_For_Sample(const OpenAudioFile &open_audio); + + virtual bool Load_File(File *file, OpenAudioFile &audio_file) = 0; + virtual void Release_Open_Audio(OpenAudioFile *open_audio) = 0; + +protected: + audiocachemap_t m_cacheMap; + unsigned m_currentSize; + unsigned m_maxSize; + SimpleMutexClass m_mutex; +}; +} // namespace Thyme diff --git a/src/platform/audio/ffmpegaudiofilecache.cpp b/src/platform/audio/ffmpegaudiofilecache.cpp index 2a7dd9fde..70936cc8e 100644 --- a/src/platform/audio/ffmpegaudiofilecache.cpp +++ b/src/platform/audio/ffmpegaudiofilecache.cpp @@ -13,20 +13,25 @@ * LICENSE */ +#include "ffmpegaudiofilecache.h" +#include +#include + extern "C" { #include #include } -#include "audioeventrts.h" -#include "audiomanager.h" -#include "ffmpegaudiofilecache.h" -#include "filesystem.h" -#include -#include - namespace Thyme { +struct FFmpegContext +{ + // FFmpeg handles + AVFormatContext *fmt_ctx = nullptr; + AVIOContext *avio_ctx = nullptr; + AVCodecContext *codec_ctx = nullptr; +}; + struct WavHeader { uint8_t riff_id[4] = { 'R', 'I', 'F', 'F' }; @@ -46,6 +51,13 @@ struct WavHeader uint32_t subchunk2_size; // Sampled data length }; +} // namespace Thyme + +using namespace Thyme; + +/** + * Clear all remaining open audio files + */ FFmpegAudioFileCache::~FFmpegAudioFileCache() { ScopedMutexClass lock(&m_mutex); @@ -73,7 +85,7 @@ int FFmpegAudioFileCache::Read_FFmpeg_Packet(void *opaque, uint8_t *buf, int buf /** * Open all the required FFmpeg handles for a required file. */ -bool FFmpegAudioFileCache::Open_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio, File *file) +bool FFmpegAudioFileCache::Open_FFmpeg_Contexts(OpenAudioFile *open_audio, File *file) { #if LOGGING_LEVEL != LOGLEVEL_NONE av_log_set_level(AV_LOG_INFO); @@ -84,9 +96,11 @@ bool FFmpegAudioFileCache::Open_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio, av_register_all(); #endif + FFmpegContext *ff_ctx = static_cast(open_audio->opaque); + // FFmpeg setup - open_audio->fmt_ctx = avformat_alloc_context(); - if (!open_audio->fmt_ctx) { + ff_ctx->fmt_ctx = avformat_alloc_context(); + if (!ff_ctx->fmt_ctx) { captainslog_error("Failed to alloc AVFormatContext"); return false; } @@ -99,18 +113,18 @@ bool FFmpegAudioFileCache::Open_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio, return false; } - open_audio->avio_ctx = avio_alloc_context(buffer, avio_ctx_buffer_size, 0, file, &Read_FFmpeg_Packet, nullptr, nullptr); - if (!open_audio->avio_ctx) { + ff_ctx->avio_ctx = avio_alloc_context(buffer, avio_ctx_buffer_size, 0, file, &Read_FFmpeg_Packet, nullptr, nullptr); + if (!ff_ctx->avio_ctx) { captainslog_error("Failed to alloc AVIOContext"); Close_FFmpeg_Contexts(open_audio); return false; } - open_audio->fmt_ctx->pb = open_audio->avio_ctx; - open_audio->fmt_ctx->flags |= AVFMT_FLAG_CUSTOM_IO; + ff_ctx->fmt_ctx->pb = ff_ctx->avio_ctx; + ff_ctx->fmt_ctx->flags |= AVFMT_FLAG_CUSTOM_IO; int result = 0; - result = avformat_open_input(&open_audio->fmt_ctx, nullptr, nullptr, nullptr); + result = avformat_open_input(&ff_ctx->fmt_ctx, nullptr, nullptr, nullptr); if (result < 0) { char error_buffer[1024]; av_strerror(result, error_buffer, sizeof(error_buffer)); @@ -119,7 +133,7 @@ bool FFmpegAudioFileCache::Open_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio, return false; } - result = avformat_find_stream_info(open_audio->fmt_ctx, NULL); + result = avformat_find_stream_info(ff_ctx->fmt_ctx, NULL); if (result < 0) { char error_buffer[1024]; av_strerror(result, error_buffer, sizeof(error_buffer)); @@ -128,27 +142,27 @@ bool FFmpegAudioFileCache::Open_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio, return false; } - if (open_audio->fmt_ctx->nb_streams != 1) { + if (ff_ctx->fmt_ctx->nb_streams != 1) { captainslog_error("Expected exactly one audio stream per file"); Close_FFmpeg_Contexts(open_audio); return false; } - const AVCodec *input_codec = avcodec_find_decoder(open_audio->fmt_ctx->streams[0]->codecpar->codec_id); + const AVCodec *input_codec = avcodec_find_decoder(ff_ctx->fmt_ctx->streams[0]->codecpar->codec_id); if (!input_codec) { - captainslog_error("Audio codec not supported: '%u'", open_audio->fmt_ctx->streams[0]->codecpar->codec_tag); + captainslog_error("Audio codec not supported: '%u'", ff_ctx->fmt_ctx->streams[0]->codecpar->codec_tag); Close_FFmpeg_Contexts(open_audio); return false; } - open_audio->codec_ctx = avcodec_alloc_context3(input_codec); - if (!open_audio->codec_ctx) { + ff_ctx->codec_ctx = avcodec_alloc_context3(input_codec); + if (!ff_ctx->codec_ctx) { captainslog_error("Could not allocate codec context"); Close_FFmpeg_Contexts(open_audio); return false; } - result = avcodec_parameters_to_context(open_audio->codec_ctx, open_audio->fmt_ctx->streams[0]->codecpar); + result = avcodec_parameters_to_context(ff_ctx->codec_ctx, ff_ctx->fmt_ctx->streams[0]->codecpar); if (result < 0) { char error_buffer[1024]; av_strerror(result, error_buffer, sizeof(error_buffer)); @@ -157,7 +171,7 @@ bool FFmpegAudioFileCache::Open_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio, return false; } - result = avcodec_open2(open_audio->codec_ctx, input_codec, NULL); + result = avcodec_open2(ff_ctx->codec_ctx, input_codec, NULL); if (result < 0) { char error_buffer[1024]; av_strerror(result, error_buffer, sizeof(error_buffer)); @@ -172,16 +186,17 @@ bool FFmpegAudioFileCache::Open_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio, /** * Decode the input data and append it to our wave data stream */ -bool FFmpegAudioFileCache::Decode_FFmpeg(FFmpegOpenAudioFile *file) +bool FFmpegAudioFileCache::Decode_FFmpeg(OpenAudioFile *file) { AVPacket *packet = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); + FFmpegContext *ff_ctx = static_cast(file->opaque); int result = 0; // Read all packets inside the file - while (av_read_frame(file->fmt_ctx, packet) >= 0) { - result = avcodec_send_packet(file->codec_ctx, packet); + while (av_read_frame(ff_ctx->fmt_ctx, packet) >= 0) { + result = avcodec_send_packet(ff_ctx->codec_ctx, packet); if (result < 0) { char error_buffer[1024]; av_strerror(result, error_buffer, sizeof(error_buffer)); @@ -190,7 +205,7 @@ bool FFmpegAudioFileCache::Decode_FFmpeg(FFmpegOpenAudioFile *file) } // Decode all frames contained inside the packet while (result >= 0) { - result = avcodec_receive_frame(file->codec_ctx, frame); + result = avcodec_receive_frame(ff_ctx->codec_ctx, frame); // Check if we need more data if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) break; @@ -202,9 +217,9 @@ bool FFmpegAudioFileCache::Decode_FFmpeg(FFmpegOpenAudioFile *file) } int frame_data_size = av_samples_get_buffer_size( - NULL, file->codec_ctx->channels, frame->nb_samples, file->codec_ctx->sample_fmt, 1); + NULL, ff_ctx->codec_ctx->channels, frame->nb_samples, ff_ctx->codec_ctx->sample_fmt, 1); file->wave_data = static_cast(av_realloc(file->wave_data, file->data_size + frame_data_size)); - memcpy(file->wave_data + file->data_size, frame->data[0], frame_data_size); + memcpy(static_cast(file->wave_data) + file->data_size, frame->data[0], frame_data_size); file->data_size += frame_data_size; } @@ -220,76 +235,55 @@ bool FFmpegAudioFileCache::Decode_FFmpeg(FFmpegOpenAudioFile *file) /** * Close all the open FFmpeg handles for an open file. */ -void FFmpegAudioFileCache::Close_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio) +void FFmpegAudioFileCache::Close_FFmpeg_Contexts(OpenAudioFile *open_audio) { - if (open_audio->fmt_ctx) { - avformat_close_input(&open_audio->fmt_ctx); + FFmpegContext *ff_ctx = static_cast(open_audio->opaque); + if (ff_ctx->fmt_ctx) { + avformat_close_input(&ff_ctx->fmt_ctx); } - if (open_audio->codec_ctx) { - avcodec_free_context(&open_audio->codec_ctx); + if (ff_ctx->codec_ctx) { + avcodec_free_context(&ff_ctx->codec_ctx); } - if (open_audio->avio_ctx->buffer) { - av_freep(&open_audio->avio_ctx->buffer); + if (ff_ctx->avio_ctx && ff_ctx->avio_ctx->buffer) { + av_freep(&ff_ctx->avio_ctx->buffer); } - if (open_audio->avio_ctx) { - avio_context_free(&open_audio->avio_ctx); + if (ff_ctx->avio_ctx) { + avio_context_free(&ff_ctx->avio_ctx); } } -void FFmpegAudioFileCache::Fill_Wave_Data(FFmpegOpenAudioFile *open_audio) +void FFmpegAudioFileCache::Fill_Wave_Data(OpenAudioFile *open_audio) { + FFmpegContext *ff_ctx = static_cast(open_audio->opaque); WavHeader wav; wav.chunk_size = open_audio->data_size - (offsetof(WavHeader, chunk_size) + sizeof(uint32_t)); wav.subchunk2_size = open_audio->data_size - (offsetof(WavHeader, subchunk2_size) + sizeof(uint32_t)); - wav.channels = open_audio->codec_ctx->channels; - wav.bits_per_sample = av_get_bytes_per_sample(open_audio->codec_ctx->sample_fmt) * 8; - wav.samples_per_sec = open_audio->codec_ctx->sample_rate; - wav.bytes_per_sec = open_audio->codec_ctx->sample_rate * open_audio->codec_ctx->channels * wav.bits_per_sample / 8; - wav.block_align = open_audio->codec_ctx->channels * wav.bits_per_sample / 8; + wav.channels = ff_ctx->codec_ctx->channels; + wav.bits_per_sample = av_get_bytes_per_sample(ff_ctx->codec_ctx->sample_fmt) * 8; + wav.samples_per_sec = ff_ctx->codec_ctx->sample_rate; + wav.bytes_per_sec = ff_ctx->codec_ctx->sample_rate * ff_ctx->codec_ctx->channels * wav.bits_per_sample / 8; + wav.block_align = ff_ctx->codec_ctx->channels * wav.bits_per_sample / 8; memcpy(open_audio->wave_data, &wav, sizeof(WavHeader)); } /** - * Opens an audio file. Reads from the cache if available or loads from file if not. + * Load audio data with FFmpeg */ -AudioDataHandle FFmpegAudioFileCache::Open_File(const Utf8String &filename) +bool FFmpegAudioFileCache::Load_File(File *file, OpenAudioFile &open_audio) { - ScopedMutexClass lock(&m_mutex); - - captainslog_trace("FFmpegAudioFileCache: opening file %s", filename.Str()); - - // Try to find existing data for this file to avoid loading it if unneeded. - auto it = m_cacheMap.find(filename); - - if (it != m_cacheMap.end()) { - ++(it->second.ref_count); - - return static_cast(it->second.wave_data); - } - - // Load the file from disk - File *file = g_theFileSystem->Open_File(filename.Str(), File::READ | File::BINARY | File::BUFFERED); - - if (file == nullptr) { - if (filename.Is_Not_Empty()) { - captainslog_warn("Missing audio file '%s', could not cache.", filename.Str()); - } - - return nullptr; - } - - FFmpegOpenAudioFile open_audio; + Utf8String filename = file->Get_Name(); open_audio.wave_data = static_cast(av_malloc(sizeof(WavHeader))); open_audio.data_size = sizeof(WavHeader); + open_audio.opaque = av_malloc(sizeof(FFmpegContext)); if (!Open_FFmpeg_Contexts(&open_audio, file)) { captainslog_warn("Failed to load audio file '%s', could not cache.", filename.Str()); Release_Open_Audio(&open_audio); file->Close(); - return nullptr; + return false; } if (!Decode_FFmpeg(&open_audio)) { @@ -297,222 +291,12 @@ AudioDataHandle FFmpegAudioFileCache::Open_File(const Utf8String &filename) Close_FFmpeg_Contexts(&open_audio); Release_Open_Audio(&open_audio); file->Close(); - return nullptr; - } - - Fill_Wave_Data(&open_audio); - Close_FFmpeg_Contexts(&open_audio); - file->Close(); - - open_audio.ref_count = 1; - m_currentSize += open_audio.data_size; - - // m_maxSize prevents using overly large amounts of memory, so if we are over it, unload some other samples. - if (m_currentSize > m_maxSize && !Free_Space_For_Sample(open_audio)) { - captainslog_warn("Cannot play audio file since cache is full: %s", filename.Str()); - m_currentSize -= open_audio.data_size; - Release_Open_Audio(&open_audio); - - return nullptr; - } - - m_cacheMap[filename] = open_audio; - - return static_cast(open_audio.wave_data); -} -/** - * Opens an audio file for an event. Reads from the cache if available or loads from file if not. - */ -AudioDataHandle FFmpegAudioFileCache::Open_File(AudioEventRTS *audio_event) -{ - ScopedMutexClass lock(&m_mutex); - Utf8String filename; - - // What part of an event are we playing? - switch (audio_event->Get_Next_Play_Portion()) { - case 0: - filename = audio_event->Get_Attack_Name(); - break; - case 1: - filename = audio_event->Get_File_Name(); - break; - case 2: - filename = audio_event->Get_Decay_Name(); - break; - case 3: - return nullptr; - default: - break; - } - - captainslog_trace("FFmpegAudioFileCache: opening file %s", filename.Str()); - - // Try to find existing data for this file to avoid loading it if unneeded. - auto it = m_cacheMap.find(filename); - - if (it != m_cacheMap.end()) { - ++(it->second.ref_count); - - return static_cast(it->second.wave_data); - } - - // Load the file from disk - File *file = g_theFileSystem->Open_File(filename.Str(), File::READ | File::BINARY | File::BUFFERED); - - if (file == nullptr) { - if (!filename.Is_Empty()) { - captainslog_warn("Missing audio file '%s', could not cache.", filename.Str()); - } - - return nullptr; - } - - FFmpegOpenAudioFile open_audio; - open_audio.wave_data = static_cast(av_malloc(sizeof(WavHeader))); - open_audio.data_size = sizeof(WavHeader); - open_audio.audio_event_info = audio_event->Get_Event_Info(); - - if (!Open_FFmpeg_Contexts(&open_audio, file)) { - captainslog_warn("Failed to load audio file '%s', could not cache.", filename.Str()); - return nullptr; - } - - if (audio_event->Is_Positional_Audio() && open_audio.codec_ctx->channels > 1) { - captainslog_error("Audio marked as positional audio cannot have more than one channel."); - return nullptr; - } - - if (!Decode_FFmpeg(&open_audio)) { - captainslog_warn("Failed to decode audio file '%s', could not cache.", filename.Str()); - Close_FFmpeg_Contexts(&open_audio); - return nullptr; + return false; } Fill_Wave_Data(&open_audio); Close_FFmpeg_Contexts(&open_audio); - - open_audio.ref_count = 1; - m_currentSize += open_audio.data_size; - - // m_maxSize prevents using overly large amounts of memory, so if we are over it, unload some other samples. - if (m_currentSize > m_maxSize && !Free_Space_For_Sample(open_audio)) { - captainslog_warn("Cannot play audio file since cache is full: %s", filename.Str()); - m_currentSize -= open_audio.data_size; - Release_Open_Audio(&open_audio); - - return nullptr; - } - - m_cacheMap[filename] = open_audio; - - return static_cast(open_audio.wave_data); -} - -/** - * Closes a file, reducing the references to it. Does not actually free the cache. - */ -void FFmpegAudioFileCache::Close_File(AudioDataHandle file) -{ - if (file == nullptr) { - return; - } - - ScopedMutexClass lock(&m_mutex); - - for (auto it = m_cacheMap.begin(); it != m_cacheMap.end(); ++it) { - if (static_cast(it->second.wave_data) == file) { - --(it->second.ref_count); - - break; - } - } -} - -/** - * Sets the maximum amount of memory in bytes that the cache should use. - */ -void FFmpegAudioFileCache::Set_Max_Size(unsigned size) -{ - ScopedMutexClass lock(&m_mutex); - m_maxSize = size; -} - -/** - * Attempts to free space by releasing files with no references - */ -unsigned FFmpegAudioFileCache::Free_Space(unsigned required) -{ - std::list to_free; - unsigned freed = 0; - - // First check for samples that don't have any references. - for (const auto &cached : m_cacheMap) { - if (cached.second.ref_count == 0) { - to_free.push_back(cached.first); - freed += cached.second.data_size; - - // If required is "0" we free as much as possible - if (required && freed >= required) { - break; - } - } - } - - for (const auto &file : to_free) { - auto to_remove = m_cacheMap.find(file); - - if (to_remove != m_cacheMap.end()) { - Release_Open_Audio(&to_remove->second); - m_currentSize -= to_remove->second.data_size; - m_cacheMap.erase(to_remove); - } - } - - return freed; -} - -/** - * Attempts to free space for a file by releasing files with no references and lower priority sounds. - */ -bool FFmpegAudioFileCache::Free_Space_For_Sample(const FFmpegOpenAudioFile &file) -{ - captainslog_assert(m_currentSize >= m_maxSize); // Assumed to be called only when we need more than allowed. - std::list to_free; - unsigned required = m_currentSize - m_maxSize; - unsigned freed = 0; - - // First check for samples that don't have any references. - freed = Free_Space(required); - - // If we still don't have enough potential space freed up, look for lower priority sounds to remove. - if (freed < required) { - for (const auto &cached : m_cacheMap) { - if (cached.second.ref_count != 0 - && cached.second.audio_event_info->Get_Priority() < file.audio_event_info->Get_Priority()) { - to_free.push_back(cached.first); - freed += cached.second.data_size; - - if (freed >= required) { - break; - } - } - } - } - - // If we have enough space to free, do the actual freeing, otherwise we didn't succeed, no point bothering. - if (freed < required) { - return false; - } - - for (const auto &file : to_free) { - auto to_remove = m_cacheMap.find(file); - - if (to_remove != m_cacheMap.end()) { - Release_Open_Audio(&to_remove->second); - m_currentSize -= to_remove->second.data_size; - m_cacheMap.erase(to_remove); - } - } + av_freep(&open_audio.opaque); return true; } @@ -520,8 +304,13 @@ bool FFmpegAudioFileCache::Free_Space_For_Sample(const FFmpegOpenAudioFile &file /** * Closes any playing instances of an audio file and then frees the memory for it. */ -void FFmpegAudioFileCache::Release_Open_Audio(FFmpegOpenAudioFile *open_audio) +void FFmpegAudioFileCache::Release_Open_Audio(OpenAudioFile *open_audio) { + if (open_audio->opaque) { + Close_FFmpeg_Contexts(open_audio); + av_freep(&open_audio->opaque); + } + // Close any playing samples that use this data. if (open_audio->ref_count != 0 && g_theAudio) { g_theAudio->Close_Any_Sample_Using_File(open_audio->wave_data); @@ -533,4 +322,3 @@ void FFmpegAudioFileCache::Release_Open_Audio(FFmpegOpenAudioFile *open_audio) open_audio->audio_event_info = nullptr; } } -} // namespace Thyme diff --git a/src/platform/audio/ffmpegaudiofilecache.h b/src/platform/audio/ffmpegaudiofilecache.h index aeafaf888..38146e8b9 100644 --- a/src/platform/audio/ffmpegaudiofilecache.h +++ b/src/platform/audio/ffmpegaudiofilecache.h @@ -15,78 +15,25 @@ #pragma once #include "always.h" -#include "asciistring.h" -#include "audiomanager.h" -#include "mutex.h" -#include "rtsutils.h" - -#ifdef THYME_USE_STLPORT -#include -#else -#include -#endif - -class AudioEventInfo; -class AudioEventRTS; - -struct AVFormatContext; -struct AVIOContext; -struct AVCodecContext; +#include "audiofilecache.h" namespace Thyme { -struct FFmpegOpenAudioFile -{ - // FFmpeg handles - AVFormatContext *fmt_ctx = nullptr; - AVIOContext *avio_ctx = nullptr; - AVCodecContext *codec_ctx = nullptr; - uint8_t *wave_data = nullptr; - int ref_count = 0; - int data_size = 0; - const AudioEventInfo *audio_event_info = nullptr; -}; - -#ifdef THYME_USE_STLPORT -typedef std::hash_map, std::equal_to> - ffmpegaudiocachemap_t; -#else -typedef std::unordered_map, std::equal_to> - ffmpegaudiocachemap_t; -#endif - -class FFmpegAudioFileCache +class FFmpegAudioFileCache : public AudioFileCache { public: - FFmpegAudioFileCache() : m_maxSize(0), m_currentSize(0), m_mutex("AudioFileCacheMutex") {} - virtual ~FFmpegAudioFileCache(); - AudioDataHandle Open_File(AudioEventRTS *file); - AudioDataHandle Open_File(const Utf8String &filename); - - void Close_File(AudioDataHandle file); - void Set_Max_Size(unsigned size); - inline unsigned Get_Max_Size() const { return m_maxSize; } - inline unsigned Get_Current_Size() const { return m_currentSize; } + ~FFmpegAudioFileCache(); - // #FEATURE: We can maybe call this during loading to free any old sounds we won't need ingame and decrease computation - // ingame - unsigned Free_Space(unsigned required = 0); +protected: + void Release_Open_Audio(OpenAudioFile *open_audio) override; + bool Load_File(File *file, OpenAudioFile &open_audio) override; private: - bool Free_Space_For_Sample(const FFmpegOpenAudioFile &open_audio); - void Release_Open_Audio(FFmpegOpenAudioFile *open_audio); - - bool Open_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio, File *file); - bool Decode_FFmpeg(FFmpegOpenAudioFile *open_audio); - void Close_FFmpeg_Contexts(FFmpegOpenAudioFile *open_audio); + bool Open_FFmpeg_Contexts(OpenAudioFile *open_audio, File *file); + bool Decode_FFmpeg(OpenAudioFile *open_audio); + void Close_FFmpeg_Contexts(OpenAudioFile *open_audio); static int Read_FFmpeg_Packet(void *opaque, uint8_t *buf, int buf_size); - void Fill_Wave_Data(FFmpegOpenAudioFile *open_audio); - -private: - ffmpegaudiocachemap_t m_cacheMap; - unsigned m_currentSize; - unsigned m_maxSize; - SimpleMutexClass m_mutex; + void Fill_Wave_Data(OpenAudioFile *open_audio); }; } // namespace Thyme diff --git a/src/platform/audio/milesaudiofilecache.cpp b/src/platform/audio/milesaudiofilecache.cpp index d70f5a4c1..3e60be45a 100644 --- a/src/platform/audio/milesaudiofilecache.cpp +++ b/src/platform/audio/milesaudiofilecache.cpp @@ -13,14 +13,23 @@ * LICENSE */ #include "milesaudiofilecache.h" -#include "audioeventrts.h" -#include "audiomanager.h" -#include "filesystem.h" #include #include +#include + +namespace Thyme +{ +struct MilesContext +{ + bool miles_allocated; +}; +} // namespace Thyme + +using namespace Thyme; + /** - * 0x00780D30 + * Clear all remaining open audio files */ MilesAudioFileCache::~MilesAudioFileCache() { @@ -33,89 +42,39 @@ MilesAudioFileCache::~MilesAudioFileCache() /** * Opens an audio file for an event. Reads from the cache if available or loads from file if not. - * - * 0x00780F80 */ -AudioDataHandle MilesAudioFileCache::Open_File(const AudioEventRTS *audio_event) +bool MilesAudioFileCache::Load_File(File *file, OpenAudioFile &open_audio) { - ScopedMutexClass lock(&m_mutex); - Utf8String filename; - - // What part of an event are we playing? - switch (audio_event->Get_Next_Play_Portion()) { - case 0: - filename = audio_event->Get_Attack_Name(); - break; - case 1: - filename = audio_event->Get_File_Name(); - break; - case 2: - filename = audio_event->Get_Decay_Name(); - break; - case 3: - return nullptr; - default: - break; - } - - // Try to find existing data for this file to avoid loading it if unneeded. - auto it = m_cacheMap.find(filename); - - if (it != m_cacheMap.end()) { - ++(it->second.ref_count); - - return it->second.wave_data; - } - - // Load the file from disk - File *file = g_theFileSystem->Open_File(filename.Str(), File::READ | File::BINARY); - - if (file == nullptr) { - if (!filename.Is_Empty()) { - captainslog_warn("Missing audio file '%s', could not cache.", filename.Str()); - } - - return nullptr; - } - + Utf8String filename = file->Get_Name(); uint32_t file_size = file->Size(); AudioDataHandle file_data = static_cast(file->Read_Entire_And_Close()); - - OpenAudioFile open_audio; - open_audio.audio_event_info = audio_event->Get_Event_Info(); + MilesContext *miles_ctx = static_cast(malloc(sizeof(MilesContext))); + open_audio.opaque = miles_ctx; AILSOUNDINFO sound_info; AIL_WAV_info(file_data, &sound_info); - if (audio_event->Is_Positional_Audio() && sound_info.channels > 1) { - captainslog_error("Audio marked as positional audio cannot have more than one channel."); - delete[] file_data; - - return nullptr; - } - - if (sound_info.format == 17) { // ADPCM, need to decompress. + if (sound_info.format == WAVE_FORMAT_IMA_ADPCM) { // ADPCM, need to decompress. AudioDataHandle decomp_data; uint32_t decomp_size; AIL_decompress_ADPCM(&sound_info, &decomp_data, &decomp_size); file_size = decomp_size; - open_audio.miles_allocated = true; + miles_ctx->miles_allocated = true; open_audio.wave_data = decomp_data; delete[] file_data; } else { - if (sound_info.format != 1) { // Must be PCM otherwise. + if (sound_info.format != WAVE_FORMAT_PCM) { // Must be PCM otherwise. captainslog_error( "Audio file '%s' is not PCM or ADPCM and is unsupported by the MSS based audio engine.", filename.Str()); delete[] file_data; - return nullptr; + return false; } - open_audio.miles_allocated = false; + miles_ctx->miles_allocated = false; open_audio.wave_data = file_data; } - memcpy(&open_audio.info, &sound_info, sizeof(sound_info)); open_audio.data_size = file_size; open_audio.ref_count = 1; m_currentSize += open_audio.data_size; @@ -125,98 +84,10 @@ AudioDataHandle MilesAudioFileCache::Open_File(const AudioEventRTS *audio_event) m_currentSize -= open_audio.data_size; Release_Open_Audio(&open_audio); - return nullptr; - } - - m_cacheMap[filename] = open_audio; - - return open_audio.wave_data; -} - -/** - * Closes a file, reducing the references to it. Does not actually free the cache. - * - * 0x007813D0 - */ -void MilesAudioFileCache::Close_File(AudioDataHandle file) -{ - if (file == nullptr) { - return; - } - - ScopedMutexClass lock(&m_mutex); - - for (auto it = m_cacheMap.begin(); it != m_cacheMap.end(); ++it) { - if (it->second.wave_data == file) { - --it->second.ref_count; - - break; - } - } -} - -/** - * Sets the maximum amount of memory in bytes that the cache should use. - */ -void MilesAudioFileCache::Set_Max_Size(unsigned size) -{ - ScopedMutexClass lock(&m_mutex); - m_maxSize = size; -} - -/** - * Attempts to free space for a file by releasing files with no references and lower priority sounds. - * - * 0x007814D0 - */ -bool MilesAudioFileCache::Free_Space_For_Sample(const OpenAudioFile &file) -{ - captainslog_assert(m_currentSize >= m_maxSize); // Assumed to be called only when we need more than allowed. - std::list to_free; - unsigned required = m_currentSize - m_maxSize; - unsigned freed = 0; - - // First check for samples that don't have any references. - for (auto it = m_cacheMap.begin(); it != m_cacheMap.end(); ++it) { - if (it->second.ref_count == 0) { - to_free.push_back(it->first); - freed += it->second.data_size; - - if (freed >= required) { - break; - } - } - } - - // If we still don't have enough potential space freed up, look for lower priority sounds to remove. - if (freed < required) { - for (auto it = m_cacheMap.begin(); it != m_cacheMap.end(); ++it) { - if (it->second.ref_count != 0 - && it->second.audio_event_info->Get_Priority() < file.audio_event_info->Get_Priority()) { - to_free.push_back(it->first); - freed += it->second.data_size; - - if (freed >= required) { - break; - } - } - } - } - - // If we have enough space to free, do the actual freeing, otherwise we didn't succeed, no point bothering. - if (freed < required) { return false; } - for (auto it = to_free.begin(); it != to_free.end(); ++it) { - auto to_remove = m_cacheMap.find(*it); - - if (to_remove != m_cacheMap.end()) { - Release_Open_Audio(&to_remove->second); - m_currentSize -= to_remove->second.data_size; - m_cacheMap.erase(to_remove); - } - } + m_cacheMap[filename] = open_audio; return true; } @@ -226,6 +97,8 @@ bool MilesAudioFileCache::Free_Space_For_Sample(const OpenAudioFile &file) */ void MilesAudioFileCache::Release_Open_Audio(OpenAudioFile *file) { + MilesContext *mss_ctx = static_cast(file->opaque); + // Close any playing samples that use this data. if (file->ref_count != 0) { g_theAudio->Close_Any_Sample_Using_File(file->wave_data); @@ -233,7 +106,7 @@ void MilesAudioFileCache::Release_Open_Audio(OpenAudioFile *file) // Deallocate the data buffer depending on how it was allocated. if (file->wave_data != nullptr) { - if (file->miles_allocated) { + if (mss_ctx && mss_ctx->miles_allocated) { AIL_mem_free_lock(file->wave_data); } else { delete[] file->wave_data; @@ -242,4 +115,9 @@ void MilesAudioFileCache::Release_Open_Audio(OpenAudioFile *file) file->wave_data = nullptr; file->audio_event_info = nullptr; } + + if (file->opaque) { + free(file->opaque); + file->opaque = nullptr; + } } diff --git a/src/platform/audio/milesaudiofilecache.h b/src/platform/audio/milesaudiofilecache.h index 31efcf4ec..0a0b51730 100644 --- a/src/platform/audio/milesaudiofilecache.h +++ b/src/platform/audio/milesaudiofilecache.h @@ -15,55 +15,15 @@ #pragma once #include "always.h" -#include "asciistring.h" -#include "audiomanager.h" -#include "mutex.h" -#include "rtsutils.h" -#include +#include "audiofilecache.h" -#ifdef THYME_USE_STLPORT -#include -#else -#include -#endif - -class AudioEventInfo; -class AudioEventRTS; - -struct OpenAudioFile -{ - AILSOUNDINFO info; - AudioDataHandle wave_data; - int ref_count; - int data_size; - bool miles_allocated; - const AudioEventInfo *audio_event_info; -}; - -#ifdef THYME_USE_STLPORT -typedef std::hash_map, std::equal_to> audiocachemap_t; -#else -typedef std::unordered_map, std::equal_to> - audiocachemap_t; -#endif - -class MilesAudioFileCache +class MilesAudioFileCache : public Thyme::AudioFileCache { ALLOW_HOOKING public: - MilesAudioFileCache() : m_maxSize(0), m_currentSize(0), m_mutex("AudioFileCacheMutex") {} - virtual ~MilesAudioFileCache(); - AudioDataHandle Open_File(const AudioEventRTS *file); - void Close_File(AudioDataHandle file); - void Set_Max_Size(unsigned size); - -private: - bool Free_Space_For_Sample(const OpenAudioFile &file); - void Release_Open_Audio(OpenAudioFile *file); + ~MilesAudioFileCache(); -private: - audiocachemap_t m_cacheMap; - unsigned m_currentSize; - unsigned m_maxSize; - SimpleMutexClass m_mutex; +protected: + void Release_Open_Audio(OpenAudioFile *open_audio) override; + bool Load_File(File *file, OpenAudioFile &open_audio) override; }; diff --git a/tests/test_audiofilecache.cpp b/tests/test_audiofilecache.cpp index 471fb7ad5..01294c794 100644 --- a/tests/test_audiofilecache.cpp +++ b/tests/test_audiofilecache.cpp @@ -14,20 +14,21 @@ */ #include #include -#include +#include #include #ifdef BUILD_WITH_FFMPEG #include #endif +#ifdef BUILD_WITH_MILES +#include +#endif + +#include extern LocalFileSystem *g_theLocalFileSystem; -#ifdef BUILD_WITH_FFMPEG -TEST(audio, ffmpegaudiofilecache) +void test_audiofilecache(Thyme::AudioFileCache &cache) { - g_theLocalFileSystem = new Win32LocalFileSystem; - Thyme::FFmpegAudioFileCache cache; - EXPECT_EQ(cache.Get_Max_Size(), 0); EXPECT_EQ(cache.Get_Current_Size(), 0); @@ -54,4 +55,21 @@ TEST(audio, ffmpegaudiofilecache) EXPECT_EQ(cache.Free_Space(), cache_size); EXPECT_EQ(cache.Get_Current_Size(), 0); } + +#ifdef BUILD_WITH_FFMPEG +TEST(audio, ffmpegaudiofilecache) +{ + g_theLocalFileSystem = new Win32LocalFileSystem; + Thyme::FFmpegAudioFileCache cache; + test_audiofilecache(cache); +} #endif +// This would only work when we have more than miles stubs +// #ifdef BUILD_WITH_MILES +// TEST(audio, milesaudiofilecache) +// { +// g_theLocalFileSystem = new Win32LocalFileSystem; +// MilesAudioFileCache cache; +// test_audiofilecache(cache); +// } +// #endif