/* ** soundemitter.cpp ** ** This file is part of mkxp. ** ** Copyright (C) 2014 - 2021 Amaryllis Kulla ** ** mkxp 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. ** ** mkxp is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with mkxp. If not, see . */ #include "soundemitter.h" #include "mkxp-polyfill.h" // snprintf #include "sharedstate.h" #include "filesystem.h" #include "exception.h" #include "config.h" #include "util.h" #include "debugwriter.h" #ifdef MKXPZ_RETRO # include # include "sandbox-serial-util.h" #else # include #endif // MKXPZ_RETRO #define SE_CACHE_MEM (10*1024*1024) // 10 MB struct SoundBuffer { /* Uniquely identifies this or equal buffer */ std::string key; AL::Buffer::ID alBuffer; /* Link into the buffer cache priority list */ IntruListLink link; /* Buffer byte count */ uint32_t bytes; /* Reference count */ uint8_t refCount; SoundBuffer() : link(this), refCount(1) { alBuffer = AL::Buffer::gen(); } static SoundBuffer *ref(SoundBuffer *buffer) { ++buffer->refCount; return buffer; } static void deref(SoundBuffer *buffer) { if (--buffer->refCount == 0) delete buffer; } private: ~SoundBuffer() { AL::Buffer::del(alBuffer); } }; /* Before: [a][b][c][d], After (index=1): [a][c][d][b] */ static void arrayPushBack(std::vector &array, size_t size, size_t index) { size_t v = array[index]; for (size_t t = index; t < size-1; ++t) array[t] = array[t+1]; array[size-1] = v; } SoundEmitter::SoundEmitter(const Config &conf) : bufferBytes(0), srcCount(conf.SE.sourceCount), alSrcs(srcCount), atchBufs(srcCount), filenames(srcCount), srcPrio(srcCount) { for (size_t i = 0; i < srcCount; ++i) { alSrcs[i] = AL::Source::gen(); atchBufs[i] = 0; srcPrio[i] = i; } } SoundEmitter::~SoundEmitter() { for (size_t i = 0; i < srcCount; ++i) { AL::Source::stop(alSrcs[i]); AL::Source::del(alSrcs[i]); if (atchBufs[i]) SoundBuffer::deref(atchBufs[i]); } BufferHash::const_iterator iter; for (iter = bufferHash.cbegin(); iter != bufferHash.cend(); ++iter) SoundBuffer::deref(iter->second); } void SoundEmitter::play(const std::string &filename, int volume, int pitch) { float _volume = clamp(volume, 0, 100) / 100.0f; float _pitch = clamp(pitch, 50, 150) / 100.0f; SoundBuffer *buffer = allocateBuffer(filename); if (!buffer) return; /* Try to find first free source */ size_t i; for (i = 0; i < srcCount; ++i) if (AL::Source::getState(alSrcs[srcPrio[i]]) != AL_PLAYING) break; /* If we didn't find any, try to find the lowest priority source * with the same buffer to overtake */ if (i == srcCount) for (size_t j = 0; j < srcCount; ++j) if (atchBufs[srcPrio[j]] == buffer) i = j; /* If we didn't find any, overtake the one with lowest priority */ if (i == srcCount) i = 0; size_t srcIndex = srcPrio[i]; /* Only detach/reattach if it's actually a different buffer */ bool switchBuffer = (atchBufs[srcIndex] != buffer); /* Push the used source to the back of the priority list */ arrayPushBack(srcPrio, srcCount, i); AL::Source::ID src = alSrcs[srcIndex]; AL::Source::stop(src); if (switchBuffer) AL::Source::detachBuffer(src); SoundBuffer *old = atchBufs[srcIndex]; if (old) SoundBuffer::deref(old); atchBufs[srcIndex] = SoundBuffer::ref(buffer); if (switchBuffer) AL::Source::attachBuffer(src, buffer->alBuffer); AL::Source::setVolume(src, _volume); AL::Source::setPitch(src, _pitch); AL::Source::play(src); filenames[srcIndex] = filename; } void SoundEmitter::stop() { for (size_t i = 0; i < srcCount; i++) AL::Source::stop(alSrcs[i]); } struct SoundOpenHandler : FileSystem::OpenHandler { #ifdef MKXPZ_RETRO int errnum; #endif // MKXPZ_RETRO SoundBuffer *buffer; SoundOpenHandler() : buffer(0) {} bool tryRead( #ifdef MKXPZ_RETRO std::shared_ptr ops, #else SDL_RWops &ops, #endif // MKXPZ_RETRO const char *ext ) { #ifdef MKXPZ_RETRO extern SF_VIRTUAL_IO sfvirtual; SndfileHandle handle(sfvirtual, ops.get()); #else Sound_Sample *sample = Sound_NewSample(&ops, ext, 0, STREAM_BUF_SIZE); #endif // MKXPZ_RETRO #ifdef MKXPZ_RETRO if ((errnum = handle.error())) #else if (!sample) #endif // MKXPZ_RETRO { #ifndef MKXPZ_RETRO SDL_RWclose(&ops); #endif // MKXPZ_RETRO return false; } /* Do all of the decoding in the handler so we don't have * to keep the source ops around */ #ifdef MKXPZ_RETRO uint8_t sampleSize = 2; uint32_t sampleCount = handle.frames(); #else uint32_t decBytes = Sound_DecodeAll(sample); uint8_t sampleSize = formatSampleSize(sample->actual.format); uint32_t sampleCount = decBytes / sampleSize; #endif // MKXPZ_RETRO buffer = new SoundBuffer; buffer->bytes = sampleSize * sampleCount; ALenum alFormat = chooseALFormat( sampleSize, #ifdef MKXPZ_RETRO handle.channels() #else sample->actual.channels #endif // MKXPZ_RETRO ); #ifdef MKXPZ_RETRO int16_t *buf = (int16_t *)std::malloc(buffer->bytes); if (buf == NULL) { return false; } handle.read(buf, sampleCount); AL::Buffer::uploadData(buffer->alBuffer, alFormat, buf, buffer->bytes, handle.samplerate()); std::free(buf); #else AL::Buffer::uploadData(buffer->alBuffer, alFormat, sample->buffer, buffer->bytes, sample->actual.rate); Sound_FreeSample(sample); #endif // MKXPZ_RETRO return true; } }; SoundBuffer *SoundEmitter::allocateBuffer(const std::string &filename) { SoundBuffer *buffer = bufferHash.value(filename, 0); if (buffer) { /* Buffer still in cashe. * Move to front of priority list */ buffers.remove(buffer->link); buffers.append(buffer->link); return buffer; } else { /* Buffer not in cache, needs to be loaded */ SoundOpenHandler handler; #ifdef MKXPZ_RETRO mkxp_retro::fs->openRead(handler, filename.c_str()); // TODO: move into shState #else shState->fileSystem().openRead(handler, filename.c_str()); #endif // MKXPZ_RETRO buffer = handler.buffer; if (!buffer) { char buf[512]; snprintf( buf, sizeof(buf), "Unable to decode sound: %s: %s", filename.c_str(), #ifdef MKXPZ_RETRO sf_error_number(handler.errnum) #else Sound_GetError() #endif // MKXPZ_RETRO ); Debug() << buf; return 0; } buffer->key = filename; uint32_t wouldBeBytes = bufferBytes + buffer->bytes; /* If memory limit is reached, delete lowest priority buffer * until there is room or no buffers left */ while (wouldBeBytes > SE_CACHE_MEM && !buffers.isEmpty()) { SoundBuffer *last = buffers.tail(); bufferHash.remove(last->key); buffers.remove(last->link); wouldBeBytes -= last->bytes; SoundBuffer::deref(last); } bufferHash.insert(filename, buffer); buffers.prepend(buffer->link); bufferBytes = wouldBeBytes; return buffer; } } #ifdef MKXPZ_RETRO bool SoundEmitter::sandbox_serialize(void *&data, mkxp_sandbox::wasm_size_t &max_size) { if (!mkxp_sandbox::sandbox_serialize((mkxp_sandbox::wasm_size_t)srcCount, data, max_size)) return false; for (size_t i = 0; i < srcCount; ++i) { if (!mkxp_sandbox::sandbox_serialize(filenames[i], data, max_size)) return false; AL::Source::ID source = alSrcs[i]; ALenum state = AL::Source::getState(source); if (!mkxp_sandbox::sandbox_serialize((int32_t)state, data, max_size)) return false; { ALfloat value; alGetSourcef(source.al, AL_SEC_OFFSET, &value); if (!mkxp_sandbox::sandbox_serialize(value, data, max_size)) return false; } { ALfloat value; alGetSourcef(source.al, AL_PITCH, &value); if (!mkxp_sandbox::sandbox_serialize(value, data, max_size)) return false; } { ALfloat value; alGetSourcef(source.al, AL_GAIN, &value); if (!mkxp_sandbox::sandbox_serialize(value, data, max_size)) return false; } } return true; } bool SoundEmitter::sandbox_deserialize(const void *&data, mkxp_sandbox::wasm_size_t &max_size) { { mkxp_sandbox::wasm_size_t count; if (!mkxp_sandbox::sandbox_deserialize(count, data, max_size)) return false; if (count != srcCount) return false; } for (size_t i = 0; i < srcCount; ++i) { AL::Source::ID source = alSrcs[i]; { std::string value = filenames[i]; if (!mkxp_sandbox::sandbox_deserialize(filenames[i], data, max_size)) return false; SoundBuffer *old_buffer = atchBufs[i]; if (filenames[i] != value) { if (filenames[i].empty()) { if (old_buffer != nullptr) { SoundBuffer::deref(old_buffer); } atchBufs[i] = nullptr; } else { SoundBuffer *new_buffer = allocateBuffer(filenames[i]); if (new_buffer == nullptr) return false; if (new_buffer != old_buffer) { if (old_buffer != nullptr) { SoundBuffer::deref(old_buffer); } atchBufs[i] = SoundBuffer::ref(new_buffer); AL::Source::attachBuffer(source, new_buffer->alBuffer); } } } } int32_t state; if (!mkxp_sandbox::sandbox_deserialize(state, data, max_size)) return false; if (state != AL::Source::getState(source)) { switch (state) { case AL_PLAYING: AL::Source::play(source); break; case AL_PAUSED: AL::Source::pause(source); break; case AL_STOPPED: default: AL::Source::stop(source); break; } } { ALfloat value; if (!mkxp_sandbox::sandbox_deserialize(value, data, max_size)) return false; alSourcef(source.al, AL_SEC_OFFSET, value); } { ALfloat value; if (!mkxp_sandbox::sandbox_deserialize(value, data, max_size)) return false; alSourcef(source.al, AL_PITCH, value); } { ALfloat value; if (!mkxp_sandbox::sandbox_deserialize(value, data, max_size)) return false; alSourcef(source.al, AL_GAIN, value); } } return true; } #endif // MKXPZ_RETRO