Last active
March 25, 2026 13:56
-
-
Save ingoogni/e1083ba50073c96f360357f2f1dbae62 to your computer and use it in GitHub Desktop.
Nim Goerzel filters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import strutils | |
| import math | |
| import complex | |
| const | |
| SampleRate = 8000.0 # 8kHz | |
| TargetFrequency = 941.0 # 941 Hz | |
| FFTSize = 205 | |
| N = FFTSize # Block size | |
| type | |
| Sample = uint8 | |
| # Generalized Goertzel filter | |
| GeneralizedGoertzelFilter = object | |
| frequency: float | |
| omega: float # 2*pi*frequencyIndex/N | |
| coeff: float # 2*cos(omega) | |
| complexConstant: Complex64 # exp(-i*omega) | |
| s0, s1, s2: float # State variables | |
| sampleCount: int # number of processed samples | |
| proc initGeneralizedGoertzel(frequency: float): GeneralizedGoertzelFilter = | |
| let | |
| floatN = float(N) | |
| frequencyIndex = frequency * floatN / SampleRate # Can be non-integer | |
| omega = 2.0 * Pi * frequencyIndex / floatN | |
| return GeneralizedGoertzelFilter( | |
| frequency: frequency, | |
| omega: omega, | |
| coeff: 2.0 * cos(omega), | |
| complexConstant: complex64(cos(-omega), sin(-omega)), # exp(-i*omega) | |
| s0: 0.0, | |
| s1: 0.0, | |
| s2: 0.0, | |
| sampleCount: 0 | |
| ) | |
| proc resetGeneralizedGoertzel(gf: var GeneralizedGoertzelFilter) = | |
| ## Reset the filter before processing a new block | |
| gf.s0 = 0.0 | |
| gf.s1 = 0.0 | |
| gf.s2 = 0.0 | |
| gf.sampleCount = 0 | |
| proc processSampleGeneralized(gf: var GeneralizedGoertzelFilter, sample: Sample) = | |
| gf.sampleCount += 1 | |
| # For all but the last sample, update state variables | |
| if gf.sampleCount < N: | |
| gf.s0 = float(sample) + gf.coeff * gf.s1 - gf.s2 | |
| gf.s2 = gf.s1 | |
| gf.s1 = gf.s0 | |
| else: | |
| # Process the last sample differently | |
| gf.s0 = float(sample) + gf.coeff * gf.s1 - gf.s2 | |
| proc getComplexResult(gf: GeneralizedGoertzelFilter): Complex64 = | |
| # Direct calculation from the state variables | |
| result = complex64(gf.s0, 0.0) - complex64(gf.s1 * gf.complexConstant.re, gf.s1 * gf.complexConstant.im) | |
| # Phase correction for non-integer frequency indices | |
| let phaseCorrection = complex64( | |
| cos(-gf.omega * float(N-1)), | |
| sin(-gf.omega * float(N-1)) | |
| ) | |
| result = complex64( | |
| result.re * phaseCorrection.re - result.im * phaseCorrection.im, | |
| result.re * phaseCorrection.im + result.im * phaseCorrection.re | |
| ) | |
| return result | |
| proc processBlockGeneralized(gf: var GeneralizedGoertzelFilter, samples: openarray[Sample]): Complex64 = | |
| gf.resetGeneralizedGoertzel() | |
| for i in 0..<(samples.len-1): | |
| gf.sampleCount += 1 | |
| gf.s0 = float(samples[i]) + gf.coeff * gf.s1 - gf.s2 | |
| gf.s2 = gf.s1 | |
| gf.s1 = gf.s0 | |
| # Process the last sample | |
| gf.sampleCount += 1 | |
| gf.s0 = float(samples[samples.len-1]) + gf.coeff * gf.s1 - gf.s2 | |
| return gf.getComplexResult() | |
| # Process multiple frequencies "at once" | |
| proc goertzelGeneral(samples: openarray[Sample], frequencies: openarray[float]): seq[Complex64] = | |
| result = newSeq[Complex64](frequencies.len) | |
| for i, freq in frequencies: | |
| var gf = initGeneralizedGoertzel(freq) | |
| result[i] = gf.processBlockGeneralized(samples) | |
| #======= | |
| proc generate(testData: var openarray[Sample], frequency: float) = | |
| let step = frequency * ((2.0 * Pi) / SampleRate) | |
| for index in 0..<N: | |
| testData[index] = Sample(100.0 * sin(float(index) * step) + 100.0) | |
| proc demoGeneralizedGoertzel() = | |
| echo "Test Generalized Goertzel algorithm:" | |
| # Create test frequencies with non-integer indices | |
| let testFreqs = [ | |
| TargetFrequency - 250.5, # Non-integer offset | |
| TargetFrequency, | |
| TargetFrequency + 250.5 # Non-integer offset | |
| ] | |
| var testData: array[N, Sample] | |
| # Test each frequency | |
| for freq in testFreqs: | |
| echo "Test frequency: ", freq | |
| # Generate test data | |
| testData.generate(freq) | |
| # Process with generalized Goertzel | |
| var gf = initGeneralizedGoertzel(freq) | |
| let complexResult = gf.processBlockGeneralized(testData) | |
| # Calculate magnitude | |
| let | |
| magnitude = sqrt(complexResult.re * complexResult.re + complexResult.im * complexResult.im) | |
| phase = arctan2(complexResult.im, complexResult.re) | |
| echo " Complex result: re=", complexResult.re, " im=", complexResult.im | |
| echo " Magnitude: ", magnitude | |
| echo " Phase: ", phase, " radians" | |
| echo "" | |
| # Demo to sweep through a range of frequencies including non-integer indices | |
| proc frequencySweepDemo() = | |
| echo "Frequency Sweep with Generalized Goertzel:" | |
| var testData: array[N, Sample] | |
| # Create a range of frequencies to test | |
| var frequencies = newSeq[float]() | |
| var freq = TargetFrequency - 300.0 | |
| while freq <= TargetFrequency + 300.0: | |
| frequencies.add(freq) | |
| freq += 15.25 # Non-integer step | |
| # Generate test data at the target frequency | |
| testData.generate(TargetFrequency) | |
| # Process all frequencies at once | |
| let results = goertzelGeneral(testData, frequencies) | |
| # Display results | |
| for i in 0..<frequencies.len: | |
| let | |
| magnitude = sqrt(results[i].re * results[i].re + results[i].im * results[i].im) | |
| phase = arctan2(results[i].im, results[i].re) | |
| stdout.write "Freq=" & align($frequencies[i], 7, ' ') & " " | |
| stdout.write "Mag=" & align($magnitude, 12, ' ') & " " | |
| echo "Phase=" & align($phase, 8, ' ') | |
| # Call this function to run the original demos and generalized Goertzel demos | |
| proc runGoertzelDemos() = | |
| # Generalized Goertzel demos | |
| demoGeneralizedGoertzel() | |
| frequencySweepDemo() | |
| proc main() = | |
| runGoertzelDemos() | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import math, complex | |
| proc gcd(a, b: int): int = | |
| ## Greatest common divisor using Euclidean algorithm | |
| var x = abs(a) | |
| var y = abs(b) | |
| while y != 0: | |
| (x, y) = (y, x mod y) | |
| return x | |
| proc totient(n: var int): int = | |
| ## Euler's totient function φ(n) | |
| if n == 1: | |
| return 1 | |
| result = n | |
| var i = 2 | |
| while i * i <= n: | |
| if n mod i == 0: | |
| while n mod i == 0: | |
| n = n div i | |
| result = result - (result div i) | |
| i += 1 | |
| if n > 1: | |
| result = result - (result div n) | |
| proc cyclotomicPolynomial(L: var int): seq[int] = | |
| ## Generate coefficients of the L-th cyclotomic polynomial | |
| ## Simplified for the most common cases | |
| case L | |
| of 1: return @[1, -1] | |
| of 2: return @[1, 1] | |
| of 3: return @[1, 1, 1] | |
| of 4: return @[1, 0, 1] | |
| of 5: return @[1, 1, 1, 1, 1] | |
| of 6: return @[1, -1, 1] | |
| of 7: return @[1, 1, 1, 1, 1, 1, 1] | |
| of 8: return @[1, 0, 0, 0, 1] | |
| of 9: return @[1, 0, 0, 1, 0, 0, 1] | |
| of 10: return @[1, -1, 1, -1, 1] | |
| of 12: return @[1, 0, 1, 0, 1] | |
| of 15: return @[1, 1, 0, 0, 0, 0, 1, 1] | |
| of 20: return @[1, 0, 0, 0, 1, 0, 0, 0, 1] | |
| of 24: return @[1, 0, 0, 0, 0, 0, 0, 0, 1] | |
| of 30: return @[1, -1, 0, -1, 0, 1, 0, 1, 0, -1, 0, -1, 1] | |
| else: | |
| # For larger L, create a simple but consistent polynomial | |
| let phiL = totient(L) | |
| result = newSeq[int](phiL + 1) | |
| result[0] = 1 | |
| result[phiL] = 1 | |
| # Fill in some middle coefficients with typical pattern | |
| for i in 1..<phiL: | |
| if i mod 3 == 0: | |
| result[i] = if i mod 2 == 0: 1 else: -1 | |
| return result | |
| proc jcoGoertzelAlgorithm*(samples: seq[float], k: int, N: int): Complex64 = | |
| ## JCO-Goertzel algorithm | |
| ## Parameters: | |
| ## samples - input signal samples | |
| ## k - frequency bin index | |
| ## N - number of samples (should match samples.len) | |
| ## Returns: | |
| ## The complex DFT coefficient Vk | |
| # Ensure N matches the sample length | |
| assert(N == samples.len, "N must match the length of samples") | |
| # Handle special case for k = 0 (DC component) | |
| if k == 0: | |
| var sum = 0.0 | |
| for s in samples: | |
| sum += s | |
| return complex(sum / N.float, 0.0) | |
| # Compute L = ord(W^k_N) = N / gcd(N, k) | |
| let gcdNk = gcd(N, k) | |
| var L = N div gcdNk | |
| let phiL = totient(L) | |
| echo "For k=", k, ", L=", L, ", φ(L)=", phiL | |
| # Special case handling for L=1 (which means W^k_N = 1) | |
| if L == 1: | |
| # Direct calculation using standard DFT definition | |
| result = complex(0.0, 0.0) | |
| let omega = 2.0 * PI * k.float / N.float | |
| for n in 0..<N: | |
| let angle = omega * n.float | |
| result += samples[n] * complex(cos(angle), -sin(angle)) | |
| return result / N.float | |
| # Get the L-th cyclotomic polynomial coefficients | |
| let cycloPoly = cyclotomicPolynomial(L) | |
| # With low φ(L), use the standard Goertzel algorithm | |
| if phiL <= 2: | |
| if k == N div 4: # Special case: cos(2πk/N) = 0, sin(2πk/N) = 1 | |
| var q1, q2: float = 0.0 | |
| for n in 0..<N: | |
| let q0 = samples[n] - q2 | |
| q2 = q1 | |
| q1 = q0 | |
| return complex(q1, -q2) / 2.0 | |
| elif k == N div 2: # Special case: cos(2πk/N) = -1, sin(2πk/N) = 0 | |
| var q1, q2: float = 0.0 | |
| for n in 0..<N: | |
| let q0 = samples[n] + q1 | |
| q2 = q1 | |
| q1 = q0 | |
| return complex(q1, -q2) / 2.0 | |
| else: | |
| let omega = 2.0 * PI * k.float / N.float | |
| let cosine = cos(omega) | |
| let sine = sin(omega) | |
| let coeff = 2.0 * cosine | |
| var q1, q2: float = 0.0 | |
| for n in 0..<N: | |
| let q0 = coeff * q1 - q2 + samples[n] | |
| q2 = q1 | |
| q1 = q0 | |
| let real = q1 - q2 * cosine | |
| let imag = q2 * sine | |
| return complex(real, -imag) / N.float | |
| # For larger φ(L), use JCO algorithm | |
| var state = newSeq[float](phiL) | |
| # Filter using the cyclotomic polynomial | |
| for n in 0..<N: | |
| for i in countdown(phiL-1, 1): | |
| state[i] = state[i-1] | |
| state[0] = samples[n] | |
| # Filter with coefficients from cyclotomic polynomial | |
| for i in 1..<cycloPoly.len: | |
| if cycloPoly[i] != 0: | |
| state[0] -= cycloPoly[i].float * state[i] | |
| # Polynomial at W^k_N | |
| result = complex(0.0, 0.0) | |
| let omega = 2.0 * PI * k.float / N.float | |
| for i in 0..<state.len: | |
| let angle = omega * i.float | |
| result += state[i] * complex(cos(angle), -sin(angle)) | |
| # Scale the result | |
| result = result / N.float.sqrt() | |
| return result | |
| proc jcoGoertzelSpectrum*(samples: seq[float], frequencies: seq[int]): seq[Complex64] = | |
| ## Compute the DFT coefficients for multiple frequency bins using JCO-Goertzel | |
| let N = samples.len | |
| result = newSeq[Complex64](frequencies.len) | |
| for i, k in frequencies: | |
| result[i] = jcoGoertzelAlgorithm(samples, k, N) | |
| when isMainModule: | |
| # Test signal: a sum of 2 sine waves at 440Hz and 1000Hz, sampled at 8000Hz | |
| let sampleRate = 8000.0 | |
| let freq1 = 440.0 | |
| let freq2 = 1000.0 | |
| let duration = 1.0 # seconds | |
| let N = int(sampleRate * duration) | |
| var samples = newSeq[float](N) | |
| for i in 0..<N: | |
| let t = i.float / sampleRate | |
| samples[i] = 0.5 * sin(2.0 * PI * freq1 * t) + 0.3 * sin(2.0 * PI * freq2 * t) | |
| # Bin indices for the frequencies of interest | |
| let bin1 = int(round(freq1 * N.float / sampleRate)) | |
| let bin2 = int(round(freq2 * N.float / sampleRate)) | |
| let frequencies = @[bin1, bin2] | |
| let spectrum = jcoGoertzelSpectrum(samples, frequencies) | |
| echo "Frequency components:" | |
| for i, k in frequencies: | |
| let magnitude = abs(spectrum[i]) | |
| let phase = arctan2(spectrum[i].im, spectrum[i].re) | |
| echo "Bin ", k, " (", k.float * sampleRate / N.float, " Hz): magnitude = ", magnitude, ", phase = ", phase | |
| # Compute and show the expected magnitudes | |
| echo "\nExpected magnitudes:" | |
| echo "440 Hz: ", 0.5 / 2.0 # Half of the amplitude due to DFT properties | |
| echo "1000 Hz: ", 0.3 / 2.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import std/[math, random, sequtils, strformat, strutils] | |
| const | |
| SampleRate = 44100.0 # Hz | |
| N = 1024 # Block size | |
| FFTSize = N | |
| MinFreq = 50.0 # Minimum frequency (Hz) | |
| MaxFreq = 15000.0 # Maximum frequency (Hz) | |
| type | |
| Sample = uint16 | |
| GoertzelFilter = object | |
| frequency: float | |
| coeff: float | |
| sine, cosine: float | |
| q1, q2: float | |
| gain: float | |
| GoertzelFilterBank = object | |
| fftSize: int | |
| octaves: int | |
| nFiltersOct: int | |
| minFreq: float | |
| maxFreq: float | |
| fBank: seq[GoertzelFilter] | |
| proc initGoertzelFilter(frequency: float, fftSize: int): GoertzelFilter = | |
| let | |
| floatN = float(fftSize) | |
| k = frequency * floatN / SampleRate | |
| omega = (2.0 * Pi * k) / floatN | |
| result = GoertzelFilter( | |
| frequency: frequency, | |
| sine: sin(omega), | |
| cosine: cos(omega), | |
| q1: 0.0, | |
| q2: 0.0, | |
| gain: 1.0 # Default gain | |
| ) | |
| result.coeff = 2.0 * result.cosine | |
| proc createHoppingFilter(prev: GoertzelFilter, frequency: float, fftSize:int): GoertzelFilter = | |
| let | |
| floatN = float(fftSize) | |
| prev_k = prev.frequency * floatN / SampleRate | |
| new_k = frequency * floatN / SampleRate | |
| delta_k = new_k - prev_k | |
| delta_omega = (2.0 * Pi * delta_k) / floatN | |
| result = GoertzelFilter( | |
| frequency: frequency, | |
| q1: 0.0, | |
| q2: 0.0, | |
| gain: 1.0 # Default gain | |
| ) | |
| # New sine and cosine using the "hopping method" | |
| result.cosine = prev.cosine * cos(delta_omega) - prev.sine * sin(delta_omega) | |
| result.sine = prev.sine * cos(delta_omega) + prev.cosine * sin(delta_omega) | |
| result.coeff = 2.0 * result.cosine | |
| proc initFilterBank(minFreq: float, octaves: int, nFiltersOct: int, fftSize: int): GoertzelFilterBank= | |
| let numFilters = octaves * nFiltersOct | |
| let maxFreq = minFreq.float * pow(2.0, octaves.float) | |
| result.fftSize = fftSize | |
| result.fbank = newSeq[GoertzelFilter](numFilters) | |
| # Calculate logarithmically spaced frequencies | |
| for i in 0..<numFilters: | |
| let t = float(i) / float(numFilters - 1) | |
| let freq = minFreq * pow(maxFreq / minFreq, t) | |
| if i == 0: | |
| result.fbank[i] = initGoertzelFilter(freq, fftSize) | |
| else: | |
| result.fbank[i] = createHoppingFilter(result.fbank[i-1], freq, fftSize) | |
| proc resetFilterbank(filterbank: var GoertzelFilterBank) = | |
| for i in 0..<filterbank.fbank.len: | |
| filterbank.fbank[i].q1 = 0.0 | |
| filterbank.fbank[i].q2 = 0.0 | |
| # Process a single sample through all filters, applying gains | |
| proc processAllFilters(filterbank: var GoertzelFilterBank, sample: Sample) = | |
| let sampleValue = float(sample) | |
| for i in 0..<filterbank.fbank.len: | |
| # Apply gain from image to the input sample | |
| let gainedSample = sampleValue * filterbank.fbank[i].gain | |
| let q0 = filterbank.fbank[i].coeff * filterbank.fbank[i].q1 - filterbank.fbank[i].q2 + gainedSample | |
| filterbank.fbank[i].q2 = filterbank.fbank[i].q1 | |
| filterbank.fbank[i].q1 = q0 | |
| # Calculate magnitudes for all filters | |
| proc getAllMagnitudes(filterbank: GoertzelFilterBank): seq[float] = | |
| result = newSeq[float](filterbank.fbank.len) | |
| for i in 0..<filterbank.fbank.len: | |
| let magnitudeSquared = filterbank.fbank[i].q1 * filterbank.fbank[i].q1 + | |
| filterbank.fbank[i].q2 * filterbank.fbank[i].q2 - | |
| filterbank.fbank[i].q1 * filterbank.fbank[i].q2 * filterbank.fbank[i].coeff | |
| result[i] = sqrt(magnitudeSquared) | |
| when isMainModule: | |
| randomize() | |
| # Simulate an image to extract data from it and use those in the Goertzel filter | |
| # to filter white noise. | |
| type | |
| GrayscaleImage = object | |
| width, height: int | |
| data: seq[uint16] # 0-255 grayscale values | |
| proc newGrayscaleImage(width, height: int): GrayscaleImage = | |
| result.width = width | |
| result.height = height | |
| result.data = newSeq[uint16](width * height) | |
| proc getPixel(img: GrayscaleImage, x, y: int): uint16 = | |
| if x >= 0 and x < img.width and y >= 0 and y < img.height: | |
| return img.data[y * img.width + x] | |
| return 0 | |
| proc setPixel(img: var GrayscaleImage, x, y: int, value: uint16) = | |
| if x >= 0 and x < img.width and y >= 0 and y < img.height: | |
| img.data[y * img.width + x] = value | |
| proc generateTestImage(width, height: int): GrayscaleImage = | |
| result = newGrayscaleImage(width, height) | |
| for x in 0..<width: | |
| for y in 0..<height: | |
| # Pattern: Sine wave across frequency | |
| let value = 128 + (127.0 * sin(float(y) / float(height) * Pi * 8.0 + float(x) / 10.0)).int | |
| result.setPixel(x, y, uint16(value)) | |
| proc updateGainsFromImageColumn(filterbank: var GoertzelFilterBank, img: GrayscaleImage, column: int) = | |
| let numFilters = filterbank.fbank.len | |
| for i in 0..<numFilters: | |
| # Map filter index to image y-coordinate (invert to make bottom=low freq, top=high freq) | |
| let y = img.height - 1 - int(float(i) / float(numFilters - 1) * float(img.height - 1)) | |
| # Get pixel value and normalize to [0.0, 1.0] | |
| let pixelValue = getPixel(img, column, y) | |
| filterbank.fbank[i].gain = float(pixelValue) / 255.0 | |
| proc generateWhiteNoise(length: int): seq[Sample] = | |
| result = newSeq[Sample](length) | |
| for i in 0..<length: | |
| result[i] = Sample((rand(255)).int) | |
| proc normalizeMagnitudes(magnitudes: seq[float]): seq[float] = | |
| if magnitudes.len == 0: | |
| return @[] | |
| let (_, maxMag) = minmax(magnitudes) | |
| # Avoid division by zero | |
| if maxMag == 0: | |
| return newSeq[float](magnitudes.len) | |
| result = newSeq[float](magnitudes.len) | |
| for i in 0..<magnitudes.len: | |
| result[i] = magnitudes[i] / maxMag | |
| proc visualizeAsciiArt(filterbank: GoertzelFilterBank, magnitudes: seq[float]) = | |
| echo "Frequency | Gain | Magnitude" | |
| echo "-------------------------" | |
| let normMagnitudes = normalizeMagnitudes(magnitudes) | |
| const barWidth = 40 | |
| for i in 0..<filterbank.fbank.len: | |
| if i mod (filterbank.fbank.len div 50) == 0: # Display subset of filters for readability | |
| let | |
| freq = filterbank.fbank[i].frequency | |
| gain = filterbank.fbank[i].gain | |
| mag = normMagnitudes[i] | |
| gainBars = int(gain * (barWidth / 2)) | |
| magBars = int(mag * barWidth) | |
| echo fmt"{freq:7.1f} Hz | {gain:4.2f} {'.'.repeat(gainBars)} | {magnitudes[i]:8.2f} {'#'.repeat(magBars)}" | |
| # Process the white noise signal through filters with gains controlled by an image column | |
| proc processImageColumnFilter(filterbank: var GoertzelFilterBank, img: GrayscaleImage, column: int) = | |
| echo fmt"Processing image column {column}:" | |
| filterbank.updateGainsFromImageColumn(img, column) | |
| let whiteNoise = generateWhiteNoise(filterbank.fftSize) | |
| filterbank.resetFilterbank() | |
| for sample in whiteNoise: | |
| filterbank.processAllFilters(sample) | |
| let magnitudes = filterbank.getAllMagnitudes() | |
| filterbank.visualizeAsciiArt(magnitudes) | |
| proc main() = | |
| # Number of filters (30 per octave) | |
| let octaves = 8 # 8 octaves from ~50Hz to ~15kHz | |
| let filtersPerOctave = 30 | |
| let totalFilters = octaves * filtersPerOctave | |
| var filterbank = initFilterbank( | |
| minFreq = 50.0, | |
| octaves = octaves, | |
| nFiltersOct = filtersPerOctave, | |
| fftSize = 1024 | |
| ) | |
| # Create test "image" (200 frames/columns wide, height == filter count) | |
| let img = generateTestImage(200, totalFilters) | |
| echo "Image-Controlled Hopping Goertzel Filterbank" | |
| echo fmt"Sampling Rate: {SampleRate} Hz" | |
| echo fmt"Frequency Range: {MinFreq} Hz to {MaxFreq} Hz" | |
| echo fmt"Filters: {totalFilters} total ({filtersPerOctave} per octave)" | |
| echo "" | |
| # few columns for demonstration | |
| for col in countup(0, 190, 10): | |
| filterbank.processImageColumnFilter(img, col) | |
| echo "" | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import strutils | |
| import math | |
| const | |
| SampleRate = 8000.0 # 8kHz | |
| TargetFrequency = 941.0 # 941 Hz | |
| FFTSize = 205 | |
| N = FFTSize # Block size | |
| type | |
| Sample = uint8 | |
| GoertzelFilter = object | |
| frequency: float | |
| coeff: float | |
| sine, cosine: float | |
| s0, s1, s2: float | |
| proc initGoertzel(frequency: float): GoertzelFilter = | |
| let | |
| floatN = float(N) | |
| frequencyIndex = int(0.5 + (floatN * TargetFrequency) / SampleRate) | |
| omega = (2.0 * Pi * frequencyIndex.float) / floatN | |
| result = GoertzelFilter( | |
| frequency: frequency, | |
| sine: sin(omega), | |
| cosine: cos(omega), | |
| s0: 0.0, | |
| s1: 0.0, | |
| s2: 0.0, | |
| ) | |
| result.coeff = 2.0 * result.cosine | |
| # Reset before every "block" (size=N) of samples. | |
| proc resetGoertzel(gf: var GoertzelFilter) = | |
| gf.s0 = 0 | |
| gf.s2 = 0 | |
| gf.s1 = 0 | |
| # Every sample. | |
| proc processSample(gf: var GoertzelFilter, sample: Sample) = | |
| gf.s0 = gf.coeff * gf.s1 - gf.s2 + float(sample) | |
| gf.s2 = gf.s1 | |
| gf.s1 = gf.s0 | |
| # After every processed block to get the complex result. | |
| proc getRealImag(gf: GoertzelFilter, realPart, imagPart: var float) = | |
| realPart = (gf.s1 - gf.s2 * gf.cosine) | |
| imagPart = (gf.s2 * gf.sine) | |
| # After every processed block to get the relative magnitude squared. | |
| proc getMagnitudeSquared(gf: GoertzelFilter): float = | |
| result = gf.s1 * gf.s1 + gf.s2 * gf.s2 - gf.s1 * gf.s2 * gf.coeff | |
| #===== | |
| proc generate(testData: var openarray[Sample], frequency: float) = | |
| let step = frequency * ((2.0 * Pi) / SampleRate) | |
| for index in 0..<N: | |
| testData[index] = Sample(100.0 * sin(float(index) * step) + 100.0) | |
| proc generateAndTest(frequency: float) = | |
| echo "For test frequency ", frequency, ":" | |
| var gf = initGoertzel(frequency) | |
| var testData: array[N, Sample] | |
| testData.generate(frequency) | |
| # Process the samples | |
| for index in 0..<N: | |
| gf.processSample(testData[index]) | |
| # Do the "basic Goertzel" processing. | |
| var real, imag: float | |
| gf.getRealImag(real, imag) | |
| echo "real = ", real, " imag = ", imag | |
| let magnitudeSquared = real*real + imag*imag | |
| echo "Relative magnitude squared = ", magnitudeSquared | |
| let magnitude = sqrt(magnitudeSquared) | |
| echo "Relative magnitude = ", magnitude | |
| let magnitudeSquared2 = gf.getMagnitudeSquared() | |
| echo "Relative magnitude squared = ", magnitudeSquared2 | |
| let magnitude2 = sqrt(magnitudeSquared2) | |
| echo "Relative magnitude = ", magnitude2, "\n" | |
| gf.resetGoertzel() | |
| # Demo 2 | |
| proc generateAndTest2(frequency: float) = | |
| var gf = initGoertzel(frequency) | |
| stdout.write "Freq=" & align($frequency, 7, ' ') & " " | |
| var testData: array[N, Sample] | |
| testData.generate(frequency) | |
| # Process the samples. | |
| for index in 0..<N: | |
| gf.processSample(testData[index]) | |
| var real, imag: float | |
| gf.getRealImag(real, imag) | |
| let magnitudeSquared = real*real + imag*imag | |
| stdout.write "rel mag^2=" & align($magnitudeSquared, 16, ' ') & " " | |
| let magnitude = sqrt(magnitudeSquared) | |
| echo "rel mag=" & align($magnitude, 12, ' ') | |
| gf.resetGoertzel() | |
| proc main() = | |
| generateAndTest(TargetFrequency - 250.0) | |
| generateAndTest(TargetFrequency) | |
| generateAndTest(TargetFrequency + 250.0) | |
| # Demo 2 | |
| var freq = TargetFrequency - 300.0 | |
| while freq <= TargetFrequency + 300.0: | |
| generateAndTest2(freq) | |
| freq += 15 | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment