Fix reading 24-bit samples in allafplay
[openal-soft.git] / examples / allafplay.cpp
blob5ce628a306491676d27bab97dff2f1b3fefabeb7
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 * - LFE channels are silenced. Since LFE signals can really contain anything,
51 * and may expect to be low-pass filtered for/by the subwoofer it's sent to,
52 * it's best to not play them raw. This can be fixed with AL_EXT_DEDICATED's
53 * AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT to silence the direct output and
54 * send the signal to the LFE output if it exists.
56 * - The LAF documentation doesn't prohibit object position tracks from being
57 * separated with audio tracks in between, or from being the first tracks
58 * followed by the audio tracks. It's not known if this is intended to be
59 * allowed, but it's not supported. Object position tracks must be last.
61 * Some remaining issues:
63 * - There are bursts of static on some channels. This doesn't appear to be a
64 * parsing error since the bursts last less than the chunk size, and it never
65 * loses sync with the remaining chunks. Might be an encoding error with the
66 * files tested.
68 * - Positions are specified in left-handed coordinates, despite the LAF
69 * documentation saying it's right-handed. Might be an encoding error with
70 * the files tested, or might be a misunderstanding about which is which. How
71 * to proceed may depend on how wide-spread this issue ends up being, but for
72 * now, they're treated as left-handed here.
74 * - The LAF documentation doesn't specify the range or direction for the
75 * channels' X and Y axis rotation in Channels mode. Presumably X rotation
76 * (elevation) goes from -pi/2...+pi/2 and Y rotation (azimuth) goes from
77 * either -pi...+pi or 0...pi*2, but the direction of movement isn't
78 * specified. Currently positive azimuth moves from center rightward and
79 * positive elevation moves from head-level upward.
82 #include <algorithm>
83 #include <array>
84 #include <bitset>
85 #include <cassert>
86 #include <cstdint>
87 #include <deque>
88 #include <filesystem>
89 #include <fstream>
90 #include <iostream>
91 #include <memory>
92 #include <numeric>
93 #include <string>
94 #include <string_view>
95 #include <thread>
96 #include <type_traits>
97 #include <vector>
99 #include "AL/alc.h"
100 #include "AL/al.h"
101 #include "AL/alext.h"
103 #include "albit.h"
104 #include "almalloc.h"
105 #include "alnumeric.h"
106 #include "alspan.h"
107 #include "alstring.h"
108 #include "common/alhelpers.h"
110 #include "win_main_utf8.h"
112 namespace {
114 namespace fs = std::filesystem;
115 using namespace std::string_view_literals;
117 [[noreturn]]
118 void do_assert(const char *message, int linenum, const char *filename, const char *funcname)
120 auto errstr = std::string{filename};
121 errstr += ':';
122 errstr += std::to_string(linenum);
123 errstr += ": ";
124 errstr += funcname;
125 errstr += ": ";
126 errstr += message;
127 throw std::runtime_error{errstr};
130 #define MyAssert(cond) do { \
131 if(!(cond)) UNLIKELY \
132 do_assert("Assertion '" #cond "' failed", __LINE__, __FILE__, \
133 std::data(__func__)); \
134 } while(0)
137 enum class Quality : std::uint8_t {
138 s8, s16, f32, s24
140 enum class Mode : bool {
141 Channels, Objects
144 auto GetQualityName(Quality quality) noexcept -> std::string_view
146 switch(quality)
148 case Quality::s8: return "8-bit int"sv;
149 case Quality::s16: return "16-bit int"sv;
150 case Quality::f32: return "32-bit float"sv;
151 case Quality::s24: return "24-bit int"sv;
153 return "<unknown>"sv;
156 auto GetModeName(Mode mode) noexcept -> std::string_view
158 switch(mode)
160 case Mode::Channels: return "channels"sv;
161 case Mode::Objects: return "objects"sv;
163 return "<unknown>"sv;
166 auto BytesFromQuality(Quality quality) noexcept -> size_t
168 switch(quality)
170 case Quality::s8: return 1;
171 case Quality::s16: return 2;
172 case Quality::f32: return 4;
173 case Quality::s24: return 3;
175 return 4;
178 auto BufferBytesFromQuality(Quality quality) noexcept -> size_t
180 switch(quality)
182 case Quality::s8: return 1;
183 case Quality::s16: return 2;
184 case Quality::f32: return 4;
185 /* 24-bit samples are converted to 32-bit for OpenAL. */
186 case Quality::s24: return 4;
188 return 4;
192 /* Helper class for reading little-endian samples on big-endian targets, or
193 * convert 24-bit samples.
195 template<Quality Q>
196 struct SampleReader;
198 template<>
199 struct SampleReader<Quality::s8> {
200 using src_t = int8_t;
201 using dst_t = int8_t;
203 [[nodiscard]] static
204 auto read(const src_t &in) noexcept -> dst_t { return in; }
207 template<>
208 struct SampleReader<Quality::s16> {
209 using src_t = int16_t;
210 using dst_t = int16_t;
212 [[nodiscard]] static
213 auto read(const src_t &in) noexcept -> dst_t
215 if constexpr(al::endian::native == al::endian::little)
216 return in;
217 else
218 return al::byteswap(in);
222 template<>
223 struct SampleReader<Quality::f32> {
224 /* 32-bit float samples are read as 32-bit integer on big-endian systems,
225 * so that they can be byteswapped before being reinterpreted as float.
227 using src_t = std::conditional_t<al::endian::native==al::endian::little, float,uint32_t>;
228 using dst_t = float;
230 [[nodiscard]] static
231 auto read(const src_t &in) noexcept -> dst_t
233 if constexpr(al::endian::native == al::endian::little)
234 return in;
235 else
236 return al::bit_cast<dst_t>(al::byteswap(static_cast<uint32_t>(in)));
240 template<>
241 struct SampleReader<Quality::s24> {
242 /* 24-bit samples are converted to 32-bit integer. */
243 using src_t = std::array<uint8_t,3>;
244 using dst_t = int32_t;
246 [[nodiscard]] static
247 auto read(const src_t &in) noexcept -> dst_t
249 return static_cast<int32_t>((uint32_t{in[0]}<<8) | (uint32_t{in[1]}<<16)
250 | (uint32_t{in[2]}<<24));
255 /* Each track with position data consists of a set of 3 samples per 16 audio
256 * channels, resulting in a full set of positions being specified over 48
257 * sample frames.
259 constexpr auto FramesPerPos = 48_uz;
261 struct Channel {
262 ALuint mSource{};
263 std::array<ALuint,2> mBuffers{};
264 float mAzimuth{};
265 float mElevation{};
266 bool mIsLfe{};
268 Channel() = default;
269 Channel(const Channel&) = delete;
270 Channel(Channel&& rhs)
271 : mSource{rhs.mSource}, mBuffers{rhs.mBuffers}, mAzimuth{rhs.mAzimuth}
272 , mElevation{rhs.mElevation}, mIsLfe{rhs.mIsLfe}
274 rhs.mSource = 0;
275 rhs.mBuffers.fill(0);
277 ~Channel()
279 if(mSource) alDeleteSources(1, &mSource);
280 if(mBuffers[0]) alDeleteBuffers(ALsizei(mBuffers.size()), mBuffers.data());
283 auto operator=(const Channel&) -> Channel& = delete;
284 auto operator=(Channel&& rhs) -> Channel&
286 std::swap(mSource, rhs.mSource);
287 std::swap(mBuffers, rhs.mBuffers);
288 std::swap(mAzimuth, rhs.mAzimuth);
289 std::swap(mElevation, rhs.mElevation);
290 std::swap(mIsLfe, rhs.mIsLfe);
291 return *this;
295 struct LafStream {
296 std::ifstream mInFile;
298 Quality mQuality{};
299 Mode mMode{};
300 uint32_t mNumTracks{};
301 uint32_t mSampleRate{};
302 ALenum mALFormat{};
303 uint64_t mSampleCount{};
305 uint64_t mCurrentSample{};
307 std::array<uint8_t,32> mEnabledTracks{};
308 uint32_t mNumEnabled{};
309 std::vector<char> mSampleChunk;
310 al::span<char> mSampleLine;
312 std::vector<Channel> mChannels;
313 std::vector<std::vector<float>> mPosTracks;
315 LafStream() = default;
316 LafStream(const LafStream&) = delete;
317 ~LafStream() = default;
318 auto operator=(const LafStream&) -> LafStream& = delete;
320 [[nodiscard]]
321 auto readChunk() -> uint32_t;
323 void convertSamples(const al::span<char> samples) const;
325 void convertPositions(const al::span<float> dst, const al::span<const char> src) const;
327 template<Quality Q>
328 void copySamples(char *dst, const char *src, size_t idx, size_t count) const;
330 [[nodiscard]]
331 auto prepareTrack(size_t trackidx, size_t count) -> al::span<char>;
333 [[nodiscard]]
334 auto isAtEnd() const noexcept -> bool { return mCurrentSample >= mSampleCount; }
337 auto LafStream::readChunk() -> uint32_t
339 mEnabledTracks.fill(0);
340 mInFile.read(reinterpret_cast<char*>(mEnabledTracks.data()), (mNumTracks+7_z)>>3);
341 mNumEnabled = std::accumulate(mEnabledTracks.cbegin(), mEnabledTracks.cend(), 0u,
342 [](const unsigned int val, const uint8_t in)
343 { return val + unsigned(al::popcount(unsigned(in))); });
345 /* Make sure enable bits aren't set for non-existent tracks. */
346 if(mEnabledTracks[((mNumTracks+7_uz)>>3) - 1] >= (1u<<(mNumTracks&7)))
347 throw std::runtime_error{"Invalid channel enable bits"};
349 /* Each chunk is exactly one second long, with samples interleaved for each
350 * enabled track. The last chunk may be shorter if there isn't enough time
351 * remaining for a full second.
353 const auto numsamples = std::min(uint64_t{mSampleRate}, mSampleCount-mCurrentSample);
355 const auto toread = std::streamsize(numsamples * BytesFromQuality(mQuality) * mNumEnabled);
356 mInFile.read(mSampleChunk.data(), toread);
357 if(mInFile.gcount() != toread)
358 throw std::runtime_error{"Failed to read sample chunk"};
360 std::fill(mSampleChunk.begin()+toread, mSampleChunk.end(), char{});
362 mCurrentSample += numsamples;
363 return static_cast<uint32_t>(numsamples);
366 void LafStream::convertSamples(const al::span<char> samples) const
368 /* OpenAL uses unsigned 8-bit samples (0...255), so signed 8-bit samples
369 * (-128...+127) need conversion. The other formats are fine.
371 if(mQuality == Quality::s8)
372 std::transform(samples.begin(), samples.end(), samples.begin(),
373 [](const char sample) noexcept { return char(sample^0x80); });
376 void LafStream::convertPositions(const al::span<float> dst, const al::span<const char> src) const
378 switch(mQuality)
380 case Quality::s8:
381 std::transform(src.begin(), src.end(), dst.begin(),
382 [](const int8_t in) { return float(in) / 127.0f; });
383 break;
384 case Quality::s16:
386 auto i16src = al::span{reinterpret_cast<const int16_t*>(src.data()),
387 src.size()/sizeof(int16_t)};
388 std::transform(i16src.begin(), i16src.end(), dst.begin(),
389 [](const int16_t in) { return float(in) / 32767.0f; });
391 break;
392 case Quality::f32:
394 auto f32src = al::span{reinterpret_cast<const float*>(src.data()),
395 src.size()/sizeof(float)};
396 std::copy(f32src.begin(), f32src.end(), dst.begin());
398 break;
399 case Quality::s24:
401 /* 24-bit samples are converted to 32-bit in copySamples. */
402 auto i32src = al::span{reinterpret_cast<const int32_t*>(src.data()),
403 src.size()/sizeof(int32_t)};
404 std::transform(i32src.begin(), i32src.end(), dst.begin(),
405 [](const int32_t in) { return float(in>>8) / 8388607.0f; });
407 break;
411 template<Quality Q>
412 void LafStream::copySamples(char *dst, const char *src, const size_t idx, const size_t count) const
414 using reader_t = SampleReader<Q>;
415 using src_t = typename reader_t::src_t;
416 using dst_t = typename reader_t::dst_t;
418 const auto step = size_t{mNumEnabled};
419 assert(idx < step);
421 auto input = al::span{reinterpret_cast<const src_t*>(src), count*step};
422 auto output = al::span{reinterpret_cast<dst_t*>(dst), count};
424 auto inptr = input.begin();
425 std::generate_n(output.begin(), output.size(), [&inptr,idx,step]
427 auto ret = reader_t::read(inptr[idx]);
428 inptr += ptrdiff_t(step);
429 return ret;
433 auto LafStream::prepareTrack(const size_t trackidx, const size_t count) -> al::span<char>
435 const auto todo = std::min(size_t{mSampleRate}, count);
436 if((mEnabledTracks[trackidx>>3] & (1_uz<<(trackidx&7))))
438 /* If the track is enabled, get the real index (skipping disabled
439 * tracks), and deinterlace it into the mono line.
441 const auto idx = [this,trackidx]() -> unsigned int
443 const auto bits = al::span{mEnabledTracks}.first(trackidx>>3);
444 const auto res = std::accumulate(bits.begin(), bits.end(), 0u,
445 [](const unsigned int val, const uint8_t in)
446 { return val + unsigned(al::popcount(unsigned(in))); });
447 return unsigned(al::popcount(mEnabledTracks[trackidx>>3] & ((1u<<(trackidx&7))-1)))
448 + res;
449 }();
451 switch(mQuality)
453 case Quality::s8:
454 copySamples<Quality::s8>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
455 break;
456 case Quality::s16:
457 copySamples<Quality::s16>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
458 break;
459 case Quality::f32:
460 copySamples<Quality::f32>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
461 break;
462 case Quality::s24:
463 copySamples<Quality::s24>(mSampleLine.data(), mSampleChunk.data(), idx, todo);
464 break;
467 else
469 /* If the track is disabled, provide silence. */
470 std::fill_n(mSampleLine.begin(), mSampleLine.size(), char{});
473 return mSampleLine.first(todo * BufferBytesFromQuality(mQuality));
477 auto LoadLAF(const fs::path &fname) -> std::unique_ptr<LafStream>
479 auto laf = std::make_unique<LafStream>();
480 laf->mInFile.open(fname, std::ios_base::binary);
482 auto marker = std::array<char,9>{};
483 laf->mInFile.read(marker.data(), marker.size());
484 if(laf->mInFile.gcount() != marker.size())
485 throw std::runtime_error{"Failed to read file marker"};
486 if(std::string_view{marker.data(), marker.size()} != "LIMITLESS"sv)
487 throw std::runtime_error{"Not an LAF file"};
489 auto header = std::array<char,10>{};
490 laf->mInFile.read(header.data(), header.size());
491 if(laf->mInFile.gcount() != header.size())
492 throw std::runtime_error{"Failed to read header"};
493 while(std::string_view{header.data(), 4} != "HEAD"sv)
495 auto headview = std::string_view{header.data(), header.size()};
496 auto hiter = header.begin();
497 if(const auto hpos = std::min(headview.find("HEAD"sv), headview.size());
498 hpos < headview.size())
500 /* Found the HEAD marker. Copy what was read of the header to the
501 * front, fill in the rest of the header, and continue loading.
503 hiter = std::copy(header.begin()+hpos, header.end(), hiter);
505 else if(al::ends_with(headview, "HEA"sv))
507 /* Found what might be the HEAD marker at the end. Copy it to the
508 * front, refill the header, and check again.
510 hiter = std::copy_n(header.end()-3, 3, hiter);
512 else if(al::ends_with(headview, "HE"sv))
513 hiter = std::copy_n(header.end()-2, 2, hiter);
514 else if(headview.back() == 'H')
515 hiter = std::copy_n(header.end()-1, 1, hiter);
517 const auto toread = std::distance(hiter, header.end());
518 laf->mInFile.read(al::to_address(hiter), toread);
519 if(laf->mInFile.gcount() != toread)
520 throw std::runtime_error{"Failed to read header"};
523 laf->mQuality = [stype=int{header[4]}] {
524 if(stype == 0) return Quality::s8;
525 if(stype == 1) return Quality::s16;
526 if(stype == 2) return Quality::f32;
527 if(stype == 3) return Quality::s24;
528 throw std::runtime_error{"Invalid quality type: "+std::to_string(stype)};
529 }();
531 laf->mMode = [mode=int{header[5]}] {
532 if(mode == 0) return Mode::Channels;
533 if(mode == 1) return Mode::Objects;
534 throw std::runtime_error{"Invalid mode: "+std::to_string(mode)};
535 }();
537 laf->mNumTracks = [input=al::span{header}.subspan<6,4>()] {
538 return uint32_t{uint8_t(input[0]) | (uint32_t{uint8_t(input[1])}<<8)
539 | (uint32_t{uint8_t(input[2])}<<16) | (uint32_t{uint8_t(input[3])}<<24)};
540 }();
542 std::cout<< "Filename: "<<fname<<'\n';
543 std::cout<< " quality: "<<GetQualityName(laf->mQuality)<<'\n';
544 std::cout<< " mode: "<<GetModeName(laf->mMode)<<'\n';
545 std::cout<< " track count: "<<laf->mNumTracks<<'\n';
547 if(laf->mNumTracks == 0)
548 throw std::runtime_error{"No tracks"};
549 if(laf->mNumTracks > 256)
550 throw std::runtime_error{"Too many tracks: "+std::to_string(laf->mNumTracks)};
552 auto chandata = std::vector<char>(laf->mNumTracks*9_uz);
553 laf->mInFile.read(chandata.data(), std::streamsize(chandata.size()));
554 if(laf->mInFile.gcount() != std::streamsize(chandata.size()))
555 throw std::runtime_error{"Failed to read channel header data"};
557 if(laf->mMode == Mode::Channels)
558 laf->mChannels.reserve(laf->mNumTracks);
559 else
561 if(laf->mNumTracks < 2)
562 throw std::runtime_error{"Not enough tracks"};
564 auto numchans = uint32_t{laf->mNumTracks - 1};
565 auto numpostracks = uint32_t{1};
566 while(numpostracks*16 < numchans)
568 --numchans;
569 ++numpostracks;
571 laf->mChannels.reserve(numchans);
572 laf->mPosTracks.reserve(numpostracks);
575 for(uint32_t i{0};i < laf->mNumTracks;++i)
577 static constexpr auto read_float = [](al::span<char,4> input)
579 const auto value = uint32_t{uint8_t(input[0]) | (uint32_t{uint8_t(input[1])}<<8)
580 | (uint32_t{uint8_t(input[2])}<<16) | (uint32_t{uint8_t(input[3])}<<24)};
581 return al::bit_cast<float>(value);
584 auto chan = al::span{chandata}.subspan(i*9_uz, 9);
585 auto x_axis = read_float(chan.first<4>());
586 auto y_axis = read_float(chan.subspan<4,4>());
587 auto lfe_flag = int{chan[8]};
589 std::cout<< "Track "<<i<<": E="<<x_axis<<", A="<<y_axis<<" (LFE: "<<lfe_flag<<")\n";
591 if(x_axis != x_axis && y_axis == 0.0)
593 MyAssert(laf->mMode == Mode::Objects);
594 MyAssert(i != 0);
595 laf->mPosTracks.emplace_back();
597 else
599 MyAssert(laf->mPosTracks.empty());
600 MyAssert(std::isfinite(x_axis) && std::isfinite(y_axis));
601 auto &channel = laf->mChannels.emplace_back();
602 channel.mAzimuth = y_axis;
603 channel.mElevation = x_axis;
604 channel.mIsLfe = lfe_flag != 0;
607 std::cout<< "Channels: "<<laf->mChannels.size()<<'\n';
609 /* For "objects" mode, ensure there's enough tracks with position data to
610 * handle the audio channels.
612 if(laf->mMode == Mode::Objects)
613 MyAssert(((laf->mChannels.size()-1)>>4) == laf->mPosTracks.size()-1);
615 auto footer = std::array<char,12>{};
616 laf->mInFile.read(footer.data(), footer.size());
617 if(laf->mInFile.gcount() != footer.size())
618 throw std::runtime_error{"Failed to read sample header data"};
620 laf->mSampleRate = [input=al::span{footer}.first<4>()] {
621 return uint32_t{uint8_t(input[0]) | (uint32_t{uint8_t(input[1])}<<8)
622 | (uint32_t{uint8_t(input[2])}<<16) | (uint32_t{uint8_t(input[3])}<<24)};
623 }();
624 laf->mSampleCount = [input=al::span{footer}.last<8>()] {
625 return uint64_t{uint8_t(input[0]) | (uint64_t{uint8_t(input[1])}<<8)
626 | (uint64_t{uint8_t(input[2])}<<16) | (uint64_t{uint8_t(input[3])}<<24)
627 | (uint64_t{uint8_t(input[4])}<<32) | (uint64_t{uint8_t(input[5])}<<40)
628 | (uint64_t{uint8_t(input[6])}<<48) | (uint64_t{uint8_t(input[7])}<<56)};
629 }();
630 std::cout<< "Sample rate: "<<laf->mSampleRate<<'\n';
631 std::cout<< "Length: "<<laf->mSampleCount<<" samples ("
632 <<(static_cast<double>(laf->mSampleCount)/static_cast<double>(laf->mSampleRate))<<" sec)\n";
634 /* Position vectors get split across the PCM chunks if the sample rate
635 * isn't a multiple of 48. Each PCM chunk is exactly one second (the sample
636 * rate in sample frames). Each track with position data consists of a set
637 * of 3 samples for 16 audio channels, resuling in 48 sample frames for a
638 * full set of positions. Extra logic will be needed to manage the position
639 * frame offset separate from each chunk.
641 MyAssert(laf->mMode == Mode::Channels || (laf->mSampleRate%FramesPerPos) == 0);
643 for(size_t i{0};i < laf->mPosTracks.size();++i)
644 laf->mPosTracks[i].resize(laf->mSampleRate*2_uz, 0.0f);
646 laf->mSampleChunk.resize(laf->mSampleRate*BytesFromQuality(laf->mQuality)*laf->mNumTracks
647 + laf->mSampleRate*BufferBytesFromQuality(laf->mQuality));
648 laf->mSampleLine = al::span{laf->mSampleChunk}.last(laf->mSampleRate
649 * BufferBytesFromQuality(laf->mQuality));
651 return laf;
654 void PlayLAF(std::string_view fname)
655 try {
656 auto laf = LoadLAF(fs::u8path(fname));
658 switch(laf->mQuality)
660 case Quality::s8:
661 laf->mALFormat = AL_FORMAT_MONO8;
662 break;
663 case Quality::s16:
664 laf->mALFormat = AL_FORMAT_MONO16;
665 break;
666 case Quality::f32:
667 if(alIsExtensionPresent("AL_EXT_FLOAT32"))
668 laf->mALFormat = AL_FORMAT_MONO_FLOAT32;
669 break;
670 case Quality::s24:
671 laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO32");
672 if(!laf->mALFormat || laf->mALFormat == -1)
673 laf->mALFormat = alGetEnumValue("AL_FORMAT_MONO_I32");
674 break;
676 if(!laf->mALFormat || laf->mALFormat == -1)
677 throw std::runtime_error{"No supported format for "+std::string{GetQualityName(laf->mQuality)}+" samples"};
679 auto alloc_channel = [](Channel &channel)
681 alGenSources(1, &channel.mSource);
682 alGenBuffers(ALsizei(channel.mBuffers.size()), channel.mBuffers.data());
684 /* Disable distance attenuation, and make sure the source stays locked
685 * relative to the listener.
687 alSourcef(channel.mSource, AL_ROLLOFF_FACTOR, 0.0f);
688 alSourcei(channel.mSource, AL_SOURCE_RELATIVE, AL_TRUE);
690 /* FIXME: Is the Y rotation/azimuth clockwise or counter-clockwise?
691 * Does +azimuth move a front sound right or left?
693 const auto x = std::sin(channel.mAzimuth) * std::cos(channel.mElevation);
694 const auto y = std::sin(channel.mElevation);
695 const auto z = -std::cos(channel.mAzimuth) * std::cos(channel.mElevation);
696 alSource3f(channel.mSource, AL_POSITION, x, y, z);
698 /* Silence LFE channels since they may not be appropriate to play
699 * normally. AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT could be used to
700 * send them to the proper output.
702 if(channel.mIsLfe)
703 alSourcef(channel.mSource, AL_GAIN, 0.0f);
705 if(auto err=alGetError())
706 throw std::runtime_error{std::string{"OpenAL error: "} + alGetString(err)};
708 std::for_each(laf->mChannels.begin(), laf->mChannels.end(), alloc_channel);
710 while(!laf->isAtEnd())
712 auto state = ALenum{};
713 auto offset = ALint{};
714 auto processed = ALint{};
715 /* All sources are played in sync, so they'll all be at the same offset
716 * with the same state and number of processed buffers. Query the back
717 * source just in case the previous update ran really late and missed
718 * updating only some sources on time (in which case, the latter ones
719 * will underrun, which this will detect and restart them all as
720 * needed).
722 alGetSourcei(laf->mChannels.back().mSource, AL_BUFFERS_PROCESSED, &processed);
723 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
724 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
726 if(state == AL_PLAYING || state == AL_PAUSED)
728 if(!laf->mPosTracks.empty())
730 alcSuspendContext(alcGetCurrentContext());
731 for(size_t i{0};i < laf->mChannels.size();++i)
733 const auto trackidx = i>>4;
735 const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15);
736 const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0];
737 const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1];
738 const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2];
740 /* Contrary to the docs, the position is left-handed and
741 * needs to be converted to right-handed.
743 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
745 alcProcessContext(alcGetCurrentContext());
748 if(processed > 0)
750 const auto numsamples = laf->readChunk();
751 for(size_t i{0};i < laf->mChannels.size();++i)
753 const auto samples = laf->prepareTrack(i, numsamples);
754 laf->convertSamples(samples);
756 auto bufid = ALuint{};
757 alSourceUnqueueBuffers(laf->mChannels[i].mSource, 1, &bufid);
758 alBufferData(bufid, laf->mALFormat, samples.data(), ALsizei(samples.size()),
759 ALsizei(laf->mSampleRate));
760 alSourceQueueBuffers(laf->mChannels[i].mSource, 1, &bufid);
762 for(size_t i{0};i < laf->mPosTracks.size();++i)
764 std::copy(laf->mPosTracks[i].begin() + ptrdiff_t(laf->mSampleRate),
765 laf->mPosTracks[i].end(), laf->mPosTracks[i].begin());
767 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
768 laf->convertPositions(al::span{laf->mPosTracks[i]}.last(laf->mSampleRate),
769 positions);
772 else
773 std::this_thread::sleep_for(std::chrono::milliseconds{10});
775 else if(state == AL_STOPPED)
777 auto sources = std::array<ALuint,256>{};
778 for(size_t i{0};i < laf->mChannels.size();++i)
779 sources[i] = laf->mChannels[i].mSource;
780 alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data());
782 else if(state == AL_INITIAL)
784 auto sources = std::array<ALuint,256>{};
785 auto numsamples = laf->readChunk();
786 for(size_t i{0};i < laf->mChannels.size();++i)
788 const auto samples = laf->prepareTrack(i, numsamples);
789 laf->convertSamples(samples);
790 alBufferData(laf->mChannels[i].mBuffers[0], laf->mALFormat, samples.data(),
791 ALsizei(samples.size()), ALsizei(laf->mSampleRate));
793 for(size_t i{0};i < laf->mPosTracks.size();++i)
795 const auto positions = laf->prepareTrack(laf->mChannels.size()+i, numsamples);
796 laf->convertPositions(al::span{laf->mPosTracks[i]}.first(laf->mSampleRate),
797 positions);
800 numsamples = laf->readChunk();
801 for(size_t i{0};i < laf->mChannels.size();++i)
803 const auto samples = laf->prepareTrack(i, numsamples);
804 laf->convertSamples(samples);
805 alBufferData(laf->mChannels[i].mBuffers[1], laf->mALFormat, samples.data(),
806 ALsizei(samples.size()), ALsizei(laf->mSampleRate));
807 alSourceQueueBuffers(laf->mChannels[i].mSource,
808 ALsizei(laf->mChannels[i].mBuffers.size()), laf->mChannels[i].mBuffers.data());
809 sources[i] = laf->mChannels[i].mSource;
811 for(size_t i{0};i < laf->mPosTracks.size();++i)
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 if(!laf->mPosTracks.empty())
820 for(size_t i{0};i < laf->mChannels.size();++i)
822 const auto trackidx = i>>4;
824 const auto x = laf->mPosTracks[trackidx][(i&15)*3 + 0];
825 const auto y = laf->mPosTracks[trackidx][(i&15)*3 + 1];
826 const auto z = laf->mPosTracks[trackidx][(i&15)*3 + 2];
828 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
832 alSourcePlayv(ALsizei(laf->mChannels.size()), sources.data());
834 else
835 break;
838 auto state = ALenum{};
839 auto offset = ALint{};
840 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
841 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
842 while(alGetError() == AL_NO_ERROR && state == AL_PLAYING)
844 if(!laf->mPosTracks.empty())
846 alcSuspendContext(alcGetCurrentContext());
847 for(size_t i{0};i < laf->mChannels.size();++i)
849 const auto trackidx = i>>4;
851 const auto posoffset = unsigned(offset)/FramesPerPos*16_uz + (i&15);
852 const auto x = laf->mPosTracks[trackidx][posoffset*3 + 0];
853 const auto y = laf->mPosTracks[trackidx][posoffset*3 + 1];
854 const auto z = laf->mPosTracks[trackidx][posoffset*3 + 2];
856 alSource3f(laf->mChannels[i].mSource, AL_POSITION, x, y, -z);
858 alcProcessContext(alcGetCurrentContext());
860 std::this_thread::sleep_for(std::chrono::milliseconds{10});
861 alGetSourcei(laf->mChannels.back().mSource, AL_SAMPLE_OFFSET, &offset);
862 alGetSourcei(laf->mChannels.back().mSource, AL_SOURCE_STATE, &state);
865 catch(std::exception& e) {
866 std::cerr<< "Error playing "<<fname<<":\n "<<e.what()<<'\n';
869 auto main(al::span<std::string_view> args) -> int
871 /* Print out usage if no arguments were specified */
872 if(args.size() < 2)
874 fprintf(stderr, "Usage: %.*s [-device <name>] <filenames...>\n", al::sizei(args[0]),
875 args[0].data());
876 return 1;
878 args = args.subspan(1);
880 /* A simple RAII container for OpenAL startup and shutdown. */
881 struct AudioManager {
882 AudioManager(al::span<std::string_view> &args_)
884 if(InitAL(args_) != 0)
885 throw std::runtime_error{"Failed to initialize OpenAL"};
887 ~AudioManager() { CloseAL(); }
889 AudioManager almgr{args};
891 std::for_each(args.begin(), args.end(), PlayLAF);
893 return 0;
896 } // namespace
898 int main(int argc, char **argv)
900 MyAssert(argc >= 0);
901 auto args = std::vector<std::string_view>(static_cast<unsigned int>(argc));
902 std::copy_n(argv, args.size(), args.begin());
903 return main(al::span{args});