aubio 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +16 -8
- data/lib/aubio.rb +67 -2
- data/lib/aubio/beats.rb +75 -0
- data/lib/aubio/onsets.rb +25 -12
- data/lib/aubio/pitches.rb +75 -0
- data/lib/aubio/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1acc7f11dab076c36e450cf7a1d4ec1c96aed13f
|
4
|
+
data.tar.gz: 8a47f61e2797d48ea428914328ba32e0f772b630
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf26ded45b899acf39ec1b82e6a373506df549e18f5acc441265e9d914943053988d09fb4c5ce648e184c18c99830ff0192fdbd8e2b9de72f99295dd850d0620
|
7
|
+
data.tar.gz: fb09d46d4a1e70f2a6a5d73b8e5a6f69934b8dc3fc9e9e3a555576c3826290ce853d776fcad1f962f0b48b582fcbc78b33b9a6a23e727b2a3907be267ba2afd3
|
data/README.md
CHANGED
@@ -42,8 +42,8 @@ From there you can access the following:
|
|
42
42
|
|
43
43
|
```
|
44
44
|
my_file.onsets # list of extracted onsets
|
45
|
-
|
46
|
-
|
45
|
+
my_file.pitches # list of extracted pitches
|
46
|
+
my_file.beats # where one would tap their foot
|
47
47
|
# NOT YET IMPLEMENTED # my_file.notes # list of onsets with pitches
|
48
48
|
# NOT YET IMPLEMENTED # my_file.silences # list of silent regions
|
49
49
|
# NOT YET IMPLEMENTED # my_file.mel_frequency_cepstral_coefficients # list of mfccs
|
@@ -51,15 +51,25 @@ my_file.onsets # list of extracted onsets
|
|
51
51
|
|
52
52
|
All of these are Ruby `Enumerator`s which means they'll respond to `.next`, and so on. If you want the whole list all at once call `.to_a` on it.
|
53
53
|
|
54
|
+
### Avoiding memory leaks
|
55
|
+
|
56
|
+
The underlying C library allocates some memory so to clean this up you'll need to run
|
57
|
+
|
58
|
+
my_file.close
|
59
|
+
|
60
|
+
to free it up.
|
61
|
+
|
54
62
|
## Data format
|
55
63
|
|
56
64
|
Each "event" that `aubio` describes is represented as a `Hash` like so:
|
57
65
|
|
58
66
|
```
|
59
|
-
my_file.onsets.first #=> {s: 0.0, ms: 0.0}
|
67
|
+
my_file.onsets.first #=> {s: 0.0, ms: 0.0, start: 1, end: 0}
|
60
68
|
```
|
61
69
|
|
62
|
-
`s` and `ms` refer to seconds and milliseconds respectively.
|
70
|
+
`s` and `ms` refer to seconds and milliseconds respectively.
|
71
|
+
|
72
|
+
`start: 1` and `end: 1` are special events that describe the start and the end of the audio file respectively. Whilst the `start` event at `0.0s` is usually an onset, the `end` is a convenience added by the Ruby wrapper to allow for easier slicing of sound files into samples.
|
63
73
|
|
64
74
|
### Still to implement
|
65
75
|
|
@@ -88,10 +98,8 @@ Aubio.open("/path/to/audio/file", sample_rate: 44100)
|
|
88
98
|
## Bugs / Still to do
|
89
99
|
|
90
100
|
* better tests
|
91
|
-
*
|
92
|
-
*
|
93
|
-
* use `Offsets` class as a basis to implement the other functions
|
94
|
-
* implement class level methods for "bpm"
|
101
|
+
* use `Onsets` class as a basis to implement the other functions
|
102
|
+
* improve accuracy of bpm - seems to be consistently too fast on things I've tried
|
95
103
|
* look into streaming results for live inputs
|
96
104
|
|
97
105
|
## Development
|
data/lib/aubio.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
require_relative "aubio/version"
|
2
2
|
require_relative "aubio/api"
|
3
3
|
require_relative "aubio/onsets"
|
4
|
+
require_relative "aubio/pitches"
|
5
|
+
require_relative "aubio/beats"
|
4
6
|
|
5
7
|
module Aubio
|
6
|
-
class
|
7
|
-
|
8
|
+
class AubioException < Exception; end
|
9
|
+
class FileNotFound < AubioException; end
|
10
|
+
class AlreadyClosed < AubioException; end
|
11
|
+
class InvalidAudioInput < AubioException; end
|
8
12
|
|
13
|
+
class Base
|
9
14
|
def initialize(path, params)
|
10
15
|
raise FileNotFound unless File.file?(path)
|
11
16
|
|
@@ -14,11 +19,71 @@ module Aubio
|
|
14
19
|
|
15
20
|
@source = Api.new_aubio_source(path, sample_rate, hop_size)
|
16
21
|
@params = params
|
22
|
+
|
23
|
+
check_for_valid_audio_source(path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
Api.del_aubio_source(@source)
|
28
|
+
@is_closed = true
|
17
29
|
end
|
18
30
|
|
19
31
|
def onsets
|
32
|
+
check_for_closed
|
33
|
+
|
20
34
|
Onsets.new(@source, @params).each
|
21
35
|
end
|
36
|
+
|
37
|
+
def pitches
|
38
|
+
check_for_closed
|
39
|
+
|
40
|
+
Pitches.new(@source, @params).each
|
41
|
+
end
|
42
|
+
|
43
|
+
def beats
|
44
|
+
check_for_closed
|
45
|
+
|
46
|
+
Beats.new(@source, @params).each
|
47
|
+
end
|
48
|
+
|
49
|
+
def bpm
|
50
|
+
check_for_closed
|
51
|
+
|
52
|
+
beat_locations = Beats.new(@source, @params).each.to_a
|
53
|
+
beat_periods = beat_locations.each_cons(2).map {|a,b| b[:s] - a[:s] }
|
54
|
+
|
55
|
+
# use interquartile median to discourage outliers
|
56
|
+
s = beat_periods.length
|
57
|
+
qrt_lower_idx = (s/4.0).floor
|
58
|
+
qrt_upper_idx = qrt_lower_idx * 3
|
59
|
+
interquartile_beat_periods = beat_periods[qrt_lower_idx..qrt_upper_idx]
|
60
|
+
|
61
|
+
# Calculate median
|
62
|
+
iqs = interquartile_beat_periods.length
|
63
|
+
|
64
|
+
# debug
|
65
|
+
interquartile_beat_periods.sort.each {|x| puts x }
|
66
|
+
|
67
|
+
iq_median_beat_period = interquartile_beat_periods.sort[(iqs/2.0).floor() - 1]
|
68
|
+
60.0 / iq_median_beat_period
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def check_for_closed
|
73
|
+
raise AlreadyClosed if @is_closed
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_for_valid_audio_source(path)
|
77
|
+
begin
|
78
|
+
@source.read_pointer
|
79
|
+
rescue FFI::NullPointerError
|
80
|
+
raise InvalidAudioInput.new(%Q{
|
81
|
+
|
82
|
+
Couldn't read file at #{path}
|
83
|
+
Did you install aubio with libsndfile support?
|
84
|
+
})
|
85
|
+
end
|
86
|
+
end
|
22
87
|
end
|
23
88
|
end
|
24
89
|
|
data/lib/aubio/beats.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
module Aubio
|
2
|
+
class Beats
|
3
|
+
|
4
|
+
def initialize(aubio_source, params)
|
5
|
+
# TODO: cleanup param dups
|
6
|
+
@sample_rate = params[:sample_rate] || 44100
|
7
|
+
@window_size = params[:window_size] || 1024
|
8
|
+
@hop_size = params[:hop_size] || 512
|
9
|
+
|
10
|
+
@source = aubio_source
|
11
|
+
@tempo = Api.new_aubio_tempo('specdiff', @window_size, @hop_size, @sample_rate)
|
12
|
+
|
13
|
+
# create output for source
|
14
|
+
@sample_buffer = Api.new_fvec(@hop_size)
|
15
|
+
# create output for beat
|
16
|
+
@out_fvec = Api.new_fvec(1)
|
17
|
+
end
|
18
|
+
|
19
|
+
def each
|
20
|
+
return enum_for(:each) unless block_given?
|
21
|
+
|
22
|
+
total_frames_counter = 0
|
23
|
+
read_buffer = FFI::MemoryPointer.new(:int)
|
24
|
+
|
25
|
+
loop do
|
26
|
+
# Perform tempo calculation
|
27
|
+
Api.aubio_source_do(@source, @sample_buffer, read_buffer)
|
28
|
+
Api.aubio_tempo_do(@tempo, @sample_buffer, @out_fvec)
|
29
|
+
|
30
|
+
# Retrieve result
|
31
|
+
is_beat = Api.fvec_get_sample(@out_fvec, 0)
|
32
|
+
no_of_bytes_read = read_buffer.read_int
|
33
|
+
total_frames_counter += no_of_bytes_read
|
34
|
+
|
35
|
+
if is_beat > 0.0
|
36
|
+
tempo_seconds = Api.aubio_tempo_get_last_s(@tempo)
|
37
|
+
tempo_milliseconds = Api.aubio_tempo_get_last_ms(@tempo)
|
38
|
+
tempo_confidence = Api.aubio_tempo_get_confidence(@tempo)
|
39
|
+
|
40
|
+
output = {
|
41
|
+
:confidence => tempo_confidence,
|
42
|
+
:s => tempo_seconds,
|
43
|
+
:ms => tempo_milliseconds,
|
44
|
+
:start => (tempo_seconds == 0.0 ? 1 : 0),
|
45
|
+
:end => 0
|
46
|
+
}
|
47
|
+
yield output
|
48
|
+
end
|
49
|
+
|
50
|
+
if no_of_bytes_read != @hop_size
|
51
|
+
# there's no more audio to look at
|
52
|
+
|
53
|
+
# Let's output one last tempo to mark the end of the file
|
54
|
+
total_time = total_frames_counter.to_f / @sample_rate.to_f
|
55
|
+
output = {
|
56
|
+
:confidence => 1.0,
|
57
|
+
:s => total_time,
|
58
|
+
:ms => total_time/1000.0,
|
59
|
+
:start => 0,
|
60
|
+
:end => 1
|
61
|
+
}
|
62
|
+
yield output
|
63
|
+
|
64
|
+
# clean up
|
65
|
+
Api.del_aubio_tempo(@tempo)
|
66
|
+
Api.del_fvec(@sample_buffer)
|
67
|
+
Api.del_fvec(@out_fvec)
|
68
|
+
|
69
|
+
break
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
data/lib/aubio/onsets.rb
CHANGED
@@ -22,37 +22,50 @@ module Aubio
|
|
22
22
|
return enum_for(:each) unless block_given?
|
23
23
|
|
24
24
|
total_frames_counter = 0
|
25
|
-
|
25
|
+
read_buffer = FFI::MemoryPointer.new(:int)
|
26
26
|
|
27
27
|
loop do
|
28
28
|
# Perform onset calculation
|
29
|
-
Api.aubio_source_do(@source, @sample_buffer,
|
29
|
+
Api.aubio_source_do(@source, @sample_buffer, read_buffer)
|
30
30
|
Api.aubio_onset_do(@onset, @sample_buffer, @out_fvec)
|
31
31
|
|
32
32
|
# Retrieve result
|
33
33
|
onset_new_peak = Api.fvec_get_sample(@out_fvec, 0)
|
34
|
+
no_of_bytes_read = read_buffer.read_int
|
35
|
+
total_frames_counter += no_of_bytes_read
|
34
36
|
|
35
37
|
if onset_new_peak > 0.0
|
36
38
|
onset_seconds = Api.aubio_onset_get_last_s(@onset)
|
37
39
|
onset_milliseconds = Api.aubio_onset_get_last_ms(@onset)
|
38
|
-
# TODO: implement relative here
|
39
40
|
output = {
|
40
41
|
:s => onset_seconds,
|
41
|
-
:ms => onset_milliseconds
|
42
|
+
:ms => onset_milliseconds,
|
43
|
+
:start => (onset_seconds == 0.0 ? 1 : 0),
|
44
|
+
:end => 0
|
42
45
|
}
|
43
46
|
yield output
|
44
47
|
end
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
if no_of_bytes_read != @hop_size
|
50
|
+
# there's no more audio to look at
|
51
|
+
|
52
|
+
# Let's output one last onset to mark the end of the file
|
53
|
+
total_time = total_frames_counter.to_f / @sample_rate.to_f
|
54
|
+
output = {
|
55
|
+
:s => total_time,
|
56
|
+
:ms => total_time/1000.0,
|
57
|
+
:start => 0,
|
58
|
+
:end => 1
|
59
|
+
}
|
60
|
+
yield output
|
61
|
+
|
49
62
|
# clean up
|
50
|
-
Api.
|
51
|
-
|
52
|
-
|
63
|
+
Api.del_aubio_onset(@onset)
|
64
|
+
Api.del_fvec(@sample_buffer)
|
65
|
+
Api.del_fvec(@out_fvec)
|
53
66
|
|
54
|
-
|
55
|
-
|
67
|
+
break
|
68
|
+
end
|
56
69
|
end
|
57
70
|
end
|
58
71
|
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Aubio
|
2
|
+
class Pitches
|
3
|
+
|
4
|
+
def initialize(aubio_source, params)
|
5
|
+
# TODO: cleanup param dups
|
6
|
+
@sample_rate = params[:sample_rate] || 44100
|
7
|
+
@window_size = params[:window_size] || 1024
|
8
|
+
@hop_size = params[:hop_size] || 512
|
9
|
+
|
10
|
+
@source = aubio_source
|
11
|
+
@pitch = Api.new_aubio_pitch('yin', @window_size, @hop_size, @sample_rate)
|
12
|
+
Api.aubio_pitch_set_unit(@pitch, 'midi')
|
13
|
+
Api.aubio_pitch_set_tolerance(@pitch, 0.8)
|
14
|
+
|
15
|
+
# create output for source
|
16
|
+
@sample_buffer = Api.new_fvec(@hop_size)
|
17
|
+
# create output for pitch and beat
|
18
|
+
@out_fvec = Api.new_fvec(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
return enum_for(:each) unless block_given?
|
23
|
+
|
24
|
+
total_frames_counter = 0
|
25
|
+
read_buffer = FFI::MemoryPointer.new(:int)
|
26
|
+
last_pitch = 0
|
27
|
+
|
28
|
+
loop do
|
29
|
+
# Perform pitch calculation
|
30
|
+
Api.aubio_source_do(@source, @sample_buffer, read_buffer)
|
31
|
+
Api.aubio_pitch_do(@pitch, @sample_buffer, @out_fvec)
|
32
|
+
|
33
|
+
# Retrieve result
|
34
|
+
pitch = Api.fvec_get_sample(@out_fvec, 0)
|
35
|
+
confidence = Api.aubio_pitch_get_confidence(@pitch)
|
36
|
+
no_of_bytes_read = read_buffer.read_int
|
37
|
+
total_frames_counter += no_of_bytes_read
|
38
|
+
|
39
|
+
if (last_pitch - pitch).abs >= 1 and confidence > 0.9
|
40
|
+
output = {
|
41
|
+
:pitch => pitch,
|
42
|
+
:confidence => confidence,
|
43
|
+
:start => (total_frames_counter == 0 ? 1 : 0),
|
44
|
+
:end => 0
|
45
|
+
}
|
46
|
+
yield output
|
47
|
+
end
|
48
|
+
|
49
|
+
last_pitch = pitch
|
50
|
+
|
51
|
+
if no_of_bytes_read != @hop_size
|
52
|
+
# there's no more audio to look at
|
53
|
+
|
54
|
+
# Let's output one last pitch to mark the end of the file
|
55
|
+
total_time = total_frames_counter.to_f / @sample_rate.to_f
|
56
|
+
output = {
|
57
|
+
:pitch => pitch,
|
58
|
+
:confidence => confidence,
|
59
|
+
:start => 0,
|
60
|
+
:end => 1
|
61
|
+
}
|
62
|
+
yield output
|
63
|
+
|
64
|
+
# clean up
|
65
|
+
Api.del_aubio_pitch(@pitch)
|
66
|
+
Api.del_fvec(@sample_buffer)
|
67
|
+
Api.del_fvec(@out_fvec)
|
68
|
+
|
69
|
+
break
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
data/lib/aubio/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aubio
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xavier Riley
|
@@ -89,7 +89,9 @@ files:
|
|
89
89
|
- bin/setup
|
90
90
|
- lib/aubio.rb
|
91
91
|
- lib/aubio/api.rb
|
92
|
+
- lib/aubio/beats.rb
|
92
93
|
- lib/aubio/onsets.rb
|
94
|
+
- lib/aubio/pitches.rb
|
93
95
|
- lib/aubio/version.rb
|
94
96
|
homepage: https://github.com/xavriley/ruby-aubio
|
95
97
|
licenses:
|