Include fmt 11.0.2
[openal-soft.git] / examples / allafplay.cpp
blobe28c7374b9dd59b8d2346f15647aeefd9193a995
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 <iostream>
83 #include <memory>
84 #include <numeric>
85 #include <string>
86 #include <string_view>
87 #include <thread>
88 #include <type_traits>
89 #include <vector>
91 #include "AL/alc.h"
92 #include "AL/al.h"
93 #include "AL/alext.h"
95 #include "albit.h"
96 #include "almalloc.h"
97 #include "alnumeric.h"
98 #include "alspan.h"
99 #include "alstring.h"
100 #include "common/alhelpers.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 = std::string{filename};
158 errstr += ':';
159 errstr += std::to_string(linenum);
160 errstr += ": ";
161 errstr += funcname;
162 errstr += ": ";
163 errstr += message;
164 throw std::runtime_error{errstr};
167 #define MyAssert(cond) do { \
168 if(!(cond)) UNLIKELY \
169 do_assert("Assertion '" #cond "' failed", __LINE__, __FILE__, \
170 std::data(__func__)); \
171 } while(0)
174 enum class Quality : std::uint8_t {
175 s8, s16, f32, s24
177 enum class Mode : bool {
178 Channels, Objects
181 auto GetQualityName(Quality quality) noexcept -> std::string_view
183 switch(quality)
185 case Quality::s8: return "8-bit int"sv;
186 case Quality::s16: return "16-bit int"sv;
187 case Quality::f32: return "32-bit float"sv;
188 case Quality::s24: return "24-bit int"sv;
190 return "<unknown>"sv;
193 auto GetModeName(Mode mode) noexcept -> std::string_view
195 switch(mode)
197 case Mode::Channels: return "channels"sv;
198 case Mode::Objects: return "objects"sv;
200 return "<unknown>"sv;
203 auto BytesFromQuality(Quality quality) noexcept -> size_t
205 switch(quality)
207 case Quality::s8: return 1;
208 case Quality::s16: return 2;
209 case Quality::f32: return 4;
210 case Quality::s24: return 3;
212 return 4;
215 auto BufferBytesFromQuality(Quality quality) noexcept -> size_t
217 switch(quality)
219 case Quality::s8: return 1;
220 case Quality::s16: return 2;
221 case Quality::f32: return 4;
222 /* 24-bit samples are converted to 32-bit for OpenAL. */
223 case Quality::s24: return 4;
225 return 4;
229 /* Helper class for reading little-endian samples on big-endian targets, or
230 * convert 24-bit samples.
232 template<Quality Q>
233 struct SampleReader;
235 template<>
236 struct SampleReader<Quality::s8> {
237 using src_t = int8_t;
238 using dst_t = int8_t;
240 [[nodiscard]] static
241 auto read(const src_t &in) noexcept -> dst_t { return in; }
244 template<>
245 struct SampleReader<Quality::s16> {
246 using src_t = int16_t;
247 using dst_t = int16_t;
249 [[nodiscard]] static
250 auto read(const src_t &in) noexcept -> dst_t
252 if constexpr(al::endian::native == al::endian::little)
253 return in;
254 else
255 return al::byteswap(in);
259 template<>
260 struct SampleReader<Quality::f32> {
261 /* 32-bit float samples are read as 32-bit integer on big-endian systems,
262 * so that they can be byteswapped before being reinterpreted as float.
264 using src_t = std::conditional_t<al::endian::native==al::endian::little, float,uint32_t>;
265 using dst_t = float;
267 [[nodiscard]] static
268 auto read(const src_t &in) noexcept -> dst_t
270 if constexpr(al::endian::native == al::endian::little)
271 return in;
272 else
273 return al::bit_cast<dst_t>(al::byteswap(static_cast<uint32_t>(in)));
277 template<>
278 struct SampleReader<Quality::s24> {
279 /* 24-bit samples are converted to 32-bit integer. */
280 using src_t = std::array<uint8_t,3>;
281 using dst_t = int32_t;
283 [[nodiscard]] static
284 auto read(const src_t &in) noexcept -> dst_t
286 return static_cast<int32_t>((uint32_t{in[0]}<<8) | (uint32_t{in[1]}<<16)
287 | (uint32_t{in[2]}<<24));
292 /* Each track with position data consists of a set of 3 samples per 16 audio
293 * channels, resulting in a full set of positions being specified over 48
294 * sample frames.
296 constexpr auto FramesPerPos = 48_uz;
298 struct Channel {
299 ALuint mSource{};
300 std::array<ALuint,2> mBuffers{};
301 float mAzimuth{};
302 float mElevation{};
303 bool mIsLfe{};
305 Channel() = default;
306 Channel(const Channel&) = delete;
307 Channel(Channel&& rhs)
308 : mSource{rhs.mSource}, mBuffers{rhs.mBuffers}, mAzimuth{rhs.mAzimuth}
309 , mElevation{rhs.mElevation}, mIsLfe{rhs.mIsLfe}
311 rhs.mSource = 0;
312 rhs.mBuffers.fill(0);
314 ~Channel()
316 if(mSource) alDeleteSources(1, &mSource);
317 if(mBuffers[0]) alDeleteBuffers(ALsizei(mBuffers.size()), mBuffers.data());
320 auto operator=(const Channel&) -> Channel& = delete;
321 auto operator=(Channel&& rhs) -> Channel&
323 std::swap(mSource, rhs.mSource);
324 std::swap(mBuffers, rhs.mBuffers);
325 std::swap(mAzimuth, rhs.mAzimuth);
326 std::swap(mElevation, rhs.mElevation);
327 std::swap(mIsLfe, rhs.mIsLfe);
328 return *this;
332 struct LafStream {
333 std::ifstream mInFile;
335 Quality mQuality{};
336 Mode mMode{};
337 uint32_t mNumTracks{};
338 uint32_t mSampleRate{};
339 ALenum mALFormat{};
340 uint64_t mSampleCount{};
342 uint64_t mCurrentSample{};
344 std::array<uint8_t,32> mEnabledTracks{};
345 uint32_t mNumEnabled{};
346 std::vector<char> mSampleChunk;
347 al::span<char> mSampleLine;
349 std::vector<Channel> mChannels;
350 std::vector<std::vector<float>> mPosTracks;
352 LafStream() = default;
353 LafStream(const LafStream&) = delete;
354 ~LafStream() = default;
355 auto operator=(const LafStream&) -> LafStream& = delete;
357 [[nodiscard]]
358 auto readChunk() -> uint32_t;
360 void convertSamples(const al::span<char> samples) const;
362 void convertPositions(const al::span<float> dst, const al::span<const char> src) const;
364 template<Quality Q>
365 void copySamples(char *dst, const char *src, size_t idx, size_t count) const;
367 [[nodiscard]]
368 auto prepareTrack(size_t trackidx, size_t count) -> al::span<char>;
370 [[nodiscard]]
371 auto isAtEnd() const noexcept -> bool { return mCurrentSample >= mSampleCount; }
374 auto LafStream::readChunk() -> uint32_t
376 mEnabledTracks.fill(0);
377 mInFile.read(reinterpret_cast<char*>(mEnabledTracks.data()), (mNumTracks+7_z)>>3);
378 mNumEnabled = std::accumulate(mEnabledTracks.cbegin(), mEnabledTracks.cend(), 0u,
379 [](const unsigned int val, const uint8_t in)
380 { return val + unsigned(al::popcount(unsigned(in))); });
382 /* Make sure enable bits aren't set for non-existent tracks. */
383 if(mEnabledTracks[((mNumTracks+7_uz)>>3) - 1] >= (1u<<(mNumTracks&7)))
384 throw std::runtime_error{"Invalid channel enable bits"};
386 /* Each chunk is exactly one second long, with samples interleaved for each
387 * enabled track. The last chunk may be shorter if there isn't enough time
388 * remaining for a full second.
390 const auto numsamples = std::min(uint64_t{mSampleRate}, mSampleCount-mCurrentSample);
392 const auto toread = std::streamsize(numsamples * BytesFromQuality(mQuality) * mNumEnabled);
393 mInFile.read(mSampleChunk.data(), toread);
394 if(mInFile.gcount() != toread)
395 throw std::runtime_error{"Failed to read sample chunk"};
397 std::fill(mSampleChunk.begin()+toread, mSampleChunk.end(), char{});
399 mCurrentSample += numsamples;
400 return static_cast<uint32_t>(numsamples);
403 void LafStream::convertSamples(const al::span<char> samples) const
405 /* OpenAL uses unsigned 8-bit samples (0...255), so signed 8-bit samples
406 * (-128...+127) need conversion. The other formats are fine.
408 if(mQuality == Quality::s8)
409 std::transform(samples.begin(), samples.end(), samples.begin(),
410 [](const char sample) noexcept { return char(sample^0x80); });
413 void LafStream::convertPositions(const al::span<float> dst, const al::span<const char> src) const
415 switch(mQuality)
417 case Quality::s8:
418 std::transform(src.begin(), src.end(), dst.begin(),
419 [](const int8_t in) { return float(in) / 127.0f; });
420 break;
421 case Quality::s16:
423 auto i16src = al::span{reinterpret_cast<const int16_t*>(src.data()),
424 src.size()/sizeof(int16_t)};
425 std::transform(i16src.begin(), i16src.end(), dst.begin(),
426 [](const int16_t in) { return float(in) / 32767.0f; });
428 break;
429 case Quality::f32:
431 auto f32src = al::span{reinterpret_cast<const float*>(src.data()),
432 src.size()/sizeof(float)};
433 std::copy(f32src.begin(), f32src.end(), dst.begin());
435 break;
436 case Quality::s24:
438 /* 24-bit samples are converted to 32-bit in copySamples. */
439 auto i32src = al::span{reinterpret_cast<const int32_t*>(src.data()),
440 src.size()/sizeof(int32_t)};
441 std::transform(i32src.begin(), i32src.end(), dst.begin(),
442 [](const int32_t in) { return float(in>>8) / 8388607.0f; });
444 break;
448 template<Quality Q>
449 void LafStream::copySamples(char *dst, const char *src, const size_t idx, const size_t count) const
451 using reader_t = SampleReader<Q>;
452 using src_t = typename reader_t::src_t;
453 using dst_t = typename reader_t::dst_t;
455 const auto step = size_t{mNumEnabled};
456 assert(idx < step);
458 auto input = al::span{reinterpret_cast<const src_t*>(src), count*step};
459 auto output = al::span{reinterpret_cast<dst_t*>(dst), count};
461 auto inptr = input.begin();
462 std::generate_n(output.begin(), output.size(), [&inptr,idx,step]
464 auto ret = reader_t::read(inptr[idx]);
465 inptr += ptrdiff_t(step);
466 return ret;
470 auto LafStream::prepareTrack(const size_t trackidx, const size_t count) -> al::span<char>
472 const auto todo = std::min(size_t{mSampleRate}, count);
473 if((mEnabledTracks[trackidx>>3] & (1_uz<<(trackidx&7))))
475 /* If the track is enabled, get the real index (skipping disabled
476 * tracks), and deinterlace it into the mono line.
478 const auto idx = [this,trackidx]() -> unsigned int
480 const auto bits = al::span{mEnabledTracks}.first(trackidx>>3);
481 const auto res = std::accumulate(bits.begin(), bits.end(), 0u,
482 [](const unsigned int val, const uint8_t in)
483 { return val + unsigned(al::popcount(unsigned(in))); });
484 return unsigned(al::popcount(mEnabledTracks[trackidx>>3] & ((1u<<(trackidx&7))-1)))
485 + res;
486 }();
488 switch(mQuality)
490 case Quality::s8:
491 copySamples<Quality::s8>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
492 break;
493 case Quality::s16:
494 copySamples<Quality::s16>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
495 break;
496 case Quality::f32:
497 copySamples<Quality::f32>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
498 break;
499 case Quality::s24:
500 copySamples<Quality::s24>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
501 break;
504 else
506 /* If the track is disabled, provide silence. */
507 std::fill_n(mSampleLine.begin(), mSampleLine.size(), char{});
510 return mSampleLine.first(todo * BufferBytesFromQuality(mQuality));
514 auto LoadLAF(const fs::path &fname) -> std::unique_ptr<LafStream>
516 auto laf = std::make_unique<LafStream>();
517 laf->mInFile.open(fname, std::ios_base::binary);
519 auto marker = std::array<char,9>{};
520 laf->mInFile.read(marker.data(), marker.size());
521 if(laf->mInFile.gcount() != marker.size())
522 throw std::runtime_error{"Failed to read file marker"};
523 if(std::string_view{marker.data(), marker.size()} != "LIMITLESS"sv)
524 throw std::runtime_error{"Not an LAF file"};
526 auto header = std::array<char,10>{};
527 laf->mInFile.read(header.data(), header.size());
528 if(laf->mInFile.gcount() != header.size())
529 throw std::runtime_error{"Failed to read header"};
530 while(std::string_view{header.data(), 4} != "HEAD"sv)
532 auto headview = std::string_view{header.data(), header.size()};
533 auto hiter = header.begin();
534 if(const auto hpos = std::min(headview.find("HEAD"sv), headview.size());
535 hpos < headview.size())
537 /* Found the HEAD marker. Copy what was read of the header to the
538 * front, fill in the rest of the header, and continue loading.
540 hiter = std::copy(header.begin()+hpos, header.end(), hiter);
542 else if(al::ends_with(headview, "HEA"sv))
544 /* Found what might be the HEAD marker at the end. Copy it to the
545 * front, refill the header, and check again.
547 hiter = std::copy_n(header.end()-3, 3, hiter);
549 else if(al::ends_with(headview, "HE"sv))
550 hiter = std::copy_n(header.end()-2, 2, hiter);
551 else if(headview.back() == 'H')
552 hiter = std::copy_n(header.end()-1, 1, hiter);
554 const auto toread = std::distance(hiter, header.end());
555 laf->mInFile.read(al::to_address(hiter), toread);
556 if(laf->mInFile.gcount() != toread)
557 throw std::runtime_error{"Failed to read header"};
560 laf->mQuality = [stype=int{header[4]}] {
561 if(stype == 0) return Quality::s8;
562 if(stype == 1) return Quality::s16;
563 if(stype == 2) return Quality::f32;
564 if(stype == 3) return Quality::s24;
565 throw std::runtime_error{"Invalid quality type: "+std::to_string(stype)};
566 }();
568 laf->mMode = [mode=int{header[5]}] {
569 if(mode == 0) return Mode::Channels;
570 if(mode == 1) return Mode::Objects;
571 throw std::runtime_error{"Invalid mode: "+std::to_string(mode)};
572 }();
574 laf->mNumTracks = [input=al::span{header}.subspan<6,4>()] {
575 return uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
576 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
577 }();
579 std::cout<< "Filename: "<<fname<<'\n';
580 std::cout<< " quality: "<<GetQualityName(laf->mQuality)<<'\n';
581 std::cout<< " mode: "<<GetModeName(laf->mMode)<<'\n';
582 std::cout<< " track count: "<<laf->mNumTracks<<'\n';
584 if(laf->mNumTracks == 0)
585 throw std::runtime_error{"No tracks"};
586 if(laf->mNumTracks > 256)
587 throw std::runtime_error{"Too many tracks: "+std::to_string(laf->mNumTracks)};
589 auto chandata = std::vector<char>(laf->mNumTracks*9_uz);
590 laf->mInFile.read(chandata.data(), std::streamsize(chandata.size()));
591 if(laf->mInFile.gcount() != std::streamsize(chandata.size()))
592 throw std::runtime_error{"Failed to read channel header data"};
594 if(laf->mMode == Mode::Channels)
595 laf->mChannels.reserve(laf->mNumTracks);
596 else
598 if(laf->mNumTracks < 2)
599 throw std::runtime_error{"Not enough tracks"};
601 auto numchans = uint32_t{laf->mNumTracks - 1};
602 auto numpostracks = uint32_t{1};
603 while(numpostracks*16 < numchans)
605 --numchans;
606 ++numpostracks;
608 laf->mChannels.reserve(numchans);
609 laf->mPosTracks.reserve(numpostracks);
612 for(uint32_t i{0};i < laf->mNumTracks;++i)
614 static constexpr auto read_float = [](al::span<char,4> input)
616 const auto value = uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
617 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
618 return al::bit_cast<float>(value);
621 auto chan = al::span{chandata}.subspan(i*9_uz, 9);
622 auto x_axis = read_float(chan.first<4>());
623 auto y_axis = read_float(chan.subspan<4,4>());
624 auto lfe_flag = int{chan[8]};
626 std::cout<< "Track "<<i<<": E="<<x_axis<<", A="<<y_axis<<" (LFE: "<<lfe_flag<<")\n";
628 if(x_axis != x_axis && y_axis == 0.0)
630 MyAssert(laf->mMode == Mode::Objects);
631 MyAssert(i != 0);
632 laf->mPosTracks.emplace_back();
634 else
636 MyAssert(laf->mPosTracks.empty());
637 MyAssert(std::isfinite(x_axis) && std::isfinite(y_axis));
638 auto &channel = laf->mChannels.emplace_back();
639 channel.mAzimuth = y_axis;
640 channel.mElevation = x_axis;
641 channel.mIsLfe = lfe_flag != 0;
644 std::cout<< "Channels: "<<laf->mChannels.size()<<'\n';
646 /* For "objects" mode, ensure there's enough tracks with position data to
647 * handle the audio channels.
649 if(laf->mMode == Mode::Objects)
650 MyAssert(((laf->mChannels.size()-1)>>4) == laf->mPosTracks.size()-1);
652 auto footer = std::array<char,12>{};
653 laf->mInFile.read(footer.data(), footer.size());
654 if(laf->mInFile.gcount() != footer.size())
655 throw std::runtime_error{"Failed to read sample header data"};
657 laf->mSampleRate = [input=al::span{footer}.first<4>()] {
658 return uint32_t{uint8_t(input[0])} | (uint32_t{uint8_t(input[1])}<<8u)
659 | (uint32_t{uint8_t(input[2])}<<16u) | (uint32_t{uint8_t(input[3])}<<24u);
660 }();
661 laf->mSampleCount = [input=al::span{footer}.last<8>()] {
662 return uint64_t{uint8_t(input[0])} | (uint64_t{uint8_t(input[1])}<<8)
663 | (uint64_t{uint8_t(input[2])}<<16u) | (uint64_t{uint8_t(input[3])}<<24u)
664 | (uint64_t{uint8_t(input[4])}<<32u) | (uint64_t{uint8_t(input[5])}<<40u)
665 | (uint64_t{uint8_t(input[6])}<<48u) | (uint64_t{uint8_t(input[7])}<<56u);
666 }();
667 std::cout<< "Sample rate: "<<laf->mSampleRate<<'\n';
668 std::cout<< "Length: "<<laf->mSampleCount<<" samples ("
669 <<(static_cast<double>(laf->mSampleCount)/static_cast<double>(laf->mSampleRate))<<" sec)\n";
671 /* Position vectors get split across the PCM chunks if the sample rate
672 * isn't a multiple of 48. Each PCM chunk is exactly one second (the sample
673 * rate in sample frames). Each track with position data consists of a set
674 * of 3 samples for 16 audio channels, resuling in 48 sample frames for a
675 * full set of positions. Extra logic will be needed to manage the position
676 * frame offset separate from each chunk.
678 MyAssert(laf->mMode == Mode::Channels || (laf->mSampleRate%FramesPerPos) == 0);
680 for(size_t i{0};i < laf->mPosTracks.size();++i)
681 laf->mPosTracks[i].resize(laf->mSampleRate*2_uz, 0.0f);
683 laf->mSampleChunk.resize(laf->mSampleRate*BytesFromQuality(laf->mQuality)*laf->mNumTracks
684 + laf->mSampleRate*BufferBytesFromQuality(laf->mQuality));
685 laf->mSampleLine = al::span{laf->mSampleChunk}.last(laf->mSampleRate
686 * BufferBytesFromQuality(laf->mQuality));
688 return laf;
691 void PlayLAF(std::string_view fname)
692 try {
693 auto laf = LoadLAF(fs::u8path(fname));
695 switch(laf->mQuality)
697 case Quality::s8:
698 laf->mALFormat = AL_FORMAT_MONO8;
699 break;
700 case Quality::s16:
701 laf->mALFormat = AL_FORMAT_MONO16;
702 break;
703 case Quality::f32:
704 if(alIsExtensionPresent("AL_EXT_FLOAT32"))
705 laf->mALFormat = AL_FORMAT_MONO_FLOAT32;
706 break;
707 case Quality::s24:
708 laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO32");
709 if(!laf->mALFormat || laf->mALFormat == -1)
710 laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO_I32");
711 break;
713 if(!laf->mALFormat || laf->mALFormat == -1)
714 throw std::runtime_error{"No supported format for "+std::string{GetQualityName(laf->mQuality)}+" samples"};
716 auto alloc_channel = [](Channel &channel)
718 alGenSources(1, &channel.mSource);
719 alGenBuffers(ALsizei(channel.mBuffers.size()), channel.mBuffers.data());
721 /* Disable distance attenuation, and make sure the source stays locked
722 * relative to the listener.
724 alSourcef(channel.mSource, AL_ROLLOFF_FACTOR, 0.0f);
725 alSourcei(channel.mSource, AL_SOURCE_RELATIVE, AL_TRUE);
727 /* FIXME: Is the Y rotation/azimuth clockwise or counter-clockwise?
728 * Does +azimuth move a front sound right or left?
730 const auto x = std::sin(channel.mAzimuth) * std::cos(channel.mElevation);
731 const auto y = std::sin(channel.mElevation);
732 const auto z = -std::cos(channel.mAzimuth) * std::cos(channel.mElevation);
733 alSource3f(channel.mSource, AL_POSITION, x, y, z);
735 if(channel.mIsLfe)
737 if(LfeSlotID)
739 /* For LFE, silence the direct/dry path and connect the LFE aux
740 * slot on send 0.
742 alSourcei(channel.mSource, AL_DIRECT_FILTER, ALint(MuteFilterID));
743 alSource3i(channel.mSource, AL_AUXILIARY_SEND_FILTER, ALint(LfeSlotID), 0,
744 AL_FILTER_NULL);
746 else
748 /* If AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT isn't available,
749 * silence LFE channels since they may not be appropriate to
750 * play normally.
752 alSourcef(channel.mSource, AL_GAIN, 0.0f);
756 if(auto err=alGetError())
757 throw std::runtime_error{std::string{"OpenAL error: "} + alGetString(err)};
759 std::for_each(laf->mChannels.begin(), laf->mChannels.end(), alloc_channel);
761 while(!laf->isAtEnd())
763 auto state = ALenum{};
764 auto offset = ALint{};
765 auto processed = ALint{};
766 /* All sources are played in sync, so they'll all be at the same offset
767 * with the same state and number of processed buffers. Query the back
768 * source just in case the previous update ran really late and missed
769 * updating only some sources on time (in which case, the latter ones
770 * will underrun, which this will detect and restart them all as
771 * needed).
773 alGetSourcei(laf->mChannels.back().mSource, AL_BUFFERS_PROCESSED, &processed);
774 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
775 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
777 if(state == AL_PLAYING || state == AL_PAUSED)
779 if(!laf->mPosTracks.empty())
781 alcSuspendContext(alcGetCurrentContext());
782 for(size_t i{0};i < laf->mChannels.size();++i)
784 const auto trackidx = i>>4;
786 const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15);
787 const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0];
788 const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1];
789 const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2];
791 /* Contrary to the docs, the position is left-handed and
792 * needs to be converted to right-handed.
794 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
796 alcProcessContext(alcGetCurrentContext());
799 if(processed > 0)
801 const auto numsamples = laf->readChunk();
802 for(size_t i{0};i < laf->mChannels.size();++i)
804 const auto samples = laf->prepareTrack(i, numsamples);
805 laf->convertSamples(samples);
807 auto bufid = ALuint{};
808 alSourceUnqueueBuffers(laf->mChannels[i].mSource, 1, &bufid);
809 alBufferData(bufid, laf->mALFormat, samples.data(), ALsizei(samples.size()),
810 ALsizei(laf->mSampleRate));
811 alSourceQueueBuffers(laf->mChannels[i].mSource, 1, &bufid);
813 for(size_t i{0};i < laf->mPosTracks.size();++i)
815 std::copy(laf->mPosTracks[i].begin() + ptrdiff_t(laf->mSampleRate),
816 laf->mPosTracks[i].end(), laf->mPosTracks[i].begin());
818 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
819 laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate),
820 positions);
823 else
824 std::this_thread::sleep_for(std::chrono::milliseconds{10});
826 else if(state == AL_STOPPED)
828 auto sources = std::array<ALuint,256>{};
829 for(size_t i{0};i < laf->mChannels.size();++i)
830 sources[i] = laf->mChannels[i].mSource;
831 alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data());
833 else if(state == AL_INITIAL)
835 auto sources = std::array<ALuint,256>{};
836 auto numsamples = laf->readChunk();
837 for(size_t i{0};i < laf->mChannels.size();++i)
839 const auto samples = laf->prepareTrack(i, numsamples);
840 laf->convertSamples(samples);
841 alBufferData(laf->mChannels[i].mBuffers[0], laf->mALFormat, samples.data(),
842 ALsizei(samples.size()), ALsizei(laf->mSampleRate));
844 for(size_t i{0};i < laf->mPosTracks.size();++i)
846 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
847 laf->convertPositions(al::span{laf->mPosTracks[i]}.first(laf->mSampleRate),
848 positions);
851 numsamples = laf->readChunk();
852 for(size_t i{0};i < laf->mChannels.size();++i)
854 const auto samples = laf->prepareTrack(i, numsamples);
855 laf->convertSamples(samples);
856 alBufferData(laf->mChannels[i].mBuffers[1], laf->mALFormat, samples.data(),
857 ALsizei(samples.size()), ALsizei(laf->mSampleRate));
858 alSourceQueueBuffers(laf->mChannels[i].mSource,
859 ALsizei(laf->mChannels[i].mBuffers.size()), laf->mChannels[i].mBuffers.data());
860 sources[i] = laf->mChannels[i].mSource;
862 for(size_t i{0};i < laf->mPosTracks.size();++i)
864 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
865 laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate),
866 positions);
869 if(!laf->mPosTracks.empty())
871 for(size_t i{0};i < laf->mChannels.size();++i)
873 const auto trackidx = i>>4;
875 const auto x = laf->mPosTracks[trackidx][(i&15)*3 + 0];
876 const auto y = laf->mPosTracks[trackidx][(i&15)*3 + 1];
877 const auto z = laf->mPosTracks[trackidx][(i&15)*3 + 2];
879 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
883 alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data());
885 else
886 break;
889 auto state = ALenum{};
890 auto offset = ALint{};
891 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
892 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
893 while(alGetError() == AL_NO_ERROR && state == AL_PLAYING)
895 if(!laf->mPosTracks.empty())
897 alcSuspendContext(alcGetCurrentContext());
898 for(size_t i{0};i < laf->mChannels.size();++i)
900 const auto trackidx = i>>4;
902 const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15);
903 const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0];
904 const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1];
905 const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2];
907 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
909 alcProcessContext(alcGetCurrentContext());
911 std::this_thread::sleep_for(std::chrono::milliseconds{10});
912 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
913 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
916 catch(std::exception& e) {
917 std::cerr<< "Error playing "<<fname<<":\n "<<e.what()<<'\n';
920 auto main(al::span<std::string_view> args) -> int
922 /* Print out usage if no arguments were specified */
923 if(args.size() < 2)
925 fprintf(stderr, "Usage: %.*s [-device <name>] <filenames...>\n", al::sizei(args[0]),
926 args[0].data());
927 return 1;
929 args = args.subspan(1);
931 if(InitAL(args) != 0)
932 throw std::runtime_error{"Failed to initialize OpenAL"};
933 /* A simple RAII container for automating OpenAL shutdown. */
934 struct AudioManager {
935 AudioManager() = default;
936 AudioManager(const AudioManager&) = delete;
937 auto operator=(const AudioManager&) -> AudioManager& = delete;
938 ~AudioManager()
940 if(LfeSlotID)
942 alDeleteAuxiliaryEffectSlots(1, &LfeSlotID);
943 alDeleteEffects(1, &LowFrequencyEffectID);
944 alDeleteFilters(1, &MuteFilterID);
946 CloseAL();
949 AudioManager almgr;
951 if(auto *device = alcGetContextsDevice(alcGetCurrentContext());
952 alcIsExtensionPresent(device, "ALC_EXT_EFX")
953 && alcIsExtensionPresent(device, "ALC_EXT_DEDICATED"))
955 #define LOAD_PROC(x) do { \
956 x = reinterpret_cast<decltype(x)>(alGetProcAddress(#x)); \
957 if(!x) fprintf(stderr, "Failed to find function '%s'\n", #x); \
958 } while(0)
959 LOAD_PROC(alGenFilters);
960 LOAD_PROC(alDeleteFilters);
961 LOAD_PROC(alIsFilter);
962 LOAD_PROC(alFilterf);
963 LOAD_PROC(alFilterfv);
964 LOAD_PROC(alFilteri);
965 LOAD_PROC(alFilteriv);
966 LOAD_PROC(alGetFilterf);
967 LOAD_PROC(alGetFilterfv);
968 LOAD_PROC(alGetFilteri);
969 LOAD_PROC(alGetFilteriv);
970 LOAD_PROC(alGenEffects);
971 LOAD_PROC(alDeleteEffects);
972 LOAD_PROC(alIsEffect);
973 LOAD_PROC(alEffectf);
974 LOAD_PROC(alEffectfv);
975 LOAD_PROC(alEffecti);
976 LOAD_PROC(alEffectiv);
977 LOAD_PROC(alGetEffectf);
978 LOAD_PROC(alGetEffectfv);
979 LOAD_PROC(alGetEffecti);
980 LOAD_PROC(alGetEffectiv);
981 LOAD_PROC(alGenAuxiliaryEffectSlots);
982 LOAD_PROC(alDeleteAuxiliaryEffectSlots);
983 LOAD_PROC(alIsAuxiliaryEffectSlot);
984 LOAD_PROC(alAuxiliaryEffectSlotf);
985 LOAD_PROC(alAuxiliaryEffectSlotfv);
986 LOAD_PROC(alAuxiliaryEffectSloti);
987 LOAD_PROC(alAuxiliaryEffectSlotiv);
988 LOAD_PROC(alGetAuxiliaryEffectSlotf);
989 LOAD_PROC(alGetAuxiliaryEffectSlotfv);
990 LOAD_PROC(alGetAuxiliaryEffectSloti);
991 LOAD_PROC(alGetAuxiliaryEffectSlotiv);
992 #undef LOAD_PROC
994 alGenFilters(1, &MuteFilterID);
995 alFilteri(MuteFilterID, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
996 alFilterf(MuteFilterID, AL_LOWPASS_GAIN, 0.0f);
997 MyAssert(alGetError() == AL_NO_ERROR);
999 alGenEffects(1, &LowFrequencyEffectID);
1000 alEffecti(LowFrequencyEffectID, AL_EFFECT_TYPE, AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT);
1001 MyAssert(alGetError() == AL_NO_ERROR);
1003 alGenAuxiliaryEffectSlots(1, &LfeSlotID);
1004 alAuxiliaryEffectSloti(LfeSlotID, AL_EFFECTSLOT_EFFECT, ALint(LowFrequencyEffectID));
1005 MyAssert(alGetError() == AL_NO_ERROR);
1008 std::for_each(args.begin(), args.end(), PlayLAF);
1010 return 0;
1013 } // namespace
1015 int main(int argc, char **argv)
1017 MyAssert(argc >= 0);
1018 auto args = std::vector<std::string_view>(static_cast<unsigned int>(argc));
1019 std::copy_n(argv, args.size(), args.begin());
1020 return main(al::span{args});