mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-19 22:53:32 -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]
|
## [Unreleased]
|
||||||
### Added
|
### 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
|
- 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])
|
unit selected, it's frequency response is plotted on top. ([#67][i67])
|
||||||
- belleq unit: a bell-shaped second-order filter for equalization. Belleq unit
|
- 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()
|
w := t.Scope().Waveform()
|
||||||
cx := float32(w.Cursor) / float32(len(w.Buffer))
|
cx := float32(w.Cursor) / float32(len(w.Buffer))
|
||||||
|
|
||||||
|
env, envOk := t.Scope().Envelope()
|
||||||
|
|
||||||
data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
|
data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
|
||||||
x1 := max(int(xr.a*float32(len(w.Buffer))), 0)
|
x1 := max(int(xr.a*float32(len(w.Buffer))), 0)
|
||||||
x2 := min(int(xr.b*float32(len(w.Buffer))), len(w.Buffer)-1)
|
x2 := min(int(xr.b*float32(len(w.Buffer))), len(w.Buffer)-1)
|
||||||
if x1 > x2 {
|
if x1 > x2 {
|
||||||
return plotRange{}, false
|
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
|
step := max((x2-x1)/1000, 1) // if the range is too large, sample only ~ 1000 points
|
||||||
y1 := float32(math.Inf(-1))
|
y1 := float32(math.Inf(-1))
|
||||||
y2 := float32(math.Inf(+1))
|
y2 := float32(math.Inf(+1))
|
||||||
@ -101,7 +108,12 @@ func (s *Oscilloscope) Layout(gtx C) D {
|
|||||||
yield(1, "")
|
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 {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package tracker
|
package tracker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
@ -68,6 +69,69 @@ func (s *scopeTriggerChannel) StringOf(value int) string {
|
|||||||
// Waveform returns the oscilloscope waveform buffer.
|
// Waveform returns the oscilloscope waveform buffer.
|
||||||
func (s *ScopeModel) Waveform() RingBuffer[[2]float32] { return s.scopeData.waveForm }
|
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
|
// processAudioBuffer fills the oscilloscope buffer with audio data from the
|
||||||
// given buffer.
|
// given buffer.
|
||||||
func (s *ScopeModel) processAudioBuffer(bufPtr *sointu.AudioBuffer) {
|
func (s *ScopeModel) processAudioBuffer(bufPtr *sointu.AudioBuffer) {
|
||||||
|
|||||||
Reference in New Issue
Block a user