diff options
author | bmdhacks <bmd@bmdhacks.com> | 2025-02-13 16:01:47 -0800 |
---|---|---|
committer | bmdhacks <bmd@bmdhacks.com> | 2025-02-15 14:37:57 -0800 |
commit | 4ece0d79210c0f0351c16a0611f40308ceb8efb0 (patch) | |
tree | 7f27a77ccc27cdf0486d45feb6aa5ca484474e84 /source/core/StarAudio.cpp | |
parent | eedd20da00e873ae188634f5e9af4a2e9efe8607 (diff) |
Streaming Audio
Problem: The current implementation reads the entire sound file in to memory in order to play it.
For OGG background music, this uses 91 Megs of RAM by injesting all ogg songs as well as all
wav assets.
Solution: Change StarAudio to play the asset from a file handle and stream it off the disk.
This results in a massive savings of RAM and doesn't really affect audio quality unless you're
doing massive disk operations such as compiling Starbound.
Diffstat (limited to 'source/core/StarAudio.cpp')
-rw-r--r-- | source/core/StarAudio.cpp | 191 |
1 files changed, 112 insertions, 79 deletions
diff --git a/source/core/StarAudio.cpp b/source/core/StarAudio.cpp index a1f2e62..1b7e78e 100644 --- a/source/core/StarAudio.cpp +++ b/source/core/StarAudio.cpp @@ -6,10 +6,13 @@ #include "StarAudio.hpp" #include "StarBuffer.hpp" +#include "StarIODeviceCallbacks.hpp" #include "StarFile.hpp" #include "StarFormat.hpp" #include "StarLogging.hpp" #include "StarDataStreamDevices.hpp" +#include "StarSha256.hpp" +#include "StarEncode.hpp" namespace Star { @@ -35,9 +38,10 @@ float amplitudeToPerceptual(float amp, float normalizedMax, float range, float b namespace { struct WaveData { - ByteArrayPtr byteArray; + IODevicePtr device; unsigned channels; unsigned sampleRate; + size_t dataSize; // get the data size from the header to avoid id3 tag }; template <typename T> @@ -141,59 +145,59 @@ namespace { device->size(), wavDataSize + wavDataOffset)); } - ByteArrayPtr pcmData = make_shared<ByteArray>(); - pcmData->resize(wavDataSize); - - // Copy across data and perform and endianess conversion if needed - device->readFull(pcmData->ptr(), pcmData->size()); - for (size_t i = 0; i < pcmData->size() / 2; ++i) - fromByteOrder(ByteOrder::LittleEndian, pcmData->ptr() + i * 2, 2); - - return WaveData{std::move(pcmData), wavChannels, wavSampleRate}; + // Return the original device positioned at the PCM data + // Note: This means the caller owns handling endianness conversion + device->seek(wavDataOffset); + + return WaveData{device, wavChannels, wavSampleRate, wavDataSize}; } } class CompressedAudioImpl { public: - static size_t readFunc(void* ptr, size_t size, size_t nmemb, void* datasource) { - return static_cast<ExternalBuffer*>(datasource)->read((char*)ptr, size * nmemb) / size; - } - static int seekFunc(void* datasource, ogg_int64_t offset, int whence) { - static_cast<ExternalBuffer*>(datasource)->seek(offset, (IOSeek)whence); - return 0; - }; + CompressedAudioImpl(CompressedAudioImpl const& impl) + : m_audioData(impl.m_audioData->clone()) // Clone instead of sharing + , m_deviceCallbacks(m_audioData) // Pass reference to cloned data + , m_vorbisInfo(nullptr) { + setupCallbacks(); - static long int tellFunc(void* datasource) { - return (long int)static_cast<ExternalBuffer*>(datasource)->pos(); - }; + // Make sure data stream is ready to be read + m_audioData->open(IOMode::Read); + m_audioData->seek(0); + + // Add error checking to see what's happening with the clone + if (!m_audioData->isOpen()) + throw AudioException("Failed to open cloned audio device"); - CompressedAudioImpl(CompressedAudioImpl const& impl) { - m_audioData = impl.m_audioData; - m_memoryFile.reset(m_audioData->ptr(), m_audioData->size()); - m_vorbisInfo = nullptr; + auto size = m_audioData->size(); + if (size <= 0) + throw AudioException("Cloned audio device has no data"); } - CompressedAudioImpl(IODevicePtr audioData) { - audioData->open(IOMode::Read); - audioData->seek(0); - m_audioData = make_shared<ByteArray>(audioData->readBytes((size_t)audioData->size())); - m_memoryFile.reset(m_audioData->ptr(), m_audioData->size()); - m_vorbisInfo = nullptr; + CompressedAudioImpl(IODevicePtr audioData) + : m_audioData(audioData->clone()) // Clone instead of taking ownership + , m_deviceCallbacks(m_audioData) // Pass reference + , m_vorbisInfo(nullptr) { + setupCallbacks(); + m_audioData->open(IOMode::Read); + m_audioData->seek(0); } ~CompressedAudioImpl() { ov_clear(&m_vorbisFile); } - bool open() { - m_callbacks.read_func = readFunc; - m_callbacks.seek_func = seekFunc; - m_callbacks.tell_func = tellFunc; - m_callbacks.close_func = NULL; + void setupCallbacks() { + m_deviceCallbacks.setupOggCallbacks(m_callbacks); + } - if (ov_open_callbacks(&m_memoryFile, &m_vorbisFile, NULL, 0, m_callbacks) < 0) + bool open() { + int result = ov_open_callbacks(&m_deviceCallbacks, &m_vorbisFile, NULL, 0, m_callbacks); + if (result < 0) { + Logger::error("Failed to open ogg stream: error code {}", result); return false; + } m_vorbisInfo = ov_info(&m_vorbisFile, -1); return true; @@ -252,14 +256,14 @@ public: } while (read == OV_HOLE); if (read < 0) throw AudioException::format("Error in Audio::read ({})", read); - + // read in bytes, returning number of int16_t samples. return read / 2; } - + private: - ByteArrayConstPtr m_audioData; - ExternalBuffer m_memoryFile; + IODevicePtr m_audioData; + IODeviceCallbacks m_deviceCallbacks; ov_callbacks m_callbacks; OggVorbis_File m_vorbisFile; vorbis_info* m_vorbisInfo; @@ -267,41 +271,49 @@ private: class UncompressedAudioImpl { public: - UncompressedAudioImpl(UncompressedAudioImpl const& impl) { - m_channels = impl.m_channels; - m_sampleRate = impl.m_sampleRate; - m_audioData = impl.m_audioData; - m_memoryFile.reset(m_audioData->ptr(), m_audioData->size()); - } - + UncompressedAudioImpl(UncompressedAudioImpl const& impl) + : m_device(impl.m_device->clone()) + , m_channels(impl.m_channels) + , m_sampleRate(impl.m_sampleRate) + , m_dataSize(impl.m_dataSize) + , m_dataStart(impl.m_dataStart) + + { + StreamOffset initialPos = m_device->pos(); // Store initial position + if (!m_device->isOpen()) + m_device->open(IOMode::Read); + m_device->seek(initialPos); // Restore position after open + } + UncompressedAudioImpl(CompressedAudioImpl& impl) { m_channels = impl.channels(); m_sampleRate = impl.sampleRate(); + // Create a memory buffer to store decompressed data + auto memDevice = make_shared<Buffer>(); + int16_t buffer[1024]; - Buffer uncompressBuffer; while (true) { size_t ramt = impl.readPartial(buffer, 1024); - - if (ramt == 0) { - // End of stream reached + if (ramt == 0) break; - } else { - uncompressBuffer.writeFull((char*)buffer, ramt * 2); - } + memDevice->writeFull((char*)buffer, ramt * 2); } - m_audioData = make_shared<ByteArray>(uncompressBuffer.takeData()); - m_memoryFile.reset(m_audioData->ptr(), m_audioData->size()); + m_device = memDevice; } - UncompressedAudioImpl(ByteArrayConstPtr data, unsigned channels, unsigned sampleRate) { - m_channels = channels; - m_sampleRate = sampleRate; - m_audioData = std::move(data); - m_memoryFile.reset(m_audioData->ptr(), m_audioData->size()); + UncompressedAudioImpl(IODevicePtr device, unsigned channels, unsigned sampleRate, size_t dataSize) + : m_device(std::move(device)) + , m_channels(channels) + , m_sampleRate(sampleRate) + , m_dataSize(dataSize) + , m_dataStart((size_t)m_device->pos()) // Store current position as data start + { + if (!m_device->isOpen()) + m_device->open(IOMode::Read); } - + bool open() { return true; } @@ -319,7 +331,7 @@ public: } uint64_t totalSamples() { - return m_memoryFile.dataSize() / 2 / m_channels; + return m_device->size() / 2 / m_channels; } void seekTime(double time) { @@ -327,7 +339,7 @@ public: } void seekSample(uint64_t pos) { - m_memoryFile.seek(pos * 2 * m_channels); + m_device->seek(pos * 2 * m_channels); } double currentTime() { @@ -335,20 +347,41 @@ public: } uint64_t currentSample() { - return m_memoryFile.pos() / 2 / m_channels; + return m_device->pos() / 2 / m_channels; } + size_t readPartial(int16_t* buffer, size_t bufferSize) { if (bufferSize != NPos) bufferSize = bufferSize * 2; - return m_memoryFile.read((char*)buffer, bufferSize) / 2; + + // Calculate remaining valid data + size_t currentPos = m_device->pos() - m_dataStart; + size_t remainingBytes = m_dataSize - currentPos; + + // Limit read to remaining valid data + if (bufferSize > remainingBytes) + bufferSize = remainingBytes; + + if (bufferSize == 0) + return 0; + + size_t bytesRead = m_device->read((char*)buffer, bufferSize); + + // Handle endianness conversion + for (size_t i = 0; i < bytesRead / 2; ++i) + fromByteOrder(ByteOrder::LittleEndian, ((char*)buffer) + i * 2, 2); + + return bytesRead / 2; + } private: + IODevicePtr m_device; unsigned m_channels; unsigned m_sampleRate; - ByteArrayConstPtr m_audioData; - ExternalBuffer m_memoryFile; + size_t m_dataSize; + size_t m_dataStart; }; Audio::Audio(IODevicePtr device, String name) { @@ -358,7 +391,7 @@ Audio::Audio(IODevicePtr device, String name) { if (isUncompressed(device)) { WaveData data = parseWav(device); - m_uncompressed = make_shared<UncompressedAudioImpl>(std::move(data.byteArray), data.channels, data.sampleRate); + m_uncompressed = make_shared<UncompressedAudioImpl>(std::move(data.device), data.channels, data.sampleRate, data.dataSize); } else { m_compressed = make_shared<CompressedAudioImpl>(device); if (!m_compressed->open()) @@ -375,16 +408,16 @@ Audio::Audio(Audio&& audio) { } Audio& Audio::operator=(Audio const& audio) { - if (audio.m_uncompressed) { - m_uncompressed = make_shared<UncompressedAudioImpl>(*audio.m_uncompressed); - m_uncompressed->open(); - } else { - m_compressed = make_shared<CompressedAudioImpl>(*audio.m_compressed); - m_compressed->open(); - } - - seekSample(audio.currentSample()); - return *this; + if (audio.m_uncompressed) { + m_uncompressed = make_shared<UncompressedAudioImpl>(*audio.m_uncompressed); + m_uncompressed->open(); + } else { + m_compressed = make_shared<CompressedAudioImpl>(*audio.m_compressed); + if (!m_compressed->open()) // Check the return value + throw AudioException("Failed to open compressed audio stream during copy"); + seekSample(audio.currentSample()); // Only seek after successful open + } + return *this; } Audio& Audio::operator=(Audio&& audio) { |