mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 03:59:19 +02:00
feat(loading): optional background loading: sound files decoded by worker threads (sound.asyncload), data of dds textures read by worker threads (gfx.textures.streaming), gl textures of vehicles farther than a multiple of the draw range released (gfx.textures.releasedistance); all off by default
This commit is contained in:
242
audio/audio.cpp
242
audio/audio.cpp
@@ -19,35 +19,42 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
namespace audio {
|
||||
|
||||
openal_buffer::openal_buffer( std::string const &Filename ) :
|
||||
name( Filename ) {
|
||||
namespace {
|
||||
|
||||
// content of a sound file, mixed down to 16 bit mono
|
||||
struct decoded_sound {
|
||||
std::vector<std::int16_t> samples;
|
||||
unsigned int rate { 0 };
|
||||
bool mixed { false }; // the file had more than one channel
|
||||
};
|
||||
|
||||
// reads and decodes whole sound file. throws std::runtime_error on failure
|
||||
// NOTE: uses no simulator state, it's also run by the background decoder threads
|
||||
decoded_sound decode_sound( std::string const &File ) {
|
||||
|
||||
SF_INFO si;
|
||||
si.format = 0;
|
||||
|
||||
std::string file = Filename;
|
||||
|
||||
WriteLog("sound: loading file: " + file);
|
||||
|
||||
SNDFILE *sf = sf_open(file.c_str(), SFM_READ, &si);
|
||||
SNDFILE *sf = sf_open(File.c_str(), SFM_READ, &si);
|
||||
|
||||
if (sf == nullptr)
|
||||
throw std::runtime_error("sound: sf_open failed");
|
||||
|
||||
sf_command(sf, SFC_SET_NORM_FLOAT, nullptr, SF_TRUE);
|
||||
|
||||
float *fbuf = new float[si.frames * si.channels];
|
||||
if (sf_readf_float(sf, fbuf, si.frames) != si.frames)
|
||||
std::unique_ptr<float[]> fbuf { new float[si.frames * si.channels] };
|
||||
if (sf_readf_float(sf, fbuf.get(), si.frames) != si.frames) {
|
||||
sf_close(sf);
|
||||
throw std::runtime_error("sound: incomplete file");
|
||||
}
|
||||
|
||||
sf_close(sf);
|
||||
|
||||
rate = si.samplerate;
|
||||
decoded_sound sound;
|
||||
sound.rate = si.samplerate;
|
||||
sound.mixed = ( si.channels != 1 );
|
||||
sound.samples.resize( si.frames );
|
||||
|
||||
if (si.channels != 1)
|
||||
WriteLog("sound: warning: mixing multichannel file to mono");
|
||||
|
||||
int16_t *buf = new int16_t[si.frames];
|
||||
for (size_t i = 0; i < si.frames; i++)
|
||||
{
|
||||
float accum = 0;
|
||||
@@ -59,26 +66,149 @@ openal_buffer::openal_buffer( std::string const &Filename ) :
|
||||
val = 32767;
|
||||
if (val < -32767)
|
||||
val = -32767;
|
||||
buf[i] = val;
|
||||
sound.samples[i] = val;
|
||||
}
|
||||
|
||||
return sound;
|
||||
}
|
||||
|
||||
// creates AL buffer holding provided sound. returns: id of the buffer, or null_resource on failure
|
||||
ALuint upload_sound( decoded_sound const &Sound ) {
|
||||
|
||||
ALuint id { null_resource };
|
||||
alGenBuffers(1, &id);
|
||||
if (id != null_resource && alIsBuffer(id)) {
|
||||
alGetError();
|
||||
alBufferData(id, AL_FORMAT_MONO16, buf, si.frames * 2, rate);
|
||||
alBufferData(id, AL_FORMAT_MONO16, Sound.samples.data(), static_cast<ALsizei>(Sound.samples.size() * 2), Sound.rate);
|
||||
}
|
||||
else {
|
||||
id = null_resource;
|
||||
const char *str = alGetString(alGetError());
|
||||
ErrorLog("sound: failed to create AL buffer: " + (str != nullptr ? std::string(str) : ""));
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
delete[] buf;
|
||||
delete[] fbuf;
|
||||
} // namespace
|
||||
|
||||
openal_buffer::openal_buffer( std::string const &Filename ) :
|
||||
name( Filename ) {
|
||||
|
||||
WriteLog("sound: loading file: " + Filename);
|
||||
|
||||
auto const sound { decode_sound( Filename ) };
|
||||
|
||||
rate = sound.rate;
|
||||
|
||||
if (sound.mixed)
|
||||
WriteLog("sound: warning: mixing multichannel file to mono");
|
||||
|
||||
id = upload_sound( sound );
|
||||
|
||||
fetch_caption();
|
||||
}
|
||||
|
||||
// sound files decoded by worker threads. AL buffers for them are made on the main thread
|
||||
struct buffer_manager::decoder {
|
||||
|
||||
struct job {
|
||||
audio::buffer_handle handle;
|
||||
std::string file;
|
||||
decoded_sound sound; // written by the thread decoding the file
|
||||
std::string error;
|
||||
bool done { false }; // guarded by the mutex
|
||||
};
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable queued; // wakes workers when files are added
|
||||
std::condition_variable decoded; // wakes the main thread waiting for a file
|
||||
std::deque<std::shared_ptr<job>> queue; // files no worker has taken yet
|
||||
std::vector<std::shared_ptr<job>> pending; // files without AL buffer yet, in order of request. main thread only
|
||||
bool stop { false };
|
||||
std::vector<std::thread> workers;
|
||||
|
||||
decoder() {
|
||||
auto const count { std::clamp<unsigned int>( std::thread::hardware_concurrency(), 2, 9 ) - 1 };
|
||||
for( unsigned int idx = 0; idx < count; ++idx ) {
|
||||
workers.emplace_back( [ this ]() { work(); } );
|
||||
}
|
||||
}
|
||||
~decoder() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( mutex );
|
||||
stop = true;
|
||||
}
|
||||
queued.notify_all();
|
||||
for( auto &worker : workers ) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
static void decode( job &Item ) {
|
||||
try {
|
||||
Item.sound = decode_sound( Item.file );
|
||||
}
|
||||
catch( std::exception const &Error ) {
|
||||
Item.error = Error.what();
|
||||
}
|
||||
}
|
||||
|
||||
void work() {
|
||||
std::unique_lock<std::mutex> lock( mutex );
|
||||
while( true ) {
|
||||
queued.wait( lock, [ this ]() { return stop || ( false == queue.empty() ); } );
|
||||
if( stop ) {
|
||||
return;
|
||||
}
|
||||
auto item { queue.front() };
|
||||
queue.pop_front();
|
||||
lock.unlock();
|
||||
decode( *item );
|
||||
lock.lock();
|
||||
item->done = true;
|
||||
decoded.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
// waits until the specified file is decoded. a file no worker has taken yet is decoded right away
|
||||
void wait( std::shared_ptr<job> const &Item ) {
|
||||
std::unique_lock<std::mutex> lock( mutex );
|
||||
if( Item->done ) {
|
||||
return;
|
||||
}
|
||||
auto const lookup { std::find( std::begin( queue ), std::end( queue ), Item ) };
|
||||
if( lookup != std::end( queue ) ) {
|
||||
queue.erase( lookup );
|
||||
lock.unlock();
|
||||
decode( *Item );
|
||||
lock.lock();
|
||||
Item->done = true;
|
||||
return;
|
||||
}
|
||||
decoded.wait( lock, [ &Item ]() { return Item->done; } );
|
||||
}
|
||||
|
||||
bool is_done( job const &Item ) {
|
||||
std::lock_guard<std::mutex> lock( mutex );
|
||||
return Item.done;
|
||||
}
|
||||
|
||||
// makes the AL buffer out of the decoded file
|
||||
static void finish( job &Item, openal_buffer &Buffer ) {
|
||||
if( false == Item.error.empty() ) {
|
||||
ErrorLog( "Bad file: failed to load audio file \"" + Item.file + "\" (" + Item.error + ")", logtype::file );
|
||||
return;
|
||||
}
|
||||
Buffer.rate = Item.sound.rate;
|
||||
if( Item.sound.mixed ) {
|
||||
WriteLog( "sound: warning: mixing multichannel file \"" + Item.file + "\" to mono" );
|
||||
}
|
||||
Buffer.id = upload_sound( Item.sound );
|
||||
Item.sound = {};
|
||||
Buffer.fetch_caption();
|
||||
}
|
||||
};
|
||||
|
||||
// retrieves sound caption in currently set language
|
||||
void
|
||||
openal_buffer::fetch_caption() {
|
||||
@@ -93,8 +223,16 @@ openal_buffer::fetch_caption() {
|
||||
}
|
||||
}
|
||||
|
||||
buffer_manager::buffer_manager() {
|
||||
|
||||
m_buffers.emplace_back( openal_buffer() ); // empty bindings for null buffer
|
||||
}
|
||||
|
||||
buffer_manager::~buffer_manager() {
|
||||
|
||||
// workers are stopped before the buffers go
|
||||
m_decoder.reset();
|
||||
|
||||
for( auto &buffer : m_buffers ) {
|
||||
if( buffer.id != null_resource ) {
|
||||
::alDeleteBuffers( 1, &buffer.id );
|
||||
@@ -152,15 +290,81 @@ buffer_manager::create( std::string const &Filename ) {
|
||||
audio::openal_buffer const &
|
||||
buffer_manager::buffer( audio::buffer_handle const Buffer ) const {
|
||||
|
||||
if( ( m_decoder != nullptr )
|
||||
&& ( false == m_decoder->pending.empty() )
|
||||
&& ( m_buffers[ Buffer ].id == null_resource ) ) {
|
||||
complete( Buffer );
|
||||
}
|
||||
return m_buffers[ Buffer ];
|
||||
}
|
||||
|
||||
// creates AL buffers for files decoded in the background so far, or for all of them if requested
|
||||
void
|
||||
buffer_manager::update( bool const Wait ) {
|
||||
|
||||
if( ( m_decoder == nullptr )
|
||||
|| ( m_decoder->pending.empty() ) ) {
|
||||
return;
|
||||
}
|
||||
std::vector<std::shared_ptr<decoder::job>> undone;
|
||||
for( auto &item : m_decoder->pending ) {
|
||||
if( Wait ) {
|
||||
m_decoder->wait( item );
|
||||
}
|
||||
else if( false == m_decoder->is_done( *item ) ) {
|
||||
undone.emplace_back( item );
|
||||
continue;
|
||||
}
|
||||
decoder::finish( *item, m_buffers[ item->handle ] );
|
||||
}
|
||||
m_decoder->pending.swap( undone );
|
||||
}
|
||||
|
||||
// creates AL buffer for a file decoded in the background, waiting for the decoder if needed
|
||||
void
|
||||
buffer_manager::complete( audio::buffer_handle const Buffer ) const {
|
||||
|
||||
auto &pending { m_decoder->pending };
|
||||
auto const lookup {
|
||||
std::find_if(
|
||||
std::begin( pending ), std::end( pending ),
|
||||
[ Buffer ]( std::shared_ptr<decoder::job> const &Item ) { return Item->handle == Buffer; } ) };
|
||||
if( lookup == std::end( pending ) ) {
|
||||
return;
|
||||
}
|
||||
auto const item { *lookup };
|
||||
pending.erase( lookup );
|
||||
m_decoder->wait( item );
|
||||
decoder::finish( *item, m_buffers[ Buffer ] );
|
||||
}
|
||||
|
||||
// places in the bank a buffer containing data stored in specified file. returns: handle to the buffer
|
||||
audio::buffer_handle
|
||||
buffer_manager::emplace( std::string Filename ) {
|
||||
|
||||
buffer_handle const handle { m_buffers.size() };
|
||||
m_buffers.emplace_back( Filename );
|
||||
if( true == Global.AudioAsyncLoad ) {
|
||||
// the file is decoded in the background, the AL buffer is made once the data is ready or when the buffer is needed
|
||||
WriteLog( "sound: loading file: " + Filename );
|
||||
openal_buffer buffer;
|
||||
buffer.name = Filename;
|
||||
m_buffers.emplace_back( std::move( buffer ) );
|
||||
if( m_decoder == nullptr ) {
|
||||
m_decoder = std::make_unique<decoder>();
|
||||
}
|
||||
auto item { std::make_shared<decoder::job>() };
|
||||
item->handle = handle;
|
||||
item->file = Filename;
|
||||
m_decoder->pending.emplace_back( item );
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( m_decoder->mutex );
|
||||
m_decoder->queue.emplace_back( item );
|
||||
}
|
||||
m_decoder->queued.notify_one();
|
||||
}
|
||||
else {
|
||||
m_buffers.emplace_back( Filename );
|
||||
}
|
||||
|
||||
// NOTE: we store mapping without file type extension, to simplify lookups
|
||||
erase_extension( Filename );
|
||||
|
||||
@@ -47,21 +47,25 @@ class buffer_manager {
|
||||
|
||||
public:
|
||||
// constructors
|
||||
buffer_manager() { m_buffers.emplace_back( openal_buffer() ); } // empty bindings for null buffer
|
||||
buffer_manager(); // makes empty bindings for null buffer
|
||||
// destructor
|
||||
~buffer_manager();
|
||||
// methods
|
||||
// creates buffer object out of data stored in specified file. returns: handle to the buffer or null_handle if creation failed
|
||||
buffer_handle
|
||||
create( std::string const &Filename );
|
||||
// provides direct access to a specified buffer
|
||||
// provides direct access to a specified buffer. a buffer still decoded in the background is completed first
|
||||
audio::openal_buffer const &
|
||||
buffer( audio::buffer_handle const Buffer ) const;
|
||||
// creates AL buffers for files decoded in the background so far, or for all of them if requested
|
||||
void
|
||||
update( bool const Wait = false );
|
||||
|
||||
private:
|
||||
// types
|
||||
using buffer_sequence = std::vector<openal_buffer>;
|
||||
using index_map = std::unordered_map<std::string, std::size_t>;
|
||||
struct decoder; // background decoding of sound files, used with sound.asyncload
|
||||
// methods
|
||||
// places in the bank a buffer containing data stored in specified file. returns: handle to the buffer
|
||||
buffer_handle
|
||||
@@ -72,9 +76,13 @@ private:
|
||||
// checks whether specified file exists. returns: name of the located file, or empty string.
|
||||
std::string
|
||||
find_file( std::string const &Filename ) const;
|
||||
// creates AL buffer for a file decoded in the background, waiting for the decoder if needed
|
||||
void
|
||||
complete( audio::buffer_handle const Buffer ) const;
|
||||
// members
|
||||
buffer_sequence m_buffers;
|
||||
mutable buffer_sequence m_buffers; // buffers decoded in the background are completed on first access
|
||||
index_map m_buffermappings;
|
||||
mutable std::unique_ptr<decoder> m_decoder;
|
||||
};
|
||||
|
||||
} // audio
|
||||
|
||||
@@ -305,6 +305,13 @@ openal_renderer::buffer( audio::buffer_handle const Buffer ) const {
|
||||
return m_buffers.buffer( Buffer );
|
||||
}
|
||||
|
||||
// makes buffers of all sound files still decoded in the background, waiting for them
|
||||
void
|
||||
openal_renderer::complete_buffers() {
|
||||
|
||||
m_buffers.update( true );
|
||||
}
|
||||
|
||||
// initializes the service
|
||||
bool
|
||||
openal_renderer::init() {
|
||||
@@ -350,6 +357,9 @@ openal_renderer::erase( sound_source const *Controller ) {
|
||||
void
|
||||
openal_renderer::update( double const Deltatime ) {
|
||||
|
||||
// buffers of sound files decoded in the background since the last update
|
||||
m_buffers.update();
|
||||
|
||||
ALenum err = alGetError();
|
||||
if (err != AL_NO_ERROR)
|
||||
{
|
||||
|
||||
@@ -109,6 +109,8 @@ public:
|
||||
|
||||
audio::buffer_handle fetch_buffer( std::string const &Filename );
|
||||
audio::openal_buffer const &buffer( audio::buffer_handle const Buffer ) const;
|
||||
// makes buffers of all sound files still decoded in the background (sound.asyncload), waiting for them
|
||||
void complete_buffers();
|
||||
|
||||
bool init();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user