hanvox 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +0 -0
- data/LICENSE +20 -0
- data/README +12 -0
- data/ROADMAP +0 -0
- data/ext/kissfft/_kiss_fft_guts.h +150 -0
- data/ext/kissfft/extconf.rb +5 -0
- data/ext/kissfft/kiss_fft.c +427 -0
- data/ext/kissfft/kiss_fft.h +123 -0
- data/ext/kissfft/kiss_fft.o +0 -0
- data/ext/kissfft/kiss_fftr.c +159 -0
- data/ext/kissfft/kiss_fftr.h +46 -0
- data/ext/kissfft/kiss_fftr.o +0 -0
- data/ext/kissfft/kissfft.bundle +0 -0
- data/ext/kissfft/main.c +155 -0
- data/ext/kissfft/main.o +0 -0
- data/ext/kissfft/mkmf.log +22 -0
- data/ext/kissfft/sample.data +0 -0
- data/ext/kissfft/test_kissfft.rb +47 -0
- data/lib/hanvox.rb +7 -0
- data/lib/hanvox/proc_audio.rb +244 -0
- data/lib/hanvox/raw.rb +323 -0
- data/lib/hanvox/version.rb +3 -0
- data/lib/signatures.rb +10 -0
- data/lib/signatures/base.rb +23 -0
- data/lib/signatures/dialtone.rb +12 -0
- data/lib/signatures/fax.rb +16 -0
- data/lib/signatures/modem.rb +23 -0
- data/lib/signatures/voice.rb +7 -0
- data/lib/signatures/voicemail.rb +20 -0
- metadata +85 -0
data/ext/kissfft/main.o
ADDED
Binary file
|
@@ -0,0 +1,22 @@
|
|
1
|
+
have_library: checking for main() in -lm... -------------------- yes
|
2
|
+
|
3
|
+
"gcc -o conftest -I/usr/local/rvm/rubies/ruby-1.9.2-p180/include/ruby-1.9.1/x86_64-darwin10.7.1 -I/usr/local/rvm/rubies/ruby-1.9.2-p180/include/ruby-1.9.1/ruby/backward -I/usr/local/rvm/rubies/ruby-1.9.2-p180/include/ruby-1.9.1 -I. -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -O3 -ggdb -Wextra -Wno-unused-parameter -Wno-parentheses -Wpointer-arith -Wwrite-strings -Wno-missing-field-initializers -Wshorten-64-to-32 -Wno-long-long -fno-common -pipe conftest.c -L. -L/usr/local/rvm/rubies/ruby-1.9.2-p180/lib -L. -lruby.1.9.1-static -lpthread -ldl -lobjc "
|
4
|
+
checked program was:
|
5
|
+
/* begin */
|
6
|
+
1: #include "ruby.h"
|
7
|
+
2:
|
8
|
+
3: int main() {return 0;}
|
9
|
+
/* end */
|
10
|
+
|
11
|
+
"gcc -o conftest -I/usr/local/rvm/rubies/ruby-1.9.2-p180/include/ruby-1.9.1/x86_64-darwin10.7.1 -I/usr/local/rvm/rubies/ruby-1.9.2-p180/include/ruby-1.9.1/ruby/backward -I/usr/local/rvm/rubies/ruby-1.9.2-p180/include/ruby-1.9.1 -I. -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -O3 -ggdb -Wextra -Wno-unused-parameter -Wno-parentheses -Wpointer-arith -Wwrite-strings -Wno-missing-field-initializers -Wshorten-64-to-32 -Wno-long-long -fno-common -pipe conftest.c -L. -L/usr/local/rvm/rubies/ruby-1.9.2-p180/lib -L. -lruby.1.9.1-static -lm -lpthread -ldl -lobjc "
|
12
|
+
checked program was:
|
13
|
+
/* begin */
|
14
|
+
1: #include "ruby.h"
|
15
|
+
2:
|
16
|
+
3: /*top*/
|
17
|
+
4: int main() {return 0;}
|
18
|
+
5: int t() { void ((*volatile p)()); p = (void ((*)()))main; return 0; }
|
19
|
+
/* end */
|
20
|
+
|
21
|
+
--------------------
|
22
|
+
|
Binary file
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
base = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
|
4
|
+
$:.unshift(File.join(File.dirname(base)))
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'kissfft'
|
8
|
+
require 'pp'
|
9
|
+
|
10
|
+
#
|
11
|
+
# Simple unit test
|
12
|
+
#
|
13
|
+
|
14
|
+
class KissFFT::UnitTest < Test::Unit::TestCase
|
15
|
+
def test_version
|
16
|
+
assert_equal(String, KissFFT.version.class)
|
17
|
+
puts "KissFFT version: #{KissFFT.version}"
|
18
|
+
end
|
19
|
+
def test_fftr
|
20
|
+
data = File.read('sample.data').unpack('s*')
|
21
|
+
|
22
|
+
min = 1
|
23
|
+
res = KissFFT.fftr(8192, 8000, 1, data)
|
24
|
+
|
25
|
+
tones = {}
|
26
|
+
res.each do |x|
|
27
|
+
rank = x.sort{|a,b| a[1].to_i <=> b[1].to_i }.reverse
|
28
|
+
rank[0..10].each do |t|
|
29
|
+
f = t[0].round
|
30
|
+
p = t[1].round
|
31
|
+
next if f == 0
|
32
|
+
next if p < min
|
33
|
+
tones[ f ] ||= []
|
34
|
+
tones[ f ] << t
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
tones.keys.sort.each do |t|
|
39
|
+
next if tones[t].length < 2
|
40
|
+
puts "#{t}hz"
|
41
|
+
tones[t].each do |x|
|
42
|
+
puts "\t#{x[0]}hz @ #{x[1]}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/lib/hanvox.rb
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
require "kissfft/kissfft"
|
2
|
+
require "signatures"
|
3
|
+
require "tempfile"
|
4
|
+
|
5
|
+
module Hanvox
|
6
|
+
class ProcAudio
|
7
|
+
attr_accessor :channels, :data, :header, :file, :oname, :path, :processors, :results, :sample_count, :save_dir
|
8
|
+
CHUNK_IDS = {:header => "RIFF",
|
9
|
+
:format => "fmt ",
|
10
|
+
:data => "data",
|
11
|
+
:fact => "fact",
|
12
|
+
:silence => "slnt",
|
13
|
+
:cue => "cue ",
|
14
|
+
:playlist => "plst",
|
15
|
+
:list => "list",
|
16
|
+
:label => "labl",
|
17
|
+
:labeled_text => "ltxt",
|
18
|
+
:note => "note",
|
19
|
+
:sample => "smpl",
|
20
|
+
:instrument => "inst" }
|
21
|
+
PACK_CODES = {8 => "C*", 16 => "s*", 32 => "V*"}
|
22
|
+
|
23
|
+
def initialize path, save_dir=nil
|
24
|
+
@channels, @data, @results = [], [], []
|
25
|
+
@header = {}
|
26
|
+
@save_dir = save_dir || `pwd`.gsub("\n", "")
|
27
|
+
@path = path
|
28
|
+
|
29
|
+
@file = File.open path
|
30
|
+
@oname = File.basename path, ".wav"
|
31
|
+
read_header
|
32
|
+
end
|
33
|
+
|
34
|
+
def process opts={}
|
35
|
+
(@header[:channels]).times do |c|
|
36
|
+
c += 1
|
37
|
+
system "sox #{@path} -s #{oname}_#{c}.wav remix #{c}"
|
38
|
+
system "sox #{oname}_#{c}.wav -s #{oname}_#{c}.raw"
|
39
|
+
system "rm -f #{oname}_#{c}.wav"
|
40
|
+
@channels << "#{oname}_#{c}.raw"
|
41
|
+
end
|
42
|
+
|
43
|
+
if opts[:channel].nil?
|
44
|
+
@channels.each do |chan|
|
45
|
+
process_audio chan
|
46
|
+
end
|
47
|
+
else
|
48
|
+
process_audio @channels[opts[:channel]-1]
|
49
|
+
end
|
50
|
+
cleanup
|
51
|
+
end
|
52
|
+
|
53
|
+
def process_audio input
|
54
|
+
bname = File.expand_path(File.dirname(input))
|
55
|
+
num = File.basename(input, ".raw").split("_").last
|
56
|
+
res = {}
|
57
|
+
|
58
|
+
#
|
59
|
+
# Create the signature database
|
60
|
+
#
|
61
|
+
raw = Hanvox::Raw.from_file(input)
|
62
|
+
fft = KissFFT.fftr(8192, 8000, 1, raw.samples)
|
63
|
+
|
64
|
+
freq = raw.to_freq_sig_txt()
|
65
|
+
|
66
|
+
# Save the signature data
|
67
|
+
res[:fprint] = freq
|
68
|
+
|
69
|
+
#
|
70
|
+
# Create a raw decompressed file
|
71
|
+
#
|
72
|
+
|
73
|
+
# Decompress the audio file
|
74
|
+
datfile = Tempfile.new("datfile")
|
75
|
+
|
76
|
+
# Data files for audio processing and signal graph
|
77
|
+
cnt = 0
|
78
|
+
datfile.write(raw.samples.map{|val| cnt +=1; "#{cnt/8000.0} #{val}"}.join("\n"))
|
79
|
+
datfile.flush
|
80
|
+
|
81
|
+
# Data files for spectrum plotting
|
82
|
+
frefile = Tempfile.new("frefile")
|
83
|
+
|
84
|
+
# Calculate the peak frequencies for the sample
|
85
|
+
maxf = 0
|
86
|
+
maxp = 0
|
87
|
+
tones = {}
|
88
|
+
fft.each do |x|
|
89
|
+
rank = x.sort{|a,b| a[1].to_i <=> b[1].to_i }.reverse
|
90
|
+
rank[0..10].each do |t|
|
91
|
+
f = t[0].round
|
92
|
+
p = t[1].round
|
93
|
+
next if f == 0
|
94
|
+
next if p < 1
|
95
|
+
tones[ f ] ||= []
|
96
|
+
tones[ f ] << t
|
97
|
+
if(t[1] > maxp)
|
98
|
+
maxf = t[0]
|
99
|
+
maxp = t[1]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Save the peak frequency
|
105
|
+
res[:peak_freq] = maxf
|
106
|
+
|
107
|
+
# Calculate average frequency and peaks over time
|
108
|
+
avg = {}
|
109
|
+
pks = []
|
110
|
+
pkz = []
|
111
|
+
fft.each do |slot|
|
112
|
+
pks << slot.sort{|a,b| a[1] <=> b[1] }.reverse[0]
|
113
|
+
pkz << slot.sort{|a,b| a[1] <=> b[1] }.reverse[0..9]
|
114
|
+
slot.each do |f|
|
115
|
+
avg[ f[0] ] ||= 0
|
116
|
+
avg[ f[0] ] += f[1]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Save the peak frequencies over time
|
121
|
+
res[:peak_freq_data] = pks.map{|f| "#{f[0]}-#{f[1]}" }.join(" ")
|
122
|
+
|
123
|
+
# Generate the frequency file
|
124
|
+
avg.keys.sort.each do |k|
|
125
|
+
avg[k] = avg[k] / fft.length
|
126
|
+
frefile.write("#{k} #{avg[k]}\n")
|
127
|
+
end
|
128
|
+
frefile.flush
|
129
|
+
|
130
|
+
# Count significant frequencies across the sample
|
131
|
+
fcnt = {}
|
132
|
+
0.step(4000, 5) {|f| fcnt[f] = 0 }
|
133
|
+
pkz.each do |fb|
|
134
|
+
fb.each do |f|
|
135
|
+
fdx = ((f[0] / 5.0).round * 5.0).to_i
|
136
|
+
fcnt[fdx] += 0.1
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
@data << { :raw => raw, :freq => freq, :fcnt => fcnt, :fft => fft,
|
141
|
+
:pks => pks, :pkz => pkz, :maxf => maxf, :maxp => maxp }
|
142
|
+
|
143
|
+
sigs = Signatures::Base.new @data.last
|
144
|
+
res[:line_type] = sigs.process
|
145
|
+
|
146
|
+
# Plot samples to a graph
|
147
|
+
plotter = Tempfile.new("gnuplot")
|
148
|
+
|
149
|
+
plotter.puts("set ylabel \"Signal\"")
|
150
|
+
plotter.puts("set xlabel \"Seconds\"")
|
151
|
+
plotter.puts("set terminal png medium size 640,480 transparent")
|
152
|
+
plotter.puts("set output \"#{save_dir}/#{oname}_#{num}_big.png\"")
|
153
|
+
plotter.puts("plot \"#{datfile.path}\" using 1:2 title \"#{num}\" with lines")
|
154
|
+
plotter.puts("set output \"#{save_dir}/#{oname}_#{num}_big_dots.png\"")
|
155
|
+
plotter.puts("plot \"#{datfile.path}\" using 1:2 title \"#{num}\" with dots")
|
156
|
+
|
157
|
+
plotter.puts("set terminal png medium size 640,480 transparent")
|
158
|
+
plotter.puts("set ylabel \"Power\"")
|
159
|
+
plotter.puts("set xlabel \"Frequency\"")
|
160
|
+
plotter.puts("set output \"#{save_dir}/#{oname}_#{num}_big_freq.png\"")
|
161
|
+
plotter.puts("plot \"#{frefile.path}\" using 1:2 title \"#{num} - Peak #{maxf.round}hz\" with lines")
|
162
|
+
|
163
|
+
plotter.puts("set ylabel \"Signal\"")
|
164
|
+
plotter.puts("set xlabel \"Seconds\"")
|
165
|
+
plotter.puts("set terminal png small size 160,120 transparent")
|
166
|
+
plotter.puts("set format x ''")
|
167
|
+
plotter.puts("set format y ''")
|
168
|
+
plotter.puts("set output \"#{save_dir}/#{oname}_#{num}_sig.png\"")
|
169
|
+
plotter.puts("plot \"#{datfile.path}\" using 1:2 notitle with lines")
|
170
|
+
|
171
|
+
plotter.puts("set ylabel \"Power\"")
|
172
|
+
plotter.puts("set xlabel \"Frequency\"")
|
173
|
+
plotter.puts("set terminal png small size 160,120 transparent")
|
174
|
+
plotter.puts("set format x ''")
|
175
|
+
plotter.puts("set format y ''")
|
176
|
+
plotter.puts("set output \"#{save_dir}/#{oname}_#{num}_sig_freq.png\"")
|
177
|
+
plotter.puts("plot \"#{frefile.path}\" using 1:2 notitle with lines")
|
178
|
+
plotter.flush
|
179
|
+
|
180
|
+
puts `gnuplot #{plotter.path}&`
|
181
|
+
File.unlink(plotter.path)
|
182
|
+
File.unlink(datfile.path)
|
183
|
+
File.unlink(frefile.path)
|
184
|
+
plotter.close
|
185
|
+
datfile.close
|
186
|
+
frefile.path
|
187
|
+
|
188
|
+
@results << res
|
189
|
+
end
|
190
|
+
|
191
|
+
def cleanup
|
192
|
+
@channels.each do |c|
|
193
|
+
system "rm -f #{c}"
|
194
|
+
end
|
195
|
+
@file.close
|
196
|
+
|
197
|
+
true
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
def read_header
|
202
|
+
# Read RIFF header
|
203
|
+
riff_header = @file.sysread(12).unpack("a4Va4")
|
204
|
+
@header[:chunk_id] = riff_header[0]
|
205
|
+
@header[:chunk_size] = riff_header[1]
|
206
|
+
@header[:format] = riff_header[2]
|
207
|
+
|
208
|
+
# Read format subchunk
|
209
|
+
@header[:sub_chunk1_id], @header[:sub_chunk1_size] = read_to_chunk(CHUNK_IDS[:format])
|
210
|
+
format_subchunk_str = @file.sysread(@header[:sub_chunk1_size])
|
211
|
+
format_subchunk = format_subchunk_str.unpack("vvVVvv") # Any extra parameters are ignored
|
212
|
+
@header[:audio_format] = format_subchunk[0]
|
213
|
+
@header[:channels] = format_subchunk[1]
|
214
|
+
@header[:sample_rate] = format_subchunk[2]
|
215
|
+
@header[:byte_rate] = format_subchunk[3]
|
216
|
+
@header[:block_align] = format_subchunk[4]
|
217
|
+
@header[:bits_per_sample] = format_subchunk[5]
|
218
|
+
|
219
|
+
# Read data subchunk
|
220
|
+
@header[:sub_chunk2_id], @header[:sub_chunk2_size] = read_to_chunk(CHUNK_IDS[:data])
|
221
|
+
|
222
|
+
@sample_count = @header[:sub_chunk2_size] / @header[:block_align]
|
223
|
+
end
|
224
|
+
|
225
|
+
def read_to_chunk(expected_chunk_id)
|
226
|
+
chunk_id = @file.sysread(4)
|
227
|
+
chunk_size = @file.sysread(4).unpack("V")[0]
|
228
|
+
|
229
|
+
while chunk_id != expected_chunk_id
|
230
|
+
# Skip chunk
|
231
|
+
file.sysread(chunk_size)
|
232
|
+
|
233
|
+
chunk_id = @file.sysread(4)
|
234
|
+
chunk_size = @file.sysread(4).unpack("V")[0]
|
235
|
+
end
|
236
|
+
|
237
|
+
return chunk_id, chunk_size
|
238
|
+
end
|
239
|
+
|
240
|
+
def format
|
241
|
+
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
data/lib/hanvox/raw.rb
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
module Hanvox
|
2
|
+
class Raw
|
3
|
+
|
4
|
+
@@kissfft_loaded = false
|
5
|
+
begin
|
6
|
+
require 'kissfft'
|
7
|
+
@@kissfft_loaded = true
|
8
|
+
rescue ::LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'zlib'
|
12
|
+
|
13
|
+
##
|
14
|
+
# RAW AUDIO - 8khz little-endian 16-bit signed
|
15
|
+
##
|
16
|
+
|
17
|
+
##
|
18
|
+
# Static methods
|
19
|
+
##
|
20
|
+
|
21
|
+
def self.from_str(str)
|
22
|
+
self.class.new(str)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.from_file(path)
|
26
|
+
if(not path)
|
27
|
+
raise Error, "No audio path specified"
|
28
|
+
end
|
29
|
+
|
30
|
+
if(path == "-")
|
31
|
+
return self.new($stdin.read)
|
32
|
+
end
|
33
|
+
|
34
|
+
if(not File.readable?(path))
|
35
|
+
raise Error, "The specified audio file does not exist"
|
36
|
+
end
|
37
|
+
|
38
|
+
if(path =~ /\.gz$/)
|
39
|
+
return self.new(Zlib::GzipReader.open(path).read)
|
40
|
+
end
|
41
|
+
|
42
|
+
self.new(File.read(path, File.size(path)))
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Class methods
|
47
|
+
##
|
48
|
+
|
49
|
+
attr_accessor :samples
|
50
|
+
|
51
|
+
def initialize(data)
|
52
|
+
self.samples = data.unpack('v*').map do |s|
|
53
|
+
(s > 0x7fff) ? (0x10000 - s) * -1 : s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_raw
|
58
|
+
self.samples.pack("v*")
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_wav
|
62
|
+
raw = self.to_raw
|
63
|
+
wav =
|
64
|
+
"RIFF" +
|
65
|
+
[raw.length + 36].pack("V") +
|
66
|
+
"WAVE" +
|
67
|
+
"fmt " +
|
68
|
+
[16, 1, 1, 8000, 16000, 2, 16 ].pack("VvvVVvv") +
|
69
|
+
"data" +
|
70
|
+
[ raw.length ].pack("V") +
|
71
|
+
raw
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_flow(opts={})
|
75
|
+
|
76
|
+
lo_lim = (opts[:lo_lim] || 100).to_i
|
77
|
+
lo_min = (opts[:lo_min] || 5).to_i
|
78
|
+
hi_min = (opts[:hi_min] || 5).to_i
|
79
|
+
lo_cnt = 0
|
80
|
+
hi_cnt = 0
|
81
|
+
|
82
|
+
data = self.samples.map {|c| c.abs}
|
83
|
+
|
84
|
+
#
|
85
|
+
# Granular hi/low state change list
|
86
|
+
#
|
87
|
+
fprint = []
|
88
|
+
state = :lo
|
89
|
+
idx = 0
|
90
|
+
buff = []
|
91
|
+
|
92
|
+
while (idx < data.length)
|
93
|
+
case state
|
94
|
+
when :lo
|
95
|
+
while(idx < data.length and data[idx] <= lo_lim)
|
96
|
+
buff << data[idx]
|
97
|
+
idx += 1
|
98
|
+
end
|
99
|
+
|
100
|
+
# Ignore any sequence that is too small
|
101
|
+
fprint << [:lo, buff.length, buff - [0]] if buff.length > lo_min
|
102
|
+
state = :hi
|
103
|
+
buff = []
|
104
|
+
next
|
105
|
+
when :hi
|
106
|
+
while(idx < data.length and data[idx] > lo_lim)
|
107
|
+
buff << data[idx]
|
108
|
+
idx += 1
|
109
|
+
end
|
110
|
+
|
111
|
+
# Ignore any sequence that is too small
|
112
|
+
fprint << [:hi, buff.length, buff] if buff.length > hi_min
|
113
|
+
state = :lo
|
114
|
+
buff = []
|
115
|
+
next
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Merge similar blocks
|
121
|
+
#
|
122
|
+
final = []
|
123
|
+
prev = fprint[0]
|
124
|
+
idx = 1
|
125
|
+
|
126
|
+
while(idx < fprint.length)
|
127
|
+
|
128
|
+
if(fprint[idx][0] == prev[0])
|
129
|
+
prev[1] += fprint[idx][1]
|
130
|
+
prev[2] += fprint[idx][2]
|
131
|
+
else
|
132
|
+
final << prev
|
133
|
+
prev = fprint[idx]
|
134
|
+
end
|
135
|
+
|
136
|
+
idx += 1
|
137
|
+
end
|
138
|
+
final << prev
|
139
|
+
|
140
|
+
#
|
141
|
+
# Process results
|
142
|
+
#
|
143
|
+
sig = ""
|
144
|
+
|
145
|
+
final.each do |f|
|
146
|
+
sum = 0
|
147
|
+
f[2].each {|i| sum += i }
|
148
|
+
avg = (sum == 0) ? 0 : sum / f[2].length
|
149
|
+
sig << "#{f[0].to_s.upcase[0,1]},#{f[1]},#{avg} "
|
150
|
+
end
|
151
|
+
|
152
|
+
# Return the results
|
153
|
+
return sig
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_freq(opts={})
|
157
|
+
|
158
|
+
if(not @@kissfft_loaded)
|
159
|
+
raise RuntimeError, "The KissFFT module is not availabale, raw.to_freq() failed"
|
160
|
+
end
|
161
|
+
|
162
|
+
freq_cnt = opts[:frequency_count] || 20
|
163
|
+
|
164
|
+
# Perform a DFT on the samples
|
165
|
+
ffts = KissFFT.fftr(8192, 8000, 1, self.samples)
|
166
|
+
|
167
|
+
self.class.fft_to_freq_sig(ffts, freq_cnt)
|
168
|
+
end
|
169
|
+
|
170
|
+
def to_freq_sig(opts={})
|
171
|
+
fcnt = opts[:frequency_count] || 5
|
172
|
+
|
173
|
+
ffts = []
|
174
|
+
|
175
|
+
# Obtain 20 DFTs for the sample, at 1/20th second offsets into the stream
|
176
|
+
0.upto(19) do |i|
|
177
|
+
ffts[i] = KissFFT.fftr(8192, 8000, 1, self.samples[ i * 400, self.samples.length - (i * 400)])
|
178
|
+
end
|
179
|
+
|
180
|
+
# Create a frequency table at 100hz boundaries
|
181
|
+
f = [ *(0.step(4000, 100)) ]
|
182
|
+
|
183
|
+
# Create a worker method to find the closest frequency
|
184
|
+
barker = Proc.new do |t|
|
185
|
+
t = t.to_i
|
186
|
+
f.sort { |a,b|
|
187
|
+
(a-t).abs <=> (b-t).abs
|
188
|
+
}.first
|
189
|
+
end
|
190
|
+
|
191
|
+
# Map each slice of the audio's FFT with each FFT chunk (8k samples) and then work on it
|
192
|
+
tops = ffts.map{|x| x.map{|y| y.map{|z|
|
193
|
+
|
194
|
+
frq,pwr = z
|
195
|
+
|
196
|
+
# Toss any signals with a strength under 100
|
197
|
+
if pwr < 100.0
|
198
|
+
frq,pwr = [0,0]
|
199
|
+
# Map the signal to the closest offset of 100hz
|
200
|
+
else
|
201
|
+
frq = barker.call(frq)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Make sure the strength is an integer
|
205
|
+
pwr = pwr.to_i
|
206
|
+
|
207
|
+
# Sort by signal strength and take the top fcnt items
|
208
|
+
[frq, pwr]}.sort{|a,b|
|
209
|
+
b[1] <=> a[1]
|
210
|
+
}[0, fcnt].map{|w|
|
211
|
+
# Grab just the frequency (drop the strength)
|
212
|
+
w[0]
|
213
|
+
# Remove any duplicates due to hz mapping
|
214
|
+
}.uniq
|
215
|
+
|
216
|
+
} }
|
217
|
+
|
218
|
+
# Track the generated 4-second chunk signatures
|
219
|
+
sigs = []
|
220
|
+
|
221
|
+
# Expand the list of top frequencies per sample into a flat list of each permutation
|
222
|
+
tops.each do |t|
|
223
|
+
next if t.length < 4
|
224
|
+
0.upto(t.length - 4) { |i| t[i].each { |a| t[i+1].each { |b| t[i+2].each { |c| t[i+3].each { |d| sigs << [a,b,c,d] } } } } }
|
225
|
+
end
|
226
|
+
|
227
|
+
# Dump any duplicate signatures
|
228
|
+
sigs = sigs.uniq
|
229
|
+
|
230
|
+
# Convert each signature into a single 32-bit integer
|
231
|
+
# This is essentially [0-40, 0-40, 0-40, 0-40]
|
232
|
+
sigs.map{|x| x.map{|y| y / 100}.pack("C4").unpack("N")[0] }
|
233
|
+
end
|
234
|
+
|
235
|
+
# Converts a signature to a postgresql integer array (text) format
|
236
|
+
def to_freq_sig_txt(opts={})
|
237
|
+
"{" + to_freq_sig(opts).sort.join(",") + "}"
|
238
|
+
end
|
239
|
+
|
240
|
+
def self.fft_to_freq_sig(ffts, freq_cnt)
|
241
|
+
sig = []
|
242
|
+
ffts.each do |s|
|
243
|
+
res = []
|
244
|
+
maxp = 0
|
245
|
+
maxf = 0
|
246
|
+
s.each do |f|
|
247
|
+
if( f[1] > maxp )
|
248
|
+
maxf,maxp = f
|
249
|
+
end
|
250
|
+
|
251
|
+
if(maxf > 0 and f[1] < maxp and (maxf + 4.5 < f[0]))
|
252
|
+
res << [maxf, maxp]
|
253
|
+
maxf,maxp = [0,0]
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
sig << res.sort{ |a,b| # sort by signal strength
|
258
|
+
a[1] <=> b[1]
|
259
|
+
}.reverse[0,freq_cnt].sort { |a,b| # take the top 20 and sort by frequency
|
260
|
+
a[0] <=> b[0]
|
261
|
+
}.map {|a| [a[0].round, a[1].round ] } # round to whole numbers
|
262
|
+
end
|
263
|
+
|
264
|
+
sig
|
265
|
+
end
|
266
|
+
|
267
|
+
# Find pattern inside of sample
|
268
|
+
def self.compare_freq_sig(pat, zam, opts)
|
269
|
+
|
270
|
+
fuzz_f = opts[:fuzz_f] || 7
|
271
|
+
fuzz_p = opts[:fuzz_p] || 10
|
272
|
+
final = []
|
273
|
+
|
274
|
+
0.upto(zam.length - 1) do |si|
|
275
|
+
res = []
|
276
|
+
sam = zam[si, zam.length]
|
277
|
+
|
278
|
+
0.upto(pat.length - 1) do |pi|
|
279
|
+
diff = []
|
280
|
+
next if not pat[pi]
|
281
|
+
next if pat[pi].length == 0
|
282
|
+
pat[pi].each do |x|
|
283
|
+
next if not sam[pi]
|
284
|
+
next if sam[pi].length == 0
|
285
|
+
sam[pi].each do |y|
|
286
|
+
if(
|
287
|
+
(x[0] - fuzz_f) < y[0] and
|
288
|
+
(x[0] + fuzz_f) > y[0] and
|
289
|
+
(x[1] - fuzz_p) < y[1] and
|
290
|
+
(x[1] + fuzz_p) > y[1]
|
291
|
+
)
|
292
|
+
diff << x
|
293
|
+
break
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
res << diff
|
298
|
+
end
|
299
|
+
next if res.length == 0
|
300
|
+
|
301
|
+
prev = 0
|
302
|
+
rsum = 0
|
303
|
+
ridx = 0
|
304
|
+
res.each_index do |xi|
|
305
|
+
len = res[xi].length
|
306
|
+
if(xi == 0)
|
307
|
+
rsum += (len < 2) ? -40 : +20
|
308
|
+
else
|
309
|
+
rsum += 20 if(prev > 11 and len > 11)
|
310
|
+
rsum += len
|
311
|
+
end
|
312
|
+
prev = len
|
313
|
+
end
|
314
|
+
|
315
|
+
final << [ (rsum / res.length.to_f), res.map {|x| x.length}]
|
316
|
+
end
|
317
|
+
|
318
|
+
final
|
319
|
+
end
|
320
|
+
|
321
|
+
|
322
|
+
end
|
323
|
+
end
|