Files
chromaprint/lib/chromaprint.ex
T
2026-05-20 21:37:39 +02:00

167 lines
5.3 KiB
Elixir

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