stft_spectrogram 1.0.0
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/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: []
|