stft_spectrogram 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/stft_spectrogram +4 -0
- data/lib/audio/audio_file.rb +79 -0
- data/lib/gui/gui.rb +265 -0
- data/lib/plot/plot.rb +84 -0
- data/lib/stft/fft.rb +82 -0
- data/lib/stft/spectrogram.rb +61 -0
- data/lib/stft/stft_slice.rb +68 -0
- data/lib/stft_spectrogram.rb +10 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 99877f8a2002a628db8806a2c1fb849697db4a33
|
4
|
+
data.tar.gz: d4e8f28a846a6bdad15df267554a62b27d5e355c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7c80c44e7c8a2f80505822f4995ec41e847369382b4bf1dafa087b5d28ad6e207f1b131fa48e554f23217f7658a2e6dbed694bac8dc2d3f952ec20a29272e99f
|
7
|
+
data.tar.gz: 4f6ddcc453bf7f1f984941a016b45d553a1ffef472c90f8ac94965ffeea18f5240aa392686152c4f565395e66cffc0a9af3bf11c917aef647baa6f81566b7036
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'wavefile'
|
2
|
+
|
3
|
+
module STFTSpectrogram
|
4
|
+
# Represents wave file
|
5
|
+
class AudioFile
|
6
|
+
include WaveFile
|
7
|
+
|
8
|
+
attr_reader :window_overlap
|
9
|
+
attr_reader :window_size
|
10
|
+
|
11
|
+
SAMPLE_RATE = 441_00
|
12
|
+
|
13
|
+
def initialize(path, wsize = 1, woverlap = 0)
|
14
|
+
unless File.file?(path)
|
15
|
+
raise IOError, "File '" + path + "' does not exist!"
|
16
|
+
end
|
17
|
+
@samples = []
|
18
|
+
self.window_size = wsize
|
19
|
+
self.window_overlap = woverlap
|
20
|
+
@window_overlap = @window_size / 2 if @window_size <= @window_overlap
|
21
|
+
@window_pos = 0
|
22
|
+
read_data(path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def read_data(path)
|
26
|
+
format = Format.new(:mono, :float, SAMPLE_RATE)
|
27
|
+
Reader.new(path, format).each_buffer do |buffer|
|
28
|
+
buffer.samples.each { |s| @samples.push(s) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Nearest power of 2 to num
|
33
|
+
def nearest_pow2(num)
|
34
|
+
return 0 if num <= 0
|
35
|
+
2**Math.log2(num).round
|
36
|
+
end
|
37
|
+
|
38
|
+
def window_overlap=(overlap)
|
39
|
+
@window_overlap = nearest_pow2(overlap * (SAMPLE_RATE / 1000))
|
40
|
+
end
|
41
|
+
|
42
|
+
def window_size=(size)
|
43
|
+
@window_size = nearest_pow2(size * (SAMPLE_RATE / 1000))
|
44
|
+
end
|
45
|
+
|
46
|
+
# Gets window_size of data from audio samples
|
47
|
+
# and moves the window pointer
|
48
|
+
def next_window
|
49
|
+
return [] if end?
|
50
|
+
|
51
|
+
slice = @samples[@window_pos, @window_size]
|
52
|
+
slice.fill(0, slice.length..@window_size - 1)
|
53
|
+
@window_pos += @window_size - @window_overlap
|
54
|
+
|
55
|
+
slice
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.sample_rate
|
59
|
+
SAMPLE_RATE
|
60
|
+
end
|
61
|
+
|
62
|
+
# Resets the window pointer
|
63
|
+
def reset
|
64
|
+
@window_pos = 0
|
65
|
+
end
|
66
|
+
|
67
|
+
# Gets the time corresponding to the window pointers location
|
68
|
+
def current_time
|
69
|
+
(@window_pos + @window_size / 2) / (SAMPLE_RATE / 1000)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Checks if the window pointer is at the end of the file
|
73
|
+
def end?
|
74
|
+
@window_pos + @window_size >= @samples.length
|
75
|
+
end
|
76
|
+
|
77
|
+
private :read_data, :nearest_pow2
|
78
|
+
end
|
79
|
+
end
|
data/lib/gui/gui.rb
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'fox16'
|
2
|
+
require_relative '../stft/spectrogram.rb'
|
3
|
+
require_relative '../plot/plot.rb'
|
4
|
+
require_relative '../audio/audio_file.rb'
|
5
|
+
|
6
|
+
include Fox
|
7
|
+
|
8
|
+
# Main module
|
9
|
+
module STFTSpectrogram
|
10
|
+
# User interface
|
11
|
+
class GUI < FXMainWindow
|
12
|
+
PLOT_TYPE_SPECTROGRAM = 1
|
13
|
+
PLOT_TYPE_TOP_MAGNITUDES = 2
|
14
|
+
MIN_WINDOW_WIDTH = 960
|
15
|
+
MIN_WINDOW_HEIGHT = 960
|
16
|
+
|
17
|
+
def initialize(app)
|
18
|
+
super(app, 'STFT Spectrogram generator', width: 1280, height: 720,
|
19
|
+
opts: DECOR_ALL &
|
20
|
+
~DECOR_SHRINKABLE)
|
21
|
+
|
22
|
+
@plot = Plot.new
|
23
|
+
|
24
|
+
@audio = nil
|
25
|
+
@spectrogram = nil
|
26
|
+
|
27
|
+
vframe1 = FXVerticalFrame.new(self, width: 1280, opts: LAYOUT_FILL)
|
28
|
+
hframe1 = FXHorizontalFrame.new(vframe1, opts: LAYOUT_FILL | FRAME_SUNKEN)
|
29
|
+
@img_spectrum = FXImageView.new(hframe1, opts: LAYOUT_FILL)
|
30
|
+
|
31
|
+
@current_spec_img = nil
|
32
|
+
|
33
|
+
hframe2 = FXHorizontalFrame.new(vframe1)
|
34
|
+
|
35
|
+
vframe_window = FXVerticalFrame.new(hframe2, opts: LAYOUT_FILL)
|
36
|
+
hframe_window_size = FXHorizontalFrame.new(vframe_window,
|
37
|
+
opts: LAYOUT_RIGHT)
|
38
|
+
hframe_window_overlap = FXHorizontalFrame.new(vframe_window,
|
39
|
+
opts: LAYOUT_RIGHT)
|
40
|
+
FXLabel.new(hframe_window_size, 'Window size:')
|
41
|
+
@spin_window_size = FXSpinner.new(hframe_window_size, 10)
|
42
|
+
@spin_window_size.range = 1..(2**31 - 1)
|
43
|
+
@spin_window_size.value = 1
|
44
|
+
FXLabel.new(hframe_window_size, 'ms')
|
45
|
+
FXLabel.new(hframe_window_overlap, 'Window overlap:')
|
46
|
+
@spin_window_overlap = FXSpinner.new(hframe_window_overlap, 10)
|
47
|
+
@spin_window_overlap.range = 0..(2**31 - 1)
|
48
|
+
FXLabel.new(hframe_window_overlap, 'ms')
|
49
|
+
|
50
|
+
FXVerticalSeparator.new(hframe2)
|
51
|
+
|
52
|
+
vframe_freq = FXVerticalFrame.new(hframe2, opts: LAYOUT_FILL)
|
53
|
+
hframe_freq_low = FXHorizontalFrame.new(vframe_freq, opts: LAYOUT_RIGHT)
|
54
|
+
hframe_freq_high = FXHorizontalFrame.new(vframe_freq, opts: LAYOUT_RIGHT)
|
55
|
+
FXLabel.new(hframe_freq_low, 'Low pass:')
|
56
|
+
@spin_freq_low = FXSpinner.new(hframe_freq_low, 10)
|
57
|
+
@spin_freq_low.range = 0..(2**31 - 1)
|
58
|
+
FXLabel.new(hframe_freq_low, 'Hz')
|
59
|
+
FXLabel.new(hframe_freq_high, 'High pass:')
|
60
|
+
@spin_freq_high = FXSpinner.new(hframe_freq_high, 10)
|
61
|
+
@spin_freq_high.range = 0..(2**31 - 1)
|
62
|
+
FXLabel.new(hframe_freq_high, 'Hz')
|
63
|
+
|
64
|
+
FXVerticalSeparator.new(hframe2)
|
65
|
+
|
66
|
+
vframe_wav = FXVerticalFrame.new(hframe2, opts: LAYOUT_FILL)
|
67
|
+
hframe_wav_txt = FXHorizontalFrame.new(vframe_wav, opts: LAYOUT_RIGHT)
|
68
|
+
hframe_wav_btn = FXHorizontalFrame.new(vframe_wav, opts: LAYOUT_RIGHT)
|
69
|
+
FXLabel.new(hframe_wav_txt, 'Loaded wave file:')
|
70
|
+
@txt_wav_file = FXTextField.new(hframe_wav_txt, 60,
|
71
|
+
opts: TEXTFIELD_READONLY)
|
72
|
+
btn_wav_open = FXButton.new(hframe_wav_btn, 'Select WAV file')
|
73
|
+
btn_wav_open.connect(SEL_COMMAND) { open_wav_file }
|
74
|
+
|
75
|
+
FXVerticalSeparator.new(hframe2)
|
76
|
+
|
77
|
+
vframe_plot = FXVerticalFrame.new(hframe2, opts: LAYOUT_FILL)
|
78
|
+
hframe_plot_type = FXHorizontalFrame.new(vframe_plot, opts: LAYOUT_RIGHT)
|
79
|
+
hframe_gen_plot = FXHorizontalFrame.new(vframe_plot, opts: LAYOUT_RIGHT)
|
80
|
+
|
81
|
+
@combo_plot_type = FXComboBox.new(hframe_plot_type, 25,
|
82
|
+
opts: COMBOBOX_STATIC)
|
83
|
+
@combo_plot_type.appendItem('Spectrogram', PLOT_TYPE_SPECTROGRAM)
|
84
|
+
@combo_plot_type.appendItem('Most significant frequencies only',
|
85
|
+
PLOT_TYPE_TOP_MAGNITUDES)
|
86
|
+
|
87
|
+
btn_plot = FXButton.new(hframe_gen_plot, 'Generate plot')
|
88
|
+
btn_plot.connect(SEL_COMMAND) { on_btnplot_pressed }
|
89
|
+
|
90
|
+
FXVerticalSeparator.new(hframe2)
|
91
|
+
|
92
|
+
hframe_save_img = FXHorizontalFrame.new(hframe2, opts: LAYOUT_RIGHT)
|
93
|
+
btn_save_img = FXButton.new(hframe_save_img, 'Save graph')
|
94
|
+
btn_save_img.connect(SEL_COMMAND) { on_save_graph }
|
95
|
+
|
96
|
+
@spin_window_size.connect(SEL_COMMAND) { on_window_changed }
|
97
|
+
@spin_window_overlap.connect(SEL_COMMAND) { on_window_changed }
|
98
|
+
end
|
99
|
+
|
100
|
+
def create
|
101
|
+
super
|
102
|
+
show(PLACEMENT_SCREEN)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def open_wav_file
|
108
|
+
dialog = FXFileDialog.new(self, 'Open WAV File')
|
109
|
+
dialog.patternList = ['WAV Files (*.wav)']
|
110
|
+
dialog.selectMode = SELECTFILE_EXISTING
|
111
|
+
return if dialog.execute.zero?
|
112
|
+
@txt_wav_file.text = ''
|
113
|
+
@spectrogram = nil
|
114
|
+
@audio = load_data(dialog.filename)
|
115
|
+
@txt_wav_file.text = dialog.filename unless @audio.nil?
|
116
|
+
end
|
117
|
+
|
118
|
+
def save_graph(path)
|
119
|
+
getApp.beginWaitCursor do
|
120
|
+
FXFileStream.open(path, FXStreamSave) do |stream|
|
121
|
+
@current_spec_img.savePixels(stream)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_save_graph
|
127
|
+
if @current_spec_img.nil?
|
128
|
+
FXMessageBox.error(self, MBOX_OK, 'No image!',
|
129
|
+
'Generate the graph first!')
|
130
|
+
return
|
131
|
+
end
|
132
|
+
dialog = FXFileDialog.new(self, 'Save Graph')
|
133
|
+
dialog.patternList = ['PNG Files (*.png)']
|
134
|
+
dialog.filename = '*.png'
|
135
|
+
unless dialog.execute.zero?
|
136
|
+
if File.exist? dialog.filename
|
137
|
+
overwrite = FXMessageBox.question(self, MBOX_YES_NO,
|
138
|
+
'File exists!',
|
139
|
+
'Overwrite file?')
|
140
|
+
return 1 if overwrite == MBOX_CLICKED_NO
|
141
|
+
end
|
142
|
+
save_graph(dialog.filename)
|
143
|
+
end
|
144
|
+
1
|
145
|
+
end
|
146
|
+
|
147
|
+
def load_data(path)
|
148
|
+
audio = nil
|
149
|
+
begin
|
150
|
+
audio = AudioFile.new(path)
|
151
|
+
rescue IOError => e
|
152
|
+
FXMessageBox.error(self, MBOX_OK, 'Non-existing file!', e.message)
|
153
|
+
return nil
|
154
|
+
rescue WaveFile::FormatError => e
|
155
|
+
FXMessageBox.error(self, MBOX_OK, 'Bad format!', e.message)
|
156
|
+
return nil
|
157
|
+
end
|
158
|
+
audio
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_spectrogram
|
162
|
+
return nil if @audio.nil?
|
163
|
+
return @spectrogram unless @spectrogram.nil?
|
164
|
+
spectrogram = nil
|
165
|
+
begin
|
166
|
+
spectrogram = Spectrogram.new(@audio, @spin_window_size.value,
|
167
|
+
@spin_window_overlap.value)
|
168
|
+
rescue ArgumentError => e
|
169
|
+
FXMessageBox.error(self, MBOX_OK, 'Bad arguments!', e.message)
|
170
|
+
return nil
|
171
|
+
end
|
172
|
+
spectrogram
|
173
|
+
end
|
174
|
+
|
175
|
+
def on_window_changed
|
176
|
+
@spectrogram = nil
|
177
|
+
end
|
178
|
+
|
179
|
+
def on_btnplot_pressed
|
180
|
+
@spectrogram = create_spectrogram
|
181
|
+
return if @spectrogram.nil?
|
182
|
+
@spectrogram.transform! unless @spectrogram.transformed?
|
183
|
+
@spectrogram.filter(@spin_freq_low.value, @spin_freq_high.value)
|
184
|
+
|
185
|
+
index = @combo_plot_type.currentItem
|
186
|
+
return if index < 0
|
187
|
+
|
188
|
+
type = @combo_plot_type.getItemData(index)
|
189
|
+
|
190
|
+
if type == PLOT_TYPE_SPECTROGRAM
|
191
|
+
plot_spectrogram
|
192
|
+
elsif type == PLOT_TYPE_TOP_MAGNITUDES
|
193
|
+
plot_top_freqs
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def load_generated_image(path)
|
198
|
+
@current_spec_img = FXPNGImage.new(getApp, nil, IMAGE_KEEP |
|
199
|
+
IMAGE_OWNED | IMAGE_SHMP |
|
200
|
+
IMAGE_SHMI)
|
201
|
+
getApp.beginWaitCursor do
|
202
|
+
FXFileStream.open(path, FXStreamLoad) do |stream|
|
203
|
+
@current_spec_img.loadPixels(stream)
|
204
|
+
end
|
205
|
+
@current_spec_img.create
|
206
|
+
@img_spectrum.image = @current_spec_img
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def no_data?(data)
|
211
|
+
if data.empty?
|
212
|
+
FXMessageBox.error(self, MBOX_OK, 'No data!',
|
213
|
+
'Selected slice contains no data!')
|
214
|
+
return true
|
215
|
+
end
|
216
|
+
false
|
217
|
+
end
|
218
|
+
|
219
|
+
def plot_top_freqs
|
220
|
+
freqs = @spectrogram.freqs
|
221
|
+
return if no_data? freqs
|
222
|
+
y = @spectrogram.windows.map { |w| w.strongest_freq[0] / 1000.0 }
|
223
|
+
x = @spectrogram.windows.map(&:time)
|
224
|
+
@plot.xtics = 'auto'
|
225
|
+
@plot.ytics = 'auto'
|
226
|
+
@plot.plot(x, y, @img_spectrum.width, @img_spectrum.height)
|
227
|
+
load_generated_image(@plot.imgfile.path)
|
228
|
+
end
|
229
|
+
|
230
|
+
def plot_spectrogram
|
231
|
+
freqs = @spectrogram.freqs
|
232
|
+
return if no_data? freqs
|
233
|
+
data = @spectrogram.windows.map { |w| w.spectrum.values }
|
234
|
+
|
235
|
+
x = @spectrogram.windows.map { |w| w.time.to_i }.uniq
|
236
|
+
len = data.length / x.length
|
237
|
+
xmap = x.map.with_index do |t, i|
|
238
|
+
'"' + t.to_s + '" ' + (i * len).to_s
|
239
|
+
end
|
240
|
+
len = x.length / [x.length, 15].min
|
241
|
+
xmap = (len - 1).step(xmap.size - 1, len).map { |i| xmap[i] }
|
242
|
+
data = data.transpose
|
243
|
+
len = data.length / freqs.length
|
244
|
+
ymap = freqs.map.with_index do |f, i|
|
245
|
+
'"' + (f / 1000.0).to_s + '" ' + (i * len).to_s
|
246
|
+
end
|
247
|
+
len = freqs.length / [freqs.length, 20].min
|
248
|
+
ymap = (len - 1).step(ymap.size - 1, len).map { |i| ymap[i] }
|
249
|
+
|
250
|
+
@plot.xtics = '(' + xmap.join(', ') + ')'
|
251
|
+
@plot.ytics = '(' + ymap.join(', ') + ')'
|
252
|
+
@plot.plot_matrix(data, @img_spectrum.width, @img_spectrum.height)
|
253
|
+
|
254
|
+
load_generated_image(@plot.imgfile.path)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def self.start
|
259
|
+
FXApp.new do |app|
|
260
|
+
STFTSpectrogram::GUI.new(app)
|
261
|
+
app.create
|
262
|
+
app.run
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
data/lib/plot/plot.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'numo/gnuplot'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module STFTSpectrogram
|
5
|
+
# Creates plots using gnuplot
|
6
|
+
class Plot
|
7
|
+
TEMP_SPECTR_IMG_FILE = 'spec'.freeze
|
8
|
+
TEMP_SPECTR_DAT_FILE = 'spec.dat'.freeze
|
9
|
+
|
10
|
+
attr_reader :imgfile
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@imgfile = Tempfile.new([TEMP_SPECTR_IMG_FILE, '.png'])
|
14
|
+
@imgfile.close
|
15
|
+
set_gnuplot_defaults
|
16
|
+
end
|
17
|
+
|
18
|
+
def set_gnuplot_defaults
|
19
|
+
Numo.gnuplot do
|
20
|
+
set title: ''
|
21
|
+
set palette: 'rgb 34,35,36'
|
22
|
+
set cblabel: 'Magnitude'
|
23
|
+
set xtics: 'auto'
|
24
|
+
set ytics: 'auto'
|
25
|
+
set xlabel: 'Time (s)'
|
26
|
+
set ylabel: 'Frequency (kHz)'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Sets x axis tics
|
31
|
+
def xtics=(str)
|
32
|
+
Numo.gnuplot { set xtics: str }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Sets y axis tics
|
36
|
+
def ytics=(str)
|
37
|
+
Numo.gnuplot { set ytics: str }
|
38
|
+
end
|
39
|
+
|
40
|
+
def write_temp_data(data)
|
41
|
+
datafile = Tempfile.new(TEMP_SPECTR_DAT_FILE)
|
42
|
+
datafile.write(data.map { |row| row.join(' ') }.join("\n"))
|
43
|
+
datafile.close
|
44
|
+
datafile
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_term_str(w, h, fnt_size)
|
48
|
+
'png font arial ' + fnt_size.to_s + ' size ' + w.to_s + ',' + h.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
# Plots data in a matrix format using a temporary file
|
52
|
+
# Data are plotted to 2D with their magnitude being differentiated
|
53
|
+
# by color gradient
|
54
|
+
def plot_matrix(data, w = 1920, h = 1080, fnt_size = 12)
|
55
|
+
tmpfile = write_temp_data(data)
|
56
|
+
outfile = @imgfile.path
|
57
|
+
termstr = build_term_str(w, h, fnt_size)
|
58
|
+
Numo.gnuplot do
|
59
|
+
set term: termstr
|
60
|
+
set output: outfile
|
61
|
+
plot "'" + tmpfile.path + "'", :matrix, w: 'image', t: ''
|
62
|
+
unset :output
|
63
|
+
end
|
64
|
+
tmpfile.unlink
|
65
|
+
end
|
66
|
+
|
67
|
+
# Plots data contained int x and y parameters
|
68
|
+
def plot(x, y, w = 1920, h = 1080, fnt_size = 12)
|
69
|
+
outfile = @imgfile.path
|
70
|
+
termstr = build_term_str(w, h, fnt_size)
|
71
|
+
Numo.gnuplot do
|
72
|
+
set term: termstr
|
73
|
+
set output: outfile
|
74
|
+
set autoscale: 'xfix'
|
75
|
+
set autoscale: 'yfix'
|
76
|
+
set autoscale: 'cbfix'
|
77
|
+
plot x, y, w: 'lines', t: '', lc_rgb: 'red'
|
78
|
+
unset :output
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private :set_gnuplot_defaults
|
83
|
+
end
|
84
|
+
end
|
data/lib/stft/fft.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
|
2
|
+
module STFTSpectrogram
|
3
|
+
# Computes FFT
|
4
|
+
module FFT
|
5
|
+
# Implementation of the Fast Fourier Transform algorithm
|
6
|
+
class FFTComputation
|
7
|
+
# Performs FFT on data
|
8
|
+
# Expects data in complex format - even indexes contain real numbers
|
9
|
+
# odd indexes contain imaginary numbers
|
10
|
+
def perform(data)
|
11
|
+
unless pow2? data.length
|
12
|
+
raise ArgumentError, 'Data array for FFT has to be power of 2 long'
|
13
|
+
end
|
14
|
+
data = reindex(data)
|
15
|
+
danielson_lanzcos(data)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Reindexing part of the algorithm
|
19
|
+
# Data are shifted in preparation
|
20
|
+
# for the Danielson-Lanczos
|
21
|
+
def reindex(data)
|
22
|
+
j = 1
|
23
|
+
(1..data.length - 1).step(2) do |i|
|
24
|
+
data[j], data[i] = data[i], data[j] if j > i
|
25
|
+
data[j - 1], data[i - 1] = data[i - 1], data[j - 1] if j > i
|
26
|
+
m = data.length / 2
|
27
|
+
while m >= 2 && j > m
|
28
|
+
j -= m
|
29
|
+
m /= 2
|
30
|
+
end
|
31
|
+
j += m
|
32
|
+
end
|
33
|
+
data
|
34
|
+
end
|
35
|
+
|
36
|
+
# The Danielson-Lanczos part of the algorithm
|
37
|
+
def danielson_lanzcos(data)
|
38
|
+
mmax = 2
|
39
|
+
n = data.length
|
40
|
+
while n > mmax
|
41
|
+
|
42
|
+
istep = mmax * 2
|
43
|
+
theta = -(2.0 * Math::PI / mmax.to_f)
|
44
|
+
wtemp = Math.sin(theta / 2.0)
|
45
|
+
wpr = -2.0 * wtemp * wtemp
|
46
|
+
wpi = Math.sin(theta)
|
47
|
+
wr = 1.0
|
48
|
+
wi = 0.0
|
49
|
+
(1..mmax - 1).step(2) do |m|
|
50
|
+
(m..n).step(istep) do |i|
|
51
|
+
j = i + mmax
|
52
|
+
tempr = wr * data[j - 1] - wi * data[j]
|
53
|
+
tempi = wr * data[j] + wi * data[j - 1]
|
54
|
+
data[j - 1] = data[i - 1] - tempr
|
55
|
+
data[j] = data[i] - tempi
|
56
|
+
data[i - 1] += tempr
|
57
|
+
data[i] += tempi
|
58
|
+
end
|
59
|
+
wtemp = wr
|
60
|
+
wr += wr * wpr - wi * wpi
|
61
|
+
wi += wi * wpr + wtemp * wpi
|
62
|
+
end
|
63
|
+
mmax = istep
|
64
|
+
end
|
65
|
+
data
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns true is num is a power of 2
|
69
|
+
def pow2?(num)
|
70
|
+
num.to_s(2).count('1') == 1
|
71
|
+
end
|
72
|
+
|
73
|
+
private :reindex, :danielson_lanzcos, :pow2?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Performs FFT on data
|
77
|
+
def self.do_fft(data)
|
78
|
+
fft = FFTComputation.new
|
79
|
+
fft.perform(data)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'stft_slice.rb'
|
2
|
+
require_relative '../audio/audio_file.rb'
|
3
|
+
|
4
|
+
module STFTSpectrogram
|
5
|
+
# Represents time-frequency spectrogram
|
6
|
+
class Spectrogram
|
7
|
+
attr_reader :windows
|
8
|
+
|
9
|
+
def initialize(audio, window_size, window_overlap)
|
10
|
+
if window_size <= window_overlap
|
11
|
+
raise ArgumentError, 'Window size cannot be <= window overlap!'
|
12
|
+
end
|
13
|
+
|
14
|
+
@windows = []
|
15
|
+
@transformed = false
|
16
|
+
audio.window_size = window_size
|
17
|
+
audio.window_overlap = window_overlap
|
18
|
+
split_to_windows(audio)
|
19
|
+
audio.reset
|
20
|
+
end
|
21
|
+
|
22
|
+
def split_to_windows(audio)
|
23
|
+
until audio.end?
|
24
|
+
@windows.push(STFTSlice.new(audio.next_window, audio.current_time))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Performs FFT on all timed data windows
|
29
|
+
def transform!
|
30
|
+
@windows.each(&:do_fft!)
|
31
|
+
@transformed = true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Gets the highest frequency
|
35
|
+
def max_freq
|
36
|
+
return 0 unless transformed?
|
37
|
+
@windows[0].max_freq
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns an array with all frequencies
|
41
|
+
def freqs
|
42
|
+
return [] unless transformed?
|
43
|
+
@windows[0].freqs
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sets low and high frequency filters
|
47
|
+
def filter(low, high)
|
48
|
+
@windows.each do |w|
|
49
|
+
w.low = low
|
50
|
+
w.high = high
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns true if FFT was already performed
|
55
|
+
def transformed?
|
56
|
+
@transformed
|
57
|
+
end
|
58
|
+
|
59
|
+
private :split_to_windows
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require_relative 'fft.rb'
|
2
|
+
require_relative '../audio/audio_file.rb'
|
3
|
+
|
4
|
+
module STFTSpectrogram
|
5
|
+
# Represents a data slic. Performs FFT
|
6
|
+
class STFTSlice
|
7
|
+
attr_reader :data
|
8
|
+
attr_reader :time
|
9
|
+
|
10
|
+
attr_writer :low
|
11
|
+
attr_writer :high
|
12
|
+
|
13
|
+
def initialize(data, time = 0)
|
14
|
+
@spectrum = {}
|
15
|
+
@data = data.product([0]).flatten
|
16
|
+
@time = time / 1000.0 # ms to seconds
|
17
|
+
@low = 0
|
18
|
+
@high = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
# Performs FFT on this data window
|
22
|
+
def do_fft!
|
23
|
+
FFT.do_fft(@data)
|
24
|
+
@data, = @data.drop(2).each_slice((@data.size / 2.0).round).to_a
|
25
|
+
create_spectrum!
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a spectrogram from the transformed data - frequencies
|
29
|
+
# with their magnitudes
|
30
|
+
# Spectrogram is saved in a Hashmap, with frequency as the key
|
31
|
+
def create_spectrum!
|
32
|
+
# l, _r = @data.drop(2).each_slice((@data.size / 2.0).round).to_a
|
33
|
+
@data.each_slice(2).with_index do |(real, img), i|
|
34
|
+
freq = (i * (AudioFile.sample_rate / @data.length))
|
35
|
+
magnitude = Math.sqrt(real * real + img * img)
|
36
|
+
@spectrum[freq] = magnitude
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns strongest frequency with its magnitude
|
41
|
+
def strongest_freq
|
42
|
+
spectrum.max_by { |_k, v| v }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns highest frequency in this time window
|
46
|
+
def max_freq
|
47
|
+
freqs.max
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns frequencies
|
51
|
+
def freqs
|
52
|
+
ret = @spectrum.keys
|
53
|
+
ret.select! { |f| f >= @low } unless @low.zero?
|
54
|
+
ret.select! { |f| f <= @high } unless @high.zero?
|
55
|
+
ret
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns frequencies with their magnitudes in this time window
|
59
|
+
def spectrum
|
60
|
+
ret = @spectrum.dup
|
61
|
+
ret.select! { |f, _m| f >= @low } unless @low.zero?
|
62
|
+
ret.select! { |f, _m| f <= @high } unless @high.zero?
|
63
|
+
ret
|
64
|
+
end
|
65
|
+
|
66
|
+
private :create_spectrum!
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require_relative './audio/audio_file.rb'
|
2
|
+
require_relative './stft/fft.rb'
|
3
|
+
require_relative './stft/spectrogram.rb'
|
4
|
+
require_relative './stft/stft_slice.rb'
|
5
|
+
require_relative './gui/gui.rb'
|
6
|
+
require_relative './plot/plot.rb'
|
7
|
+
|
8
|
+
# Main module
|
9
|
+
module STFTSpectrogram
|
10
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stft_spectrogram
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jakub Javurek
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: numo-gnuplot
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.2'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.2.4
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.2'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.2.4
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: wavefile
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.8'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.8.1
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0.8'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 0.8.1
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: fxruby
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '1.6'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.6.39
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.6'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 1.6.39
|
73
|
+
description: Gui application for creating time-frequency spectrograms from wave files
|
74
|
+
email: javurjak@fit.cvut.cz
|
75
|
+
executables:
|
76
|
+
- stft_spectrogram
|
77
|
+
extensions: []
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- bin/stft_spectrogram
|
81
|
+
- lib/audio/audio_file.rb
|
82
|
+
- lib/gui/gui.rb
|
83
|
+
- lib/plot/plot.rb
|
84
|
+
- lib/stft/fft.rb
|
85
|
+
- lib/stft/spectrogram.rb
|
86
|
+
- lib/stft/stft_slice.rb
|
87
|
+
- lib/stft_spectrogram.rb
|
88
|
+
homepage:
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.6.13
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: App for creating spectrograms
|
112
|
+
test_files: []
|