Class: SPCore::FrequencyDomain

Inherits:
Object
  • Object
show all
Includes:
Hashmake::HashMakeable
Defined in:
lib/spcore/analysis/frequency_domain.rb

Overview

if specified in :fft_format (see FFT_FORMATS for valid values).

Constant Summary collapse

FFT_COMPLEX_VALUED =
:complexValued
FFT_MAGNITUDE_LINEAR =
:magnitudeLinear
FFT_MAGNITUDE_DECIBEL =
:magnitudeDecibel
FFT_FORMATS =

valid values to give for the :fft_format key.

[
  FFT_COMPLEX_VALUED,
  FFT_MAGNITUDE_LINEAR,
  FFT_MAGNITUDE_DECIBEL
]
ARG_SPECS =

define how the class is to be instantiated by hash.

{
  :time_data => arg_spec_array(:reqd => true, :type => Numeric),
  :sample_rate => arg_spec(:reqd => true, :type => Numeric, :validator => ->(a){ a > 0 }),
  :fft_format => arg_spec(:reqd => false, :type => Symbol, :default => FFT_MAGNITUDE_DECIBEL, :validator => ->(a){FFT_FORMATS.include?(a)})
}
GCD =
:gcd
WINDOW =
:window
HARMONIC_SERIES_APPROACHES =
[ GCD, WINDOW ]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ FrequencyDomain

Returns a new instance of FrequencyDomain.



30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/spcore/analysis/frequency_domain.rb', line 30

def initialize args
  hash_make args, FrequencyDomain::ARG_SPECS
  @fft_full = FFT.forward @time_data
  @fft_half = @fft_full[0...(@fft_full.size / 2)]
  
  case(@fft_format)
  when FFT_MAGNITUDE_LINEAR
    @fft_half = @fft_half.map {|x| x.magnitude }
  when FFT_MAGNITUDE_DECIBEL
    @fft_half = @fft_half.map {|x| Gain.linear_to_db x.magnitude }  # in decibels
  end
end

Instance Attribute Details

#fft_formatObject (readonly)

Returns the value of attribute fft_format.



28
29
30
# File 'lib/spcore/analysis/frequency_domain.rb', line 28

def fft_format
  @fft_format
end

#fft_fullObject (readonly)

Returns the value of attribute fft_full.



28
29
30
# File 'lib/spcore/analysis/frequency_domain.rb', line 28

def fft_full
  @fft_full
end

#fft_halfObject (readonly)

Returns the value of attribute fft_half.



28
29
30
# File 'lib/spcore/analysis/frequency_domain.rb', line 28

def fft_half
  @fft_half
end

#sample_rateObject (readonly)

Returns the value of attribute sample_rate.



28
29
30
# File 'lib/spcore/analysis/frequency_domain.rb', line 28

def sample_rate
  @sample_rate
end

#time_dataObject (readonly)

Returns the value of attribute time_data.



28
29
30
# File 'lib/spcore/analysis/frequency_domain.rb', line 28

def time_data
  @time_data
end

Instance Method Details

#freq_to_idx(freq) ⇒ Object

Convert an FFT frequency bin to the corresponding FFT output index



49
50
51
# File 'lib/spcore/analysis/frequency_domain.rb', line 49

def freq_to_idx(freq)
  return (freq * @fft_full.size) / @sample_rate.to_f
end

#harmonic_series(opts = {}) ⇒ Object

Find the strongest harmonic series among the given peak data.

Raises:

  • (ArgumentError)


72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/spcore/analysis/frequency_domain.rb', line 72

def harmonic_series opts = {}
  defaults = { :n_peaks => 8, :min_freq => 40.0, :approach => WINDOW }
  opts = defaults.merge(opts)
  
  n_peaks = opts[:n_peaks]
  min_freq = opts[:min_freq]
  approach = opts[:approach]
  
  raise ArgumentError, "n_peaks is < 1" if n_peaks < 1
  peaks = self.peaks
  
  if peaks.empty?
    return []
  end
  
  max_freq = peaks.keys.max
  max_idx = freq_to_idx(max_freq)
  
  sorted_pairs = peaks.sort_by {|f,m| m}
  top_n_pairs = sorted_pairs.reverse[0, n_peaks]
  
  candidate_series = []
  
  case approach
  when GCD
    for n in 1..n_peaks
      combinations = top_n_pairs.combination(n).to_a
      combinations.each do |combination|
        freq_indices = combination.map {|pair| freq_to_idx(pair[0]) }
        fund_idx = multi_gcd freq_indices
        
        if fund_idx >= freq_to_idx(min_freq)
          series = []
          idx = fund_idx
          while idx <= max_idx
            freq = idx_to_freq(idx)
            #if peaks.has_key? freq
              series.push freq
            #end
            idx += fund_idx
          end
          candidate_series.push series
        end
      end
    end
  when WINDOW
    # look for a harmonic series
    top_n_pairs.each do |pair|
      f_base = pair[0]
      
      min_idx_base = freq_to_idx(f_base) - 0.5
      max_idx_base = min_idx_base + 1.0
      
      harmonic_series = [ f_base ]
      target = 2 * f_base
      min_idx = 2 * min_idx_base
      max_idx = 2 * max_idx_base
      
      while target < max_freq
        f_l = idx_to_freq(min_idx.floor)
        f_h = idx_to_freq(max_idx.ceil)
        window = f_l..f_h
        candidates = peaks.select {|actual,magn| window.include?(actual) }
        
        if candidates.any?
          min = candidates.min_by {|actual,magn| (actual - target).abs }
          harmonic_series.push min[0]
        else
          break
        end
        
        target += f_base
        min_idx += min_idx_base
        max_idx += max_idx_base
      end
      
      candidate_series.push harmonic_series
    end
  else
    raise ArgumentError, "#{approach} approach is not supported"
  end
  
  strongest_series = candidate_series.max_by do |harmonic_series|
    sum = 0
    harmonic_series.each do |f|
      if peaks.has_key?(f)
        sum += peaks[f]**2
      else
        m = @fft_half[freq_to_idx(f)].magnitude
        sum += m**2
      end
    end
    sum
  end
  
  return strongest_series
end

#idx_to_freq(idx) ⇒ Object

Convert an FFT output index to the corresponding frequency bin



44
45
46
# File 'lib/spcore/analysis/frequency_domain.rb', line 44

def idx_to_freq(idx)
  return (idx * @sample_rate.to_f) / @fft_full.size
end

#peaksObject

Find frequency peak values.



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/spcore/analysis/frequency_domain.rb', line 54

def peaks
  # map positive maxima to indices
  positive_maxima = Features.positive_maxima(@fft_half)

  freq_peaks = {}
  positive_maxima.keys.sort.each do |idx|
    freq = idx_to_freq(idx)
    freq_peaks[freq] = positive_maxima[idx]
  end
  
  return freq_peaks
end