diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/core/StarAudio.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/core/StarAudio.cpp')
-rw-r--r-- | source/core/StarAudio.cpp | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/source/core/StarAudio.cpp b/source/core/StarAudio.cpp new file mode 100644 index 0000000..7e4cd31 --- /dev/null +++ b/source/core/StarAudio.cpp @@ -0,0 +1,562 @@ +// Fixes unused variable warning +#define OV_EXCLUDE_STATIC_CALLBACKS + +#include "vorbis/codec.h" +#include "vorbis/vorbisfile.h" + +#include "StarAudio.hpp" +#include "StarBuffer.hpp" +#include "StarFile.hpp" +#include "StarFormat.hpp" +#include "StarLogging.hpp" +#include "StarDataStreamDevices.hpp" + +namespace Star { + +namespace { + struct WaveData { + ByteArrayPtr byteArray; + unsigned channels; + unsigned sampleRate; + }; + + template <typename T> + T readLEType(IODevicePtr const& device) { + T t; + device->readFull((char*)&t, sizeof(t)); + fromByteOrder(ByteOrder::LittleEndian, (char*)&t, sizeof(t)); + return t; + } + + bool isUncompressed(IODevicePtr device) { + const size_t sigLength = 4; + unique_ptr<char[]> riffSig(new char[sigLength + 1]()); // RIFF\0 + unique_ptr<char[]> waveSig(new char[sigLength + 1]()); // WAVE\0 + + StreamOffset previousOffset = device->pos(); + device->seek(0); + device->readFull(riffSig.get(), sigLength); + device->seek(4, IOSeek::Relative); + device->readFull(waveSig.get(), sigLength); + device->seek(previousOffset); + if (strcmp(riffSig.get(), "RIFF") == 0 && strcmp(waveSig.get(), "WAVE") == 0) { // bytes are magic + return true; + } + return false; + } + + WaveData parseWav(IODevicePtr device) { + const size_t sigLength = 4; + unique_ptr<char[]> riffSig(new char[sigLength + 1]()); // RIFF\0 + unique_ptr<char[]> waveSig(new char[sigLength + 1]()); // WAVE\0 + unique_ptr<char[]> fmtSig(new char[sigLength + 1]()); // fmt \0 + unique_ptr<char[]> dataSig(new char[sigLength + 1]()); // data\0 + + // RIFF Chunk Descriptor + device->seek(0); + device->readFull(riffSig.get(), sigLength); + + uint32_t fileSize = readLEType<uint32_t>(device); + fileSize += sigLength + sizeof(fileSize); + if (fileSize != device->size()) + throw AudioException(strf("Wav file is wrong size, reports %d is actually %d", fileSize, device->size())); + + device->readFull(waveSig.get(), sigLength); + + if ((strcmp(riffSig.get(), "RIFF") != 0) || (strcmp(waveSig.get(), "WAVE") != 0)) { // bytes are not magic + auto p = [](char a) { return isprint(a) ? a : '?'; }; + throw AudioException(strf("Wav file has wrong magic bytes, got `%c%c%c%c' and `%c%c%c%c' but expected `RIFF' and `WAVE'", + p(riffSig[0]), p(riffSig[1]), p(riffSig[2]), p(riffSig[3]), p(waveSig[0]), p(waveSig[1]), p(waveSig[2]), p(waveSig[3]))); + } + + // fmt subchunk + + device->readFull(fmtSig.get(), sigLength); + if (strcmp(fmtSig.get(), "fmt ") != 0) { // friendship is magic + auto p = [](char a) { return isprint(a) ? a : '?'; }; + throw AudioException(strf("Wav file fmt subchunk has wrong magic bytes, got `%c%c%c%c' but expected `fmt '", + p(fmtSig[0]), + p(fmtSig[1]), + p(fmtSig[2]), + p(fmtSig[3]))); + } + + uint32_t fmtSubchunkSize = readLEType<uint32_t>(device); + fmtSubchunkSize += sigLength; + if (fmtSubchunkSize < 20) + throw AudioException(strf("fmt subchunk is sized wrong, expected 20 got %d. Is this wav file not PCM?", fmtSubchunkSize)); + + uint16_t audioFormat = readLEType<uint16_t>(device); + if (audioFormat != 1) + throw AudioException("audioFormat data indicates that wav file is something other than PCM format. Unsupported."); + + uint16_t wavChannels = readLEType<uint16_t>(device); + uint32_t wavSampleRate = readLEType<uint32_t>(device); + uint32_t wavByteRate = readLEType<uint32_t>(device); + uint16_t wavBlockAlign = readLEType<uint16_t>(device); + uint16_t wavBitsPerSample = readLEType<uint16_t>(device); + + if (wavBitsPerSample != 16) + throw AudioException("Only 16-bit PCM wavs are supported."); + if (wavByteRate * 8 != wavSampleRate * wavChannels * wavBitsPerSample) + throw AudioException("Sanity check failed, ByteRate is wrong"); + if (wavBlockAlign * 8 != wavChannels * wavBitsPerSample) + throw AudioException("Sanity check failed, BlockAlign is wrong"); + + device->seek(fmtSubchunkSize - 20, IOSeek::Relative); + + // data subchunk + + device->readFull(dataSig.get(), sigLength); + if (strcmp(dataSig.get(), "data") != 0) { // magic or more magic? + auto p = [](char a) { return isprint(a) ? a : '?'; }; + throw AudioException(strf("Wav file data subchunk has wrong magic bytes, got `%c%c%c%c' but expected `data'", + p(dataSig[0]), p(dataSig[1]), p(dataSig[2]), p(dataSig[3]))); + } + + uint32_t wavDataSize = readLEType<uint32_t>(device); + size_t wavDataOffset = (size_t)device->pos(); + if (wavDataSize + wavDataOffset > (size_t)device->size()) { + throw AudioException(strf("Wav file data size reported is inconsistent with file size, got %d but expected %d", + 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{move(pcmData), wavChannels, wavSampleRate}; + } +} + +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; + }; + + static long int tellFunc(void* datasource) { + return (long int)static_cast<ExternalBuffer*>(datasource)->pos(); + }; + + CompressedAudioImpl(CompressedAudioImpl const& impl) { + m_audioData = impl.m_audioData; + m_memoryFile.reset(m_audioData->ptr(), m_audioData->size()); + m_vorbisInfo = nullptr; + } + + 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() { + 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; + + if (ov_open_callbacks(&m_memoryFile, &m_vorbisFile, NULL, 0, m_callbacks) < 0) + return false; + + m_vorbisInfo = ov_info(&m_vorbisFile, -1); + return true; + } + + unsigned channels() { + return m_vorbisInfo->channels; + } + + unsigned sampleRate() { + return m_vorbisInfo->rate; + } + + double totalTime() { + return ov_time_total(&m_vorbisFile, -1); + } + + uint64_t totalSamples() { + return ov_pcm_total(&m_vorbisFile, -1); + } + + void seekTime(double time) { + int ret = ov_time_seek(&m_vorbisFile, time); + + if (ret != 0) + throw StarException("Cannot seek ogg stream Audio::seekTime"); + } + + void seekSample(uint64_t pos) { + int ret = ov_pcm_seek(&m_vorbisFile, pos); + + if (ret != 0) + throw StarException("Cannot seek ogg stream in Audio::seekSample"); + } + + double currentTime() { + return ov_time_tell(&m_vorbisFile); + } + + uint64_t currentSample() { + return ov_pcm_tell(&m_vorbisFile); + } + + size_t readPartial(int16_t* buffer, size_t bufferSize) { + int bitstream; + int read; + // ov_read takes int parameter, so do some magic here to make sure we don't + // overflow + bufferSize *= 2; +#if STAR_LITTLE_ENDIAN + read = ov_read(&m_vorbisFile, (char*)buffer, bufferSize, 0, 2, 1, &bitstream); +#else + read = ov_read(&m_vorbisFile, (char*)buffer, bufferSize, 1, 2, 1, &bitstream); +#endif + if (read < 0) + throw AudioException("Error in Audio::read"); + + // read in bytes, returning number of int16_t samples. + return read / 2; + } + +private: + ByteArrayConstPtr m_audioData; + ExternalBuffer m_memoryFile; + ov_callbacks m_callbacks; + OggVorbis_File m_vorbisFile; + vorbis_info* m_vorbisInfo; +}; + +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(CompressedAudioImpl& impl) { + m_channels = impl.channels(); + m_sampleRate = impl.sampleRate(); + + int16_t buffer[1024]; + Buffer uncompressBuffer; + while (true) { + size_t ramt = impl.readPartial(buffer, 1024); + + if (ramt == 0) { + // End of stream reached + break; + } else { + uncompressBuffer.writeFull((char*)buffer, ramt * 2); + } + } + + m_audioData = make_shared<ByteArray>(uncompressBuffer.takeData()); + m_memoryFile.reset(m_audioData->ptr(), m_audioData->size()); + } + + UncompressedAudioImpl(ByteArrayConstPtr data, unsigned channels, unsigned sampleRate) { + m_channels = channels; + m_sampleRate = sampleRate; + m_audioData = move(data); + m_memoryFile.reset(m_audioData->ptr(), m_audioData->size()); + } + + bool open() { + return true; + } + + unsigned channels() { + return m_channels; + } + + unsigned sampleRate() { + return m_sampleRate; + } + + double totalTime() { + return (double)totalSamples() / m_sampleRate; + } + + uint64_t totalSamples() { + return m_memoryFile.dataSize() / 2 / m_channels; + } + + void seekTime(double time) { + seekSample((uint64_t)(time * m_sampleRate)); + } + + void seekSample(uint64_t pos) { + m_memoryFile.seek(pos * 2 * m_channels); + } + + double currentTime() { + return (double)currentSample() / m_sampleRate; + } + + uint64_t currentSample() { + return m_memoryFile.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; + } + +private: + unsigned m_channels; + unsigned m_sampleRate; + ByteArrayConstPtr m_audioData; + ExternalBuffer m_memoryFile; +}; + +Audio::Audio(IODevicePtr device) { + if (!device->isOpen()) + device->open(IOMode::Read); + + if (isUncompressed(device)) { + WaveData data = parseWav(device); + m_uncompressed = make_shared<UncompressedAudioImpl>(move(data.byteArray), data.channels, data.sampleRate); + } else { + m_compressed = make_shared<CompressedAudioImpl>(device); + if (!m_compressed->open()) + throw AudioException("File does not appear to be a valid ogg bitstream"); + } +} + +Audio::Audio(Audio const& audio) { + *this = audio; +} + +Audio::Audio(Audio&& audio) { + operator=(move(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; +} + +Audio& Audio::operator=(Audio&& audio) { + m_compressed = move(audio.m_compressed); + m_uncompressed = move(audio.m_uncompressed); + return *this; +} + +unsigned Audio::channels() const { + if (m_uncompressed) + return m_uncompressed->channels(); + else + return m_compressed->channels(); +} + +unsigned Audio::sampleRate() const { + if (m_uncompressed) + return m_uncompressed->sampleRate(); + else + return m_compressed->sampleRate(); +} + +double Audio::totalTime() const { + if (m_uncompressed) + return m_uncompressed->totalTime(); + else + return m_compressed->totalTime(); +} + +uint64_t Audio::totalSamples() const { + if (m_uncompressed) + return m_uncompressed->totalSamples(); + else + return m_compressed->totalSamples(); +} + +bool Audio::compressed() const { + return (bool)m_compressed; +} + +void Audio::uncompress() { + if (m_compressed) { + m_uncompressed = make_shared<UncompressedAudioImpl>(*m_compressed); + m_compressed.reset(); + } +} + +void Audio::seekTime(double time) { + if (m_uncompressed) + m_uncompressed->seekTime(time); + else + m_compressed->seekTime(time); +} + +void Audio::seekSample(uint64_t pos) { + if (m_uncompressed) + m_uncompressed->seekSample(pos); + else + m_compressed->seekSample(pos); +} + +double Audio::currentTime() const { + if (m_uncompressed) + return m_uncompressed->currentTime(); + else + return m_compressed->currentTime(); +} + +uint64_t Audio::currentSample() const { + if (m_uncompressed) + return m_uncompressed->currentSample(); + else + return m_compressed->currentSample(); +} + +size_t Audio::readPartial(int16_t* buffer, size_t bufferSize) { + if (bufferSize == 0) + return 0; + + if (m_uncompressed) + return m_uncompressed->readPartial(buffer, bufferSize); + else + return m_compressed->readPartial(buffer, bufferSize); +} + +size_t Audio::read(int16_t* buffer, size_t bufferSize) { + if (bufferSize == 0) + return 0; + + size_t readTotal = 0; + while (readTotal < bufferSize) { + size_t toGo = bufferSize - readTotal; + size_t ramt = readPartial(buffer + readTotal, toGo); + readTotal += ramt; + // End of stream reached + if (ramt == 0) + break; + } + return readTotal; +} + +size_t Audio::resample(unsigned destinationChannels, unsigned destinationSampleRate, int16_t* destinationBuffer, size_t destinationBufferSize, double velocity) { + unsigned destinationSamples = destinationBufferSize / destinationChannels; + if (destinationSamples == 0) + return 0; + + unsigned sourceChannels = channels(); + unsigned sourceSampleRate = sampleRate(); + + if (velocity != 1.0) + sourceSampleRate = (unsigned)(sourceSampleRate * velocity); + + if (destinationChannels == sourceChannels && destinationSampleRate == sourceSampleRate) { + // If the destination and source channel count and sample rate are the + // same, this is the same as a read. + + return read(destinationBuffer, destinationBufferSize); + + } else if (destinationSampleRate == sourceSampleRate) { + // If the destination and source sample rate are the same, then we can skip + // the super-sampling math. + + unsigned sourceBufferSize = destinationSamples * sourceChannels; + + m_workingBuffer.resize(sourceBufferSize * sizeof(int16_t)); + int16_t* sourceBuffer = (int16_t*)m_workingBuffer.ptr(); + + unsigned readSamples = read(sourceBuffer, sourceBufferSize) / sourceChannels; + + for (unsigned sample = 0; sample < readSamples; ++sample) { + unsigned sourceBufferIndex = sample * sourceChannels; + unsigned destinationBufferIndex = sample * destinationChannels; + + for (unsigned destinationChannel = 0; destinationChannel < destinationChannels; ++destinationChannel) { + // If the destination channel count is greater than the source + // channels, simply copy the last channel + unsigned sourceChannel = min(destinationChannel, sourceChannels - 1); + destinationBuffer[destinationBufferIndex + destinationChannel] = + sourceBuffer[sourceBufferIndex + sourceChannel]; + } + } + + return readSamples * destinationChannels; + + } else { + // Otherwise, we have to do a full resample. + + unsigned sourceSamples = ((uint64_t)sourceSampleRate * destinationSamples + destinationSampleRate - 1) / destinationSampleRate; + unsigned sourceBufferSize = sourceSamples * sourceChannels; + + m_workingBuffer.resize(sourceBufferSize * sizeof(int16_t)); + int16_t* sourceBuffer = (int16_t*)m_workingBuffer.ptr(); + + unsigned readSamples = read(sourceBuffer, sourceBufferSize) / sourceChannels; + + if (readSamples == 0) + return 0; + + unsigned writtenSamples = 0; + + for (unsigned destinationSample = 0; destinationSample < destinationSamples; ++destinationSample) { + unsigned destinationBufferIndex = destinationSample * destinationChannels; + + for (unsigned destinationChannel = 0; destinationChannel < destinationChannels; ++destinationChannel) { + static int const SuperSampleFactor = 8; + + // If the destination channel count is greater than the source + // channels, simply copy the last channel + unsigned sourceChannel = min(destinationChannel, sourceChannels - 1); + + int sample = 0; + int sampleCount = 0; + for (int superSample = 0; superSample < SuperSampleFactor; ++superSample) { + unsigned sourceSample = (unsigned)((destinationSample * SuperSampleFactor + superSample) * sourceSamples / destinationSamples) / SuperSampleFactor; + if (sourceSample < readSamples) { + unsigned sourceBufferIndex = sourceSample * sourceChannels; + starAssert(sourceBufferIndex + sourceChannel < sourceBufferSize); + sample += sourceBuffer[sourceBufferIndex + sourceChannel]; + ++sampleCount; + } + } + + // If sampleCount is zero, then we are past the end of our read data + // completely, and can stop + if (sampleCount == 0) + return writtenSamples * destinationChannels; + + sample /= sampleCount; + destinationBuffer[destinationBufferIndex + destinationChannel] = (int16_t)sample; + writtenSamples = destinationSample + 1; + } + } + + return writtenSamples * destinationChannels; + } +} + +} |