audio-fingerprint 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|