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/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/fun" import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" 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(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Piano") container := containers.NewContainer(layouts.Vertical { true, true }) controlBar := containers.NewContainer(layouts.Horizontal { true, false }) waveformColumn := containers.NewContainer(layouts.Vertical { true, false }) waveformList := elements.NewList ( elements.NewListEntry("Sine", func(){ waveform = 0 }), elements.NewListEntry("Triangle", func(){ waveform = 3 }), elements.NewListEntry("Square", func(){ waveform = 1 }), elements.NewListEntry("Saw", func(){ waveform = 2 }), elements.NewListEntry("Supersaw", func(){ waveform = 4 }), ) waveformList.OnNoEntrySelected (func(){waveformList.Select(0)}) waveformList.Select(0) adsrColumn := containers.NewContainer(layouts.Vertical { true, false }) adsrGroup := containers.NewContainer(layouts.Horizontal { true, false }) attackSlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Attack, true) decaySlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Decay, true) sustainSlider := elements.NewSlider(adsr.Sustain, true) releaseSlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Release, true) gainSlider := elements.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 := containers.NewContainer(layouts.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 := elements.NewList ( elements.NewListEntry ("Bones", patch ( 0, 0, 100, 0.0, 0)), elements.NewListEntry ("Staccato", patch ( 4, 70, 500, 0, 0)), elements.NewListEntry ("Sustain", patch ( 4, 70, 200, 0.8, 500)), elements.NewListEntry ("Upright", patch ( 1, 0, 500, 0.4, 70)), elements.NewListEntry ("Space Pad", patch ( 4, 1500, 0, 1.0, 3000)), elements.NewListEntry ("Popcorn", patch ( 2, 0, 40, 0.0, 0)), elements.NewListEntry ("Racer", patch ( 3, 70, 0, 0.7, 400)), elements.NewListEntry ("Reverse", patch ( 2, 3000, 60, 0, 0)), ) patchList.Collapse(0, 32) patchScrollBox := containers.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(elements.NewLabel("Presets", false), false) patchColumn.Adopt(patchScrollBox, true) patchScrollBox.Adopt(patchList) controlBar.Adopt(elements.NewSpacer(true), false) controlBar.Adopt(waveformColumn, false) waveformColumn.Adopt(elements.NewLabel("Waveform", false), false) waveformColumn.Adopt(waveformList, true) controlBar.Adopt(elements.NewSpacer(true), false) adsrColumn.Adopt(elements.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() } 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 }