Skip to main content

lib/air_play/alac.ex

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