File: aubio.rb

package info (click to toggle)
ruby-aubio 0.3.6-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, sid, trixie
  • size: 7,236 kB
  • sloc: ruby: 420; sh: 4; makefile: 3
file content (132 lines) | stat: -rw-r--r-- 3,302 bytes parent folder | download | duplicates (2)
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