defmodule Chromaprint do @moduledoc """ Elixir bindings for the [Chromaprint](https://github.com/acoustid/chromaprint) audio fingerprinting library. Two ways to use it: * **Streaming** - `new/1` → `start/2` → `feed/2` (any number of times) → `finish/1` → `fingerprint/1` (or `raw_fingerprint/1`, `hash/1`). * **One-shot** - `compute/2` takes the entire 16-bit PCM buffer and returns `{:ok, fingerprint_string}`. Audio input is **little-endian signed 16-bit PCM**, interleaved across channels. """ alias Chromaprint.NIF @algorithms %{test1: 0, test2: 1, test3: 2, test4: 3, test5: 4} @algorithm_codes Map.new(@algorithms, fn {k, v} -> {v, k} end) @default_algorithm :test2 defmodule Context do @moduledoc """ Opaque handle to a Chromaprint fingerprinting session. """ @enforce_keys [:ref, :algorithm] defstruct [:ref, :algorithm] @type t :: %__MODULE__{ref: reference(), algorithm: atom()} end @type algorithm :: :test1 | :test2 | :test3 | :test4 | :test5 @type raw_fingerprint :: [non_neg_integer()] @doc "Names of the supported algorithms." @spec algorithms() :: [algorithm()] def algorithms, do: Map.keys(@algorithms) @doc "The version string of the linked chromaprint library." @spec version() :: String.t() def version, do: NIF.version() @doc """ Create a new fingerprinting context. ## Options * `:algorithm` - one of `#{inspect(Map.keys(@algorithms))}`. Defaults to `#{inspect(@default_algorithm)}` (chromaprint's `DEFAULT`). """ @spec new(keyword()) :: {:ok, Context.t()} | {:error, term()} def new(opts \\ []) do algorithm = Keyword.get(opts, :algorithm, @default_algorithm) with {:ok, code} <- algorithm_to_code(algorithm), {:ok, ref} <- NIF.new_context(code) do {:ok, %Context{ref: ref, algorithm: algorithm}} end end @doc """ Start a stream. Must be called before any `feed/2`. ## Options * `:sample_rate` - required, e.g. `44_100`. * `:channels` - required, `1` or `2`. """ @spec start(Context.t(), keyword()) :: :ok | {:error, term()} def start(%Context{ref: ref}, opts) do sample_rate = Keyword.fetch!(opts, :sample_rate) channels = Keyword.fetch!(opts, :channels) NIF.start(ref, sample_rate, channels) end @doc """ Feed PCM samples. `samples` is a binary (or iolist) of interleaved, little-endian signed 16-bit integers. """ @spec feed(Context.t(), iodata()) :: :ok | {:error, term()} def feed(%Context{ref: ref}, samples) do bin = IO.iodata_to_binary(samples) NIF.feed(ref, bin) end @doc "Signal end of input. Must precede `fingerprint/1` / `raw_fingerprint/1`." @spec finish(Context.t()) :: :ok | {:error, term()} def finish(%Context{ref: ref}), do: NIF.finish(ref) @doc "Get the compressed base64-style fingerprint string." @spec fingerprint(Context.t()) :: {:ok, String.t()} | {:error, term()} def fingerprint(%Context{ref: ref}), do: NIF.get_fingerprint(ref) @doc "Get the raw fingerprint as a list of unsigned 32-bit integers." @spec raw_fingerprint(Context.t()) :: {:ok, raw_fingerprint()} | {:error, term()} def raw_fingerprint(%Context{ref: ref}), do: NIF.get_raw_fingerprint(ref) @doc "Get a compact 32-bit hash of the fingerprint." @spec hash(Context.t()) :: {:ok, non_neg_integer()} | {:error, term()} def hash(%Context{ref: ref}), do: NIF.get_fingerprint_hash(ref) @doc """ Encode a raw fingerprint to its compressed form. ## Options * `:algorithm` - required. * `:base64` - defaults to `true`. When `false`, returns the raw byte form. """ @spec encode(raw_fingerprint(), keyword()) :: {:ok, binary()} | {:error, term()} def encode(raw, opts) when is_list(raw) do algorithm = Keyword.fetch!(opts, :algorithm) base64? = Keyword.get(opts, :base64, true) with {:ok, code} <- algorithm_to_code(algorithm) do NIF.encode_fingerprint(raw, code, base64?) end end @doc """ Decode a compressed fingerprint. ## Options * `:base64` - defaults to `true`. Returns `{:ok, %{algorithm: atom(), raw: [non_neg_integer()]}}`. """ @spec decode(binary(), keyword()) :: {:ok, %{algorithm: algorithm() | non_neg_integer(), raw: raw_fingerprint()}} | {:error, term()} def decode(encoded, opts \\ []) when is_binary(encoded) do base64? = Keyword.get(opts, :base64, true) case NIF.decode_fingerprint(encoded, base64?) do {:ok, {algo_code, raw}} -> {:ok, %{algorithm: code_to_algorithm(algo_code), raw: raw}} other -> other end end @doc """ One-shot fingerprint of an entire PCM buffer. ## Options * `:sample_rate` - required. * `:channels` - required. * `:algorithm` - defaults to `:test2`. """ @spec compute(iodata(), keyword()) :: {:ok, String.t()} | {:error, term()} def compute(pcm, opts) do with {:ok, ctx} <- new(opts), :ok <- start(ctx, opts), :ok <- feed(ctx, pcm), :ok <- finish(ctx) do fingerprint(ctx) end end defp algorithm_to_code(algo) when is_map_key(@algorithms, algo), do: {:ok, Map.fetch!(@algorithms, algo)} defp algorithm_to_code(code) when is_integer(code), do: {:ok, code} defp algorithm_to_code(other), do: {:error, {:unknown_algorithm, other}} defp code_to_algorithm(code), do: Map.get(@algorithm_codes, code, code) end