Module: Shazamio::AudioLoader

Defined in:
lib/shazamio/audio_loader.rb

Overview

The Python project decodes arbitrary audio formats (mp3, ogg, m4a, ...) via pydub, which itself just shells out to ffmpeg. Ruby has no standard-library audio decoder either, so this does the same thing directly: pipe the input through ffmpeg and ask for raw signed 16-bit little-endian mono 16kHz PCM, which is exactly what the signature algorithm expects.

Requires ffmpeg to be installed and on PATH (same real-world requirement the Python library has via pydub).

Defined Under Namespace

Classes: DecodeError, FFmpegNotFound

Class Method Summary collapse

Class Method Details

.ensure_ffmpeg!Object

Raises:



47
48
49
50
51
52
# File 'lib/shazamio/audio_loader.rb', line 47

def ensure_ffmpeg!
  return if system("ffmpeg", "-version", out: File::NULL, err: File::NULL)

  raise FFmpegNotFound, "ffmpeg was not found on PATH. Install it (e.g. `apt install ffmpeg` " \
                         "or `brew install ffmpeg`) to decode audio files."
end

.pcm_s16le_mono_16k(source) ⇒ Array<Integer>

Returns signed 16-bit mono samples at 16kHz.

Parameters:

  • source (String, Pathname, StringIO, #read)

    a file path, or an object holding raw audio bytes (e.g. a String of bytes, or anything responding to #read).

Returns:

  • (Array<Integer>)

    signed 16-bit mono samples at 16kHz



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/shazamio/audio_loader.rb', line 26

def pcm_s16le_mono_16k(source)
  ensure_ffmpeg!

  path, cleanup = resolve_path(source)
  begin
    stdout, stderr, status = Open3.capture3(
      "ffmpeg", "-v", "error", "-y",
      "-i", path,
      "-ac", "1",
      "-ar", "16000",
      "-f", "s16le",
      "pipe:1"
    )
    raise DecodeError, "ffmpeg failed: #{stderr}" unless status.success?

    stdout.unpack("s<*")
  ensure
    cleanup&.call
  end
end

.resolve_path(source) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/shazamio/audio_loader.rb', line 54

def resolve_path(source)
  case source
  when String
    if File.exist?(source)
      [source, nil]
    else
      raise DecodeError,
            "No such file: #{source.inspect}. `recognize`/`pcm_s16le_mono_16k` treats a String " \
            "as a file path. If you meant to pass raw audio bytes instead of a path, wrap them " \
            "first, e.g. StringIO.new(bytes)."
    end
  else
    bytes = source.respond_to?(:read) ? source.read : source.to_s
    tmp = Tempfile.new(["shazamio", ".audio"])
    tmp.binmode
    tmp.write(bytes)
    tmp.close
    [tmp.path, -> { tmp.unlink }]
  end
end