Last active
August 25, 2022 08:51
-
-
Save xkisu/b144a2291cc2e7ac8b0a0de43dba7ed7 to your computer and use it in GitHub Desktop.
Golang Audio Autocorrelation
This file contains 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
package main | |
import ( | |
"errors" | |
"math/cmplx" | |
"fmt" | |
"log" | |
"math" | |
"math/rand" | |
"os" | |
"github.com/mjibson/go-dsp/fft" | |
"golang.org/x/exp/constraints" | |
"github.com/go-echarts/go-echarts/v2/charts" | |
"github.com/go-echarts/go-echarts/v2/components" | |
"github.com/go-echarts/go-echarts/v2/opts" | |
"github.com/go-echarts/go-echarts/v2/types" | |
) | |
// AutoCorrelationRaster calculates full spatial auto-correlation on a raster using fft. | |
// | |
// More info: https://dsp.stackexchange.com/a/388 | |
// | |
// Reference implementation: | |
// https://github.com/sevagh/pitch-detection/blob/34b198b661839a5c2dc007103128b6a1725e469d/src/autocorrelation.cpp | |
func AutoCorrelationRaster[T constraints.Float](buffer []T) ([]complex128, []T, error) { | |
if len(buffer) == 0 { | |
return nil, nil, errors.New("buffer cannot be empty") | |
} | |
bufferLength := len(buffer) | |
// Step 1. Convert the real-based daa in the buffer into complex numbers. | |
complexData := make([]complex128, bufferLength) | |
for i := 0; i < len(buffer); i++ { | |
complexData[i] = complex(float64(buffer[i]), 0.0) | |
} | |
// Step 2. Perform a forward fast Fourier transform on the complex data. | |
complexData = fft.FFT(complexData) | |
// Step 3. Multiply the FFT by its [complex conjugate](https://en.wikipedia.org/wiki/Complex_conjugate) | |
var scale = complex(1.0/(float64)(bufferLength*2), 0.0) | |
for i := 0; i < bufferLength; i++ { | |
complexData[i] = complexData[i] * cmplx.Conj(complexData[i]) * scale | |
} | |
// Step 4. Get the inverse FFT to get the autocorrelation. | |
complexData = fft.IFFT(complexData) | |
// Step 5. Convert the data back to real numbers. | |
reals := make([]T, bufferLength) | |
for i := 0; i < bufferLength; i++ { | |
reals[i] = (T)(real(complexData[i])) | |
} | |
return complexData, reals, nil | |
} | |
func generateSine() []float32 { | |
var amplitude float64 = 1 | |
sampleCount := 900 | |
var tau = math.Pi * 2 | |
var frequency float64 = 440 //Hz | |
var angle = tau / float64(sampleCount) | |
samples := make([]float32, sampleCount) | |
for i := 0; i < sampleCount; i++ { | |
samples[i] = float32(amplitude * math.Sin(angle*frequency*float64(i))) | |
} | |
return samples | |
} | |
func generateNoiseSine() []float32 { | |
var amplitude float64 = 1 | |
sampleCount := 900 | |
var tau = math.Pi * 2 | |
var frequency float64 = 440 //Hz | |
var angle = tau / float64(sampleCount) | |
samples := make([]float32, sampleCount) | |
for i := 0; i < sampleCount; i++ { | |
samples[i] = float32(amplitude*math.Sin(angle*frequency*float64(i))) - (rand.Float32() * 0.1) | |
} | |
return samples | |
} | |
func main() { | |
// Swiching between these should produce similar looking autocorrolation graphs. | |
// | |
// If the Autocorrelation graph for the noisey sine is a match to the original sine, | |
// with the rise on the autocorrolated graph maching the dip in the normaly graph, | |
// then it proves it's working correctly. | |
//sine := generateSine() | |
sine := generateNoiseSine() | |
originalChartItems := make([]opts.LineData, 0) | |
var sampleCount []int | |
for i, sample := range sine { | |
originalChartItems = append(originalChartItems, opts.LineData{ | |
Value: sample, | |
}) | |
sampleCount = append(sampleCount, i) | |
} | |
correlationChartItems := make([]opts.LineData, 0) | |
_, ac, err := dsp.AutoCorrelationRaster(sine) | |
if err != nil { | |
panic(err) | |
} | |
fmt.Println("autocorrelation samples:", len(ac)) | |
correlatedSampleCount := make([]int, len(ac)) | |
for i, sample := range ac { | |
correlationChartItems = append(correlationChartItems, opts.LineData{ | |
Value: sample, | |
}) | |
correlatedSampleCount[i] = i | |
} | |
createSignalChart := func(title string) *charts.Line { | |
// create a new line instance | |
line := charts.NewLine() | |
// set some global options like Title/Legend/ToolTip or anything else | |
line.SetGlobalOptions( | |
charts.WithInitializationOpts(opts.Initialization{Theme: types.ThemeWesteros}), | |
charts.WithTitleOpts(opts.Title{ | |
Title: title, | |
}), | |
charts.WithXAxisOpts(opts.XAxis{ | |
Name: "Time", | |
Type: "category", | |
}), | |
charts.WithYAxisOpts(opts.YAxis{ | |
Name: "Signal", | |
Type: "value", | |
}), | |
charts.WithLegendOpts(opts.Legend{ | |
Show: true, | |
Left: "center", | |
Bottom: "0%", | |
})) | |
return line | |
} | |
originalChart := createSignalChart("Original sine wave").SetXAxis(sampleCount). | |
AddSeries("Original", originalChartItems) | |
correlationChart := createSignalChart("Autocorrolated sine wave").SetXAxis(correlatedSampleCount). | |
AddSeries("Autocorrelation", correlationChartItems) | |
page := components.NewPage() | |
page.AddCharts( | |
originalChart, | |
correlationChart) | |
f, err := os.Create("autocorrelation.html") | |
if err != nil { | |
panic(err) | |
} | |
defer f.Close() | |
if err := page.Render(f); err != nil { | |
log.Fatal(err) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment