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 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,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'stft_spectrogram'
4
+ STFTSpectrogram.start
@@ -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: []