/* * OpenAL LAF Playback Example * * Copyright (c) 2024 by Chris Robinson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* This file contains an example for playback of Limitless Audio Format files. * * Some current shortcomings: * * - 256 track limit. Could be made higher, but making it too flexible would * necessitate more micro-allocations. * * - "Objects" mode only supports sample rates that are a multiple of 48. Since * positions are specified as samples in extra channels/tracks, and 3*16 * samples are needed per track to specify the full set of positions, and * each chunk is exactly one second long, other sample rates would result in * the positions being split across chunks, causing the source playback * offset to go out of sync with the offset used to look up the current * spatial positions. Fixing this will require slightly more work to update * and synchronize the spatial position arrays against the playback offset. * * - Updates are specified as fast as the app can detect and react to the * reported source offset (that in turn depends on how often OpenAL renders). * This can cause some positions to be a touch late and lose some granular * temporal movement. In practice, this should probably be good enough for * most use-cases. Fixing this would need either a new extension to queue * position changes to apply when needed, or use a separate loopback device * to render with and control the number of samples rendered between updates * (with a second device to do the actual playback). * * - The LAF documentation doesn't prohibit object position tracks from being * separated with audio tracks in between, or from being the first tracks * followed by the audio tracks. It's not known if this is intended to be * allowed, but it's not supported. Object position tracks must be last. * * Some remaining issues: * * - There are bursts of static on some channels. This doesn't appear to be a * parsing error since the bursts last less than the chunk size, and it never * loses sync with the remaining chunks. Might be an encoding error with the * files tested. * * - Positions are specified in left-handed coordinates, despite the LAF * documentation saying it's right-handed. Might be an encoding error with * the files tested, or might be a misunderstanding about which is which. How * to proceed may depend on how wide-spread this issue ends up being, but for * now, they're treated as left-handed here. * * - The LAF documentation doesn't specify the range or direction for the * channels' X and Y axis rotation in Channels mode. Presumably X rotation * (elevation) goes from -pi/2...+pi/2 and Y rotation (azimuth) goes from * either -pi...+pi or 0...pi*2, but the direction of movement isn't * specified. Currently positive azimuth moves from center rightward and * positive elevation moves from head-level upward. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "AL/alc.h" #include "AL/al.h" #include "AL/alext.h" #include "albit.h" #include "almalloc.h" #include "alnumeric.h" #include "alspan.h" #include "alstring.h" #include "common/alhelpers.h" #include "filesystem.h" #include "fmt/core.h" #include "fmt/std.h" #include "win_main_utf8.h" namespace { /* Filter object functions */ auto alGenFilters = LPALGENFILTERS{}; auto alDeleteFilters = LPALDELETEFILTERS{}; auto alIsFilter = LPALISFILTER{}; auto alFilteri = LPALFILTERI{}; auto alFilteriv = LPALFILTERIV{}; auto alFilterf = LPALFILTERF{}; auto alFilterfv = LPALFILTERFV{}; auto alGetFilteri = LPALGETFILTERI{}; auto alGetFilteriv = LPALGETFILTERIV{}; auto alGetFilterf = LPALGETFILTERF{}; auto alGetFilterfv = LPALGETFILTERFV{}; /* Effect object functions */ auto alGenEffects = LPALGENEFFECTS{}; auto alDeleteEffects = LPALDELETEEFFECTS{}; auto alIsEffect = LPALISEFFECT{}; auto alEffecti = LPALEFFECTI{}; auto alEffectiv = LPALEFFECTIV{}; auto alEffectf = LPALEFFECTF{}; auto alEffectfv = LPALEFFECTFV{}; auto alGetEffecti = LPALGETEFFECTI{}; auto alGetEffectiv = LPALGETEFFECTIV{}; auto alGetEffectf = LPALGETEFFECTF{}; auto alGetEffectfv = LPALGETEFFECTFV{}; /* Auxiliary Effect Slot object functions */ auto alGenAuxiliaryEffectSlots = LPALGENAUXILIARYEFFECTSLOTS{}; auto alDeleteAuxiliaryEffectSlots = LPALDELETEAUXILIARYEFFECTSLOTS{}; auto alIsAuxiliaryEffectSlot = LPALISAUXILIARYEFFECTSLOT{}; auto alAuxiliaryEffectSloti = LPALAUXILIARYEFFECTSLOTI{}; auto alAuxiliaryEffectSlotiv = LPALAUXILIARYEFFECTSLOTIV{}; auto alAuxiliaryEffectSlotf = LPALAUXILIARYEFFECTSLOTF{}; auto alAuxiliaryEffectSlotfv = LPALAUXILIARYEFFECTSLOTFV{}; auto alGetAuxiliaryEffectSloti = LPALGETAUXILIARYEFFECTSLOTI{}; auto alGetAuxiliaryEffectSlotiv = LPALGETAUXILIARYEFFECTSLOTIV{}; auto alGetAuxiliaryEffectSlotf = LPALGETAUXILIARYEFFECTSLOTF{}; auto alGetAuxiliaryEffectSlotfv = LPALGETAUXILIARYEFFECTSLOTFV{}; auto MuteFilterID = ALuint{}; auto LowFrequencyEffectID = ALuint{}; auto LfeSlotID = ALuint{}; using namespace std::string_view_literals; [[noreturn]] void do_assert(const char *message, int linenum, const char *filename, const char *funcname) { auto errstr = fmt::format("{}:{}: {}: {}", filename, linenum, funcname, message); throw std::runtime_error{errstr}; } #define MyAssert(cond) do { \ if(!(cond)) UNLIKELY \ do_assert("Assertion '" #cond "' failed", __LINE__, __FILE__, \ std::data(__func__)); \ } while(0) enum class Quality : std::uint8_t { s8, s16, f32, s24 }; enum class Mode : bool { Channels, Objects }; auto GetQualityName(Quality quality) noexcept -> std::string_view { switch(quality) { case Quality::s8: return "8-bit int"sv; case Quality::s16: return "16-bit int"sv; case Quality::f32: return "32-bit float"sv; case Quality::s24: return "24-bit int"sv; } return ""sv; } auto GetModeName(Mode mode) noexcept -> std::string_view { switch(mode) { case Mode::Channels: return "channels"sv; case Mode::Objects: return "objects"sv; } return ""sv; } auto BytesFromQuality(Quality quality) noexcept -> size_t { switch(quality) { case Quality::s8: return 1; case Quality::s16: return 2; case Quality::f32: return 4; case Quality::s24: return 3; } return 4; } auto BufferBytesFromQuality(Quality quality) noexcept -> size_t { switch(quality) { case Quality::s8: return 1; case Quality::s16: return 2; case Quality::f32: return 4; /* 24-bit samples are converted to 32-bit for OpenAL. */ case Quality::s24: return 4; } return 4; } /* Helper class for reading little-endian samples on big-endian targets, or * convert 24-bit samples. */ template struct SampleReader; template<> struct SampleReader { using src_t = int8_t; using dst_t = int8_t; [[nodiscard]] static auto read(const src_t &in) noexcept -> dst_t { return in; } }; template<> struct SampleReader { using src_t = int16_t; using dst_t = int16_t; [[nodiscard]] static auto read(const src_t &in) noexcept -> dst_t { if constexpr(al::endian::native == al::endian::little) return in; else return al::byteswap(in); } }; template<> struct SampleReader { /* 32-bit float samples are read as 32-bit integer on big-endian systems, * so that they can be byteswapped before being reinterpreted as float. */ using src_t = std::conditional_t; using dst_t = float; [[nodiscard]] static auto read(const src_t &in) noexcept -> dst_t { if constexpr(al::endian::native == al::endian::little) return in; else return al::bit_cast(al::byteswap(static_cast(in))); } }; template<> struct SampleReader { /* 24-bit samples are converted to 32-bit integer. */ using src_t = std::array; using dst_t = int32_t; [[nodiscard]] static auto read(const src_t &in) noexcept -> dst_t { return static_cast((uint32_t{in[0]}<<8) | (uint32_t{in[1]}<<16) | (uint32_t{in[2]}<<24)); } }; /* Each track with position data consists of a set of 3 samples per 16 audio * channels, resulting in a full set of positions being specified over 48 * sample frames. */ constexpr auto FramesPerPos = 48_uz; struct Channel { ALuint mSource{}; std::array mBuffers{}; float mAzimuth{}; float mElevation{}; bool mIsLfe{}; Channel() = default; Channel(const Channel&) = delete; Channel(Channel&& rhs) : mSource{rhs.mSource}, mBuffers{rhs.mBuffers}, mAzimuth{rhs.mAzimuth} , mElevation{rhs.mElevation}, mIsLfe{rhs.mIsLfe} { rhs.mSource = 0; rhs.mBuffers.fill(0); } ~Channel() { if(mSource) alDeleteSources(1, &mSource); if(mBuffers[0]) alDeleteBuffers(ALsizei(mBuffers.size()), mBuffers.data()); } auto operator=(const Channel&) -> Channel& = delete; auto operator=(Channel&& rhs) -> Channel& { std::swap(mSource, rhs.mSource); std::swap(mBuffers, rhs.mBuffers); std::swap(mAzimuth, rhs.mAzimuth); std::swap(mElevation, rhs.mElevation); std::swap(mIsLfe, rhs.mIsLfe); return *this; } }; struct LafStream { std::filebuf mInFile; Quality mQuality{}; Mode mMode{}; uint32_t mNumTracks{}; uint32_t mSampleRate{}; ALenum mALFormat{}; uint64_t mSampleCount{}; uint64_t mCurrentSample{}; std::array mEnabledTracks{}; uint32_t mNumEnabled{}; std::vector mSampleChunk; al::span mSampleLine; std::vector mChannels; std::vector> mPosTracks; LafStream() = default; LafStream(const LafStream&) = delete; ~LafStream() = default; auto operator=(const LafStream&) -> LafStream& = delete; [[nodiscard]] auto readChunk() -> uint32_t; void convertSamples(const al::span samples) const; void convertPositions(const al::span dst, const al::span src) const; template void copySamples(char *dst, const char *src, size_t idx, size_t count) const; [[nodiscard]] auto prepareTrack(size_t trackidx, size_t count) -> al::span; [[nodiscard]] auto isAtEnd() const noexcept -> bool { return mCurrentSample >= mSampleCount; } }; auto LafStream::readChunk() -> uint32_t { mEnabledTracks.fill(0); mInFile.sgetn(reinterpret_cast(mEnabledTracks.data()), (mNumTracks+7_z)>>3); mNumEnabled = std::accumulate(mEnabledTracks.cbegin(), mEnabledTracks.cend(), 0u, [](const unsigned int val, const uint8_t in) { return val + unsigned(al::popcount(unsigned(in))); }); /* Make sure enable bits aren't set for non-existent tracks. */ if(mEnabledTracks[((mNumTracks+7_uz)>>3) - 1] >= (1u<<(mNumTracks&7))) throw std::runtime_error{"Invalid channel enable bits"}; /* Each chunk is exactly one second long, with samples interleaved for each * enabled track. The last chunk may be shorter if there isn't enough time * remaining for a full second. */ const auto numsamples = std::min(uint64_t{mSampleRate}, mSampleCount-mCurrentSample); const auto toread = std::streamsize(numsamples * BytesFromQuality(mQuality) * mNumEnabled); if(mInFile.sgetn(mSampleChunk.data(), toread) != toread) throw std::runtime_error{"Failed to read sample chunk"}; std::fill(mSampleChunk.begin()+toread, mSampleChunk.end(), char{}); mCurrentSample += numsamples; return static_cast(numsamples); } void LafStream::convertSamples(const al::span samples) const { /* OpenAL uses unsigned 8-bit samples (0...255), so signed 8-bit samples * (-128...+127) need conversion. The other formats are fine. */ if(mQuality == Quality::s8) std::transform(samples.begin(), samples.end(), samples.begin(), [](const char sample) noexcept { return char(sample^0x80); }); } void LafStream::convertPositions(const al::span dst, const al::span src) const { switch(mQuality) { case Quality::s8: std::transform(src.begin(), src.end(), dst.begin(), [](const int8_t in) { return float(in) / 127.0f; }); break; case Quality::s16: { auto i16src = al::span{reinterpret_cast(src.data()), src.size()/sizeof(int16_t)}; std::transform(i16src.begin(), i16src.end(), dst.begin(), [](const int16_t in) { return float(in) / 32767.0f; }); } break; case Quality::f32: { auto f32src = al::span{reinterpret_cast(src.data()), src.size()/sizeof(float)}; std::copy(f32src.begin(), f32src.end(), dst.begin()); } break; case Quality::s24: { /* 24-bit samples are converted to 32-bit in copySamples. */ auto i32src = al::span{reinterpret_cast(src.data()), src.size()/sizeof(int32_t)}; std::transform(i32src.begin(), i32src.end(), dst.begin(), [](const int32_t in) { return float(in>>8) / 8388607.0f; }); } break; } } template void LafStream::copySamples(char *dst, const char *src, const size_t idx, const size_t count) const { using reader_t = SampleReader; using src_t = typename reader_t::src_t; using dst_t = typename reader_t::dst_t; const auto step = size_t{mNumEnabled}; assert(idx < step); auto input = al::span{reinterpret_cast(src), count*step}; auto output = al::span{reinterpret_cast(dst), count}; auto inptr = input.begin(); std::generate_n(output.begin(), output.size(), [&inptr,idx,step] { auto ret = reader_t::read(inptr[idx]); inptr += ptrdiff_t(step); return ret; }); } auto LafStream::prepareTrack(const size_t trackidx, const size_t count) -> al::span { const auto todo = std::min(size_t{mSampleRate}, count); if((mEnabledTracks[trackidx>>3] & (1_uz<<(trackidx&7)))) { /* If the track is enabled, get the real index (skipping disabled * tracks), and deinterlace it into the mono line. */ const auto idx = [this,trackidx]() -> unsigned int { const auto bits = al::span{mEnabledTracks}.first(trackidx>>3); const auto res = std::accumulate(bits.begin(), bits.end(), 0u, [](const unsigned int val, const uint8_t in) { return val + unsigned(al::popcount(unsigned(in))); }); return unsigned(al::popcount(mEnabledTracks[trackidx>>3] & ((1u<<(trackidx&7))-1))) + res; }(); switch(mQuality) { case Quality::s8: copySamples(mSampleLine.data(), mSampleChunk.data(), idx, todo); break; case Quality::s16: copySamples(mSampleLine.data(), mSampleChunk.data(), idx, todo); break; case Quality::f32: copySamples(mSampleLine.data(), mSampleChunk.data(), idx, todo); break; case Quality::s24: copySamples(mSampleLine.data(), mSampleChunk.data(), idx, todo); break; } } else { /* If the track is disabled, provide silence. */ std::fill_n(mSampleLine.begin(), mSampleLine.size(), char{}); } return mSampleLine.first(todo * BufferBytesFromQuality(mQuality)); } auto LoadLAF(const fs::path &fname) -> std::unique_ptr { auto laf = std::make_unique(); if(!laf->mInFile.open(fname, std::ios_base::binary | std::ios_base::in)) throw std::runtime_error{"Could not open file"}; auto marker = std::array{}; if(laf->mInFile.sgetn(marker.data(), marker.size()) != marker.size()) throw std::runtime_error{"Failed to read file marker"}; if(std::string_view{marker.data(), marker.size()} != "LIMITLESS"sv) throw std::runtime_error{"Not an LAF file"}; auto header = std::array{}; if(laf->mInFile.sgetn(header.data(), header.size()) != header.size()) throw std::runtime_error{"Failed to read header"}; while(std::string_view{header.data(), 4} != "HEAD"sv) { auto headview = std::string_view{header.data(), header.size()}; auto hiter = header.begin(); if(const auto hpos = std::min(headview.find("HEAD"sv), headview.size()); hpos < headview.size()) { /* Found the HEAD marker. Copy what was read of the header to the * front, fill in the rest of the header, and continue loading. */ hiter = std::copy(header.begin()+hpos, header.end(), hiter); } else if(al::ends_with(headview, "HEA"sv)) { /* Found what might be the HEAD marker at the end. Copy it to the * front, refill the header, and check again. */ hiter = std::copy_n(header.end()-3, 3, hiter); } else if(al::ends_with(headview, "HE"sv)) hiter = std::copy_n(header.end()-2, 2, hiter); else if(headview.back() == 'H') hiter = std::copy_n(header.end()-1, 1, hiter); const auto toread = std::distance(hiter, header.end()); if(laf->mInFile.sgetn(al::to_address(hiter), toread) != toread) throw std::runtime_error{"Failed to read header"}; } laf->mQuality = [stype=int{header[4]}] { if(stype == 0) return Quality::s8; if(stype == 1) return Quality::s16; if(stype == 2) return Quality::f32; if(stype == 3) return Quality::s24; throw std::runtime_error{fmt::format("Invalid quality type: {}", stype)}; }(); laf->mMode = [mode=int{header[5]}] { if(mode == 0) return Mode::Channels; if(mode == 1) return Mode::Objects; throw std::runtime_error{fmt::format("Invalid mode: {}", mode)}; }(); laf->mNumTracks = [input=al::span{header}.subspan<6,4>()] { return uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u) | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u); }(); fmt::println("Filename: {}", fname.string()); fmt::println(" quality: {}", GetQualityName(laf->mQuality)); fmt::println(" mode: {}", GetModeName(laf->mMode)); fmt::println(" track count: {}", laf->mNumTracks); if(laf->mNumTracks == 0) throw std::runtime_error{"No tracks"}; if(laf->mNumTracks > 256) throw std::runtime_error{fmt::format("Too many tracks: {}", laf->mNumTracks)}; auto chandata = std::vector(laf->mNumTracks*9_uz); auto headersize = std::streamsize(chandata.size()); if(laf->mInFile.sgetn(chandata.data(), headersize) != headersize) throw std::runtime_error{"Failed to read channel header data"}; if(laf->mMode == Mode::Channels) laf->mChannels.reserve(laf->mNumTracks); else { if(laf->mNumTracks < 2) throw std::runtime_error{"Not enough tracks"}; auto numchans = uint32_t{laf->mNumTracks - 1}; auto numpostracks = uint32_t{1}; while(numpostracks*16 < numchans) { --numchans; ++numpostracks; } laf->mChannels.reserve(numchans); laf->mPosTracks.reserve(numpostracks); } for(uint32_t i{0};i < laf->mNumTracks;++i) { static constexpr auto read_float = [](al::span input) { const auto value = uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u) | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u); return al::bit_cast(value); }; auto chan = al::span{chandata}.subspan(i*9_uz, 9); auto x_axis = read_float(chan.first<4>()); auto y_axis = read_float(chan.subspan<4,4>()); auto lfe_flag = int{chan[8]}; fmt::println("Track {}: E={:f}, A={:f} (LFE: {})", i, x_axis, y_axis, lfe_flag); if(x_axis != x_axis && y_axis == 0.0) { MyAssert(laf->mMode == Mode::Objects); MyAssert(i != 0); laf->mPosTracks.emplace_back(); } else { MyAssert(laf->mPosTracks.empty()); MyAssert(std::isfinite(x_axis) && std::isfinite(y_axis)); auto &channel = laf->mChannels.emplace_back(); channel.mAzimuth = y_axis; channel.mElevation = x_axis; channel.mIsLfe = lfe_flag != 0; } } fmt::println("Channels: {}", laf->mChannels.size()); /* For "objects" mode, ensure there's enough tracks with position data to * handle the audio channels. */ if(laf->mMode == Mode::Objects) MyAssert(((laf->mChannels.size()-1)>>4) == laf->mPosTracks.size()-1); auto footer = std::array{}; if(laf->mInFile.sgetn(footer.data(), footer.size()) != footer.size()) throw std::runtime_error{"Failed to read sample header data"}; laf->mSampleRate = [input=al::span{footer}.first<4>()] { return uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u) | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u); }(); laf->mSampleCount = [input=al::span{footer}.last<8>()] { return uint64_t{uint8_t(input[0])} | (uint64_t{uint8_t(input[1])}<<8) | (uint64_t{uint8_t(input[2])}<<16u) | (uint64_t{uint8_t(input[3])}<<24u) | (uint64_t{uint8_t(input[4])}<<32u) | (uint64_t{uint8_t(input[5])}<<40u) | (uint64_t{uint8_t(input[6])}<<48u) | (uint64_t{uint8_t(input[7])}<<56u); }(); fmt::println("Sample rate: {}", laf->mSampleRate); fmt::println("Length: {} samples ({:.2f} sec)", laf->mSampleCount, static_cast(laf->mSampleCount)/static_cast(laf->mSampleRate)); /* Position vectors get split across the PCM chunks if the sample rate * isn't a multiple of 48. Each PCM chunk is exactly one second (the sample * rate in sample frames). Each track with position data consists of a set * of 3 samples for 16 audio channels, resuling in 48 sample frames for a * full set of positions. Extra logic will be needed to manage the position * frame offset separate from each chunk. */ MyAssert(laf->mMode == Mode::Channels || (laf->mSampleRate%FramesPerPos) == 0); for(size_t i{0};i < laf->mPosTracks.size();++i) laf->mPosTracks[i].resize(laf->mSampleRate*2_uz, 0.0f); laf->mSampleChunk.resize(laf->mSampleRate*BytesFromQuality(laf->mQuality)*laf->mNumTracks + laf->mSampleRate*BufferBytesFromQuality(laf->mQuality)); laf->mSampleLine = al::span{laf->mSampleChunk}.last(laf->mSampleRate * BufferBytesFromQuality(laf->mQuality)); return laf; } void PlayLAF(std::string_view fname) try { auto laf = LoadLAF(fs::u8path(fname)); switch(laf->mQuality) { case Quality::s8: laf->mALFormat = AL_FORMAT_MONO8; break; case Quality::s16: laf->mALFormat = AL_FORMAT_MONO16; break; case Quality::f32: if(alIsExtensionPresent("AL_EXT_FLOAT32")) laf->mALFormat = AL_FORMAT_MONO_FLOAT32; break; case Quality::s24: laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO32"); if(!laf->mALFormat || laf->mALFormat == -1) laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO_I32"); break; } if(!laf->mALFormat || laf->mALFormat == -1) throw std::runtime_error{fmt::format("No supported format for {} samples", GetQualityName(laf->mQuality))}; static constexpr auto alloc_channel = [](Channel &channel) { alGenSources(1, &channel.mSource); alGenBuffers(ALsizei(channel.mBuffers.size()), channel.mBuffers.data()); /* Disable distance attenuation, and make sure the source stays locked * relative to the listener. */ alSourcef(channel.mSource, AL_ROLLOFF_FACTOR, 0.0f); alSourcei(channel.mSource, AL_SOURCE_RELATIVE, AL_TRUE); /* FIXME: Is the Y rotation/azimuth clockwise or counter-clockwise? * Does +azimuth move a front sound right or left? */ const auto x = std::sin(channel.mAzimuth) * std::cos(channel.mElevation); const auto y = std::sin(channel.mElevation); const auto z = -std::cos(channel.mAzimuth) * std::cos(channel.mElevation); alSource3f(channel.mSource, AL_POSITION, x, y, z); if(channel.mIsLfe) { if(LfeSlotID) { /* For LFE, silence the direct/dry path and connect the LFE aux * slot on send 0. */ alSourcei(channel.mSource, AL_DIRECT_FILTER, ALint(MuteFilterID)); alSource3i(channel.mSource, AL_AUXILIARY_SEND_FILTER, ALint(LfeSlotID), 0, AL_FILTER_NULL); } else { /* If AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT isn't available, * silence LFE channels since they may not be appropriate to * play normally. */ alSourcef(channel.mSource, AL_GAIN, 0.0f); } } if(auto err=alGetError()) throw std::runtime_error{fmt::format("OpenAL error: {}", alGetString(err))}; }; std::for_each(laf->mChannels.begin(), laf->mChannels.end(), alloc_channel); while(!laf->isAtEnd()) { auto state = ALenum{}; auto offset = ALint{}; auto processed = ALint{}; /* All sources are played in sync, so they'll all be at the same offset * with the same state and number of processed buffers. Query the back * source just in case the previous update ran really late and missed * updating only some sources on time (in which case, the latter ones * will underrun, which this will detect and restart them all as * needed). */ alGetSourcei(laf->mChannels.back().mSource, AL_BUFFERS_PROCESSED, &processed); alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset); alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state); if(state == AL_PLAYING || state == AL_PAUSED) { if(!laf->mPosTracks.empty()) { alcSuspendContext(alcGetCurrentContext()); for(size_t i{0};i < laf->mChannels.size();++i) { const auto trackidx = i>>4; const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15); const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0]; const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1]; const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2]; /* Contrary to the docs, the position is left-handed and * needs to be converted to right-handed. */ alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z); } alcProcessContext(alcGetCurrentContext()); } if(processed > 0) { const auto numsamples = laf->readChunk(); for(size_t i{0};i < laf->mChannels.size();++i) { const auto samples = laf->prepareTrack(i, numsamples); laf->convertSamples(samples); auto bufid = ALuint{}; alSourceUnqueueBuffers(laf->mChannels[i].mSource, 1, &bufid); alBufferData(bufid, laf->mALFormat, samples.data(), ALsizei(samples.size()), ALsizei(laf->mSampleRate)); alSourceQueueBuffers(laf->mChannels[i].mSource, 1, &bufid); } for(size_t i{0};i < laf->mPosTracks.size();++i) { std::copy(laf->mPosTracks[i].begin() + ptrdiff_t(laf->mSampleRate), laf->mPosTracks[i].end(), laf->mPosTracks[i].begin()); const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples); laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate), positions); } } else std::this_thread::sleep_for(std::chrono::milliseconds{10}); } else if(state == AL_STOPPED) { auto sources = std::array{}; for(size_t i{0};i < laf->mChannels.size();++i) sources[i] = laf->mChannels[i].mSource; alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data()); } else if(state == AL_INITIAL) { auto sources = std::array{}; auto numsamples = laf->readChunk(); for(size_t i{0};i < laf->mChannels.size();++i) { const auto samples = laf->prepareTrack(i, numsamples); laf->convertSamples(samples); alBufferData(laf->mChannels[i].mBuffers[0], laf->mALFormat, samples.data(), ALsizei(samples.size()), ALsizei(laf->mSampleRate)); } for(size_t i{0};i < laf->mPosTracks.size();++i) { const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples); laf->convertPositions(al::span{laf->mPosTracks[i]}.first(laf->mSampleRate), positions); } numsamples = laf->readChunk(); for(size_t i{0};i < laf->mChannels.size();++i) { const auto samples = laf->prepareTrack(i, numsamples); laf->convertSamples(samples); alBufferData(laf->mChannels[i].mBuffers[1], laf->mALFormat, samples.data(), ALsizei(samples.size()), ALsizei(laf->mSampleRate)); alSourceQueueBuffers(laf->mChannels[i].mSource, ALsizei(laf->mChannels[i].mBuffers.size()), laf->mChannels[i].mBuffers.data()); sources[i] = laf->mChannels[i].mSource; } for(size_t i{0};i < laf->mPosTracks.size();++i) { const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples); laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate), positions); } if(!laf->mPosTracks.empty()) { for(size_t i{0};i < laf->mChannels.size();++i) { const auto trackidx = i>>4; const auto x = laf->mPosTracks[trackidx][(i&15)*3 + 0]; const auto y = laf->mPosTracks[trackidx][(i&15)*3 + 1]; const auto z = laf->mPosTracks[trackidx][(i&15)*3 + 2]; alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z); } } alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data()); } else break; } auto state = ALenum{}; auto offset = ALint{}; alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset); alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state); while(alGetError() == AL_NO_ERROR && state == AL_PLAYING) { if(!laf->mPosTracks.empty()) { alcSuspendContext(alcGetCurrentContext()); for(size_t i{0};i < laf->mChannels.size();++i) { const auto trackidx = i>>4; const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15); const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0]; const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1]; const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2]; alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z); } alcProcessContext(alcGetCurrentContext()); } std::this_thread::sleep_for(std::chrono::milliseconds{10}); alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset); alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state); } } catch(std::exception& e) { fmt::println(stderr, "Error playing {}:\n {}", fname, e.what()); } auto main(al::span args) -> int { /* Print out usage if no arguments were specified */ if(args.size() < 2) { fmt::println(stderr, "Usage: {} [-device ] \n", args[0]); return 1; } args = args.subspan(1); if(InitAL(args) != 0) throw std::runtime_error{"Failed to initialize OpenAL"}; /* A simple RAII container for automating OpenAL shutdown. */ struct AudioManager { AudioManager() = default; AudioManager(const AudioManager&) = delete; auto operator=(const AudioManager&) -> AudioManager& = delete; ~AudioManager() { if(LfeSlotID) { alDeleteAuxiliaryEffectSlots(1, &LfeSlotID); alDeleteEffects(1, &LowFrequencyEffectID); alDeleteFilters(1, &MuteFilterID); } CloseAL(); } }; AudioManager almgr; if(auto *device = alcGetContextsDevice(alcGetCurrentContext()); alcIsExtensionPresent(device, "ALC_EXT_EFX") && alcIsExtensionPresent(device, "ALC_EXT_DEDICATED")) { #define LOAD_PROC(x) do { \ x = reinterpret_cast(alGetProcAddress(#x)); \ if(!x) fmt::println(stderr, "Failed to find function '{}'\n", #x##sv);\ } while(0) LOAD_PROC(alGenFilters); LOAD_PROC(alDeleteFilters); LOAD_PROC(alIsFilter); LOAD_PROC(alFilterf); LOAD_PROC(alFilterfv); LOAD_PROC(alFilteri); LOAD_PROC(alFilteriv); LOAD_PROC(alGetFilterf); LOAD_PROC(alGetFilterfv); LOAD_PROC(alGetFilteri); LOAD_PROC(alGetFilteriv); LOAD_PROC(alGenEffects); LOAD_PROC(alDeleteEffects); LOAD_PROC(alIsEffect); LOAD_PROC(alEffectf); LOAD_PROC(alEffectfv); LOAD_PROC(alEffecti); LOAD_PROC(alEffectiv); LOAD_PROC(alGetEffectf); LOAD_PROC(alGetEffectfv); LOAD_PROC(alGetEffecti); LOAD_PROC(alGetEffectiv); LOAD_PROC(alGenAuxiliaryEffectSlots); LOAD_PROC(alDeleteAuxiliaryEffectSlots); LOAD_PROC(alIsAuxiliaryEffectSlot); LOAD_PROC(alAuxiliaryEffectSlotf); LOAD_PROC(alAuxiliaryEffectSlotfv); LOAD_PROC(alAuxiliaryEffectSloti); LOAD_PROC(alAuxiliaryEffectSlotiv); LOAD_PROC(alGetAuxiliaryEffectSlotf); LOAD_PROC(alGetAuxiliaryEffectSlotfv); LOAD_PROC(alGetAuxiliaryEffectSloti); LOAD_PROC(alGetAuxiliaryEffectSlotiv); #undef LOAD_PROC alGenFilters(1, &MuteFilterID); alFilteri(MuteFilterID, AL_FILTER_TYPE, AL_FILTER_LOWPASS); alFilterf(MuteFilterID, AL_LOWPASS_GAIN, 0.0f); MyAssert(alGetError() == AL_NO_ERROR); alGenEffects(1, &LowFrequencyEffectID); alEffecti(LowFrequencyEffectID, AL_EFFECT_TYPE, AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT); MyAssert(alGetError() == AL_NO_ERROR); alGenAuxiliaryEffectSlots(1, &LfeSlotID); alAuxiliaryEffectSloti(LfeSlotID, AL_EFFECTSLOT_EFFECT, ALint(LowFrequencyEffectID)); MyAssert(alGetError() == AL_NO_ERROR); } std::for_each(args.begin(), args.end(), PlayLAF); return 0; } } // namespace int main(int argc, char **argv) { MyAssert(argc >= 0); auto args = std::vector(static_cast(argc)); std::copy_n(argv, args.size(), args.begin()); return main(al::span{args}); }