Use filebuf instead of ifstream in allafplay
[openal-soft.git] / examples / allafplay.cpp
blob666a39e9a6502c11068ed080b98827bd97dad10b
1 /*
2 * OpenAL LAF Playback Example
4 * Copyright (c) 2024 by Chris Robinson <chris.kcat@gmail.com>
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
25 /* This file contains an example for playback of Limitless Audio Format files.
27 * Some current shortcomings:
29 * - 256 track limit. Could be made higher, but making it too flexible would
30 * necessitate more micro-allocations.
32 * - "Objects" mode only supports sample rates that are a multiple of 48. Since
33 * positions are specified as samples in extra channels/tracks, and 3*16
34 * samples are needed per track to specify the full set of positions, and
35 * each chunk is exactly one second long, other sample rates would result in
36 * the positions being split across chunks, causing the source playback
37 * offset to go out of sync with the offset used to look up the current
38 * spatial positions. Fixing this will require slightly more work to update
39 * and synchronize the spatial position arrays against the playback offset.
41 * - Updates are specified as fast as the app can detect and react to the
42 * reported source offset (that in turn depends on how often OpenAL renders).
43 * This can cause some positions to be a touch late and lose some granular
44 * temporal movement. In practice, this should probably be good enough for
45 * most use-cases. Fixing this would need either a new extension to queue
46 * position changes to apply when needed, or use a separate loopback device
47 * to render with and control the number of samples rendered between updates
48 * (with a second device to do the actual playback).
50 * - The LAF documentation doesn't prohibit object position tracks from being
51 * separated with audio tracks in between, or from being the first tracks
52 * followed by the audio tracks. It's not known if this is intended to be
53 * allowed, but it's not supported. Object position tracks must be last.
55 * Some remaining issues:
57 * - There are bursts of static on some channels. This doesn't appear to be a
58 * parsing error since the bursts last less than the chunk size, and it never
59 * loses sync with the remaining chunks. Might be an encoding error with the
60 * files tested.
62 * - Positions are specified in left-handed coordinates, despite the LAF
63 * documentation saying it's right-handed. Might be an encoding error with
64 * the files tested, or might be a misunderstanding about which is which. How
65 * to proceed may depend on how wide-spread this issue ends up being, but for
66 * now, they're treated as left-handed here.
68 * - The LAF documentation doesn't specify the range or direction for the
69 * channels' X and Y axis rotation in Channels mode. Presumably X rotation
70 * (elevation) goes from -pi/2...+pi/2 and Y rotation (azimuth) goes from
71 * either -pi...+pi or 0...pi*2, but the direction of movement isn't
72 * specified. Currently positive azimuth moves from center rightward and
73 * positive elevation moves from head-level upward.
76 #include <algorithm>
77 #include <array>
78 #include <cassert>
79 #include <cstdint>
80 #include <filesystem>
81 #include <fstream>
82 #include <memory>
83 #include <numeric>
84 #include <string>
85 #include <string_view>
86 #include <thread>
87 #include <type_traits>
88 #include <vector>
90 #include "AL/alc.h"
91 #include "AL/al.h"
92 #include "AL/alext.h"
94 #include "albit.h"
95 #include "almalloc.h"
96 #include "alnumeric.h"
97 #include "alspan.h"
98 #include "alstring.h"
99 #include "common/alhelpers.h"
100 #include "fmt/core.h"
102 #include "win_main_utf8.h"
104 namespace {
106 /* Filter object functions */
107 auto alGenFilters = LPALGENFILTERS{};
108 auto alDeleteFilters = LPALDELETEFILTERS{};
109 auto alIsFilter = LPALISFILTER{};
110 auto alFilteri = LPALFILTERI{};
111 auto alFilteriv = LPALFILTERIV{};
112 auto alFilterf = LPALFILTERF{};
113 auto alFilterfv = LPALFILTERFV{};
114 auto alGetFilteri = LPALGETFILTERI{};
115 auto alGetFilteriv = LPALGETFILTERIV{};
116 auto alGetFilterf = LPALGETFILTERF{};
117 auto alGetFilterfv = LPALGETFILTERFV{};
119 /* Effect object functions */
120 auto alGenEffects = LPALGENEFFECTS{};
121 auto alDeleteEffects = LPALDELETEEFFECTS{};
122 auto alIsEffect = LPALISEFFECT{};
123 auto alEffecti = LPALEFFECTI{};
124 auto alEffectiv = LPALEFFECTIV{};
125 auto alEffectf = LPALEFFECTF{};
126 auto alEffectfv = LPALEFFECTFV{};
127 auto alGetEffecti = LPALGETEFFECTI{};
128 auto alGetEffectiv = LPALGETEFFECTIV{};
129 auto alGetEffectf = LPALGETEFFECTF{};
130 auto alGetEffectfv = LPALGETEFFECTFV{};
132 /* Auxiliary Effect Slot object functions */
133 auto alGenAuxiliaryEffectSlots = LPALGENAUXILIARYEFFECTSLOTS{};
134 auto alDeleteAuxiliaryEffectSlots = LPALDELETEAUXILIARYEFFECTSLOTS{};
135 auto alIsAuxiliaryEffectSlot = LPALISAUXILIARYEFFECTSLOT{};
136 auto alAuxiliaryEffectSloti = LPALAUXILIARYEFFECTSLOTI{};
137 auto alAuxiliaryEffectSlotiv = LPALAUXILIARYEFFECTSLOTIV{};
138 auto alAuxiliaryEffectSlotf = LPALAUXILIARYEFFECTSLOTF{};
139 auto alAuxiliaryEffectSlotfv = LPALAUXILIARYEFFECTSLOTFV{};
140 auto alGetAuxiliaryEffectSloti = LPALGETAUXILIARYEFFECTSLOTI{};
141 auto alGetAuxiliaryEffectSlotiv = LPALGETAUXILIARYEFFECTSLOTIV{};
142 auto alGetAuxiliaryEffectSlotf = LPALGETAUXILIARYEFFECTSLOTF{};
143 auto alGetAuxiliaryEffectSlotfv = LPALGETAUXILIARYEFFECTSLOTFV{};
146 auto MuteFilterID = ALuint{};
147 auto LowFrequencyEffectID = ALuint{};
148 auto LfeSlotID = ALuint{};
151 namespace fs = std::filesystem;
152 using namespace std::string_view_literals;
154 [[noreturn]]
155 void do_assert(const char *message, int linenum, const char *filename, const char *funcname)
157 auto errstr = fmt::format("{}:{}: {}: {}", filename, linenum, funcname, message);
158 throw std::runtime_error{errstr};
161 #define MyAssert(cond) do { \
162 if(!(cond)) UNLIKELY \
163 do_assert("Assertion '" #cond "' failed", __LINE__, __FILE__, \
164 std::data(__func__)); \
165 } while(0)
168 enum class Quality : std::uint8_t {
169 s8, s16, f32, s24
171 enum class Mode : bool {
172 Channels, Objects
175 auto GetQualityName(Quality quality) noexcept -> std::string_view
177 switch(quality)
179 case Quality::s8: return "8-bit int"sv;
180 case Quality::s16: return "16-bit int"sv;
181 case Quality::f32: return "32-bit float"sv;
182 case Quality::s24: return "24-bit int"sv;
184 return "<unknown>"sv;
187 auto GetModeName(Mode mode) noexcept -> std::string_view
189 switch(mode)
191 case Mode::Channels: return "channels"sv;
192 case Mode::Objects: return "objects"sv;
194 return "<unknown>"sv;
197 auto BytesFromQuality(Quality quality) noexcept -> size_t
199 switch(quality)
201 case Quality::s8: return 1;
202 case Quality::s16: return 2;
203 case Quality::f32: return 4;
204 case Quality::s24: return 3;
206 return 4;
209 auto BufferBytesFromQuality(Quality quality) noexcept -> size_t
211 switch(quality)
213 case Quality::s8: return 1;
214 case Quality::s16: return 2;
215 case Quality::f32: return 4;
216 /* 24-bit samples are converted to 32-bit for OpenAL. */
217 case Quality::s24: return 4;
219 return 4;
223 /* Helper class for reading little-endian samples on big-endian targets, or
224 * convert 24-bit samples.
226 template<Quality Q>
227 struct SampleReader;
229 template<>
230 struct SampleReader<Quality::s8> {
231 using src_t = int8_t;
232 using dst_t = int8_t;
234 [[nodiscard]] static
235 auto read(const src_t &in) noexcept -> dst_t { return in; }
238 template<>
239 struct SampleReader<Quality::s16> {
240 using src_t = int16_t;
241 using dst_t = int16_t;
243 [[nodiscard]] static
244 auto read(const src_t &in) noexcept -> dst_t
246 if constexpr(al::endian::native == al::endian::little)
247 return in;
248 else
249 return al::byteswap(in);
253 template<>
254 struct SampleReader<Quality::f32> {
255 /* 32-bit float samples are read as 32-bit integer on big-endian systems,
256 * so that they can be byteswapped before being reinterpreted as float.
258 using src_t = std::conditional_t<al::endian::native==al::endian::little, float,uint32_t>;
259 using dst_t = float;
261 [[nodiscard]] static
262 auto read(const src_t &in) noexcept -> dst_t
264 if constexpr(al::endian::native == al::endian::little)
265 return in;
266 else
267 return al::bit_cast<dst_t>(al::byteswap(static_cast<uint32_t>(in)));
271 template<>
272 struct SampleReader<Quality::s24> {
273 /* 24-bit samples are converted to 32-bit integer. */
274 using src_t = std::array<uint8_t,3>;
275 using dst_t = int32_t;
277 [[nodiscard]] static
278 auto read(const src_t &in) noexcept -> dst_t
280 return static_cast<int32_t>((uint32_t{in[0]}<<8) | (uint32_t{in[1]}<<16)
281 | (uint32_t{in[2]}<<24));
286 /* Each track with position data consists of a set of 3 samples per 16 audio
287 * channels, resulting in a full set of positions being specified over 48
288 * sample frames.
290 constexpr auto FramesPerPos = 48_uz;
292 struct Channel {
293 ALuint mSource{};
294 std::array<ALuint,2> mBuffers{};
295 float mAzimuth{};
296 float mElevation{};
297 bool mIsLfe{};
299 Channel() = default;
300 Channel(const Channel&) = delete;
301 Channel(Channel&& rhs)
302 : mSource{rhs.mSource}, mBuffers{rhs.mBuffers}, mAzimuth{rhs.mAzimuth}
303 , mElevation{rhs.mElevation}, mIsLfe{rhs.mIsLfe}
305 rhs.mSource = 0;
306 rhs.mBuffers.fill(0);
308 ~Channel()
310 if(mSource) alDeleteSources(1, &mSource);
311 if(mBuffers[0]) alDeleteBuffers(ALsizei(mBuffers.size()), mBuffers.data());
314 auto operator=(const Channel&) -> Channel& = delete;
315 auto operator=(Channel&& rhs) -> Channel&
317 std::swap(mSource, rhs.mSource);
318 std::swap(mBuffers, rhs.mBuffers);
319 std::swap(mAzimuth, rhs.mAzimuth);
320 std::swap(mElevation, rhs.mElevation);
321 std::swap(mIsLfe, rhs.mIsLfe);
322 return *this;
326 struct LafStream {
327 std::filebuf mInFile;
329 Quality mQuality{};
330 Mode mMode{};
331 uint32_t mNumTracks{};
332 uint32_t mSampleRate{};
333 ALenum mALFormat{};
334 uint64_t mSampleCount{};
336 uint64_t mCurrentSample{};
338 std::array<uint8_t,32> mEnabledTracks{};
339 uint32_t mNumEnabled{};
340 std::vector<char> mSampleChunk;
341 al::span<char> mSampleLine;
343 std::vector<Channel> mChannels;
344 std::vector<std::vector<float>> mPosTracks;
346 LafStream() = default;
347 LafStream(const LafStream&) = delete;
348 ~LafStream() = default;
349 auto operator=(const LafStream&) -> LafStream& = delete;
351 [[nodiscard]]
352 auto readChunk() -> uint32_t;
354 void convertSamples(const al::span<char> samples) const;
356 void convertPositions(const al::span<float> dst, const al::span<const char> src) const;
358 template<Quality Q>
359 void copySamples(char *dst, const char *src, size_t idx, size_t count) const;
361 [[nodiscard]]
362 auto prepareTrack(size_t trackidx, size_t count) -> al::span<char>;
364 [[nodiscard]]
365 auto isAtEnd() const noexcept -> bool { return mCurrentSample >= mSampleCount; }
368 auto LafStream::readChunk() -> uint32_t
370 mEnabledTracks.fill(0);
372 mInFile.sgetn(reinterpret_cast<char*>(mEnabledTracks.data()), (mNumTracks+7_z)>>3);
373 mNumEnabled = std::accumulate(mEnabledTracks.cbegin(), mEnabledTracks.cend(), 0u,
374 [](const unsigned int val, const uint8_t in)
375 { return val + unsigned(al::popcount(unsigned(in))); });
377 /* Make sure enable bits aren't set for non-existent tracks. */
378 if(mEnabledTracks[((mNumTracks+7_uz)>>3) - 1] >= (1u<<(mNumTracks&7)))
379 throw std::runtime_error{"Invalid channel enable bits"};
381 /* Each chunk is exactly one second long, with samples interleaved for each
382 * enabled track. The last chunk may be shorter if there isn't enough time
383 * remaining for a full second.
385 const auto numsamples = std::min(uint64_t{mSampleRate}, mSampleCount-mCurrentSample);
387 const auto toread = std::streamsize(numsamples * BytesFromQuality(mQuality) * mNumEnabled);
388 if(mInFile.sgetn(mSampleChunk.data(), toread) != toread)
389 throw std::runtime_error{"Failed to read sample chunk"};
391 std::fill(mSampleChunk.begin()+toread, mSampleChunk.end(), char{});
393 mCurrentSample += numsamples;
394 return static_cast<uint32_t>(numsamples);
397 void LafStream::convertSamples(const al::span<char> samples) const
399 /* OpenAL uses unsigned 8-bit samples (0...255), so signed 8-bit samples
400 * (-128...+127) need conversion. The other formats are fine.
402 if(mQuality == Quality::s8)
403 std::transform(samples.begin(), samples.end(), samples.begin(),
404 [](const char sample) noexcept { return char(sample^0x80); });
407 void LafStream::convertPositions(const al::span<float> dst, const al::span<const char> src) const
409 switch(mQuality)
411 case Quality::s8:
412 std::transform(src.begin(), src.end(), dst.begin(),
413 [](const int8_t in) { return float(in) / 127.0f; });
414 break;
415 case Quality::s16:
417 auto i16src = al::span{reinterpret_cast<const int16_t*>(src.data()),
418 src.size()/sizeof(int16_t)};
419 std::transform(i16src.begin(), i16src.end(), dst.begin(),
420 [](const int16_t in) { return float(in) / 32767.0f; });
422 break;
423 case Quality::f32:
425 auto f32src = al::span{reinterpret_cast<const float*>(src.data()),
426 src.size()/sizeof(float)};
427 std::copy(f32src.begin(), f32src.end(), dst.begin());
429 break;
430 case Quality::s24:
432 /* 24-bit samples are converted to 32-bit in copySamples. */
433 auto i32src = al::span{reinterpret_cast<const int32_t*>(src.data()),
434 src.size()/sizeof(int32_t)};
435 std::transform(i32src.begin(), i32src.end(), dst.begin(),
436 [](const int32_t in) { return float(in>>8) / 8388607.0f; });
438 break;
442 template<Quality Q>
443 void LafStream::copySamples(char *dst, const char *src, const size_t idx, const size_t count) const
445 using reader_t = SampleReader<Q>;
446 using src_t = typename reader_t::src_t;
447 using dst_t = typename reader_t::dst_t;
449 const auto step = size_t{mNumEnabled};
450 assert(idx < step);
452 auto input = al::span{reinterpret_cast<const src_t*>(src), count*step};
453 auto output = al::span{reinterpret_cast<dst_t*>(dst), count};
455 auto inptr = input.begin();
456 std::generate_n(output.begin(), output.size(), [&inptr,idx,step]
458 auto ret = reader_t::read(inptr[idx]);
459 inptr += ptrdiff_t(step);
460 return ret;
464 auto LafStream::prepareTrack(const size_t trackidx, const size_t count) -> al::span<char>
466 const auto todo = std::min(size_t{mSampleRate}, count);
467 if((mEnabledTracks[trackidx>>3] & (1_uz<<(trackidx&7))))
469 /* If the track is enabled, get the real index (skipping disabled
470 * tracks), and deinterlace it into the mono line.
472 const auto idx = [this,trackidx]() -> unsigned int
474 const auto bits = al::span{mEnabledTracks}.first(trackidx>>3);
475 const auto res = std::accumulate(bits.begin(), bits.end(), 0u,
476 [](const unsigned int val, const uint8_t in)
477 { return val + unsigned(al::popcount(unsigned(in))); });
478 return unsigned(al::popcount(mEnabledTracks[trackidx>>3] & ((1u<<(trackidx&7))-1)))
479 + res;
480 }();
482 switch(mQuality)
484 case Quality::s8:
485 copySamples<Quality::s8>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
486 break;
487 case Quality::s16:
488 copySamples<Quality::s16>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
489 break;
490 case Quality::f32:
491 copySamples<Quality::f32>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
492 break;
493 case Quality::s24:
494 copySamples<Quality::s24>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
495 break;
498 else
500 /* If the track is disabled, provide silence. */
501 std::fill_n(mSampleLine.begin(), mSampleLine.size(), char{});
504 return mSampleLine.first(todo * BufferBytesFromQuality(mQuality));
508 auto LoadLAF(const fs::path &fname) -> std::unique_ptr<LafStream>
510 auto laf = std::make_unique<LafStream>();
511 if(!laf->mInFile.open(fname, std::ios_base::binary | std::ios_base::in))
512 throw std::runtime_error{"Could not open file"};
514 auto marker = std::array<char,9>{};
515 if(laf->mInFile.sgetn(marker.data(), marker.size()) != marker.size())
516 throw std::runtime_error{"Failed to read file marker"};
517 if(std::string_view{marker.data(), marker.size()} != "LIMITLESS"sv)
518 throw std::runtime_error{"Not an LAF file"};
520 auto header = std::array<char,10>{};
521 if(laf->mInFile.sgetn(header.data(), header.size()) != header.size())
522 throw std::runtime_error{"Failed to read header"};
523 while(std::string_view{header.data(), 4} != "HEAD"sv)
525 auto headview = std::string_view{header.data(), header.size()};
526 auto hiter = header.begin();
527 if(const auto hpos = std::min(headview.find("HEAD"sv), headview.size());
528 hpos < headview.size())
530 /* Found the HEAD marker. Copy what was read of the header to the
531 * front, fill in the rest of the header, and continue loading.
533 hiter = std::copy(header.begin()+hpos, header.end(), hiter);
535 else if(al::ends_with(headview, "HEA"sv))
537 /* Found what might be the HEAD marker at the end. Copy it to the
538 * front, refill the header, and check again.
540 hiter = std::copy_n(header.end()-3, 3, hiter);
542 else if(al::ends_with(headview, "HE"sv))
543 hiter = std::copy_n(header.end()-2, 2, hiter);
544 else if(headview.back() == 'H')
545 hiter = std::copy_n(header.end()-1, 1, hiter);
547 const auto toread = std::distance(hiter, header.end());
548 if(laf->mInFile.sgetn(al::to_address(hiter), toread) != toread)
549 throw std::runtime_error{"Failed to read header"};
552 laf->mQuality = [stype=int{header[4]}] {
553 if(stype == 0) return Quality::s8;
554 if(stype == 1) return Quality::s16;
555 if(stype == 2) return Quality::f32;
556 if(stype == 3) return Quality::s24;
557 throw std::runtime_error{fmt::format("Invalid quality type: {}", stype)};
558 }();
560 laf->mMode = [mode=int{header[5]}] {
561 if(mode == 0) return Mode::Channels;
562 if(mode == 1) return Mode::Objects;
563 throw std::runtime_error{fmt::format("Invalid mode: {}", mode)};
564 }();
566 laf->mNumTracks = [input=al::span{header}.subspan<6,4>()] {
567 return uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
568 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
569 }();
571 fmt::println("Filename: {}", fname.string());
572 fmt::println(" quality: {}", GetQualityName(laf->mQuality));
573 fmt::println(" mode: {}", GetModeName(laf->mMode));
574 fmt::println(" track count: {}", laf->mNumTracks);
576 if(laf->mNumTracks == 0)
577 throw std::runtime_error{"No tracks"};
578 if(laf->mNumTracks > 256)
579 throw std::runtime_error{fmt::format("Too many tracks: {}", laf->mNumTracks)};
581 auto chandata = std::vector<char>(laf->mNumTracks*9_uz);
582 auto headersize = std::streamsize(chandata.size());
583 if(laf->mInFile.sgetn(chandata.data(), headersize) != headersize)
584 throw std::runtime_error{"Failed to read channel header data"};
586 if(laf->mMode == Mode::Channels)
587 laf->mChannels.reserve(laf->mNumTracks);
588 else
590 if(laf->mNumTracks < 2)
591 throw std::runtime_error{"Not enough tracks"};
593 auto numchans = uint32_t{laf->mNumTracks - 1};
594 auto numpostracks = uint32_t{1};
595 while(numpostracks*16 < numchans)
597 --numchans;
598 ++numpostracks;
600 laf->mChannels.reserve(numchans);
601 laf->mPosTracks.reserve(numpostracks);
604 for(uint32_t i{0};i < laf->mNumTracks;++i)
606 static constexpr auto read_float = [](al::span<char,4> input)
608 const auto value = uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
609 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
610 return al::bit_cast<float>(value);
613 auto chan = al::span{chandata}.subspan(i*9_uz, 9);
614 auto x_axis = read_float(chan.first<4>());
615 auto y_axis = read_float(chan.subspan<4,4>());
616 auto lfe_flag = int{chan[8]};
618 fmt::println("Track {}: E={:f}, A={:f} (LFE: {})", i, x_axis, y_axis, lfe_flag);
620 if(x_axis != x_axis && y_axis == 0.0)
622 MyAssert(laf->mMode == Mode::Objects);
623 MyAssert(i != 0);
624 laf->mPosTracks.emplace_back();
626 else
628 MyAssert(laf->mPosTracks.empty());
629 MyAssert(std::isfinite(x_axis) && std::isfinite(y_axis));
630 auto &channel = laf->mChannels.emplace_back();
631 channel.mAzimuth = y_axis;
632 channel.mElevation = x_axis;
633 channel.mIsLfe = lfe_flag != 0;
636 fmt::println("Channels: {}", laf->mChannels.size());
638 /* For "objects" mode, ensure there's enough tracks with position data to
639 * handle the audio channels.
641 if(laf->mMode == Mode::Objects)
642 MyAssert(((laf->mChannels.size()-1)>>4) == laf->mPosTracks.size()-1);
644 auto footer = std::array<char,12>{};
645 if(laf->mInFile.sgetn(footer.data(), footer.size()) != footer.size())
646 throw std::runtime_error{"Failed to read sample header data"};
648 laf->mSampleRate = [input=al::span{footer}.first<4>()] {
649 return uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
650 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
651 }();
652 laf->mSampleCount = [input=al::span{footer}.last<8>()] {
653 return uint64_t{uint8_t(input[0])} | (uint64_t{uint8_t(input[1])}<<8)
654 | (uint64_t{uint8_t(input[2])}<<16u) | (uint64_t{uint8_t(input[3])}<<24u)
655 | (uint64_t{uint8_t(input[4])}<<32u) | (uint64_t{uint8_t(input[5])}<<40u)
656 | (uint64_t{uint8_t(input[6])}<<48u) | (uint64_t{uint8_t(input[7])}<<56u);
657 }();
658 fmt::println("Sample rate: {}", laf->mSampleRate);
659 fmt::println("Length: {} samples ({:.2f} sec)", laf->mSampleCount,
660 static_cast<double>(laf->mSampleCount)/static_cast<double>(laf->mSampleRate));
662 /* Position vectors get split across the PCM chunks if the sample rate
663 * isn't a multiple of 48. Each PCM chunk is exactly one second (the sample
664 * rate in sample frames). Each track with position data consists of a set
665 * of 3 samples for 16 audio channels, resuling in 48 sample frames for a
666 * full set of positions. Extra logic will be needed to manage the position
667 * frame offset separate from each chunk.
669 MyAssert(laf->mMode == Mode::Channels || (laf->mSampleRate%FramesPerPos) == 0);
671 for(size_t i{0};i < laf->mPosTracks.size();++i)
672 laf->mPosTracks[i].resize(laf->mSampleRate*2_uz, 0.0f);
674 laf->mSampleChunk.resize(laf->mSampleRate*BytesFromQuality(laf->mQuality)*laf->mNumTracks
675 + laf->mSampleRate*BufferBytesFromQuality(laf->mQuality));
676 laf->mSampleLine = al::span{laf->mSampleChunk}.last(laf->mSampleRate
677 * BufferBytesFromQuality(laf->mQuality));
679 return laf;
682 void PlayLAF(std::string_view fname)
683 try {
684 auto laf = LoadLAF(fs::u8path(fname));
686 switch(laf->mQuality)
688 case Quality::s8:
689 laf->mALFormat = AL_FORMAT_MONO8;
690 break;
691 case Quality::s16:
692 laf->mALFormat = AL_FORMAT_MONO16;
693 break;
694 case Quality::f32:
695 if(alIsExtensionPresent("AL_EXT_FLOAT32"))
696 laf->mALFormat = AL_FORMAT_MONO_FLOAT32;
697 break;
698 case Quality::s24:
699 laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO32");
700 if(!laf->mALFormat || laf->mALFormat == -1)
701 laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO_I32");
702 break;
704 if(!laf->mALFormat || laf->mALFormat == -1)
705 throw std::runtime_error{fmt::format("No supported format for {} samples",
706 GetQualityName(laf->mQuality))};
708 static constexpr auto alloc_channel = [](Channel &channel)
710 alGenSources(1, &channel.mSource);
711 alGenBuffers(ALsizei(channel.mBuffers.size()), channel.mBuffers.data());
713 /* Disable distance attenuation, and make sure the source stays locked
714 * relative to the listener.
716 alSourcef(channel.mSource, AL_ROLLOFF_FACTOR, 0.0f);
717 alSourcei(channel.mSource, AL_SOURCE_RELATIVE, AL_TRUE);
719 /* FIXME: Is the Y rotation/azimuth clockwise or counter-clockwise?
720 * Does +azimuth move a front sound right or left?
722 const auto x = std::sin(channel.mAzimuth) * std::cos(channel.mElevation);
723 const auto y = std::sin(channel.mElevation);
724 const auto z = -std::cos(channel.mAzimuth) * std::cos(channel.mElevation);
725 alSource3f(channel.mSource, AL_POSITION, x, y, z);
727 if(channel.mIsLfe)
729 if(LfeSlotID)
731 /* For LFE, silence the direct/dry path and connect the LFE aux
732 * slot on send 0.
734 alSourcei(channel.mSource, AL_DIRECT_FILTER, ALint(MuteFilterID));
735 alSource3i(channel.mSource, AL_AUXILIARY_SEND_FILTER, ALint(LfeSlotID), 0,
736 AL_FILTER_NULL);
738 else
740 /* If AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT isn't available,
741 * silence LFE channels since they may not be appropriate to
742 * play normally.
744 alSourcef(channel.mSource, AL_GAIN, 0.0f);
748 if(auto err=alGetError())
749 throw std::runtime_error{fmt::format("OpenAL error: {}", alGetString(err))};
751 std::for_each(laf->mChannels.begin(), laf->mChannels.end(), alloc_channel);
753 while(!laf->isAtEnd())
755 auto state = ALenum{};
756 auto offset = ALint{};
757 auto processed = ALint{};
758 /* All sources are played in sync, so they'll all be at the same offset
759 * with the same state and number of processed buffers. Query the back
760 * source just in case the previous update ran really late and missed
761 * updating only some sources on time (in which case, the latter ones
762 * will underrun, which this will detect and restart them all as
763 * needed).
765 alGetSourcei(laf->mChannels.back().mSource, AL_BUFFERS_PROCESSED, &processed);
766 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
767 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
769 if(state == AL_PLAYING || state == AL_PAUSED)
771 if(!laf->mPosTracks.empty())
773 alcSuspendContext(alcGetCurrentContext());
774 for(size_t i{0};i < laf->mChannels.size();++i)
776 const auto trackidx = i>>4;
778 const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15);
779 const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0];
780 const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1];
781 const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2];
783 /* Contrary to the docs, the position is left-handed and
784 * needs to be converted to right-handed.
786 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
788 alcProcessContext(alcGetCurrentContext());
791 if(processed > 0)
793 const auto numsamples = laf->readChunk();
794 for(size_t i{0};i < laf->mChannels.size();++i)
796 const auto samples = laf->prepareTrack(i, numsamples);
797 laf->convertSamples(samples);
799 auto bufid = ALuint{};
800 alSourceUnqueueBuffers(laf->mChannels[i].mSource, 1, &bufid);
801 alBufferData(bufid, laf->mALFormat, samples.data(), ALsizei(samples.size()),
802 ALsizei(laf->mSampleRate));
803 alSourceQueueBuffers(laf->mChannels[i].mSource, 1, &bufid);
805 for(size_t i{0};i < laf->mPosTracks.size();++i)
807 std::copy(laf->mPosTracks[i].begin() + ptrdiff_t(laf->mSampleRate),
808 laf->mPosTracks[i].end(), laf->mPosTracks[i].begin());
810 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
811 laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate),
812 positions);
815 else
816 std::this_thread::sleep_for(std::chrono::milliseconds{10});
818 else if(state == AL_STOPPED)
820 auto sources = std::array<ALuint,256>{};
821 for(size_t i{0};i < laf->mChannels.size();++i)
822 sources[i] = laf->mChannels[i].mSource;
823 alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data());
825 else if(state == AL_INITIAL)
827 auto sources = std::array<ALuint,256>{};
828 auto numsamples = laf->readChunk();
829 for(size_t i{0};i < laf->mChannels.size();++i)
831 const auto samples = laf->prepareTrack(i, numsamples);
832 laf->convertSamples(samples);
833 alBufferData(laf->mChannels[i].mBuffers[0], laf->mALFormat, samples.data(),
834 ALsizei(samples.size()), ALsizei(laf->mSampleRate));
836 for(size_t i{0};i < laf->mPosTracks.size();++i)
838 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
839 laf->convertPositions(al::span{laf->mPosTracks[i]}.first(laf->mSampleRate),
840 positions);
843 numsamples = laf->readChunk();
844 for(size_t i{0};i < laf->mChannels.size();++i)
846 const auto samples = laf->prepareTrack(i, numsamples);
847 laf->convertSamples(samples);
848 alBufferData(laf->mChannels[i].mBuffers[1], laf->mALFormat, samples.data(),
849 ALsizei(samples.size()), ALsizei(laf->mSampleRate));
850 alSourceQueueBuffers(laf->mChannels[i].mSource,
851 ALsizei(laf->mChannels[i].mBuffers.size()), laf->mChannels[i].mBuffers.data());
852 sources[i] = laf->mChannels[i].mSource;
854 for(size_t i{0};i < laf->mPosTracks.size();++i)
856 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
857 laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate),
858 positions);
861 if(!laf->mPosTracks.empty())
863 for(size_t i{0};i < laf->mChannels.size();++i)
865 const auto trackidx = i>>4;
867 const auto x = laf->mPosTracks[trackidx][(i&15)*3 + 0];
868 const auto y = laf->mPosTracks[trackidx][(i&15)*3 + 1];
869 const auto z = laf->mPosTracks[trackidx][(i&15)*3 + 2];
871 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
875 alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data());
877 else
878 break;
881 auto state = ALenum{};
882 auto offset = ALint{};
883 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
884 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
885 while(alGetError() == AL_NO_ERROR && state == AL_PLAYING)
887 if(!laf->mPosTracks.empty())
889 alcSuspendContext(alcGetCurrentContext());
890 for(size_t i{0};i < laf->mChannels.size();++i)
892 const auto trackidx = i>>4;
894 const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15);
895 const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0];
896 const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1];
897 const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2];
899 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
901 alcProcessContext(alcGetCurrentContext());
903 std::this_thread::sleep_for(std::chrono::milliseconds{10});
904 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
905 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
908 catch(std::exception& e) {
909 fmt::println(stderr, "Error playing {}:\n {}", fname, e.what());
912 auto main(al::span<std::string_view> args) -> int
914 /* Print out usage if no arguments were specified */
915 if(args.size() < 2)
917 fmt::println(stderr, "Usage: {} [-device <name>] <filenames...>\n", args[0]);
918 return 1;
920 args = args.subspan(1);
922 if(InitAL(args) != 0)
923 throw std::runtime_error{"Failed to initialize OpenAL"};
924 /* A simple RAII container for automating OpenAL shutdown. */
925 struct AudioManager {
926 AudioManager() = default;
927 AudioManager(const AudioManager&) = delete;
928 auto operator=(const AudioManager&) -> AudioManager& = delete;
929 ~AudioManager()
931 if(LfeSlotID)
933 alDeleteAuxiliaryEffectSlots(1, &LfeSlotID);
934 alDeleteEffects(1, &LowFrequencyEffectID);
935 alDeleteFilters(1, &MuteFilterID);
937 CloseAL();
940 AudioManager almgr;
942 if(auto *device = alcGetContextsDevice(alcGetCurrentContext());
943 alcIsExtensionPresent(device, "ALC_EXT_EFX")
944 && alcIsExtensionPresent(device, "ALC_EXT_DEDICATED"))
946 #define LOAD_PROC(x) do { \
947 x = reinterpret_cast<decltype(x)>(alGetProcAddress(#x)); \
948 if(!x) fmt::println(stderr, "Failed to find function '{}'\n", #x##sv);\
949 } while(0)
950 LOAD_PROC(alGenFilters);
951 LOAD_PROC(alDeleteFilters);
952 LOAD_PROC(alIsFilter);
953 LOAD_PROC(alFilterf);
954 LOAD_PROC(alFilterfv);
955 LOAD_PROC(alFilteri);
956 LOAD_PROC(alFilteriv);
957 LOAD_PROC(alGetFilterf);
958 LOAD_PROC(alGetFilterfv);
959 LOAD_PROC(alGetFilteri);
960 LOAD_PROC(alGetFilteriv);
961 LOAD_PROC(alGenEffects);
962 LOAD_PROC(alDeleteEffects);
963 LOAD_PROC(alIsEffect);
964 LOAD_PROC(alEffectf);
965 LOAD_PROC(alEffectfv);
966 LOAD_PROC(alEffecti);
967 LOAD_PROC(alEffectiv);
968 LOAD_PROC(alGetEffectf);
969 LOAD_PROC(alGetEffectfv);
970 LOAD_PROC(alGetEffecti);
971 LOAD_PROC(alGetEffectiv);
972 LOAD_PROC(alGenAuxiliaryEffectSlots);
973 LOAD_PROC(alDeleteAuxiliaryEffectSlots);
974 LOAD_PROC(alIsAuxiliaryEffectSlot);
975 LOAD_PROC(alAuxiliaryEffectSlotf);
976 LOAD_PROC(alAuxiliaryEffectSlotfv);
977 LOAD_PROC(alAuxiliaryEffectSloti);
978 LOAD_PROC(alAuxiliaryEffectSlotiv);
979 LOAD_PROC(alGetAuxiliaryEffectSlotf);
980 LOAD_PROC(alGetAuxiliaryEffectSlotfv);
981 LOAD_PROC(alGetAuxiliaryEffectSloti);
982 LOAD_PROC(alGetAuxiliaryEffectSlotiv);
983 #undef LOAD_PROC
985 alGenFilters(1, &MuteFilterID);
986 alFilteri(MuteFilterID, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
987 alFilterf(MuteFilterID, AL_LOWPASS_GAIN, 0.0f);
988 MyAssert(alGetError() == AL_NO_ERROR);
990 alGenEffects(1, &LowFrequencyEffectID);
991 alEffecti(LowFrequencyEffectID, AL_EFFECT_TYPE, AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT);
992 MyAssert(alGetError() == AL_NO_ERROR);
994 alGenAuxiliaryEffectSlots(1, &LfeSlotID);
995 alAuxiliaryEffectSloti(LfeSlotID, AL_EFFECTSLOT_EFFECT, ALint(LowFrequencyEffectID));
996 MyAssert(alGetError() == AL_NO_ERROR);
999 std::for_each(args.begin(), args.end(), PlayLAF);
1001 return 0;
1004 } // namespace
1006 int main(int argc, char **argv)
1008 MyAssert(argc >= 0);
1009 auto args = std::vector<std::string_view>(static_cast<unsigned int>(argc));
1010 std::copy_n(argv, args.size(), args.begin());
1011 return main(al::span{args});