Skip to content

Instantly share code, notes, and snippets.

@broady
Created January 17, 2018 09:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save broady/1a47e5cba7a39e5c2a695c3ad897f08a to your computer and use it in GitHub Desktop.
Save broady/1a47e5cba7a39e5c2a695c3ad897f08a 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"
"github.com/rakyll/portmidi"
"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/paint"
)
const sampleRate = 44100
const fftSize = 2048
const squareWidth = 100
const smoothing = .2
const minDB = 60
const maxDB = 200
var (
valsMu sync.Mutex
vals []float64
)
func main() {
if err := portmidi.Initialize(); err != nil {
log.Fatal(err)
}
if err := portaudio.Initialize(); err != nil {
log.Fatal(err)
}
go analyze()
driver.Main(func(s screen.Screen) {
w, err := s.NewWindow(&screen.NewWindowOptions{
Width: squareWidth * 3,
Height: squareWidth,
Title: "Viz",
})
check(err)
var paintVals []float64
for {
switch e := w.NextEvent().(type) {
case lifecycle.Event:
if e.To == lifecycle.StageDead {
return
}
case paint.Event:
valsMu.Lock()
copy(paintVals, vals)
valsMu.Unlock()
for i := range vals {
v := uint8((1 - vals[i]) * 255)
w.Fill(image.Rectangle{
Min: image.Point{squareWidth * i, 0},
Max: image.Point{squareWidth + squareWidth*i, squareWidth},
}, color.RGBA{v, v, v, 1}, draw.Src)
}
w.Publish()
w.Send(e)
case error:
log.Print(e)
}
}
})
}
func analyze() {
inputDevice, _ := portaudio.DefaultInputDevice()
log.Print("in from ", inputDevice.Name)
midiID := portmidi.DefaultOutputDeviceID()
midiOut, err := portmidi.NewOutputStream(midiID, 128, 0)
log.Print("out to ", portmidi.Info(midiID).Name)
check(err)
in := make([]int32, fftSize)
inF := make([]float64, fftSize)
buckets := []struct {
Min, Max int
}{
{
Min: freq2Bin(20), Max: freq2Bin(150),
},
{
Min: freq2Bin(150), Max: freq2Bin(450),
},
{
Min: freq2Bin(450), Max: freq2Bin(600),
},
}
thresholds := make([]float64, len(buckets))
tmpVals := make([]float64, len(buckets)) // temp buffer
valsMu.Lock()
vals = make([]float64, len(buckets))
valsMu.Unlock()
// Holds previous values of pxx
prevPxx := make([]float64, fftSize)
p := portaudio.LowLatencyParameters(inputDevice, nil)
p.Input.Channels = 1
p.Output.Channels = 0
p.SampleRate = sampleRate
p.FramesPerBuffer = fftSize
stream, err := portaudio.OpenStream(p, in)
//stream, err := portaudio.OpenDefaultStream(1, 0, sampleRate, fftSize, in)
check(err)
check(stream.Start())
for {
check(stream.Read())
for i := range in {
inF[i] = float64(in[i])
}
pxx, _ := spectral.Pwelch(inF, float64(sampleRate), &spectral.PwelchOptions{
NFFT: fftSize,
})
// 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(minDB, maxDB, 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] = vals[i] * .5 // decay
}
bendVal := int64(vals[i] * (1 << 14))
bendHi := (bendVal >> 7) & 0x7f
bendLo := bendVal & 0x7f
midiOut.WriteShort(0xE0|int64(i), bendLo, bendHi) // Bend lo hi (ch n)
}
valsMu.Unlock()
for i := range thresholds {
thresholds[i] *= .995 // decay
}
}
}
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) int {
return int(freq / (sampleRate / fftSize))
}
func bin2Freq(bin int) float64 {
return float64(bin) * (sampleRate / fftSize)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment