Skip to content

Instantly share code, notes, and snippets.

@zach-klippenstein
Last active September 1, 2022 19:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zach-klippenstein/5eaa2c96c2620b9e9cdb to your computer and use it in GitHub Desktop.
Save zach-klippenstein/5eaa2c96c2620b9e9cdb to your computer and use it in GitHub Desktop.
Terminal program that plays a sine wave.
//usr/local/bin/go run $0 "$@"; exit
/*
Terminal program that plays a sine wave.
The pitch and volume can be controlled (help is shown in the UI).
Installation:
go get github.com/gizak/termui
brew install portaudio
go get code.google.com/p/portaudio-go/portaudio
Run:
# chmod +x audiowave.go
./audiowave.go
*/
package main
import (
"fmt"
"math"
"github.com/gizak/termui"
"github.com/nsf/termbox-go"
"code.google.com/p/portaudio-go/portaudio"
)
// SquareWave represents a square wave, which, because physics, actually
// renders as a sine wave.
type SquareWave struct {
// Period used if Period is never sent on.
DefaultPeriod uint
MinPeriod uint
MaxPeriod uint
// Write a value to this channel to change the period of the wave.
Period chan uint
// Period used if Amplitude is never sent on.
DefaultAmplitude int32
MinAmplitude int32
MaxAmplitude int32
// Write a value to this channel to change the amplitude of the wave.
Amplitude chan int32
// Generated audio data is written to this channel.
Output chan int32
}
// NewSquareWave creates a SquareWave with reasonable default values.
func NewSquareWave(output chan int32) *SquareWave {
return &SquareWave{
DefaultPeriod: 100,
MinPeriod: 20,
MaxPeriod: 1000,
Period: make(chan uint),
DefaultAmplitude: math.MaxInt32 / 30,
MinAmplitude: 0,
MaxAmplitude: math.MaxInt32 / 5,
Amplitude: make(chan int32),
Output: output,
}
}
// Generate audio data and write to the Output channel.
// While this is running, write to the Period and Amplitude channels
// to control the wave.
func (wave *SquareWave) Generate() {
var period uint = wave.DefaultPeriod
var amplitude int32 = wave.DefaultAmplitude
for {
select {
case newPeriod := <-wave.Period:
// FIXME Check wave.Min/MaxPeriod
period = newPeriod
case newAmplitude := <-wave.Amplitude:
// FIXME Check wave.Min/MaxAmplitude
amplitude = newAmplitude
default:
}
for i := uint(0); i < period/2; i++ {
wave.Output <- amplitude
}
for i := uint(0); i < period/2; i++ {
wave.Output <- 0
}
}
}
func main() {
portaudio.Initialize()
defer portaudio.Terminate()
h, err := portaudio.DefaultHostApi()
chk(err)
lowLatencyParams := portaudio.LowLatencyParameters(nil, h.DefaultOutputDevice)
audioChan := make(chan int32, 1022)
wave := NewSquareWave(audioChan)
go wave.Generate()
// Feed the audio into the actual stream.
stream, err := portaudio.OpenStream(lowLatencyParams, AudioCallbackFromChan(audioChan))
chk(err)
defer stream.Close()
chk(stream.Start())
chk(termui.Init())
defer termui.Close()
UiLoop(wave)
chk(stream.Stop())
}
func AudioCallbackFromChan(audioChan <-chan int32) func([]int32) {
return func(out []int32) {
for i := range out {
out[i] = <-audioChan
}
}
}
func UiLoop(wave *SquareWave) {
fastMultiplier := 4
helpPar := termui.NewP(`w/x = inc/dec period (inverse of pitch)
e/c = inc/dec amplitude (volume)
shift to move faster`)
helpPar.Width = 200
helpPar.Height = 5
helpPar.HasBorder = false
var periodDelta uint = 2
period := wave.DefaultPeriod
periodGauge := CreateAudioGauge(1)
var amplitudeDelta int32 = math.MaxInt32 / 100
amplitude := wave.DefaultAmplitude
amplitudeGauge := CreateAudioGauge(2)
for {
termWidth, _ := termbox.Size()
periodGauge.Border.Label = fmt.Sprintf("period: %d (±%d)", period, periodDelta)
periodGauge.Percent = PeriodPercent(period, wave.MinPeriod, wave.MaxPeriod)
periodGauge.Width = termWidth
amplitudeGauge.Border.Label = fmt.Sprintf("amplitude: %d (±%d)", amplitude, amplitudeDelta)
amplitudeGauge.Percent = AmplitudePercent(amplitude, wave.MinAmplitude, wave.MaxAmplitude)
amplitudeGauge.Width = termWidth
termui.Render(helpPar, periodGauge, amplitudeGauge)
e := termbox.PollEvent()
switch e.Ch {
case 'w':
period += periodDelta
case 'W':
period += uint(fastMultiplier) * periodDelta
case 'x':
period -= periodDelta
case 'X':
period -= uint(fastMultiplier) * periodDelta
case 'e':
amplitude += amplitudeDelta
case 'E':
amplitude += int32(fastMultiplier) * amplitudeDelta
case 'c':
amplitude -= amplitudeDelta
case 'C':
amplitude -= int32(fastMultiplier) * amplitudeDelta
case 'q':
return
default:
if e.Key == termbox.KeyCtrlC {
return
}
}
if period > wave.MaxPeriod {
period = wave.MaxPeriod
} else if period < wave.MinPeriod {
period = wave.MinPeriod
}
if amplitude > wave.MaxAmplitude {
amplitude = wave.MaxAmplitude
} else if amplitude < wave.MinAmplitude {
amplitude = wave.MinAmplitude
}
wave.Period <- period
wave.Amplitude <- amplitude
}
}
func PeriodPercent(period uint, min uint, max uint) int {
return int((float32(period-min) / float32(max-min)) * 100.0)
}
func AmplitudePercent(amplitude int32, min int32, max int32) int {
return int((float32(amplitude-min) / float32(max-min)) * 100.0)
}
func CreateAudioGauge(y int) *termui.Gauge {
gauge := termui.NewGauge()
gauge.Height = 4
gauge.Y = 4 * y
return gauge
}
func chk(err error) {
if err != nil {
panic(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment