1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
|
# frozen_string_literal: true
require_relative 'aubio/version'
require_relative 'aubio/aubio-ffi'
require_relative 'aubio/onsets'
require_relative 'aubio/pitches'
require_relative 'aubio/beats'
module Aubio
class AubioException < RuntimeError; end
class FileNotFound < AubioException; end
class AlreadyClosed < AubioException; end
class InvalidAudioInput < AubioException; end
class Base
def initialize(path, params)
raise FileNotFound unless File.file?(path)
sample_rate = params[:sample_rate] || 44_100
hop_size = params[:hop_size] || 512
@is_closed = false
@source = Api.new_aubio_source(path, sample_rate, hop_size)
@params = params
check_for_valid_audio_source(path)
end
def close
Api.del_aubio_source(@source)
@is_closed = true
end
def onsets
check_for_closed
Onsets.new(@source, @params).each
end
def pitches
check_for_closed
Pitches.new(@source, @params).each
end
def enum_beats
check_for_closed
Beats.new(@source, @params).each
end
def beats
check_for_closed
beats = Beats.new(@source, @params).each.to_a
# fill in the zero beat
beats = beats.unshift(
beats.first.merge({
confidence: 1,
s: 0.0,
ms: 0.0,
sample_no: 0,
rel_start: 0.0
})
)
# fetch the rel_end from the next beat
# using 1.0 for the last beat
beats = beats.each_cons(2).map do |a, b|
a.merge({
rel_end: (b[:rel_start] || 1.0)
})
end
# set minimum inter-onset interval in seconds
# allows for 4/4 at 400bpm (faster than most music)
# filters beats detected too closely together
minioi = @params[:minioi] || 0.15
filtered_beats = [beats.first]
beats.each do |b|
filtered_beats << b if (b[:s] - filtered_beats.last[:s]) > minioi
end
# TODO: are there other smoothing methods that would be useful here?
filtered_beats
end
def bpm
check_for_closed
beat_locations = beats
beat_periods = beat_locations.each_cons(2).map { |a, b| b[:s] - a[:s] }
return 60.0 if beat_locations.length == 1
# use interquartile median to discourage outliers
s = beat_periods.length
qrt_lower_idx = (s / 4.0).floor
qrt_upper_idx = qrt_lower_idx * 3
interquartile_beat_periods = beat_periods[qrt_lower_idx..qrt_upper_idx]
# Calculate median
iqs = interquartile_beat_periods.length
iq_median_beat_period = interquartile_beat_periods.sort[(iqs / 2.0).floor - 1]
60.0 / iq_median_beat_period
end
private
def check_for_closed
raise AlreadyClosed if @is_closed
end
def check_for_valid_audio_source(path)
@source.read_pointer
rescue FFI::NullPointerError
raise InvalidAudioInput, %(
Couldn't read file at #{path}
Did you install aubio with libsndfile support?
)
end
end
end
module Aubio
def self.open(path, params = {})
Base.new(path, params)
end
end
|