stft_spectrogram 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []