This repository has been archived on 2023-08-08. You can view files and clone it, but cannot push or open issues or pull requests.
tomo-old/examples/piano/main.go
Sasha Koshka 423e6869c0 X backend better handles expose events
Previously, when an expose event was recieved, the backend would
call Window.paste, converting RGBA image data to BGRA image data.
Now we only call Window.pushRegion with the bounds given to us by
the expose event(s). This speeds up window resizing significantly.
2023-03-07 12:48:29 -05:00

336 lines
8.6 KiB
Go

package main
import "math"
import "time"
import "errors"
import "github.com/faiface/beep"
import "github.com/faiface/beep/speaker"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
import _ "net/http/pprof"
import "net/http"
const sampleRate = 44100
const bufferSize = 256
var tuning = music.EqualTemparment { A4: 440 }
var waveform = 0
var playing = map[music.Note] *toneStreamer { }
var adsr = ADSR {
Attack: 5 * time.Millisecond,
Decay: 400 * time.Millisecond,
Sustain: 0.7,
Release: 500 * time.Millisecond,
}
var gain = 0.3
func main () {
speaker.Init(sampleRate, bufferSize)
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Piano")
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
controlBar := basicElements.NewContainer(basicLayouts.Horizontal { true, false })
waveformColumn := basicElements.NewContainer(basicLayouts.Vertical { true, false })
waveformList := basicElements.NewList (
basicElements.NewListEntry("Sine", func(){ waveform = 0 }),
basicElements.NewListEntry("Triangle", func(){ waveform = 3 }),
basicElements.NewListEntry("Square", func(){ waveform = 1 }),
basicElements.NewListEntry("Saw", func(){ waveform = 2 }),
basicElements.NewListEntry("Supersaw", func(){ waveform = 4 }),
)
waveformList.OnNoEntrySelected (func(){waveformList.Select(0)})
waveformList.Select(0)
adsrColumn := basicElements.NewContainer(basicLayouts.Vertical { true, false })
adsrGroup := basicElements.NewContainer(basicLayouts.Horizontal { true, false })
attackSlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Attack, true)
decaySlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Decay, true)
sustainSlider := basicElements.NewSlider(adsr.Sustain, true)
releaseSlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Release, true)
gainSlider := basicElements.NewSlider(math.Sqrt(gain), false)
attackSlider.OnRelease (func () {
adsr.Attack = attackSlider.Value()
})
decaySlider.OnRelease (func () {
adsr.Decay = decaySlider.Value()
})
sustainSlider.OnRelease (func () {
adsr.Sustain = sustainSlider.Value()
})
releaseSlider.OnRelease (func () {
adsr.Release = releaseSlider.Value()
})
gainSlider.OnRelease (func () {
gain = math.Pow(gainSlider.Value(), 2)
})
patchColumn := basicElements.NewContainer(basicLayouts.Vertical { true, false })
patch := func (w int, a, d time.Duration, s float64, r time.Duration) func () {
return func () {
waveform = w
adsr = ADSR {
a * time.Millisecond,
d * time.Millisecond,
s,
r * time.Millisecond,
}
waveformList.Select(w)
attackSlider .SetValue(adsr.Attack)
decaySlider .SetValue(adsr.Decay)
sustainSlider.SetValue(adsr.Sustain)
releaseSlider.SetValue(adsr.Release)
}
}
patchList := basicElements.NewList (
basicElements.NewListEntry ("Bones", patch (
0, 0, 100, 0.0, 0)),
basicElements.NewListEntry ("Staccato", patch (
4, 70, 500, 0, 0)),
basicElements.NewListEntry ("Sustain", patch (
4, 70, 200, 0.8, 500)),
basicElements.NewListEntry ("Upright", patch (
1, 0, 500, 0.4, 70)),
basicElements.NewListEntry ("Space Pad", patch (
4, 1500, 0, 1.0, 3000)),
basicElements.NewListEntry ("Popcorn", patch (
2, 0, 40, 0.0, 0)),
basicElements.NewListEntry ("Racer", patch (
3, 70, 0, 0.7, 400)),
basicElements.NewListEntry ("Reverse", patch (
2, 3000, 60, 0, 0)),
)
patchList.Collapse(0, 32)
patchScrollBox := basicElements.NewScrollContainer(false, true)
piano := fun.NewPiano(2, 5)
piano.OnPress(playNote)
piano.OnRelease(stopNote)
// honestly, if you were doing something like this for real, i'd
// encourage you to build a custom layout because this is a bit cursed.
// i need to add more layouts...
window.Adopt(container)
controlBar.Adopt(patchColumn, true)
patchColumn.Adopt(basicElements.NewLabel("Presets", false), false)
patchColumn.Adopt(patchScrollBox, true)
patchScrollBox.Adopt(patchList)
controlBar.Adopt(basicElements.NewSpacer(true), false)
controlBar.Adopt(waveformColumn, false)
waveformColumn.Adopt(basicElements.NewLabel("Waveform", false), false)
waveformColumn.Adopt(waveformList, true)
controlBar.Adopt(basicElements.NewSpacer(true), false)
adsrColumn.Adopt(basicElements.NewLabel("ADSR", false), false)
adsrGroup.Adopt(attackSlider, false)
adsrGroup.Adopt(decaySlider, false)
adsrGroup.Adopt(sustainSlider, false)
adsrGroup.Adopt(releaseSlider, false)
adsrColumn.Adopt(adsrGroup, true)
adsrColumn.Adopt(gainSlider, false)
controlBar.Adopt(adsrColumn, false)
container.Adopt(controlBar, true)
container.Adopt(piano, false)
piano.Focus()
window.OnClose(tomo.Stop)
window.Show()
go func () {
http.ListenAndServe("localhost:9090", nil)
} ()
}
type Patch struct {
ADSR
Waveform int
}
func stopNote (note music.Note) {
if _, is := playing[note]; !is { return }
speaker.Lock()
playing[note].Release()
delete(playing, note)
speaker.Unlock()
}
func playNote (note music.Note) {
streamer, _ := Tone (
sampleRate,
int(tuning.Tune(note)),
waveform,
gain,
adsr)
stopNote(note)
speaker.Lock()
playing[note] = streamer
speaker.Unlock()
speaker.Play(playing[note])
}
// https://github.com/faiface/beep/blob/v1.1.0/generators/toner.go
// Adapted to be a bit more versatile.
type toneStreamer struct {
position float64
cycles uint64
delta float64
waveform int
gain float64
adsr ADSR
released bool
complete bool
adsrPhase int
adsrPosition float64
adsrDeltas [4]float64
}
type ADSR struct {
Attack time.Duration
Decay time.Duration
Sustain float64
Release time.Duration
}
func Tone (
sampleRate beep.SampleRate,
frequency int,
waveform int,
gain float64,
adsr ADSR,
) (
*toneStreamer,
error,
) {
if int(sampleRate) / frequency < 2 {
return nil, errors.New (
"tone generator: samplerate must be at least " +
"2 times greater then frequency")
}
tone := new(toneStreamer)
tone.waveform = waveform
tone.position = 0.0
steps := float64(sampleRate) / float64(frequency)
tone.delta = 1.0 / steps
tone.gain = gain
if adsr.Attack < time.Millisecond { adsr.Attack = time.Millisecond }
if adsr.Decay < time.Millisecond { adsr.Decay = time.Millisecond }
if adsr.Release < time.Millisecond { adsr.Release = time.Millisecond }
tone.adsr = adsr
attackSteps := adsr.Attack.Seconds() * float64(sampleRate)
decaySteps := adsr.Decay.Seconds() * float64(sampleRate)
releaseSteps := adsr.Release.Seconds() * float64(sampleRate)
tone.adsrDeltas[0] = 1 / attackSteps
tone.adsrDeltas[1] = 1 / decaySteps
tone.adsrDeltas[2] = 0
tone.adsrDeltas[3] = 1 / releaseSteps
return tone, nil
}
func (tone *toneStreamer) nextSample () (sample float64) {
switch tone.waveform {
case 0:
sample = math.Sin(tone.position * 2.0 * math.Pi)
case 1:
if tone.position > 0.5 {
sample = 1
} else {
sample = -1
}
case 2:
sample = (tone.position - 0.5) * 2
case 3:
sample = 1 - math.Abs(tone.position - 0.5) * 4
case 4:
unison := 5
detuneDelta := 0.00005
detune := 0.0 - (float64(unison) / 2) * detuneDelta
for i := 0; i < unison; i ++ {
_, offset := math.Modf(detune * float64(tone.cycles) + tone.position)
sample += (offset - 0.5) * 2
detune += detuneDelta
}
sample /= float64(unison)
}
adsrGain := 0.0
switch tone.adsrPhase {
case 0: adsrGain = tone.adsrPosition
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.adsrPhase = 1
}
case 1: adsrGain = 1 + tone.adsrPosition * (tone.adsr.Sustain - 1)
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.adsrPhase = 2
}
case 2: adsrGain = tone.adsr.Sustain
if tone.released {
tone.adsrPhase = 3
}
case 3: adsrGain = (1 - tone.adsrPosition) * tone.adsr.Sustain
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.complete = true
}
}
sample *= adsrGain * adsrGain
tone.adsrPosition += tone.adsrDeltas[tone.adsrPhase]
_, tone.position = math.Modf(tone.position + tone.delta)
tone.cycles ++
return
}
func (tone *toneStreamer) Stream (buf [][2]float64) (int, bool) {
if tone.complete {
return 0, false
}
for i := 0; i < len(buf); i++ {
sample := 0.0
if !tone.complete {
sample = tone.nextSample() * tone.gain
}
buf[i] = [2]float64{sample, sample}
}
return len(buf), true
}
func (tone *toneStreamer) Err () error {
return nil
}
func (tone *toneStreamer) Release () {
tone.released = true
}