Use fmt instead of to_string and concating strings
[openal-soft.git] / examples / allafplay.cpp
bloba4b9f6d5743e2d7e4e2741c2f4b58f105bc645dd
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::ifstream 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);
371 mInFile.read(reinterpret_cast<char*>(mEnabledTracks.data()), (mNumTracks+7_z)>>3);
372 mNumEnabled = std::accumulate(mEnabledTracks.cbegin(), mEnabledTracks.cend(), 0u,
373 [](const unsigned int val, const uint8_t in)
374 { return val + unsigned(al::popcount(unsigned(in))); });
376 /* Make sure enable bits aren't set for non-existent tracks. */
377 if(mEnabledTracks[((mNumTracks+7_uz)>>3) - 1] >= (1u<<(mNumTracks&7)))
378 throw std::runtime_error{"Invalid channel enable bits"};
380 /* Each chunk is exactly one second long, with samples interleaved for each
381 * enabled track. The last chunk may be shorter if there isn't enough time
382 * remaining for a full second.
384 const auto numsamples = std::min(uint64_t{mSampleRate}, mSampleCount-mCurrentSample);
386 const auto toread = std::streamsize(numsamples * BytesFromQuality(mQuality) * mNumEnabled);
387 mInFile.read(mSampleChunk.data(), toread);
388 if(mInFile.gcount() != 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 laf->mInFile.open(fname, std::ios_base::binary);
513 auto marker = std::array<char,9>{};
514 laf->mInFile.read(marker.data(), marker.size());
515 if(laf->mInFile.gcount() != 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 laf->mInFile.read(header.data(), header.size());
522 if(laf->mInFile.gcount() != header.size())
523 throw std::runtime_error{"Failed to read header"};
524 while(std::string_view{header.data(), 4} != "HEAD"sv)
526 auto headview = std::string_view{header.data(), header.size()};
527 auto hiter = header.begin();
528 if(const auto hpos = std::min(headview.find("HEAD"sv), headview.size());
529 hpos < headview.size())
531 /* Found the HEAD marker. Copy what was read of the header to the
532 * front, fill in the rest of the header, and continue loading.
534 hiter = std::copy(header.begin()+hpos, header.end(), hiter);
536 else if(al::ends_with(headview, "HEA"sv))
538 /* Found what might be the HEAD marker at the end. Copy it to the
539 * front, refill the header, and check again.
541 hiter = std::copy_n(header.end()-3, 3, hiter);
543 else if(al::ends_with(headview, "HE"sv))
544 hiter = std::copy_n(header.end()-2, 2, hiter);
545 else if(headview.back() == 'H')
546 hiter = std::copy_n(header.end()-1, 1, hiter);
548 const auto toread = std::distance(hiter, header.end());
549 laf->mInFile.read(al::to_address(hiter), toread);
550 if(laf->mInFile.gcount() != toread)
551 throw std::runtime_error{"Failed to read header"};
554 laf->mQuality = [stype=int{header[4]}] {
555 if(stype == 0) return Quality::s8;
556 if(stype == 1) return Quality::s16;
557 if(stype == 2) return Quality::f32;
558 if(stype == 3) return Quality::s24;
559 throw std::runtime_error{fmt::format("Invalid quality type: {}", stype)};
560 }();
562 laf->mMode = [mode=int{header[5]}] {
563 if(mode == 0) return Mode::Channels;
564 if(mode == 1) return Mode::Objects;
565 throw std::runtime_error{fmt::format("Invalid mode: {}", mode)};
566 }();
568 laf->mNumTracks = [input=al::span{header}.subspan<6,4>()] {
569 return uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
570 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
571 }();
573 fmt::println("Filename: {}", fname.string());
574 fmt::println(" quality: {}", GetQualityName(laf->mQuality));
575 fmt::println(" mode: {}", GetModeName(laf->mMode));
576 fmt::println(" track count: {}", laf->mNumTracks);
578 if(laf->mNumTracks == 0)
579 throw std::runtime_error{"No tracks"};
580 if(laf->mNumTracks > 256)
581 throw std::runtime_error{fmt::format("Too many tracks: {}", laf->mNumTracks)};
583 auto chandata = std::vector<char>(laf->mNumTracks*9_uz);
584 laf->mInFile.read(chandata.data(), std::streamsize(chandata.size()));
585 if(laf->mInFile.gcount() != std::streamsize(chandata.size()))
586 throw std::runtime_error{"Failed to read channel header data"};
588 if(laf->mMode == Mode::Channels)
589 laf->mChannels.reserve(laf->mNumTracks);
590 else
592 if(laf->mNumTracks < 2)
593 throw std::runtime_error{"Not enough tracks"};
595 auto numchans = uint32_t{laf->mNumTracks - 1};
596 auto numpostracks = uint32_t{1};
597 while(numpostracks*16 < numchans)
599 --numchans;
600 ++numpostracks;
602 laf->mChannels.reserve(numchans);
603 laf->mPosTracks.reserve(numpostracks);
606 for(uint32_t i{0};i < laf->mNumTracks;++i)
608 static constexpr auto read_float = [](al::span<char,4> input)
610 const auto value = uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
611 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
612 return al::bit_cast<float>(value);
615 auto chan = al::span{chandata}.subspan(i*9_uz, 9);
616 auto x_axis = read_float(chan.first<4>());
617 auto y_axis = read_float(chan.subspan<4,4>());
618 auto lfe_flag = int{chan[8]};
620 fmt::println("Track {}: E={:f}, A={:f} (LFE: {})", i, x_axis, y_axis, lfe_flag);
622 if(x_axis != x_axis && y_axis == 0.0)
624 MyAssert(laf->mMode == Mode::Objects);
625 MyAssert(i != 0);
626 laf->mPosTracks.emplace_back();
628 else
630 MyAssert(laf->mPosTracks.empty());
631 MyAssert(std::isfinite(x_axis) && std::isfinite(y_axis));
632 auto &channel = laf->mChannels.emplace_back();
633 channel.mAzimuth = y_axis;
634 channel.mElevation = x_axis;
635 channel.mIsLfe = lfe_flag != 0;
638 fmt::println("Channels: {}", laf->mChannels.size());
640 /* For "objects" mode, ensure there's enough tracks with position data to
641 * handle the audio channels.
643 if(laf->mMode == Mode::Objects)
644 MyAssert(((laf->mChannels.size()-1)>>4) == laf->mPosTracks.size()-1);
646 auto footer = std::array<char,12>{};
647 laf->mInFile.read(footer.data(), footer.size());
648 if(laf->mInFile.gcount() != footer.size())
649 throw std::runtime_error{"Failed to read sample header data"};
651 laf->mSampleRate = [input=al::span{footer}.first<4>()] {
652 return uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
653 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
654 }();
655 laf->mSampleCount = [input=al::span{footer}.last<8>()] {
656 return uint64_t{uint8_t(input[0])} | (uint64_t{uint8_t(input[1])}<<8)
657 | (uint64_t{uint8_t(input[2])}<<16u) | (uint64_t{uint8_t(input[3])}<<24u)
658 | (uint64_t{uint8_t(input[4])}<<32u) | (uint64_t{uint8_t(input[5])}<<40u)
659 | (uint64_t{uint8_t(input[6])}<<48u) | (uint64_t{uint8_t(input[7])}<<56u);
660 }();
661 fmt::println("Sample rate: {}", laf->mSampleRate);
662 fmt::println("Length: {} samples ({:.2f} sec)", laf->mSampleCount,
663 static_cast<double>(laf->mSampleCount)/static_cast<double>(laf->mSampleRate));
665 /* Position vectors get split across the PCM chunks if the sample rate
666 * isn't a multiple of 48. Each PCM chunk is exactly one second (the sample
667 * rate in sample frames). Each track with position data consists of a set
668 * of 3 samples for 16 audio channels, resuling in 48 sample frames for a
669 * full set of positions. Extra logic will be needed to manage the position
670 * frame offset separate from each chunk.
672 MyAssert(laf->mMode == Mode::Channels || (laf->mSampleRate%FramesPerPos) == 0);
674 for(size_t i{0};i < laf->mPosTracks.size();++i)
675 laf->mPosTracks[i].resize(laf->mSampleRate*2_uz, 0.0f);
677 laf->mSampleChunk.resize(laf->mSampleRate*BytesFromQuality(laf->mQuality)*laf->mNumTracks
678 + laf->mSampleRate*BufferBytesFromQuality(laf->mQuality));
679 laf->mSampleLine = al::span{laf->mSampleChunk}.last(laf->mSampleRate
680 * BufferBytesFromQuality(laf->mQuality));
682 return laf;
685 void PlayLAF(std::string_view fname)
686 try {
687 auto laf = LoadLAF(fs::u8path(fname));
689 switch(laf->mQuality)
691 case Quality::s8:
692 laf->mALFormat = AL_FORMAT_MONO8;
693 break;
694 case Quality::s16:
695 laf->mALFormat = AL_FORMAT_MONO16;
696 break;
697 case Quality::f32:
698 if(alIsExtensionPresent("AL_EXT_FLOAT32"))
699 laf->mALFormat = AL_FORMAT_MONO_FLOAT32;
700 break;
701 case Quality::s24:
702 laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO32");
703 if(!laf->mALFormat || laf->mALFormat == -1)
704 laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO_I32");
705 break;
707 if(!laf->mALFormat || laf->mALFormat == -1)
708 throw std::runtime_error{fmt::format("No supported format for {} samples",
709 GetQualityName(laf->mQuality))};
711 static constexpr auto alloc_channel = [](Channel &channel)
713 alGenSources(1, &channel.mSource);
714 alGenBuffers(ALsizei(channel.mBuffers.size()), channel.mBuffers.data());
716 /* Disable distance attenuation, and make sure the source stays locked
717 * relative to the listener.
719 alSourcef(channel.mSource, AL_ROLLOFF_FACTOR, 0.0f);
720 alSourcei(channel.mSource, AL_SOURCE_RELATIVE, AL_TRUE);
722 /* FIXME: Is the Y rotation/azimuth clockwise or counter-clockwise?
723 * Does +azimuth move a front sound right or left?
725 const auto x = std::sin(channel.mAzimuth) * std::cos(channel.mElevation);
726 const auto y = std::sin(channel.mElevation);
727 const auto z = -std::cos(channel.mAzimuth) * std::cos(channel.mElevation);
728 alSource3f(channel.mSource, AL_POSITION, x, y, z);
730 if(channel.mIsLfe)
732 if(LfeSlotID)
734 /* For LFE, silence the direct/dry path and connect the LFE aux
735 * slot on send 0.
737 alSourcei(channel.mSource, AL_DIRECT_FILTER, ALint(MuteFilterID));
738 alSource3i(channel.mSource, AL_AUXILIARY_SEND_FILTER, ALint(LfeSlotID), 0,
739 AL_FILTER_NULL);
741 else
743 /* If AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT isn't available,
744 * silence LFE channels since they may not be appropriate to
745 * play normally.
747 alSourcef(channel.mSource, AL_GAIN, 0.0f);
751 if(auto err=alGetError())
752 throw std::runtime_error{fmt::format("OpenAL error: {}", alGetString(err))};
754 std::for_each(laf->mChannels.begin(), laf->mChannels.end(), alloc_channel);
756 while(!laf->isAtEnd())
758 auto state = ALenum{};
759 auto offset = ALint{};
760 auto processed = ALint{};
761 /* All sources are played in sync, so they'll all be at the same offset
762 * with the same state and number of processed buffers. Query the back
763 * source just in case the previous update ran really late and missed
764 * updating only some sources on time (in which case, the latter ones
765 * will underrun, which this will detect and restart them all as
766 * needed).
768 alGetSourcei(laf->mChannels.back().mSource, AL_BUFFERS_PROCESSED, &processed);
769 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
770 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
772 if(state == AL_PLAYING || state == AL_PAUSED)
774 if(!laf->mPosTracks.empty())
776 alcSuspendContext(alcGetCurrentContext());
777 for(size_t i{0};i < laf->mChannels.size();++i)
779 const auto trackidx = i>>4;
781 const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15);
782 const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0];
783 const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1];
784 const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2];
786 /* Contrary to the docs, the position is left-handed and
787 * needs to be converted to right-handed.
789 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
791 alcProcessContext(alcGetCurrentContext());
794 if(processed > 0)
796 const auto numsamples = laf->readChunk();
797 for(size_t i{0};i < laf->mChannels.size();++i)
799 const auto samples = laf->prepareTrack(i, numsamples);
800 laf->convertSamples(samples);
802 auto bufid = ALuint{};
803 alSourceUnqueueBuffers(laf->mChannels[i].mSource, 1, &bufid);
804 alBufferData(bufid, laf->mALFormat, samples.data(), ALsizei(samples.size()),
805 ALsizei(laf->mSampleRate));
806 alSourceQueueBuffers(laf->mChannels[i].mSource, 1, &bufid);
808 for(size_t i{0};i < laf->mPosTracks.size();++i)
810 std::copy(laf->mPosTracks[i].begin() + ptrdiff_t(laf->mSampleRate),
811 laf->mPosTracks[i].end(), laf->mPosTracks[i].begin());
813 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
814 laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate),
815 positions);
818 else
819 std::this_thread::sleep_for(std::chrono::milliseconds{10});
821 else if(state == AL_STOPPED)
823 auto sources = std::array<ALuint,256>{};
824 for(size_t i{0};i < laf->mChannels.size();++i)
825 sources[i] = laf->mChannels[i].mSource;
826 alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data());
828 else if(state == AL_INITIAL)
830 auto sources = std::array<ALuint,256>{};
831 auto numsamples = laf->readChunk();
832 for(size_t i{0};i < laf->mChannels.size();++i)
834 const auto samples = laf->prepareTrack(i, numsamples);
835 laf->convertSamples(samples);
836 alBufferData(laf->mChannels[i].mBuffers[0], laf->mALFormat, samples.data(),
837 ALsizei(samples.size()), ALsizei(laf->mSampleRate));
839 for(size_t i{0};i < laf->mPosTracks.size();++i)
841 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
842 laf->convertPositions(al::span{laf->mPosTracks[i]}.first(laf->mSampleRate),
843 positions);
846 numsamples = laf->readChunk();
847 for(size_t i{0};i < laf->mChannels.size();++i)
849 const auto samples = laf->prepareTrack(i, numsamples);
850 laf->convertSamples(samples);
851 alBufferData(laf->mChannels[i].mBuffers[1], laf->mALFormat, samples.data(),
852 ALsizei(samples.size()), ALsizei(laf->mSampleRate));
853 alSourceQueueBuffers(laf->mChannels[i].mSource,
854 ALsizei(laf->mChannels[i].mBuffers.size()), laf->mChannels[i].mBuffers.data());
855 sources[i] = laf->mChannels[i].mSource;
857 for(size_t i{0};i < laf->mPosTracks.size();++i)
859 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
860 laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate),
861 positions);
864 if(!laf->mPosTracks.empty())
866 for(size_t i{0};i < laf->mChannels.size();++i)
868 const auto trackidx = i>>4;
870 const auto x = laf->mPosTracks[trackidx][(i&15)*3 + 0];
871 const auto y = laf->mPosTracks[trackidx][(i&15)*3 + 1];
872 const auto z = laf->mPosTracks[trackidx][(i&15)*3 + 2];
874 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
878 alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data());
880 else
881 break;
884 auto state = ALenum{};
885 auto offset = ALint{};
886 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
887 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
888 while(alGetError() == AL_NO_ERROR && state == AL_PLAYING)
890 if(!laf->mPosTracks.empty())
892 alcSuspendContext(alcGetCurrentContext());
893 for(size_t i{0};i < laf->mChannels.size();++i)
895 const auto trackidx = i>>4;
897 const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15);
898 const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0];
899 const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1];
900 const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2];
902 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
904 alcProcessContext(alcGetCurrentContext());
906 std::this_thread::sleep_for(std::chrono::milliseconds{10});
907 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
908 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
911 catch(std::exception& e) {
912 fmt::println(stderr, "Error playing {}:\n {}", fname, e.what());
915 auto main(al::span<std::string_view> args) -> int
917 /* Print out usage if no arguments were specified */
918 if(args.size() < 2)
920 fmt::println(stderr, "Usage: {} [-device <name>] <filenames...>\n", args[0]);
921 return 1;
923 args = args.subspan(1);
925 if(InitAL(args) != 0)
926 throw std::runtime_error{"Failed to initialize OpenAL"};
927 /* A simple RAII container for automating OpenAL shutdown. */
928 struct AudioManager {
929 AudioManager() = default;
930 AudioManager(const AudioManager&) = delete;
931 auto operator=(const AudioManager&) -> AudioManager& = delete;
932 ~AudioManager()
934 if(LfeSlotID)
936 alDeleteAuxiliaryEffectSlots(1, &LfeSlotID);
937 alDeleteEffects(1, &LowFrequencyEffectID);
938 alDeleteFilters(1, &MuteFilterID);
940 CloseAL();
943 AudioManager almgr;
945 if(auto *device = alcGetContextsDevice(alcGetCurrentContext());
946 alcIsExtensionPresent(device, "ALC_EXT_EFX")
947 && alcIsExtensionPresent(device, "ALC_EXT_DEDICATED"))
949 #define LOAD_PROC(x) do { \
950 x = reinterpret_cast<decltype(x)>(alGetProcAddress(#x)); \
951 if(!x) fmt::println(stderr, "Failed to find function '{}'\n", #x##sv);\
952 } while(0)
953 LOAD_PROC(alGenFilters);
954 LOAD_PROC(alDeleteFilters);
955 LOAD_PROC(alIsFilter);
956 LOAD_PROC(alFilterf);
957 LOAD_PROC(alFilterfv);
958 LOAD_PROC(alFilteri);
959 LOAD_PROC(alFilteriv);
960 LOAD_PROC(alGetFilterf);
961 LOAD_PROC(alGetFilterfv);
962 LOAD_PROC(alGetFilteri);
963 LOAD_PROC(alGetFilteriv);
964 LOAD_PROC(alGenEffects);
965 LOAD_PROC(alDeleteEffects);
966 LOAD_PROC(alIsEffect);
967 LOAD_PROC(alEffectf);
968 LOAD_PROC(alEffectfv);
969 LOAD_PROC(alEffecti);
970 LOAD_PROC(alEffectiv);
971 LOAD_PROC(alGetEffectf);
972 LOAD_PROC(alGetEffectfv);
973 LOAD_PROC(alGetEffecti);
974 LOAD_PROC(alGetEffectiv);
975 LOAD_PROC(alGenAuxiliaryEffectSlots);
976 LOAD_PROC(alDeleteAuxiliaryEffectSlots);
977 LOAD_PROC(alIsAuxiliaryEffectSlot);
978 LOAD_PROC(alAuxiliaryEffectSlotf);
979 LOAD_PROC(alAuxiliaryEffectSlotfv);
980 LOAD_PROC(alAuxiliaryEffectSloti);
981 LOAD_PROC(alAuxiliaryEffectSlotiv);
982 LOAD_PROC(alGetAuxiliaryEffectSlotf);
983 LOAD_PROC(alGetAuxiliaryEffectSlotfv);
984 LOAD_PROC(alGetAuxiliaryEffectSloti);
985 LOAD_PROC(alGetAuxiliaryEffectSlotiv);
986 #undef LOAD_PROC
988 alGenFilters(1, &MuteFilterID);
989 alFilteri(MuteFilterID, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
990 alFilterf(MuteFilterID, AL_LOWPASS_GAIN, 0.0f);
991 MyAssert(alGetError() == AL_NO_ERROR);
993 alGenEffects(1, &LowFrequencyEffectID);
994 alEffecti(LowFrequencyEffectID, AL_EFFECT_TYPE, AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT);
995 MyAssert(alGetError() == AL_NO_ERROR);
997 alGenAuxiliaryEffectSlots(1, &LfeSlotID);
998 alAuxiliaryEffectSloti(LfeSlotID, AL_EFFECTSLOT_EFFECT, ALint(LowFrequencyEffectID));
999 MyAssert(alGetError() == AL_NO_ERROR);
1002 std::for_each(args.begin(), args.end(), PlayLAF);
1004 return 0;
1007 } // namespace
1009 int main(int argc, char **argv)
1011 MyAssert(argc >= 0);
1012 auto args = std::vector<std::string_view>(static_cast<unsigned int>(argc));
1013 std::copy_n(argv, args.size(), args.begin());
1014 return main(al::span{args});