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
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
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.
86 #include <string_view>
88 #include <type_traits>
97 #include "alnumeric.h"
100 #include "common/alhelpers.h"
102 #include "win_main_utf8.h"
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
;
155 void do_assert(const char *message
, int linenum
, const char *filename
, const char *funcname
)
157 auto errstr
= std::string
{filename
};
159 errstr
+= std::to_string(linenum
);
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__)); \
174 enum class Quality
: std::uint8_t {
177 enum class Mode
: bool {
181 auto GetQualityName(Quality quality
) noexcept
-> std::string_view
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
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
207 case Quality::s8
: return 1;
208 case Quality::s16
: return 2;
209 case Quality::f32
: return 4;
210 case Quality::s24
: return 3;
215 auto BufferBytesFromQuality(Quality quality
) noexcept
-> size_t
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;
229 /* Helper class for reading little-endian samples on big-endian targets, or
230 * convert 24-bit samples.
236 struct SampleReader
<Quality::s8
> {
237 using src_t
= int8_t;
238 using dst_t
= int8_t;
241 auto read(const src_t
&in
) noexcept
-> dst_t
{ return in
; }
245 struct SampleReader
<Quality::s16
> {
246 using src_t
= int16_t;
247 using dst_t
= int16_t;
250 auto read(const src_t
&in
) noexcept
-> dst_t
252 if constexpr(al::endian::native
== al::endian::little
)
255 return al::byteswap(in
);
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>;
268 auto read(const src_t
&in
) noexcept
-> dst_t
270 if constexpr(al::endian::native
== al::endian::little
)
273 return al::bit_cast
<dst_t
>(al::byteswap(static_cast<uint32_t>(in
)));
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;
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
296 constexpr auto FramesPerPos
= 48_uz
;
300 std::array
<ALuint
,2> mBuffers
{};
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
}
312 rhs
.mBuffers
.fill(0);
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
);
333 std::ifstream mInFile
;
337 uint32_t mNumTracks
{};
338 uint32_t mSampleRate
{};
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;
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;
365 void copySamples(char *dst
, const char *src
, size_t idx
, size_t count
) const;
368 auto prepareTrack(size_t trackidx
, size_t count
) -> al::span
<char>;
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
418 std::transform(src
.begin(), src
.end(), dst
.begin(),
419 [](const int8_t in
) { return float(in
) / 127.0f
; });
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
; });
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());
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
; });
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
};
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
);
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)))
491 copySamples
<Quality::s8
>(mSampleLine
.data(), mSampleChunk
.data(), idx
, todo
);
494 copySamples
<Quality::s16
>(mSampleLine
.data(), mSampleChunk
.data(), idx
, todo
);
497 copySamples
<Quality::f32
>(mSampleLine
.data(), mSampleChunk
.data(), idx
, todo
);
500 copySamples
<Quality::s24
>(mSampleLine
.data(), mSampleChunk
.data(), idx
, todo
);
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
)};
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
)};
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);
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
);
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
)
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
);
632 laf
->mPosTracks
.emplace_back();
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);
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);
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
));
691 void PlayLAF(std::string_view fname
)
693 auto laf
= LoadLAF(fs::u8path(fname
));
695 switch(laf
->mQuality
)
698 laf
->mALFormat
= AL_FORMAT_MONO8
;
701 laf
->mALFormat
= AL_FORMAT_MONO16
;
704 if(alIsExtensionPresent("AL_EXT_FLOAT32"))
705 laf
->mALFormat
= AL_FORMAT_MONO_FLOAT32
;
708 laf
->mALFormat
= alGetEnumValue("AL_FORMAT_MONO32");
709 if(!laf
->mALFormat
|| laf
->mALFormat
== -1)
710 laf
->mALFormat
= alGetEnumValue("AL_FORMAT_MONO_I32");
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
);
739 /* For LFE, silence the direct/dry path and connect the LFE aux
742 alSourcei(channel
.mSource
, AL_DIRECT_FILTER
, ALint(MuteFilterID
));
743 alSource3i(channel
.mSource
, AL_AUXILIARY_SEND_FILTER
, ALint(LfeSlotID
), 0,
748 /* If AL_EFFECT_DEDICATED_LOW_FREQUENCY_EFFECT isn't available,
749 * silence LFE channels since they may not be appropriate to
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
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());
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
),
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
),
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
),
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());
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 */
925 fprintf(stderr
, "Usage: %.*s [-device <name>] <filenames...>\n", al::sizei(args
[0]),
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;
942 alDeleteAuxiliaryEffectSlots(1, &LfeSlotID
);
943 alDeleteEffects(1, &LowFrequencyEffectID
);
944 alDeleteFilters(1, &MuteFilterID
);
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); \
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
);
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
);
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
});