Skip to content

Instantly share code, notes, and snippets.

@broady
Created January 17, 2018 07:37
Show Gist options
  • Save broady/a42e8055517549fa7c39846b30aea4db to your computer and use it in GitHub Desktop.
Save broady/a42e8055517549fa7c39846b30aea4db to your computer and use it in GitHub Desktop.
package main
import (
"image"
"image/color"
"image/draw"
"log"
"math"
"sync"
"github.com/gordonklaus/portaudio"
"github.com/mjibson/go-dsp/spectral"
"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/paint"
)
func check(err error) {
if err != nil {
panic(err)
}
}
func lerp(min, max float64, val float64) float64 {
if val < min {
return 0
}
if val > max {
return 1
}
return (val - min) / (max - min)
}
func freq2Bin(freq float64, sampleRate float64, fftSize float64) int {
return int(freq / (sampleRate / fftSize))
}
func bin2Freq(bin int, sampleRate float64, fftSize float64) float64 {
return float64(bin) * (sampleRate / fftSize)
}
var (
valsMu sync.Mutex
vals []float64
)
func main() {
go analyze()
driver.Main(func(s screen.Screen) {
w, err := s.NewWindow(&screen.NewWindowOptions{
Width: 256 * 3,
Height: 256,
Title: "Viz",
})
check(err)
for {
switch e := w.NextEvent().(type) {
case lifecycle.Event:
if e.To == lifecycle.StageDead {
return
}
case paint.Event:
valsMu.Lock()
for i := range vals {
v := uint8((1 - vals[i]) * 255)
w.Fill(image.Rectangle{Min: image.Point{256 * i, 0}, Max: image.Point{256 + 256*i, 256}}, color.RGBA{v, v, v, 1}, draw.Src)
}
valsMu.Unlock()
w.Publish()
w.Send(e)
case error:
log.Print(e)
}
}
})
}
func analyze() {
if err := portaudio.Initialize(); err != nil {
log.Fatal(err)
}
defaultOutput, _ := portaudio.DefaultInputDevice()
log.Print("in from ", defaultOutput.Name)
in := make([]int32, 2048)
inF := make([]float64, 2048)
f2B := func(freq float64) int {
return freq2Bin(freq, 44100, float64(len(in)))
}
buckets := []struct {
Min, Max int
}{
{
Min: f2B(25), Max: f2B(150),
},
{
Min: f2B(150), Max: f2B(450),
},
{
Min: f2B(450), Max: f2B(700),
},
}
thresholds := make([]float64, len(buckets))
tmpVals := make([]float64, len(buckets)) // temp buffer
prevPxx := make([]float64, len(in))
valsMu.Lock()
vals = make([]float64, len(buckets))
valsMu.Unlock()
stream, err := portaudio.OpenDefaultStream(1, 0, 41000, len(in), in)
check(err)
check(stream.Start())
for {
check(stream.Read())
for i := range in {
inF[i] = float64(in[i])
}
pxx, _ := spectral.Pwelch(inF, float64(41000), &spectral.PwelchOptions{
NFFT: len(in),
})
const smoothing = .2
// smooth with previous
for i := range pxx {
pxx[i] = prevPxx[i]*smoothing + pxx[i]*(1-smoothing)
}
prevPxx = pxx
valsMu.Lock()
for i := range vals {
min := buckets[i].Min
max := buckets[i].Max + 1
tmpVals[i] = 0
for n := min; n < max; n++ {
tmpVals[i] += lerp(30, 200, math.Log10(pxx[n])*10) // db min/max
}
tmpVals[i] /= float64(max - min)
if tmpVals[i] > thresholds[i] {
thresholds[i] = tmpVals[i]
// on beat
vals[i] = tmpVals[i]
} else {
vals[i] = 0
}
}
valsMu.Unlock()
for i := range thresholds {
thresholds[i] *= .995 // decay
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment