mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-02 13:50:15 -05:00
feat(tracker): plot envelope shape in scope when envelope selected
This commit is contained in:
parent
287bd036a6
commit
6e8acc8f9b
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user