diff --git a/CHANGELOG.md b/CHANGELOG.md index 57318e0..9b200de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added +- Plot the envelope shape on top of the oscilloscope when the envelope unit is + selected. - Spectrum analyzer showing the spectrum. When the user has a filter or belleq unit selected, it's frequency response is plotted on top. ([#67][i67]) - belleq unit: a bell-shaped second-order filter for equalization. Belleq unit diff --git a/tracker/gioui/oscilloscope.go b/tracker/gioui/oscilloscope.go index 4517bdd..74532fb 100644 --- a/tracker/gioui/oscilloscope.go +++ b/tracker/gioui/oscilloscope.go @@ -58,12 +58,19 @@ func (s *Oscilloscope) Layout(gtx C) D { w := t.Scope().Waveform() cx := float32(w.Cursor) / float32(len(w.Buffer)) + env, envOk := t.Scope().Envelope() + data := func(chn int, xr plotRange) (yr plotRange, ok bool) { x1 := max(int(xr.a*float32(len(w.Buffer))), 0) x2 := min(int(xr.b*float32(len(w.Buffer))), len(w.Buffer)-1) if x1 > x2 { return plotRange{}, false } + if chn == 2 && envOk { + y1 := env.Value(x1) + y2 := env.Value(x2) + return plotRange{-max(y1, y2), -min(y1, y2)}, true + } step := max((x2-x1)/1000, 1) // if the range is too large, sample only ~ 1000 points y1 := float32(math.Inf(-1)) y2 := float32(math.Inf(+1)) @@ -101,7 +108,12 @@ func (s *Oscilloscope) Layout(gtx C) D { yield(1, "") } - return s.State.plot.Layout(gtx, data, xticks, yticks, cx, 2) + numChannels := 2 + if envOk { + numChannels = 3 + } + + return s.State.plot.Layout(gtx, data, xticks, yticks, cx, numChannels) }), layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, diff --git a/tracker/scope.go b/tracker/scope.go index d8cbaad..fbb2128 100644 --- a/tracker/scope.go +++ b/tracker/scope.go @@ -1,6 +1,7 @@ package tracker import ( + "math" "strconv" "github.com/vsariola/sointu" @@ -68,6 +69,69 @@ func (s *scopeTriggerChannel) StringOf(value int) string { // Waveform returns the oscilloscope waveform buffer. func (s *ScopeModel) Waveform() RingBuffer[[2]float32] { return s.scopeData.waveForm } +func (s *ScopeModel) Envelope() (Envelope, bool) { + i := s.d.InstrIndex + u := s.d.UnitIndex + if i < 0 || i >= len(s.d.Song.Patch) || u < 0 || u >= len(s.d.Song.Patch[i].Units) || s.d.Song.Patch[i].Units[u].Type != "envelope" { + return Envelope{}, false + } + var ret Envelope = Envelope{{Position: math.MaxInt}, {Position: math.MaxInt}, {Position: math.MaxInt}, {Position: math.MaxInt}, {Position: math.MaxInt}} + releasePos := len(s.scopeData.waveForm.Buffer) / 2 + p := s.d.Song.Patch[s.d.InstrIndex].Units[s.d.UnitIndex].Parameters + attack := nonLinearMap((float32)(p["attack"]) / 128.0) + decay := nonLinearMap((float32)(p["decay"]) / 128.0) + sustain := (float32)(p["sustain"]) / 128.0 + release := nonLinearMap((float32)(p["release"]) / 128.0) + gain := (float32)(p["gain"]) / 128.0 + curpos := 0 + for i := 0; i < 3; i++ { + var nextpos int + switch i { + case 0: + ret[i] = EnvelopePoint{Position: curpos, Level: 0, Slope: attack * gain} + nextpos = curpos + int(math.Ceil(float64(1/attack))) + case 1: + ret[i] = EnvelopePoint{Position: curpos, Level: gain, Slope: -decay * gain} + nextpos = curpos + int(math.Ceil(float64((1-sustain)/decay))) + case 2: + ret[i] = EnvelopePoint{Position: curpos, Level: sustain * gain, Slope: 0} + nextpos = math.MaxInt + } + if nextpos >= releasePos { + v := ret[i].Level + ret[i].Slope*float32(releasePos-curpos) + ret[i+1] = EnvelopePoint{Position: releasePos, Level: v, Slope: -release * gain} + ret[i+2] = EnvelopePoint{Position: releasePos + int(math.Ceil(float64(v/(release*gain)))), Level: 0, Slope: 0} + break + } + curpos = nextpos + } + return ret, true +} + +func nonLinearMap(value float32) float32 { + return float32(math.Exp2(float64(-24 * value))) +} + +type Envelope [5]EnvelopePoint + +type EnvelopePoint struct { + Position int + Level, Slope float32 +} + +func (e *Envelope) Value(position int) float32 { + for i := len(e) - 1; i >= 0; i-- { + if position >= e[i].Position { + return e[i].Value(position + 1) + } + } + return 0 +} + +func (e *EnvelopePoint) Value(position int) float32 { + return e.Level + e.Slope*float32(position-e.Position) +} + // processAudioBuffer fills the oscilloscope buffer with audio data from the // given buffer. func (s *ScopeModel) processAudioBuffer(bufPtr *sointu.AudioBuffer) {