audio-fingerprint 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +9 -0
- data/audio-fingerprint.gemspec +24 -0
- data/bin/audio_fingerprint +15 -0
- data/lib/audio_fingerprint.rb +7 -0
- data/lib/audio_fingerprint/fingerprint.rb +74 -0
- data/lib/audio_fingerprint/version.rb +3 -0
- data/lib/audio_fingerprint/wave_file.rb +416 -0
- data/test/test_fingerprint.rb +44 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7676a6ff2be66e896893e5c3eb10646c7f5298ed
|
4
|
+
data.tar.gz: 48b9e6d77ae290b6ec8c7bfef0f203eec29cb36f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5099311c77a1a6b43b20c6a5e02383cd2cfd47e934f2f5060ffbc0c1243b02d9863064670245a75946dfbca2471db975422f4860e3e628083516970c36dad496
|
7
|
+
data.tar.gz: 3faf8c32f9db80c063cc482b1cee21198503e026f91f224beb8f8548e0e1e77ed3ce7c443b6973a6c0a1031354ca87b71061a77882ba26e746ac6b9614f583a5
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'audio_fingerprint/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "audio-fingerprint"
|
8
|
+
spec.version = AudioFingerprint::VERSION
|
9
|
+
spec.authors = ["Rafael Fragoso"]
|
10
|
+
spec.email = ["rafaelfragosom@gmail.com"]
|
11
|
+
spec.summary = %q{Small gem to fingerprint .wav audio files and compare them}
|
12
|
+
spec.description = %q{This gem can fingerprint from small to large pieces of wav audio and run a math to compare them (this is very handy to compare audio notes)}
|
13
|
+
spec.homepage = "https://github.com/rafaelfragosom/audio-fingerprint"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = Dir["{lib,bin,test}/**/*", "Rakefile", "README.rdoc", "*.gemspec"]
|
17
|
+
spec.executables = %w(audio_fingerprint)
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
22
|
+
spec.add_development_dependency "rake", '~> 0'
|
23
|
+
spec.add_dependency "fftw3", '~> 0'
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/this/will/be/replaced/by/rubygems
|
2
|
+
# -*- encoding: binary -*-
|
3
|
+
|
4
|
+
require 'audio_fingerprint/version'
|
5
|
+
require 'audio_fingerprint/fingerprint'
|
6
|
+
|
7
|
+
if ARGV[0].nil?
|
8
|
+
STDERR.puts "Version: #{AudioFingerprint::VERSION}"
|
9
|
+
STDERR.puts "Usage: <input file|url>"
|
10
|
+
exit(1)
|
11
|
+
else
|
12
|
+
f = AudioFingerprint::Fingerprint.new(ARGV[0])
|
13
|
+
f.create_fingerprint
|
14
|
+
STDERR.puts f.fingerprint.inspect
|
15
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'audio_fingerprint/wave_file'
|
2
|
+
require 'fftw3'
|
3
|
+
|
4
|
+
class Float
|
5
|
+
def to_near
|
6
|
+
(self+0.5).to_i
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module AudioFingerprint
|
11
|
+
class Fingerprint
|
12
|
+
|
13
|
+
BITS_PER_RAW_ITEM = 32
|
14
|
+
THRESHOLD = 0.85
|
15
|
+
|
16
|
+
attr_accessor :fingerprint
|
17
|
+
attr_accessor :file
|
18
|
+
|
19
|
+
def initialize(file)
|
20
|
+
@fingerprint = []
|
21
|
+
@file = file
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_fingerprint
|
25
|
+
w = AudioFingerprint::WaveFile.open(@file)
|
26
|
+
samples = w.sample_data[0, [w.sample_rate * 10, w.sample_data.size].min]
|
27
|
+
duration = samples.size / w.sample_rate
|
28
|
+
na = NArray.float(2, samples.size)
|
29
|
+
@fingerprint ||= []
|
30
|
+
|
31
|
+
samples.each_with_index do |v, i|
|
32
|
+
na[0, i - 1] = i.to_f / w.sample_rate.to_f
|
33
|
+
na[1, i - 1] = v
|
34
|
+
end
|
35
|
+
|
36
|
+
fa = FFTW3.fft(na)
|
37
|
+
fa = fa.real.abs
|
38
|
+
|
39
|
+
fa.each do |f|
|
40
|
+
@fingerprint << f
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def compare(fp)
|
45
|
+
max_raw_size = [@fingerprint.size, fp.size].max
|
46
|
+
bit_size = max_raw_size * BITS_PER_RAW_ITEM
|
47
|
+
|
48
|
+
distance = hamming_distance(@fingerprint, fp)
|
49
|
+
|
50
|
+
match = 1 - distance.to_f / bit_size
|
51
|
+
|
52
|
+
if match >= THRESHOLD
|
53
|
+
match
|
54
|
+
else
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def hamming_distance(raw1, raw2)
|
62
|
+
distance = 0
|
63
|
+
|
64
|
+
min_size, max_size = [raw1, raw2].map(&:size).sort
|
65
|
+
|
66
|
+
min_size.times do |i|
|
67
|
+
distance += (raw1[i].to_near ^ raw2[i].to_near).to_s(2).count('1')
|
68
|
+
end
|
69
|
+
|
70
|
+
distance += (max_size - min_size) * BITS_PER_RAW_ITEM
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,416 @@
|
|
1
|
+
###############################################################################
|
2
|
+
### This class was took from the wavefile gem version 0.3.0 to fit my needs ###
|
3
|
+
### https://github.com/jstrait/wavefile #######################################
|
4
|
+
###############################################################################
|
5
|
+
|
6
|
+
module AudioFingerprint
|
7
|
+
class WaveFile
|
8
|
+
CHUNK_ID = "RIFF"
|
9
|
+
FORMAT = "WAVE"
|
10
|
+
FORMAT_CHUNK_ID = "fmt "
|
11
|
+
SUB_CHUNK1_SIZE = 16
|
12
|
+
PCM = 1
|
13
|
+
DATA_CHUNK_ID = "data"
|
14
|
+
HEADER_SIZE = 36
|
15
|
+
|
16
|
+
def initialize(num_channels, sample_rate, bits_per_sample, sample_data = [])
|
17
|
+
if num_channels == :mono
|
18
|
+
@num_channels = 1
|
19
|
+
elsif num_channels == :stereo
|
20
|
+
@num_channels = 2
|
21
|
+
else
|
22
|
+
@num_channels = num_channels
|
23
|
+
end
|
24
|
+
@sample_rate = sample_rate
|
25
|
+
@bits_per_sample = bits_per_sample
|
26
|
+
@sample_data = sample_data
|
27
|
+
|
28
|
+
@byte_rate = sample_rate * @num_channels * (bits_per_sample / 8)
|
29
|
+
@block_align = @num_channels * (bits_per_sample / 8)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.open(path)
|
33
|
+
file = File.open(path, "rb")
|
34
|
+
|
35
|
+
begin
|
36
|
+
header = read_header(file)
|
37
|
+
errors = validate_header(header)
|
38
|
+
|
39
|
+
if errors == []
|
40
|
+
sample_data = read_sample_data(file,
|
41
|
+
header[:num_channels],
|
42
|
+
header[:bits_per_sample],
|
43
|
+
header[:sub_chunk2_size])
|
44
|
+
|
45
|
+
wave_file = self.new(header[:num_channels],
|
46
|
+
header[:sample_rate],
|
47
|
+
header[:bits_per_sample],
|
48
|
+
sample_data)
|
49
|
+
else
|
50
|
+
error_msg = "#{path} can't be opened, due to the following errors:\n"
|
51
|
+
errors.each {|error| error_msg += " * #{error}\n" }
|
52
|
+
raise StandardError, error_msg
|
53
|
+
end
|
54
|
+
rescue EOFError
|
55
|
+
raise StandardError, "An error occured while reading #{path}."
|
56
|
+
ensure
|
57
|
+
file.close()
|
58
|
+
end
|
59
|
+
|
60
|
+
return wave_file
|
61
|
+
end
|
62
|
+
|
63
|
+
def save(path)
|
64
|
+
# All numeric values should be saved in little-endian format
|
65
|
+
|
66
|
+
sample_data_size = @sample_data.length * @num_channels * (@bits_per_sample / 8)
|
67
|
+
|
68
|
+
# Write the header
|
69
|
+
file_contents = CHUNK_ID
|
70
|
+
file_contents += [HEADER_SIZE + sample_data_size].pack("V")
|
71
|
+
file_contents += FORMAT
|
72
|
+
file_contents += FORMAT_CHUNK_ID
|
73
|
+
file_contents += [SUB_CHUNK1_SIZE].pack("V")
|
74
|
+
file_contents += [PCM].pack("v")
|
75
|
+
file_contents += [@num_channels].pack("v")
|
76
|
+
file_contents += [@sample_rate].pack("V")
|
77
|
+
file_contents += [@byte_rate].pack("V")
|
78
|
+
file_contents += [@block_align].pack("v")
|
79
|
+
file_contents += [@bits_per_sample].pack("v")
|
80
|
+
file_contents += DATA_CHUNK_ID
|
81
|
+
file_contents += [sample_data_size].pack("V")
|
82
|
+
|
83
|
+
# Write the sample data
|
84
|
+
if !mono?
|
85
|
+
output_sample_data = []
|
86
|
+
@sample_data.each{|sample|
|
87
|
+
sample.each{|sub_sample|
|
88
|
+
output_sample_data << sub_sample
|
89
|
+
}
|
90
|
+
}
|
91
|
+
else
|
92
|
+
output_sample_data = @sample_data
|
93
|
+
end
|
94
|
+
|
95
|
+
if @bits_per_sample == 8
|
96
|
+
file_contents += output_sample_data.pack("C*")
|
97
|
+
elsif @bits_per_sample == 16
|
98
|
+
file_contents += output_sample_data.pack("s*")
|
99
|
+
else
|
100
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
101
|
+
end
|
102
|
+
|
103
|
+
file = File.open(path, "w")
|
104
|
+
file.syswrite(file_contents)
|
105
|
+
file.close
|
106
|
+
end
|
107
|
+
|
108
|
+
def sample_data()
|
109
|
+
return @sample_data
|
110
|
+
end
|
111
|
+
|
112
|
+
def normalized_sample_data()
|
113
|
+
if @bits_per_sample == 8
|
114
|
+
min_value = 128.0
|
115
|
+
max_value = 127.0
|
116
|
+
midpoint = 128
|
117
|
+
elsif @bits_per_sample == 16
|
118
|
+
min_value = 32768.0
|
119
|
+
max_value = 32767.0
|
120
|
+
midpoint = 0
|
121
|
+
else
|
122
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
123
|
+
end
|
124
|
+
|
125
|
+
if mono?
|
126
|
+
normalized_sample_data = @sample_data.map {|sample|
|
127
|
+
sample -= midpoint
|
128
|
+
if sample < 0
|
129
|
+
sample.to_f / min_value
|
130
|
+
else
|
131
|
+
sample.to_f / max_value
|
132
|
+
end
|
133
|
+
}
|
134
|
+
else
|
135
|
+
normalized_sample_data = @sample_data.map {|sample|
|
136
|
+
sample.map {|sub_sample|
|
137
|
+
sub_sample -= midpoint
|
138
|
+
if sub_sample < 0
|
139
|
+
sub_sample.to_f / min_value
|
140
|
+
else
|
141
|
+
sub_sample.to_f / max_value
|
142
|
+
end
|
143
|
+
}
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
return normalized_sample_data
|
148
|
+
end
|
149
|
+
|
150
|
+
def sample_data=(sample_data)
|
151
|
+
if sample_data.length > 0 && ((mono? && sample_data[0].class == Float) ||
|
152
|
+
(!mono? && sample_data[0][0].class == Float))
|
153
|
+
if @bits_per_sample == 8
|
154
|
+
# Samples in 8-bit wave files are stored as a unsigned byte
|
155
|
+
# Effective values are 0 to 255, midpoint at 128
|
156
|
+
min_value = 128.0
|
157
|
+
max_value = 127.0
|
158
|
+
midpoint = 128
|
159
|
+
elsif @bits_per_sample == 16
|
160
|
+
# Samples in 16-bit wave files are stored as a signed little-endian short
|
161
|
+
# Effective values are -32768 to 32767, midpoint at 0
|
162
|
+
min_value = 32768.0
|
163
|
+
max_value = 32767.0
|
164
|
+
midpoint = 0
|
165
|
+
else
|
166
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
167
|
+
end
|
168
|
+
|
169
|
+
if mono?
|
170
|
+
@sample_data = sample_data.map {|sample|
|
171
|
+
if(sample < 0.0)
|
172
|
+
(sample * min_value).round + midpoint
|
173
|
+
else
|
174
|
+
(sample * max_value).round + midpoint
|
175
|
+
end
|
176
|
+
}
|
177
|
+
else
|
178
|
+
@sample_data = sample_data.map {|sample|
|
179
|
+
sample.map {|sub_sample|
|
180
|
+
if(sub_sample < 0.0)
|
181
|
+
(sub_sample * min_value).round + midpoint
|
182
|
+
else
|
183
|
+
(sub_sample * max_value).round + midpoint
|
184
|
+
end
|
185
|
+
}
|
186
|
+
}
|
187
|
+
end
|
188
|
+
else
|
189
|
+
@sample_data = sample_data
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def mono?()
|
194
|
+
return num_channels == 1
|
195
|
+
end
|
196
|
+
|
197
|
+
def stereo?()
|
198
|
+
return num_channels == 2
|
199
|
+
end
|
200
|
+
|
201
|
+
def reverse()
|
202
|
+
sample_data.reverse!()
|
203
|
+
end
|
204
|
+
|
205
|
+
def duration()
|
206
|
+
total_samples = sample_data.length
|
207
|
+
samples_per_millisecond = @sample_rate / 1000.0
|
208
|
+
samples_per_second = @sample_rate
|
209
|
+
samples_per_minute = samples_per_second * 60
|
210
|
+
samples_per_hour = samples_per_minute * 60
|
211
|
+
hours, minutes, seconds, milliseconds = 0, 0, 0, 0
|
212
|
+
|
213
|
+
if(total_samples >= samples_per_hour)
|
214
|
+
hours = total_samples / samples_per_hour
|
215
|
+
total_samples -= samples_per_hour * hours
|
216
|
+
end
|
217
|
+
|
218
|
+
if(total_samples >= samples_per_minute)
|
219
|
+
minutes = total_samples / samples_per_minute
|
220
|
+
total_samples -= samples_per_minute * minutes
|
221
|
+
end
|
222
|
+
|
223
|
+
if(total_samples >= samples_per_second)
|
224
|
+
seconds = total_samples / samples_per_second
|
225
|
+
total_samples -= samples_per_second * seconds
|
226
|
+
end
|
227
|
+
|
228
|
+
milliseconds = (total_samples / samples_per_millisecond).floor
|
229
|
+
|
230
|
+
return { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
|
231
|
+
end
|
232
|
+
|
233
|
+
def bits_per_sample=(new_bits_per_sample)
|
234
|
+
if new_bits_per_sample != 8 && new_bits_per_sample != 16
|
235
|
+
raise StandardError, "Bits per sample of #{@bits_per_samples} is invalid, only 8 or 16 are supported"
|
236
|
+
end
|
237
|
+
|
238
|
+
if @bits_per_sample == 16 && new_bits_per_sample == 8
|
239
|
+
conversion_func = lambda {|sample|
|
240
|
+
if(sample < 0)
|
241
|
+
(sample / 256) + 128
|
242
|
+
else
|
243
|
+
# Faster to just divide by integer 258?
|
244
|
+
(sample / 258.007874015748031).round + 128
|
245
|
+
end
|
246
|
+
}
|
247
|
+
|
248
|
+
if mono?
|
249
|
+
@sample_data.map! &conversion_func
|
250
|
+
else
|
251
|
+
sample_data.map! {|sample| sample.map! &conversion_func }
|
252
|
+
end
|
253
|
+
elsif @bits_per_sample == 8 && new_bits_per_sample == 16
|
254
|
+
conversion_func = lambda {|sample|
|
255
|
+
sample -= 128
|
256
|
+
if(sample < 0)
|
257
|
+
sample * 256
|
258
|
+
else
|
259
|
+
# Faster to just multiply by integer 258?
|
260
|
+
(sample * 258.007874015748031).round
|
261
|
+
end
|
262
|
+
}
|
263
|
+
|
264
|
+
if mono?
|
265
|
+
@sample_data.map! &conversion_func
|
266
|
+
else
|
267
|
+
sample_data.map! {|sample| sample.map! &conversion_func }
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
@bits_per_sample = new_bits_per_sample
|
272
|
+
end
|
273
|
+
|
274
|
+
def num_channels=(new_num_channels)
|
275
|
+
if new_num_channels == :mono
|
276
|
+
new_num_channels = 1
|
277
|
+
elsif new_num_channels == :stereo
|
278
|
+
new_num_channels = 2
|
279
|
+
end
|
280
|
+
|
281
|
+
# The cases of mono -> stereo and vice-versa are handled in specially,
|
282
|
+
# because those conversion methods are faster than the general methods,
|
283
|
+
# and the large majority of wave files are expected to be either mono or stereo.
|
284
|
+
if @num_channels == 1 && new_num_channels == 2
|
285
|
+
sample_data.map! {|sample| [sample, sample]}
|
286
|
+
elsif @num_channels == 2 && new_num_channels == 1
|
287
|
+
sample_data.map! {|sample| (sample[0] + sample[1]) / 2}
|
288
|
+
elsif @num_channels == 1 && new_num_channels >= 2
|
289
|
+
sample_data.map! {|sample| [].fill(sample, 0, new_num_channels)}
|
290
|
+
elsif @num_channels >= 2 && new_num_channels == 1
|
291
|
+
sample_data.map! {|sample| sample.inject(0) {|sub_sample, sum| sum + sub_sample } / @num_channels }
|
292
|
+
elsif @num_channels > 2 && new_num_channels == 2
|
293
|
+
sample_data.map! {|sample| [sample[0], sample[1]]}
|
294
|
+
end
|
295
|
+
|
296
|
+
@num_channels = new_num_channels
|
297
|
+
end
|
298
|
+
|
299
|
+
def inspect()
|
300
|
+
duration = self.duration()
|
301
|
+
|
302
|
+
result = "Channels: #{@num_channels}\n" +
|
303
|
+
"Sample rate: #{@sample_rate}\n" +
|
304
|
+
"Bits per sample: #{@bits_per_sample}\n" +
|
305
|
+
"Block align: #{@block_align}\n" +
|
306
|
+
"Byte rate: #{@byte_rate}\n" +
|
307
|
+
"Sample count: #{@sample_data.length}\n" +
|
308
|
+
"Duration: #{duration[:hours]}h:#{duration[:minutes]}m:#{duration[:seconds]}s:#{duration[:milliseconds]}ms\n"
|
309
|
+
end
|
310
|
+
|
311
|
+
attr_reader :num_channels, :bits_per_sample, :byte_rate, :block_align
|
312
|
+
attr_accessor :sample_rate
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
def self.read_header(file)
|
317
|
+
header = {}
|
318
|
+
|
319
|
+
# Read RIFF header
|
320
|
+
riff_header = file.sysread(12).unpack("a4Va4")
|
321
|
+
header[:chunk_id] = riff_header[0]
|
322
|
+
header[:chunk_size] = riff_header[1]
|
323
|
+
header[:format] = riff_header[2]
|
324
|
+
|
325
|
+
# Read format subchunk
|
326
|
+
header[:sub_chunk1_id], header[:sub_chunk1_size] = self.read_to_chunk(file, FORMAT_CHUNK_ID)
|
327
|
+
format_subchunk_str = file.sysread(header[:sub_chunk1_size])
|
328
|
+
format_subchunk = format_subchunk_str.unpack("vvVVvv") # Any extra parameters are ignored
|
329
|
+
header[:audio_format] = format_subchunk[0]
|
330
|
+
header[:num_channels] = format_subchunk[1]
|
331
|
+
header[:sample_rate] = format_subchunk[2]
|
332
|
+
header[:byte_rate] = format_subchunk[3]
|
333
|
+
header[:block_align] = format_subchunk[4]
|
334
|
+
header[:bits_per_sample] = format_subchunk[5]
|
335
|
+
|
336
|
+
# Read data subchunk
|
337
|
+
header[:sub_chunk2_id], header[:sub_chunk2_size] = self.read_to_chunk(file, DATA_CHUNK_ID)
|
338
|
+
|
339
|
+
return header
|
340
|
+
end
|
341
|
+
|
342
|
+
def self.read_to_chunk(file, expected_chunk_id)
|
343
|
+
chunk_id = file.sysread(4)
|
344
|
+
chunk_size = file.sysread(4).unpack("V")[0]
|
345
|
+
|
346
|
+
while chunk_id != expected_chunk_id
|
347
|
+
# Skip chunk
|
348
|
+
file.sysread(chunk_size)
|
349
|
+
|
350
|
+
chunk_id = file.sysread(4)
|
351
|
+
chunk_size = file.sysread(4).unpack("V")[0]
|
352
|
+
end
|
353
|
+
|
354
|
+
return chunk_id, chunk_size
|
355
|
+
end
|
356
|
+
|
357
|
+
def self.validate_header(header)
|
358
|
+
errors = []
|
359
|
+
|
360
|
+
unless header[:bits_per_sample] == 8 || header[:bits_per_sample] == 16
|
361
|
+
errors << "Invalid bits per sample of #{header[:bits_per_sample]}. Only 8 and 16 are supported."
|
362
|
+
end
|
363
|
+
|
364
|
+
unless (1..65535) === header[:num_channels]
|
365
|
+
errors << "Invalid number of channels. Must be between 1 and 65535."
|
366
|
+
end
|
367
|
+
|
368
|
+
unless header[:chunk_id] == CHUNK_ID
|
369
|
+
errors << "Unsupported chunk ID: '#{header[:chunk_id]}'"
|
370
|
+
end
|
371
|
+
|
372
|
+
unless header[:format] == FORMAT
|
373
|
+
errors << "Unsupported format: '#{header[:format]}'"
|
374
|
+
end
|
375
|
+
|
376
|
+
unless header[:sub_chunk1_id] == FORMAT_CHUNK_ID
|
377
|
+
errors << "Unsupported chunk id: '#{header[:sub_chunk1_id]}'"
|
378
|
+
end
|
379
|
+
|
380
|
+
unless header[:audio_format] == PCM
|
381
|
+
errors << "Unsupported audio format code: '#{header[:audio_format]}'"
|
382
|
+
end
|
383
|
+
|
384
|
+
unless header[:sub_chunk2_id] == DATA_CHUNK_ID
|
385
|
+
errors << "Unsupported chunk id: '#{header[:sub_chunk2_id]}'"
|
386
|
+
end
|
387
|
+
|
388
|
+
return errors
|
389
|
+
end
|
390
|
+
|
391
|
+
# Assumes that file is "queued up" to the first sample
|
392
|
+
def self.read_sample_data(file, num_channels, bits_per_sample, sample_data_size)
|
393
|
+
if(bits_per_sample == 8)
|
394
|
+
data = file.sysread(sample_data_size).unpack("C*")
|
395
|
+
elsif(bits_per_sample == 16)
|
396
|
+
data = file.sysread(sample_data_size).unpack("s*")
|
397
|
+
else
|
398
|
+
data = []
|
399
|
+
end
|
400
|
+
|
401
|
+
if(num_channels > 1)
|
402
|
+
multichannel_data = []
|
403
|
+
|
404
|
+
i = 0
|
405
|
+
while i < data.length
|
406
|
+
multichannel_data << data[i...(num_channels + i)]
|
407
|
+
i += num_channels
|
408
|
+
end
|
409
|
+
|
410
|
+
data = multichannel_data
|
411
|
+
end
|
412
|
+
|
413
|
+
return data
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'audio_fingerprint/fingerprint'
|
3
|
+
|
4
|
+
class TestFingerprint < Test::Unit::TestCase
|
5
|
+
|
6
|
+
FILE_PATH1 = '/Users/Rafael/Desktop/previsao.wav'
|
7
|
+
FILE_PATH2 = '/Users/Rafael/Desktop/bomdia.wav'
|
8
|
+
|
9
|
+
def test_get_fingerprint
|
10
|
+
# Create the instance
|
11
|
+
f = AudioFingerprint::Fingerprint.new(FILE_PATH1)
|
12
|
+
# Generate Fingerprint
|
13
|
+
f.create_fingerprint
|
14
|
+
# Fingerprint should be generated in a huge array
|
15
|
+
assert_kind_of(Array, f.fingerprint, "The audio fingerprinted was not correct.")
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_compare_true
|
19
|
+
# Create the instance
|
20
|
+
f1 = AudioFingerprint::Fingerprint.new(FILE_PATH1)
|
21
|
+
f2 = AudioFingerprint::Fingerprint.new(FILE_PATH1)
|
22
|
+
# Generate Fingerprint
|
23
|
+
f1.create_fingerprint
|
24
|
+
f2.create_fingerprint
|
25
|
+
|
26
|
+
compare = f1.compare(f2.fingerprint)
|
27
|
+
|
28
|
+
assert_kind_of(Float, compare, "The compare math didn't worked for equal signature.")
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_compare_false
|
32
|
+
# Create the instance
|
33
|
+
f1 = AudioFingerprint::Fingerprint.new(FILE_PATH1)
|
34
|
+
f2 = AudioFingerprint::Fingerprint.new(FILE_PATH2)
|
35
|
+
# Generate Fingerprint
|
36
|
+
f1.create_fingerprint
|
37
|
+
f2.create_fingerprint
|
38
|
+
|
39
|
+
compare = f1.compare(f2.fingerprint)
|
40
|
+
|
41
|
+
assert(!compare, "The compare math didn't worked for a different signature.")
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: audio-fingerprint
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rafael Fragoso
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: fftw3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: This gem can fingerprint from small to large pieces of wav audio and
|
56
|
+
run a math to compare them (this is very handy to compare audio notes)
|
57
|
+
email:
|
58
|
+
- rafaelfragosom@gmail.com
|
59
|
+
executables:
|
60
|
+
- audio_fingerprint
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- Rakefile
|
65
|
+
- audio-fingerprint.gemspec
|
66
|
+
- bin/audio_fingerprint
|
67
|
+
- lib/audio_fingerprint.rb
|
68
|
+
- lib/audio_fingerprint/fingerprint.rb
|
69
|
+
- lib/audio_fingerprint/version.rb
|
70
|
+
- lib/audio_fingerprint/wave_file.rb
|
71
|
+
- test/test_fingerprint.rb
|
72
|
+
homepage: https://github.com/rafaelfragosom/audio-fingerprint
|
73
|
+
licenses:
|
74
|
+
- MIT
|
75
|
+
metadata: {}
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 2.2.2
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: Small gem to fingerprint .wav audio files and compare them
|
96
|
+
test_files:
|
97
|
+
- test/test_fingerprint.rb
|