167 lines
5.3 KiB
Elixir
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
|