hanvox 0.3
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.
- 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
|