defmodule AirPlay.Alac do
@moduledoc """
Pack PCM into **uncompressed** ALAC frames for AirPlay/RAOP.
Real senders send *compressed* ALAC, but every RAOP receiver also decodes the
uncompressed/"escape" form (a per-frame flag), so we avoid needing an ALAC
compressor: we emit verbatim frames the receiver decodes identically.
Bit layout (MSB-first), confirmed against shairport's hammerton decoder: for
16-bit stereo the frame is `001` (ID_CPE), a 4-bit element-instance tag, 12
unused bits, `partialFrame`(1)=0, `bytesShifted`(2)=0, `escape`(1)=1
(uncompressed), then `frames`×(L:16, R:16) sample bits, then `111` (ID_END),
zero-padded to a byte. The 4-bit instance tag matters: without it the decoder's
`partialFrame` flag lands in the sample data and it reads a garbage sample count
(silence still decodes — its sample bits are 0 — but real audio overflows).
"""
@id_cpe 1
@id_end 7
@doc """
Encode one frame of interleaved **little-endian s16 stereo** PCM (`frames`
sample-pairs, i.e. `frames*4` bytes) into an uncompressed ALAC frame.
"""
@spec encode_stereo16(binary()) :: binary()
def encode_stereo16(pcm) when is_binary(pcm) do
# Re-emit each LE sample as 16 MSB-first bits in the ALAC bitstream.
samples =
for <<l::signed-little-16, r::signed-little-16 <- pcm>>, into: <<>> do
<<l::signed-16, r::signed-16>>
end
body = <<@id_cpe::3, 0::4, 0::12, 0::1, 0::2, 1::1, samples::bitstring, @id_end::3>>
pad = rem(8 - rem(bit_size(body), 8), 8)
<<body::bitstring, 0::size(pad)>>
end
end