Skip to content

Instantly share code, notes, and snippets.

@xkisu
Last active August 25, 2022 08:51
Show Gist options
  • Save xkisu/b144a2291cc2e7ac8b0a0de43dba7ed7 to your computer and use it in GitHub Desktop.
Save xkisu/b144a2291cc2e7ac8b0a0de43dba7ed7 to your computer and use it in GitHub Desktop.
Golang Audio Autocorrelation
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