Last active
September 1, 2022 19:39
-
-
Save zach-klippenstein/5eaa2c96c2620b9e9cdb to your computer and use it in GitHub Desktop.
Terminal program that plays a sine wave.
This file contains hidden or 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
//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