mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-02 04:02:52 -04:00
feat(sointu, tracker,...): restructure domain & tracker models
send targets are now by ID and Song has "Score" part, which is the notes for it. also, moved the model part separate of the actual gioui dependend stuff. sorry to my future self about the code bomb; ended up too far and did not find an easy way to rewrite the history to make the steps smaller, so in the end, just squashed everything.
This commit is contained in:
@ -6,6 +6,8 @@ import (
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
var UnitTypeNames []string
|
||||
|
||||
var defaultUnits = map[string]sointu.Unit{
|
||||
"envelope": {Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 64, "gain": 64}},
|
||||
"oscillator": {Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 64, "shape": 64, "gain": 64, "type": sointu.Sine}},
|
||||
@ -40,16 +42,6 @@ var defaultUnits = map[string]sointu.Unit{
|
||||
"send": {Type: "send", Parameters: map[string]int{"stereo": 0, "amount": 128, "voice": 0, "unit": 0, "port": 0, "sendpop": 1}},
|
||||
}
|
||||
|
||||
var allUnits []string
|
||||
|
||||
func init() {
|
||||
allUnits = make([]string, 0, len(sointu.UnitTypes))
|
||||
for k := range sointu.UnitTypes {
|
||||
allUnits = append(allUnits, k)
|
||||
}
|
||||
sort.Strings(allUnits)
|
||||
}
|
||||
|
||||
var defaultInstrument = sointu.Instrument{
|
||||
Name: "Instr",
|
||||
NumVoices: 1,
|
||||
@ -64,14 +56,16 @@ var defaultInstrument = sointu.Instrument{
|
||||
}
|
||||
|
||||
var defaultSong = sointu.Song{
|
||||
BPM: 100,
|
||||
RowsPerPattern: 16,
|
||||
RowsPerBeat: 4,
|
||||
Tracks: []sointu.Track{
|
||||
{NumVoices: 1, Sequence: []byte{0, 0, 0, 1}, Patterns: [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}, {64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 75, 0, 75, 0, 80, 0}}},
|
||||
BPM: 100,
|
||||
RowsPerBeat: 4,
|
||||
Score: sointu.Score{
|
||||
RowsPerPattern: 16,
|
||||
Length: 4,
|
||||
Tracks: []sointu.Track{
|
||||
{NumVoices: 1, Order: []int{0, 0, 0, 1}, Patterns: [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}, {64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 75, 0, 75, 0, 80, 0}}},
|
||||
},
|
||||
},
|
||||
Patch: sointu.Patch{Instruments: []sointu.Instrument{
|
||||
defaultInstrument,
|
||||
Patch: sointu.Patch{defaultInstrument,
|
||||
{Name: "Global", NumVoices: 1, Units: []sointu.Unit{
|
||||
defaultUnits["in"],
|
||||
{Type: "delay",
|
||||
@ -80,5 +74,13 @@ var defaultSong = sointu.Song{
|
||||
1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642,
|
||||
}},
|
||||
defaultUnits["out"],
|
||||
}}}},
|
||||
}}},
|
||||
}
|
||||
|
||||
func init() {
|
||||
UnitTypeNames = make([]string, 0, len(sointu.UnitTypes))
|
||||
for k := range sointu.UnitTypes {
|
||||
UnitTypeNames = append(UnitTypeNames, k)
|
||||
}
|
||||
sort.Strings(UnitTypeNames)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -26,7 +26,7 @@ func (t *Tracker) LoadSongFile() {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.LoadSong(song)
|
||||
t.SetSong(song)
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveSongFile() {
|
||||
@ -37,9 +37,9 @@ func (t *Tracker) SaveSongFile() {
|
||||
var extension = filepath.Ext(filename)
|
||||
var contents []byte
|
||||
if extension == "json" {
|
||||
contents, err = json.Marshal(t.song)
|
||||
contents, err = json.Marshal(t.Song())
|
||||
} else {
|
||||
contents, err = yaml.Marshal(t.song)
|
||||
contents, err = yaml.Marshal(t.Song())
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"log"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@ -26,8 +27,8 @@ func (t *Tracker) layoutInstruments(gtx C) D {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if e.Type == pointer.Press && (t.EditMode != EditUnits && t.EditMode != EditParameters) {
|
||||
t.EditMode = EditUnits
|
||||
if e.Type == pointer.Press && (t.EditMode() != tracker.EditUnits && t.EditMode() != tracker.EditParameters) {
|
||||
t.SetEditMode(tracker.EditUnits)
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
@ -36,12 +37,12 @@ func (t *Tracker) layoutInstruments(gtx C) D {
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
for t.NewInstrumentBtn.Clicked() {
|
||||
t.AddInstrument()
|
||||
t.AddInstrument(true)
|
||||
}
|
||||
btnStyle := material.IconButton(t.Theme, t.NewInstrumentBtn, widgetForIcon(icons.ContentAdd))
|
||||
btnStyle.Background = transparent
|
||||
btnStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if t.song.Patch.TotalVoices() < 32 {
|
||||
if t.CanAddInstrument() {
|
||||
btnStyle.Color = primaryColor
|
||||
} else {
|
||||
btnStyle.Color = disabledTextColor
|
||||
@ -54,7 +55,7 @@ func (t *Tracker) layoutInstruments(gtx C) D {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(t.layoutInstrumentNames),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return t.InstrumentScrollBar.Layout(gtx, unit.Dp(6), len(t.song.Patch.Instruments), &t.InstrumentDragList.List.Position)
|
||||
return t.InstrumentScrollBar.Layout(gtx, unit.Dp(6), len(t.Song().Patch), &t.InstrumentDragList.List.Position)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
@ -77,7 +78,7 @@ func (t *Tracker) layoutInstrumentHeader(gtx C) D {
|
||||
deleteInstrumentBtnStyle := material.IconButton(t.Theme, t.DeleteInstrumentBtn, widgetForIcon(icons.ActionDelete))
|
||||
deleteInstrumentBtnStyle.Background = transparent
|
||||
deleteInstrumentBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if len(t.song.Patch.Instruments) > 1 {
|
||||
if t.CanDeleteInstrument() {
|
||||
deleteInstrumentBtnStyle.Color = primaryColor
|
||||
} else {
|
||||
deleteInstrumentBtnStyle.Color = disabledTextColor
|
||||
@ -86,11 +87,8 @@ func (t *Tracker) layoutInstrumentHeader(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label("Voices: ", white)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
maxRemain := 32 - t.song.Patch.TotalVoices() + t.song.Patch.Instruments[t.CurrentInstrument].NumVoices
|
||||
if maxRemain < 0 {
|
||||
maxRemain = 0
|
||||
}
|
||||
t.InstrumentVoices.Value = t.song.Patch.Instruments[t.CurrentInstrument].NumVoices
|
||||
maxRemain := t.MaxInstrumentVoices()
|
||||
t.InstrumentVoices.Value = t.Instrument().NumVoices
|
||||
numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, 0, maxRemain)
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
|
||||
@ -103,16 +101,16 @@ func (t *Tracker) layoutInstrumentHeader(gtx C) D {
|
||||
layout.Rigid(deleteInstrumentBtnStyle.Layout))
|
||||
}
|
||||
for t.CopyInstrumentBtn.Clicked() {
|
||||
contents, err := yaml.Marshal(t.song.Patch.Instruments[t.CurrentInstrument])
|
||||
contents, err := yaml.Marshal(t.Instrument())
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Instrument copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
}
|
||||
for t.DeleteInstrumentBtn.Clicked() {
|
||||
t.DeleteInstrument()
|
||||
t.DeleteInstrument(false)
|
||||
}
|
||||
return Surface{Gray: 37, Focus: t.EditMode == EditUnits || t.EditMode == EditParameters}.Layout(gtx, header)
|
||||
return Surface{Gray: 37, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters}.Layout(gtx, header)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutInstrumentNames(gtx C) D {
|
||||
@ -120,11 +118,11 @@ func (t *Tracker) layoutInstrumentNames(gtx C) D {
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(36))
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(30))
|
||||
grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center}
|
||||
if i == t.CurrentInstrument {
|
||||
if i == t.InstrIndex() {
|
||||
grabhandle.Text = ":::"
|
||||
}
|
||||
label := func(gtx C) D {
|
||||
if i == t.CurrentInstrument {
|
||||
if i == t.InstrIndex() {
|
||||
for _, ev := range t.InstrumentNameEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
@ -132,7 +130,7 @@ func (t *Tracker) layoutInstrumentNames(gtx C) D {
|
||||
break
|
||||
}
|
||||
}
|
||||
if n := t.song.Patch.Instruments[t.CurrentInstrument].Name; n != t.InstrumentNameEditor.Text() {
|
||||
if n := t.Instrument().Name; n != t.InstrumentNameEditor.Text() {
|
||||
t.InstrumentNameEditor.SetText(n)
|
||||
}
|
||||
editor := material.Editor(t.Theme, t.InstrumentNameEditor, "Instr")
|
||||
@ -143,7 +141,7 @@ func (t *Tracker) layoutInstrumentNames(gtx C) D {
|
||||
t.SetInstrumentName(t.InstrumentNameEditor.Text())
|
||||
return dims
|
||||
}
|
||||
text := t.song.Patch.Instruments[i].Name
|
||||
text := t.Song().Patch[i].Name
|
||||
if text == "" {
|
||||
text = "Instr"
|
||||
}
|
||||
@ -159,22 +157,19 @@ func (t *Tracker) layoutInstrumentNames(gtx C) D {
|
||||
}
|
||||
|
||||
color := inactiveLightSurfaceColor
|
||||
if t.EditMode == EditUnits || t.EditMode == EditParameters {
|
||||
if t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters {
|
||||
color = activeLightSurfaceColor
|
||||
}
|
||||
instrumentList := FilledDragList(t.Theme, t.InstrumentDragList, len(t.song.Patch.Instruments), element, t.SwapInstruments)
|
||||
instrumentList := FilledDragList(t.Theme, t.InstrumentDragList, len(t.Song().Patch), element, t.SwapInstruments)
|
||||
instrumentList.SelectedColor = color
|
||||
instrumentList.HoverColor = instrumentHoverColor
|
||||
|
||||
t.InstrumentDragList.SelectedItem = t.CurrentInstrument
|
||||
t.InstrumentDragList.SelectedItem = t.InstrIndex()
|
||||
defer op.Save(gtx.Ops).Load()
|
||||
pointer.PassOp{Pass: true}.Add(gtx.Ops)
|
||||
dims := instrumentList.Layout(gtx)
|
||||
if t.CurrentInstrument != t.InstrumentDragList.SelectedItem {
|
||||
t.CurrentInstrument = t.InstrumentDragList.SelectedItem
|
||||
if l := len(t.song.Patch.Instruments[t.CurrentInstrument].Units); t.CurrentUnit >= l {
|
||||
t.CurrentUnit = l - 1
|
||||
}
|
||||
if t.InstrIndex() != t.InstrumentDragList.SelectedItem {
|
||||
t.SetInstrIndex(t.InstrumentDragList.SelectedItem)
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
return dims
|
||||
@ -188,7 +183,7 @@ func (t *Tracker) layoutInstrumentEditor(gtx C) D {
|
||||
addUnitBtnStyle.Background = t.Theme.Fg
|
||||
addUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(4))
|
||||
|
||||
units := t.song.Patch.Instruments[t.CurrentInstrument].Units
|
||||
units := t.Instrument().Units
|
||||
for len(t.StackUse) < len(units) {
|
||||
t.StackUse = append(t.StackUse, 0)
|
||||
}
|
||||
@ -238,26 +233,26 @@ func (t *Tracker) layoutInstrumentEditor(gtx C) D {
|
||||
|
||||
unitList := FilledDragList(t.Theme, t.UnitDragList, len(units), element, t.SwapUnits)
|
||||
|
||||
if t.EditMode == EditUnits {
|
||||
if t.EditMode() == tracker.EditUnits {
|
||||
unitList.SelectedColor = cursorColor
|
||||
}
|
||||
|
||||
t.UnitDragList.SelectedItem = t.CurrentUnit
|
||||
return Surface{Gray: 30, Focus: t.EditMode == EditUnits || t.EditMode == EditParameters}.Layout(gtx, func(gtx C) D {
|
||||
t.UnitDragList.SelectedItem = t.UnitIndex()
|
||||
return Surface{Gray: 30, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
dims := unitList.Layout(gtx)
|
||||
if t.CurrentUnit != t.UnitDragList.SelectedItem {
|
||||
t.CurrentUnit = t.UnitDragList.SelectedItem
|
||||
t.EditMode = EditUnits
|
||||
if t.UnitIndex() != t.UnitDragList.SelectedItem {
|
||||
t.SetUnitIndex(t.UnitDragList.SelectedItem)
|
||||
t.SetEditMode(tracker.EditUnits)
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
return dims
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return t.UnitScrollBar.Layout(gtx, unit.Dp(10), len(t.song.Patch.Instruments[t.CurrentInstrument].Units), &t.UnitDragList.List.Position)
|
||||
return t.UnitScrollBar.Layout(gtx, unit.Dp(10), len(t.Instrument().Units), &t.UnitDragList.List.Position)
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
454
tracker/gioui/keyevent.go
Normal file
454
tracker/gioui/keyevent.go
Normal file
@ -0,0 +1,454 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/key"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var noteMap = map[string]int{
|
||||
"Z": -12,
|
||||
"S": -11,
|
||||
"X": -10,
|
||||
"D": -9,
|
||||
"C": -8,
|
||||
"V": -7,
|
||||
"G": -6,
|
||||
"B": -5,
|
||||
"H": -4,
|
||||
"N": -3,
|
||||
"J": -2,
|
||||
"M": -1,
|
||||
",": 0,
|
||||
"L": 1,
|
||||
".": 2,
|
||||
"Q": 0,
|
||||
"2": 1,
|
||||
"W": 2,
|
||||
"3": 3,
|
||||
"E": 4,
|
||||
"R": 5,
|
||||
"5": 6,
|
||||
"T": 7,
|
||||
"6": 8,
|
||||
"Y": 9,
|
||||
"7": 10,
|
||||
"U": 11,
|
||||
"I": 12,
|
||||
"9": 13,
|
||||
"O": 14,
|
||||
"0": 15,
|
||||
"P": 16,
|
||||
}
|
||||
|
||||
var unitKeyMap = map[string]string{
|
||||
"e": "envelope",
|
||||
"o": "oscillator",
|
||||
"m": "mulp",
|
||||
"M": "mul",
|
||||
"a": "addp",
|
||||
"A": "add",
|
||||
"p": "pan",
|
||||
"S": "push",
|
||||
"P": "pop",
|
||||
"O": "out",
|
||||
"l": "loadnote",
|
||||
"L": "loadval",
|
||||
"h": "xch",
|
||||
"d": "delay",
|
||||
"D": "distort",
|
||||
"H": "hold",
|
||||
"b": "crush",
|
||||
"g": "gain",
|
||||
"i": "invgain",
|
||||
"f": "filter",
|
||||
"I": "clip",
|
||||
"E": "speed",
|
||||
"r": "compressor",
|
||||
"u": "outaux",
|
||||
"U": "aux",
|
||||
"s": "send",
|
||||
"n": "noise",
|
||||
"N": "in",
|
||||
"R": "receive",
|
||||
}
|
||||
|
||||
// KeyEvent handles incoming key events and returns true if repaint is needed.
|
||||
func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool {
|
||||
if e.State == key.Press {
|
||||
if t.InstrumentNameEditor.Focused() {
|
||||
return false
|
||||
}
|
||||
switch e.Name {
|
||||
case "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, err := yaml.Marshal(t.Song())
|
||||
if err == nil {
|
||||
w.WriteClipboard(string(contents))
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
w.ReadClipboard()
|
||||
return true
|
||||
}
|
||||
case "Z":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Undo()
|
||||
return true
|
||||
}
|
||||
case "Y":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Redo()
|
||||
return true
|
||||
}
|
||||
case "N":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.ResetSong()
|
||||
return true
|
||||
}
|
||||
case "S":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.SaveSongFile()
|
||||
return false
|
||||
}
|
||||
case "O":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.LoadSongFile()
|
||||
return true
|
||||
}
|
||||
case "F1":
|
||||
t.SetEditMode(tracker.EditPatterns)
|
||||
return true
|
||||
case "F2":
|
||||
t.SetEditMode(tracker.EditTracks)
|
||||
return true
|
||||
case "F3":
|
||||
t.SetEditMode(tracker.EditUnits)
|
||||
return true
|
||||
case "F4":
|
||||
t.SetEditMode(tracker.EditParameters)
|
||||
return true
|
||||
case "F5":
|
||||
t.SetNoteTracking(true)
|
||||
startRow := t.Cursor().SongRow
|
||||
if t.EditMode() == tracker.EditPatterns {
|
||||
startRow.Row = 0
|
||||
}
|
||||
t.player.Play(startRow)
|
||||
return true
|
||||
case "F6":
|
||||
t.SetNoteTracking(false)
|
||||
startRow := t.Cursor().SongRow
|
||||
if t.EditMode() == tracker.EditPatterns {
|
||||
startRow.Row = 0
|
||||
}
|
||||
t.player.Play(startRow)
|
||||
return true
|
||||
case "F8":
|
||||
t.player.Stop()
|
||||
return true
|
||||
case key.NameDeleteForward, key.NameDeleteBackward:
|
||||
switch t.EditMode() {
|
||||
case tracker.EditPatterns:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.DeleteOrderRow(e.Name == key.NameDeleteForward)
|
||||
} else {
|
||||
t.DeletePatternSelection()
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
return true
|
||||
case tracker.EditTracks:
|
||||
t.DeleteSelection()
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
return true
|
||||
case tracker.EditUnits:
|
||||
t.DeleteUnit(e.Name == key.NameDeleteForward)
|
||||
return true
|
||||
}
|
||||
case "Space":
|
||||
_, playing := t.player.Position()
|
||||
if !playing {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().SongRow
|
||||
if t.EditMode() == tracker.EditPatterns {
|
||||
startRow.Row = 0
|
||||
}
|
||||
t.player.Play(startRow)
|
||||
} else {
|
||||
t.player.Stop()
|
||||
}
|
||||
return true
|
||||
case `\`, `<`, `>`:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
return t.SetOctave(t.Octave() + 1)
|
||||
}
|
||||
return t.SetOctave(t.Octave() - 1)
|
||||
case key.NameTab:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetEditMode((t.EditMode() - 1 + 4) % 4)
|
||||
} else {
|
||||
t.SetEditMode((t.EditMode() + 1) % 4)
|
||||
}
|
||||
return true
|
||||
case key.NameReturn:
|
||||
switch t.EditMode() {
|
||||
case tracker.EditPatterns:
|
||||
t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut))
|
||||
case tracker.EditUnits:
|
||||
t.AddUnit(!e.Modifiers.Contain(key.ModShortcut))
|
||||
}
|
||||
case key.NameUpArrow:
|
||||
cursor := t.Cursor()
|
||||
switch t.EditMode() {
|
||||
case tracker.EditPatterns:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.SongRow = tracker.SongRow{}
|
||||
} else {
|
||||
cursor.Row -= t.Song().Score.RowsPerPattern
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
case tracker.EditTracks:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Row -= t.Song().Score.RowsPerPattern
|
||||
} else {
|
||||
if t.Step.Value > 0 {
|
||||
cursor.Row -= t.Step.Value
|
||||
} else {
|
||||
cursor.Row--
|
||||
}
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
case tracker.EditUnits:
|
||||
t.SetUnitIndex(t.UnitIndex() - 1)
|
||||
case tracker.EditParameters:
|
||||
t.SetParamIndex(t.ParamIndex() - 1)
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
return true
|
||||
case key.NameDownArrow:
|
||||
cursor := t.Cursor()
|
||||
switch t.EditMode() {
|
||||
case tracker.EditPatterns:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Row = t.Song().Score.LengthInRows() - 1
|
||||
} else {
|
||||
cursor.Row += t.Song().Score.RowsPerPattern
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
case tracker.EditTracks:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Row += t.Song().Score.RowsPerPattern
|
||||
} else {
|
||||
if t.Step.Value > 0 {
|
||||
cursor.Row += t.Step.Value
|
||||
} else {
|
||||
cursor.Row++
|
||||
}
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
case tracker.EditUnits:
|
||||
t.SetUnitIndex(t.UnitIndex() + 1)
|
||||
case tracker.EditParameters:
|
||||
t.SetParamIndex(t.ParamIndex() + 1)
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
return true
|
||||
case key.NameLeftArrow:
|
||||
cursor := t.Cursor()
|
||||
switch t.EditMode() {
|
||||
case tracker.EditPatterns:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track = 0
|
||||
} else {
|
||||
cursor.Track--
|
||||
}
|
||||
case tracker.EditTracks:
|
||||
if !t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track--
|
||||
t.SetLowNibble(true)
|
||||
} else {
|
||||
t.SetLowNibble(false)
|
||||
}
|
||||
case tracker.EditUnits:
|
||||
t.SetInstrIndex(t.InstrIndex() - 1)
|
||||
case tracker.EditParameters:
|
||||
param, _ := t.Param(t.ParamIndex())
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetParam(param.Value - 16)
|
||||
} else {
|
||||
t.SetParam(param.Value - 1)
|
||||
}
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
return true
|
||||
case key.NameRightArrow:
|
||||
switch t.EditMode() {
|
||||
case tracker.EditPatterns:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track = len(t.Song().Score.Tracks) - 1
|
||||
} else {
|
||||
cursor.Track++
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
case tracker.EditTracks:
|
||||
if t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor := t.Cursor()
|
||||
cursor.Track++
|
||||
t.SetCursor(cursor)
|
||||
t.SetLowNibble(false)
|
||||
} else {
|
||||
t.SetLowNibble(true)
|
||||
}
|
||||
case tracker.EditUnits:
|
||||
t.SetInstrIndex(t.InstrIndex() + 1)
|
||||
case tracker.EditParameters:
|
||||
param, _ := t.Param(t.ParamIndex())
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetParam(param.Value + 16)
|
||||
} else {
|
||||
t.SetParam(param.Value + 1)
|
||||
}
|
||||
}
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
return true
|
||||
case "+":
|
||||
switch t.EditMode() {
|
||||
case tracker.EditTracks:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AdjustSelectionPitch(12)
|
||||
} else {
|
||||
t.AdjustSelectionPitch(1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "-":
|
||||
switch t.EditMode() {
|
||||
case tracker.EditTracks:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AdjustSelectionPitch(-12)
|
||||
} else {
|
||||
t.AdjustSelectionPitch(-1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
switch t.EditMode() {
|
||||
case tracker.EditPatterns:
|
||||
if iv, err := strconv.Atoi(e.Name); err == nil {
|
||||
t.SetCurrentPattern(iv)
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
return true
|
||||
}
|
||||
if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
|
||||
t.SetCurrentPattern(b + 10)
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
return true
|
||||
}
|
||||
case tracker.EditTracks:
|
||||
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
|
||||
if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
|
||||
t.NumberPressed(byte(iv))
|
||||
}
|
||||
} else {
|
||||
if e.Name == "A" {
|
||||
t.SetNote(0)
|
||||
} else {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
n := tracker.NoteAsValue(t.OctaveNumberInput.Value, val)
|
||||
t.SetNote(n)
|
||||
trk := t.Cursor().Track
|
||||
start := t.Song().Score.FirstVoiceForTrack(trk)
|
||||
end := start + t.Song().Score.Tracks[trk].NumVoices
|
||||
t.KeyPlaying[e.Name] = t.player.Trigger(start, end, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
return true
|
||||
case tracker.EditUnits:
|
||||
name := e.Name
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
name = strings.ToLower(name)
|
||||
}
|
||||
if val, ok := unitKeyMap[name]; ok {
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.SetUnitType(val)
|
||||
return true
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case tracker.EditParameters:
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
n := tracker.NoteAsValue(t.OctaveNumberInput.Value, val)
|
||||
instr := t.InstrIndex()
|
||||
start := t.Song().Patch.FirstVoiceForInstrument(instr)
|
||||
end := start + t.Instrument().NumVoices
|
||||
t.KeyPlaying[e.Name] = t.player.Trigger(start, end, n)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.State == key.Release {
|
||||
if ID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.player.Release(ID)
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
if _, playing := t.player.Position(); t.EditMode() == tracker.EditTracks && playing && t.Note() == 1 && t.NoteTracking() {
|
||||
t.SetNote(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NumberPressed handles incoming presses while in either of the hex number columns
|
||||
func (t *Tracker) NumberPressed(iv byte) {
|
||||
val := t.Note()
|
||||
if val == 1 {
|
||||
val = 0
|
||||
}
|
||||
if t.LowNibble() {
|
||||
val = (val & 0xF0) | (iv & 0xF)
|
||||
} else {
|
||||
val = ((iv & 0xF) << 4) | (val & 0xF)
|
||||
}
|
||||
t.SetNote(val)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
@ -22,10 +23,10 @@ func (t *Tracker) Layout(gtx layout.Context) {
|
||||
func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
|
||||
return t.BottomHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return Surface{Gray: 24, Focus: t.EditMode == 0}.Layout(gtx, t.layoutPatterns)
|
||||
return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditPatterns}.Layout(gtx, t.layoutPatterns)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return Surface{Gray: 24, Focus: t.EditMode == 1}.Layout(gtx, t.layoutTracker)
|
||||
return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditTracks}.Layout(gtx, t.layoutTracker)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
205
tracker/gioui/parameter.go
Normal file
205
tracker/gioui/parameter.go
Normal file
@ -0,0 +1,205 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type ParameterWidget struct {
|
||||
floatWidget widget.Float
|
||||
boolWidget widget.Bool
|
||||
labelBtn widget.Clickable
|
||||
instrBtn widget.Clickable
|
||||
instrMenu Menu
|
||||
unitBtn widget.Clickable
|
||||
unitMenu Menu
|
||||
}
|
||||
|
||||
type ParameterStyle struct {
|
||||
tracker *Tracker
|
||||
Parameter *tracker.Parameter
|
||||
ParameterWidget *ParameterWidget
|
||||
Theme *material.Theme
|
||||
Focus bool
|
||||
}
|
||||
|
||||
func (t *Tracker) ParamStyle(th *material.Theme, param *tracker.Parameter, paramWidget *ParameterWidget) ParameterStyle {
|
||||
return ParameterStyle{
|
||||
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
|
||||
Parameter: param,
|
||||
Theme: th,
|
||||
ParameterWidget: paramWidget,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParameterWidget) Clicked() bool {
|
||||
return p.labelBtn.Clicked()
|
||||
}
|
||||
|
||||
func (p ParameterStyle) Layout(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(110))
|
||||
return layout.E.Layout(gtx, Label(p.Parameter.Name, white))
|
||||
}),
|
||||
layout.Expanded(p.ParameterWidget.labelBtn.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
switch p.Parameter.Type {
|
||||
case tracker.IntegerParameter:
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
if !p.ParameterWidget.floatWidget.Dragging() {
|
||||
p.ParameterWidget.floatWidget.Value = float32(p.Parameter.Value)
|
||||
}
|
||||
sliderStyle := material.Slider(p.Theme, &p.ParameterWidget.floatWidget, float32(p.Parameter.Min), float32(p.Parameter.Max))
|
||||
sliderStyle.Color = p.Theme.Fg
|
||||
dims := sliderStyle.Layout(gtx)
|
||||
p.Parameter.Value = int(p.ParameterWidget.floatWidget.Value + 0.5)
|
||||
return dims
|
||||
case tracker.BoolParameter:
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(60))
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
p.ParameterWidget.boolWidget.Value = p.Parameter.Value > p.Parameter.Min
|
||||
boolStyle := material.Switch(p.Theme, &p.ParameterWidget.boolWidget)
|
||||
boolStyle.Color.Disabled = p.Theme.Fg
|
||||
boolStyle.Color.Enabled = white
|
||||
dims := layout.Center.Layout(gtx, boolStyle.Layout)
|
||||
if p.ParameterWidget.boolWidget.Value {
|
||||
p.Parameter.Value = p.Parameter.Max
|
||||
} else {
|
||||
p.Parameter.Value = p.Parameter.Min
|
||||
}
|
||||
return dims
|
||||
case tracker.IDParameter:
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
for clickedItem, hasClicked := p.ParameterWidget.instrMenu.Clicked(); hasClicked; {
|
||||
p.Parameter.Value = p.tracker.Song().Patch[clickedItem].Units[0].ID
|
||||
clickedItem, hasClicked = p.ParameterWidget.instrMenu.Clicked()
|
||||
}
|
||||
instrItems := make([]MenuItem, len(p.tracker.Song().Patch))
|
||||
for i, instr := range p.tracker.Song().Patch {
|
||||
instrItems[i].Text = instr.Name
|
||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||
}
|
||||
var unitItems []MenuItem
|
||||
instrName := "<instr>"
|
||||
unitName := "<unit>"
|
||||
targetI, targetU, err := p.tracker.Song().Patch.FindSendTarget(p.Parameter.Value)
|
||||
if err == nil {
|
||||
targetInstrument := p.tracker.Song().Patch[targetI]
|
||||
instrName = targetInstrument.Name
|
||||
units := targetInstrument.Units
|
||||
unitName = fmt.Sprintf("%v: %v", targetU, units[targetU].Type)
|
||||
unitItems = make([]MenuItem, len(units))
|
||||
for clickedItem, hasClicked := p.ParameterWidget.unitMenu.Clicked(); hasClicked; {
|
||||
p.Parameter.Value = units[clickedItem].ID
|
||||
clickedItem, hasClicked = p.ParameterWidget.unitMenu.Clicked()
|
||||
}
|
||||
for j, unit := range units {
|
||||
unitItems[j].Text = fmt.Sprintf("%v: %v", j, unit.Type)
|
||||
unitItems[j].IconBytes = icons.NavigationChevronRight
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(p.tracker.layoutMenu(instrName, &p.ParameterWidget.instrBtn, &p.ParameterWidget.instrMenu, unit.Dp(200),
|
||||
instrItems...,
|
||||
)),
|
||||
layout.Rigid(p.tracker.layoutMenu(unitName, &p.ParameterWidget.unitBtn, &p.ParameterWidget.unitMenu, unit.Dp(200),
|
||||
unitItems...,
|
||||
)),
|
||||
)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if p.Parameter.Type != tracker.IDParameter {
|
||||
return Label(p.Parameter.Hint, white)(gtx)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
func (t *Tracker) layoutParameter(gtx C, index int) D {
|
||||
u := t.Unit()
|
||||
ut, _ := sointu.UnitTypes[u.Type]
|
||||
|
||||
params := u.Parameters
|
||||
var name string
|
||||
var value, min, max int
|
||||
var valueText string
|
||||
if u.Type == "oscillator" && index == len(ut) {
|
||||
name = "sample"
|
||||
key := compiler.SampleOffset{Start: uint32(params["samplestart"]), LoopStart: uint16(params["loopstart"]), LoopLength: uint16(params["looplength"])}
|
||||
if v, ok := tracker.GmDlsEntryMap[key]; ok {
|
||||
value = v + 1
|
||||
valueText = fmt.Sprintf("%v / %v", value, tracker.GmDlsEntries[v].Name)
|
||||
} else {
|
||||
value = 0
|
||||
valueText = "0 / custom"
|
||||
}
|
||||
min, max = 0, len(tracker.GmDlsEntries)
|
||||
} else {
|
||||
if ut[index].MaxValue < ut[index].MinValue {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
name = ut[index].Name
|
||||
if u.Type == "oscillator" && (name == "samplestart" || name == "loopstart" || name == "looplength") {
|
||||
if params["type"] != sointu.Sample {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
}
|
||||
value = params[name]
|
||||
min, max = ut[index].MinValue, ut[index].MaxValue
|
||||
if u.Type == "send" && name == "voice" {
|
||||
max = t.Song().Patch.NumVoices()
|
||||
} else if u.Type == "send" && name == "unit" { // set the maximum values depending on the send target
|
||||
instrIndex, _, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
|
||||
if instrIndex != -1 {
|
||||
max = len(t.Song().Patch[instrIndex].Units) - 1
|
||||
}
|
||||
} else if u.Type == "send" && name == "port" { // set the maximum values depending on the send target
|
||||
instrIndex, unitIndex, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
|
||||
if instrIndex != -1 && unitIndex != -1 {
|
||||
max = len(sointu.Ports[t.Song().Patch[instrIndex].Units[unitIndex].Type]) - 1
|
||||
}
|
||||
}
|
||||
hint := t.Song().Patch.ParamHintString(t.InstrIndex(), t.UnitIndex(), name)
|
||||
if hint != "" {
|
||||
valueText = fmt.Sprintf("%v / %v", value, hint)
|
||||
} else {
|
||||
valueText = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
const patternCellHeight = 16
|
||||
@ -29,7 +30,7 @@ func (t *Tracker) layoutPatterns(gtx C) D {
|
||||
continue
|
||||
}
|
||||
if e.Type == pointer.Press {
|
||||
t.EditMode = EditPatterns
|
||||
t.SetEditMode(tracker.EditPatterns)
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
@ -37,28 +38,30 @@ func (t *Tracker) layoutPatterns(gtx C) D {
|
||||
pointer.InputOp{Tag: &patternPointerTag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
patternRect := SongRect{
|
||||
Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track},
|
||||
Corner2: SongPoint{SongRow: SongRow{Pattern: t.SelectionCorner.Pattern}, Track: t.SelectionCorner.Track},
|
||||
patternRect := tracker.SongRect{
|
||||
Corner1: tracker.SongPoint{SongRow: tracker.SongRow{Pattern: t.Cursor().Pattern}, Track: t.Cursor().Track},
|
||||
Corner2: tracker.SongPoint{SongRow: tracker.SongRow{Pattern: t.SelectionCorner().Pattern}, Track: t.SelectionCorner().Track},
|
||||
}
|
||||
for j := 0; j < t.song.SequenceLength(); j++ {
|
||||
if j == t.PlayPosition.Pattern && t.Playing {
|
||||
for j := 0; j < t.Song().Score.Length; j++ {
|
||||
if playPos, ok := t.player.Position(); ok && j == playPos.Pattern {
|
||||
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
|
||||
}
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)))
|
||||
stack := op.Save(gtx.Ops)
|
||||
op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops)
|
||||
for i, track := range t.song.Tracks {
|
||||
for i, track := range t.Song().Score.Tracks {
|
||||
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(track.Sequence[j]))
|
||||
point := SongPoint{Track: i, SongRow: SongRow{Pattern: j}}
|
||||
if t.EditMode == EditPatterns || t.EditMode == EditTracks {
|
||||
if j < len(track.Order) && track.Order[j] >= 0 {
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(track.Order[j]))
|
||||
}
|
||||
point := tracker.SongPoint{Track: i, SongRow: tracker.SongRow{Pattern: j}}
|
||||
if t.EditMode() == tracker.EditPatterns || t.EditMode() == tracker.EditTracks {
|
||||
if patternRect.Contains(point) {
|
||||
color := inactiveSelectionColor
|
||||
if t.EditMode == EditPatterns {
|
||||
if t.EditMode() == tracker.EditPatterns {
|
||||
color = selectionColor
|
||||
if point.Pattern == t.Cursor.Pattern && point.Track == t.Cursor.Track {
|
||||
if point.Pattern == t.Cursor().Pattern && point.Track == t.Cursor().Track {
|
||||
color = cursorColor
|
||||
}
|
||||
}
|
||||
@ -73,9 +76,11 @@ func (t *Tracker) layoutPatterns(gtx C) D {
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
|
||||
func patternIndexToString(index byte) string {
|
||||
if index < 10 {
|
||||
return string([]byte{'0' + index})
|
||||
func patternIndexToString(index int) string {
|
||||
if index < 0 {
|
||||
return ""
|
||||
} else if index < 10 {
|
||||
return string('0' + byte(index))
|
||||
}
|
||||
return string([]byte{'A' + index - 10})
|
||||
return string('A' + byte(index-10))
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
72
tracker/gioui/rowmarkers.go
Normal file
72
tracker/gioui/rowmarkers.go
Normal file
@ -0,0 +1,72 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strings"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
const rowMarkerWidth = 50
|
||||
|
||||
func (t *Tracker) layoutRowMarkers(gtx C) D {
|
||||
gtx.Constraints.Min.X = rowMarkerWidth
|
||||
paint.FillShape(gtx.Ops, rowMarkerSurfaceColor, clip.Rect{
|
||||
Max: gtx.Constraints.Max,
|
||||
}.Op())
|
||||
defer op.Save(gtx.Ops).Load()
|
||||
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
|
||||
op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops)
|
||||
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
|
||||
playPos, playing := t.player.Position()
|
||||
playSongRow := playPos.Pattern*t.Song().Score.RowsPerPattern + playPos.Row
|
||||
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
|
||||
beatMarkerDensity := t.Song().RowsPerBeat
|
||||
for beatMarkerDensity <= 2 {
|
||||
beatMarkerDensity *= 2
|
||||
}
|
||||
for i := 0; i < t.Song().Score.Length; i++ {
|
||||
for j := 0; j < t.Song().Score.RowsPerPattern; j++ {
|
||||
songRow := i*t.Song().Score.RowsPerPattern + j
|
||||
if mod(songRow, beatMarkerDensity*2) == 0 {
|
||||
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
} else if mod(songRow, beatMarkerDensity) == 0 {
|
||||
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if playing && songRow == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if j == 0 {
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", i)))
|
||||
}
|
||||
if t.EditMode() == tracker.EditTracks && songRow == cursorSongRow {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
op.Offset(f32.Pt(rowMarkerWidth/2, 0)).Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)))
|
||||
op.Offset(f32.Pt(-rowMarkerWidth/2, trackRowHeight)).Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
return layout.Dimensions{Size: image.Pt(rowMarkerWidth, gtx.Constraints.Max.Y)}
|
||||
}
|
||||
|
||||
func mod(a, b int) int {
|
||||
m := a % b
|
||||
if a < 0 && b < 0 {
|
||||
m -= b
|
||||
}
|
||||
if a < 0 && b > 0 {
|
||||
m += b
|
||||
}
|
||||
return m
|
||||
}
|
||||
69
tracker/gioui/run.go
Normal file
69
tracker/gioui/run.go
Normal file
@ -0,0 +1,69 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
func (t *Tracker) Run(w *app.Window) error {
|
||||
var ops op.Ops
|
||||
for {
|
||||
if pos, playing := t.player.Position(); t.NoteTracking() && playing {
|
||||
cursor := t.Cursor()
|
||||
cursor.SongRow = pos
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
}
|
||||
select {
|
||||
case <-t.refresh:
|
||||
w.Invalidate()
|
||||
case v := <-t.volumeChan:
|
||||
t.lastVolume = v
|
||||
w.Invalidate()
|
||||
case e := <-w.Events():
|
||||
switch e := e.(type) {
|
||||
case system.DestroyEvent:
|
||||
return e.Err
|
||||
case key.Event:
|
||||
if t.KeyEvent(w, e) {
|
||||
w.Invalidate()
|
||||
}
|
||||
case clipboard.Event:
|
||||
err := t.UnmarshalContent([]byte(e.Text))
|
||||
if err == nil {
|
||||
w.Invalidate()
|
||||
}
|
||||
case system.FrameEvent:
|
||||
gtx := layout.NewContext(&ops, e)
|
||||
t.Layout(gtx)
|
||||
e.Frame(gtx.Ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Main(audioContext sointu.AudioContext, synthService sointu.SynthService) {
|
||||
go func() {
|
||||
w := app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
t := New(audioContext, synthService)
|
||||
defer t.Close()
|
||||
if err := t.Run(w); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
app.Main()
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
@ -53,7 +53,7 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
|
||||
for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; {
|
||||
switch clickedItem {
|
||||
case 0:
|
||||
t.LoadSong(defaultSong.Copy())
|
||||
t.ResetSong()
|
||||
case 1:
|
||||
t.LoadSongFile()
|
||||
case 2:
|
||||
@ -69,7 +69,7 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
|
||||
case 1:
|
||||
t.Redo()
|
||||
case 2:
|
||||
if contents, err := yaml.Marshal(t.song); err == nil {
|
||||
if contents, err := yaml.Marshal(t.Song()); err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
@ -90,8 +90,8 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
|
||||
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
|
||||
)),
|
||||
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(160),
|
||||
MenuItem{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Disabled: len(t.undoStack) == 0},
|
||||
MenuItem{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Disabled: len(t.redoStack) == 0},
|
||||
MenuItem{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Disabled: !t.CanUndo()},
|
||||
MenuItem{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Disabled: !t.CanRedo()},
|
||||
MenuItem{IconBytes: icons.ContentContentCopy, Text: "Copy", ShortcutText: shortcutKey + "C"},
|
||||
MenuItem{IconBytes: icons.ContentContentPaste, Text: "Paste", ShortcutText: shortcutKey + "V"},
|
||||
)),
|
||||
@ -104,7 +104,7 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
|
||||
panicBtnStyle := material.Button(t.Theme, t.PanicBtn, "Panic")
|
||||
if t.sequencer.Enabled() {
|
||||
if t.player.Enabled() {
|
||||
panicBtnStyle.Background = transparent
|
||||
panicBtnStyle.Color = t.Theme.Palette.Fg
|
||||
} else {
|
||||
@ -113,11 +113,7 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
}
|
||||
|
||||
for t.PanicBtn.Clicked() {
|
||||
if t.sequencer.Enabled() {
|
||||
t.sequencer.Disable()
|
||||
} else {
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
t.player.Disable()
|
||||
}
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
@ -125,7 +121,7 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("LEN:", white)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.SongLength.Value = t.song.SequenceLength()
|
||||
t.SongLength.Value = t.Song().Score.Length
|
||||
numStyle := NumericUpDown(t.Theme, t.SongLength, 1, math.MaxInt32)
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
|
||||
@ -139,7 +135,7 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("BPM:", white)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.BPM.Value = t.song.BPM
|
||||
t.BPM.Value = t.Song().BPM
|
||||
numStyle := NumericUpDown(t.Theme, t.BPM, 1, 999)
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
|
||||
@ -153,7 +149,7 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("RPP:", white)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.RowsPerPattern.Value = t.song.RowsPerPattern
|
||||
t.RowsPerPattern.Value = t.Song().Score.RowsPerPattern
|
||||
numStyle := NumericUpDown(t.Theme, t.RowsPerPattern, 1, 255)
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
|
||||
@ -167,7 +163,7 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("RPB:", white)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.RowsPerBeat.Value = t.song.RowsPerBeat
|
||||
t.RowsPerBeat.Value = t.Song().RowsPerBeat
|
||||
numStyle := NumericUpDown(t.Theme, t.RowsPerBeat, 1, 32)
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
|
||||
@ -194,6 +190,6 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return panicBtnStyle.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(t.VuMeter.Layout),
|
||||
layout.Rigid(VuMeter{Volume: t.lastVolume, Range: 100}.Layout),
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
@ -1,4 +1,4 @@
|
||||
package tracker
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
@ -25,30 +26,10 @@ var trackPointerTag bool
|
||||
var trackJumpPointerTag bool
|
||||
|
||||
func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
|
||||
t.playRowPatMutex.RLock()
|
||||
defer t.playRowPatMutex.RUnlock()
|
||||
|
||||
playPat := t.PlayPosition.Pattern
|
||||
if !t.Playing {
|
||||
playPat = -1
|
||||
}
|
||||
|
||||
rowMarkers := layout.Rigid(t.layoutRowMarkers(
|
||||
t.song.RowsPerPattern,
|
||||
len(t.song.Tracks[0].Sequence),
|
||||
t.Cursor.Row,
|
||||
t.Cursor.Pattern,
|
||||
t.CursorColumn,
|
||||
t.PlayPosition.Row,
|
||||
playPat,
|
||||
))
|
||||
rowMarkers := layout.Rigid(t.layoutRowMarkers)
|
||||
|
||||
for t.NewTrackBtn.Clicked() {
|
||||
t.AddTrack()
|
||||
}
|
||||
|
||||
for len(t.TrackShowHex) < len(t.song.Tracks) {
|
||||
t.TrackShowHex = append(t.TrackShowHex, false)
|
||||
t.AddTrack(true)
|
||||
}
|
||||
|
||||
//t.TrackHexCheckBoxes[i2].Value = t.TrackShowHex[i2]
|
||||
@ -92,31 +73,30 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
|
||||
newTrackBtnStyle := material.IconButton(t.Theme, t.NewTrackBtn, widgetForIcon(icons.ContentAdd))
|
||||
newTrackBtnStyle.Background = transparent
|
||||
newTrackBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if t.song.TotalTrackVoices() < t.song.Patch.TotalVoices() {
|
||||
if t.CanAddTrack() {
|
||||
newTrackBtnStyle.Color = primaryColor
|
||||
} else {
|
||||
newTrackBtnStyle.Color = disabledTextColor
|
||||
}
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
octave := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, t.Octave, 0, 9)
|
||||
t.OctaveNumberInput.Value = t.Octave()
|
||||
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, 0, 9)
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
|
||||
return in.Layout(gtx, numStyle.Layout)
|
||||
}
|
||||
n := t.song.Tracks[t.Cursor.Track].NumVoices
|
||||
maxRemain := t.song.Patch.TotalVoices() - t.song.TotalTrackVoices() + n
|
||||
if maxRemain < 1 {
|
||||
maxRemain = 1
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetOctave(t.OctaveNumberInput.Value)
|
||||
return dims
|
||||
}
|
||||
n := t.Song().Score.Tracks[t.Cursor().Track].NumVoices
|
||||
t.TrackVoices.Value = n
|
||||
voiceUpDown := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, t.TrackVoices, 1, maxRemain)
|
||||
numStyle := NumericUpDown(t.Theme, t.TrackVoices, 1, t.MaxTrackVoices())
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(70))
|
||||
return in.Layout(gtx, numStyle.Layout)
|
||||
}
|
||||
t.TrackHexCheckBox.Value = t.TrackShowHex[t.Cursor.Track]
|
||||
t.TrackHexCheckBox.Value = t.Song().Score.Tracks[t.Cursor().Track].Effect
|
||||
hexCheckBoxStyle := material.CheckBox(t.Theme, t.TrackHexCheckBox, "Hex")
|
||||
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label("OCT:", white)),
|
||||
@ -131,7 +111,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(newTrackBtnStyle.Layout))
|
||||
t.TrackShowHex[t.Cursor.Track] = t.TrackHexCheckBox.Value
|
||||
t.Song().Score.Tracks[t.Cursor().Track].Effect = t.TrackHexCheckBox.Value // TODO: we should not modify the model, but how should this be done
|
||||
t.SetTrackVoices(t.TrackVoices.Value)
|
||||
return dims
|
||||
}
|
||||
@ -142,7 +122,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
|
||||
continue
|
||||
}
|
||||
if e.Type == pointer.Press {
|
||||
t.EditMode = EditTracks
|
||||
t.SetEditMode(tracker.EditTracks)
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
@ -153,7 +133,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return Surface{Gray: 37, Focus: t.EditMode == 1, FitSize: true}.Layout(gtx, menu)
|
||||
return Surface{Gray: 37, Focus: t.EditMode() == tracker.EditTracks, FitSize: true}.Layout(gtx, menu)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
@ -166,20 +146,20 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
|
||||
func (t *Tracker) layoutTracks(gtx C) D {
|
||||
defer op.Save(gtx.Ops).Load()
|
||||
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
|
||||
cursorSongRow := t.Cursor.Pattern*t.song.RowsPerPattern + t.Cursor.Row
|
||||
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
|
||||
for _, ev := range gtx.Events(&trackJumpPointerTag) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if e.Type == pointer.Press {
|
||||
t.EditMode = EditTracks
|
||||
t.Cursor.Track = int(e.Position.X) / trackColWidth
|
||||
t.Cursor.Pattern = 0
|
||||
t.Cursor.Row = int((e.Position.Y-float32(gtx.Constraints.Max.Y-trackRowHeight)/2)/trackRowHeight + float32(cursorSongRow))
|
||||
t.Cursor.Clamp(t.song)
|
||||
t.SelectionCorner = t.Cursor
|
||||
cursorSongRow = t.Cursor.Pattern*t.song.RowsPerPattern + t.Cursor.Row
|
||||
t.SetEditMode(tracker.EditTracks)
|
||||
track := int(e.Position.X) / trackColWidth
|
||||
row := int((e.Position.Y-float32(gtx.Constraints.Max.Y-trackRowHeight)/2)/trackRowHeight + float32(cursorSongRow))
|
||||
cursor := tracker.SongPoint{Track: track, SongRow: tracker.SongRow{Row: row}}.Clamp(t.Song().Score)
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
cursorSongRow = cursor.Pattern*t.Song().Score.RowsPerPattern + cursor.Row
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
@ -189,9 +169,9 @@ func (t *Tracker) layoutTracks(gtx C) D {
|
||||
}.Add(gtx.Ops)
|
||||
op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops)
|
||||
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
|
||||
if t.EditMode == EditPatterns || t.EditMode == EditTracks {
|
||||
x1, y1 := t.Cursor.Track, t.Cursor.Pattern
|
||||
x2, y2 := t.SelectionCorner.Track, t.SelectionCorner.Pattern
|
||||
if t.EditMode() == tracker.EditPatterns || t.EditMode() == tracker.EditTracks {
|
||||
x1, y1 := t.Cursor().Track, t.Cursor().Pattern
|
||||
x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
@ -201,14 +181,14 @@ func (t *Tracker) layoutTracks(gtx C) D {
|
||||
x2++
|
||||
y2++
|
||||
x1 *= trackColWidth
|
||||
y1 *= trackRowHeight * t.song.RowsPerPattern
|
||||
y1 *= trackRowHeight * t.Song().Score.RowsPerPattern
|
||||
x2 *= trackColWidth
|
||||
y2 *= trackRowHeight * t.song.RowsPerPattern
|
||||
y2 *= trackRowHeight * t.Song().Score.RowsPerPattern
|
||||
paint.FillShape(gtx.Ops, inactiveSelectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
|
||||
}
|
||||
if t.EditMode == EditTracks {
|
||||
x1, y1 := t.Cursor.Track, t.Cursor.Pattern*t.song.RowsPerPattern+t.Cursor.Row
|
||||
x2, y2 := t.SelectionCorner.Track, t.SelectionCorner.Pattern*t.song.RowsPerPattern+t.SelectionCorner.Row
|
||||
if t.EditMode() == tracker.EditTracks {
|
||||
x1, y1 := t.Cursor().Track, t.Cursor().Pattern*t.Song().Score.RowsPerPattern+t.Cursor().Row
|
||||
x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern*t.Song().Score.RowsPerPattern+t.SelectionCorner().Row
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
@ -222,9 +202,16 @@ func (t *Tracker) layoutTracks(gtx C) D {
|
||||
x2 *= trackColWidth
|
||||
y2 *= trackRowHeight
|
||||
paint.FillShape(gtx.Ops, selectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
|
||||
cx := t.Cursor.Track * trackColWidth
|
||||
cy := (t.Cursor.Pattern*t.song.RowsPerPattern + t.Cursor.Row) * trackRowHeight
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{Min: image.Pt(cx, cy), Max: image.Pt(cx+trackColWidth, cy+trackRowHeight)}.Op())
|
||||
cx := t.Cursor().Track * trackColWidth
|
||||
cy := (t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row) * trackRowHeight
|
||||
cw := trackColWidth
|
||||
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
|
||||
cw /= 2
|
||||
if t.LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{Min: image.Pt(cx, cy), Max: image.Pt(cx+cw, cy+trackRowHeight)}.Op())
|
||||
}
|
||||
delta := (gtx.Constraints.Max.Y/2 + trackRowHeight - 1) / trackRowHeight
|
||||
firstRow := cursorSongRow - delta
|
||||
@ -232,28 +219,41 @@ func (t *Tracker) layoutTracks(gtx C) D {
|
||||
if firstRow < 0 {
|
||||
firstRow = 0
|
||||
}
|
||||
if l := t.song.TotalRows(); lastRow >= l {
|
||||
if l := t.Song().Score.LengthInRows(); lastRow >= l {
|
||||
lastRow = l - 1
|
||||
}
|
||||
op.Offset(f32.Pt(0, float32(trackRowHeight*firstRow))).Add(gtx.Ops)
|
||||
for trkIndex, trk := range t.song.Tracks {
|
||||
for _, trk := range t.Song().Score.Tracks {
|
||||
stack := op.Save(gtx.Ops)
|
||||
for row := firstRow; row <= lastRow; row++ {
|
||||
pat := row / t.song.RowsPerPattern
|
||||
patRow := row % t.song.RowsPerPattern
|
||||
s := trk.Sequence[pat]
|
||||
if patRow == 0 {
|
||||
pat := row / t.Song().Score.RowsPerPattern
|
||||
patRow := row % t.Song().Score.RowsPerPattern
|
||||
s := -1
|
||||
if pat >= 0 && pat < len(trk.Order) {
|
||||
s = trk.Order[pat]
|
||||
}
|
||||
if s < 0 {
|
||||
op.Offset(f32.Pt(0, trackRowHeight)).Add(gtx.Ops)
|
||||
continue
|
||||
}
|
||||
if s >= 0 && patRow == 0 {
|
||||
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(s))
|
||||
}
|
||||
op.Offset(f32.Pt(patmarkWidth, 0)).Add(gtx.Ops)
|
||||
if t.EditMode == EditTracks && t.Cursor.SongRow.Row == patRow && t.Cursor.SongRow.Pattern == pat {
|
||||
if t.EditMode() == tracker.EditTracks && t.Cursor().Row == patRow && t.Cursor().Pattern == pat {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
c := trk.Patterns[s][patRow]
|
||||
if t.TrackShowHex[trkIndex] {
|
||||
var c byte = 1
|
||||
if s >= 0 && s < len(trk.Patterns) {
|
||||
pattern := trk.Patterns[s]
|
||||
if patRow >= 0 && patRow < len(pattern) {
|
||||
c = pattern[patRow]
|
||||
}
|
||||
}
|
||||
if trk.Effect {
|
||||
var text string
|
||||
switch c {
|
||||
case 0:
|
||||
@ -265,7 +265,7 @@ func (t *Tracker) layoutTracks(gtx C) D {
|
||||
}
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(text))
|
||||
} else {
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, valueAsNote(c))
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, tracker.NoteStr(c))
|
||||
}
|
||||
op.Offset(f32.Pt(-patmarkWidth, trackRowHeight)).Add(gtx.Ops)
|
||||
}
|
||||
172
tracker/gioui/tracker.go
Normal file
172
tracker/gioui/tracker.go
Normal file
@ -0,0 +1,172 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/text"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
Theme *material.Theme
|
||||
MenuBar []widget.Clickable
|
||||
Menus []Menu
|
||||
OctaveNumberInput *NumberInput
|
||||
BPM *NumberInput
|
||||
RowsPerPattern *NumberInput
|
||||
RowsPerBeat *NumberInput
|
||||
Step *NumberInput
|
||||
InstrumentVoices *NumberInput
|
||||
TrackVoices *NumberInput
|
||||
InstrumentNameEditor *widget.Editor
|
||||
NewTrackBtn *widget.Clickable
|
||||
NewInstrumentBtn *widget.Clickable
|
||||
DeleteInstrumentBtn *widget.Clickable
|
||||
AddSemitoneBtn *widget.Clickable
|
||||
SubtractSemitoneBtn *widget.Clickable
|
||||
AddOctaveBtn *widget.Clickable
|
||||
SubtractOctaveBtn *widget.Clickable
|
||||
SongLength *NumberInput
|
||||
PanicBtn *widget.Clickable
|
||||
CopyInstrumentBtn *widget.Clickable
|
||||
ParameterList *layout.List
|
||||
ParameterScrollBar *ScrollBar
|
||||
Parameters []*ParameterWidget
|
||||
UnitDragList *DragList
|
||||
UnitScrollBar *ScrollBar
|
||||
DeleteUnitBtn *widget.Clickable
|
||||
ClearUnitBtn *widget.Clickable
|
||||
ChooseUnitTypeList *layout.List
|
||||
ChooseUnitScrollBar *ScrollBar
|
||||
ChooseUnitTypeBtns []*widget.Clickable
|
||||
AddUnitBtn *widget.Clickable
|
||||
InstrumentDragList *DragList
|
||||
InstrumentScrollBar *ScrollBar
|
||||
TrackHexCheckBox *widget.Bool
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
StackUse []int
|
||||
KeyPlaying map[string]uint32
|
||||
Alert Alert
|
||||
|
||||
lastVolume tracker.Volume
|
||||
volumeChan chan tracker.Volume
|
||||
|
||||
player *tracker.Player
|
||||
refresh chan struct{}
|
||||
playerCloser chan struct{}
|
||||
audioContext sointu.AudioContext
|
||||
|
||||
*tracker.Model
|
||||
}
|
||||
|
||||
func (t *Tracker) UnmarshalContent(bytes []byte) error {
|
||||
var instr sointu.Instrument
|
||||
if errJSON := json.Unmarshal(bytes, &instr); errJSON == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if errYaml := yaml.Unmarshal(bytes, &instr); errYaml == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
if song.BPM > 0 {
|
||||
t.SetSong(song)
|
||||
return nil
|
||||
}
|
||||
return errors.New("was able to unmarshal a song, but the bpm was 0")
|
||||
}
|
||||
|
||||
func (t *Tracker) Close() {
|
||||
t.playerCloser <- struct{}{}
|
||||
t.audioContext.Close()
|
||||
}
|
||||
|
||||
func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tracker {
|
||||
t := &Tracker{
|
||||
Theme: material.NewTheme(gofont.Collection()),
|
||||
audioContext: audioContext,
|
||||
BPM: new(NumberInput),
|
||||
OctaveNumberInput: &NumberInput{Value: 4},
|
||||
SongLength: new(NumberInput),
|
||||
RowsPerPattern: new(NumberInput),
|
||||
RowsPerBeat: new(NumberInput),
|
||||
Step: &NumberInput{Value: 1},
|
||||
InstrumentVoices: new(NumberInput),
|
||||
TrackVoices: new(NumberInput),
|
||||
InstrumentNameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
|
||||
NewTrackBtn: new(widget.Clickable),
|
||||
NewInstrumentBtn: new(widget.Clickable),
|
||||
DeleteInstrumentBtn: new(widget.Clickable),
|
||||
AddSemitoneBtn: new(widget.Clickable),
|
||||
SubtractSemitoneBtn: new(widget.Clickable),
|
||||
AddOctaveBtn: new(widget.Clickable),
|
||||
SubtractOctaveBtn: new(widget.Clickable),
|
||||
AddUnitBtn: new(widget.Clickable),
|
||||
DeleteUnitBtn: new(widget.Clickable),
|
||||
ClearUnitBtn: new(widget.Clickable),
|
||||
PanicBtn: new(widget.Clickable),
|
||||
CopyInstrumentBtn: new(widget.Clickable),
|
||||
TrackHexCheckBox: new(widget.Bool),
|
||||
Menus: make([]Menu, 2),
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
UnitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}, HoverItem: -1},
|
||||
UnitScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
refresh: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already
|
||||
InstrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}, HoverItem: -1},
|
||||
InstrumentScrollBar: &ScrollBar{Axis: layout.Horizontal},
|
||||
ParameterList: &layout.List{Axis: layout.Vertical},
|
||||
ParameterScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
TopHorizontalSplit: &Split{Ratio: -.6},
|
||||
BottomHorizontalSplit: &Split{Ratio: -.6},
|
||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||
ChooseUnitTypeList: &layout.List{Axis: layout.Vertical},
|
||||
ChooseUnitScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
KeyPlaying: make(map[string]uint32),
|
||||
volumeChan: make(chan tracker.Volume, 1),
|
||||
playerCloser: make(chan struct{}),
|
||||
}
|
||||
t.Model = tracker.NewModel()
|
||||
vuBufferObserver := make(chan []float32)
|
||||
go tracker.VuAnalyzer(0.3, 1e-4, 1, -100, vuBufferObserver, t.volumeChan)
|
||||
t.Theme.Palette.Fg = primaryColor
|
||||
t.Theme.Palette.ContrastFg = black
|
||||
t.SetEditMode(tracker.EditTracks)
|
||||
for range tracker.UnitTypeNames {
|
||||
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable))
|
||||
}
|
||||
t.SetOctave(4)
|
||||
patchObserver := make(chan sointu.Patch, 16)
|
||||
t.AddPatchObserver(patchObserver)
|
||||
scoreObserver := make(chan sointu.Score, 16)
|
||||
t.AddScoreObserver(scoreObserver)
|
||||
sprObserver := make(chan int, 16)
|
||||
t.AddSamplesPerRowObserver(sprObserver)
|
||||
audioChannel := make(chan []float32)
|
||||
t.player = tracker.NewPlayer(synthService, t.playerCloser, patchObserver, scoreObserver, sprObserver, t.refresh, audioChannel, vuBufferObserver)
|
||||
audioOut := audioContext.Output()
|
||||
go func() {
|
||||
for buf := range audioChannel {
|
||||
audioOut.WriteAudio(buf)
|
||||
}
|
||||
}()
|
||||
t.ResetSong()
|
||||
return t
|
||||
}
|
||||
146
tracker/gioui/uniteditor.go
Normal file
146
tracker/gioui/uniteditor.go
Normal file
@ -0,0 +1,146 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
func (t *Tracker) layoutUnitEditor(gtx C) D {
|
||||
editorFunc := t.layoutUnitSliders
|
||||
if t.Unit().Type == "" {
|
||||
editorFunc = t.layoutUnitTypeChooser
|
||||
}
|
||||
return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, editorFunc),
|
||||
layout.Rigid(t.layoutUnitFooter()))
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutUnitSliders(gtx C) D {
|
||||
numItems := t.NumParams()
|
||||
|
||||
for len(t.Parameters) <= numItems {
|
||||
t.Parameters = append(t.Parameters, new(ParameterWidget))
|
||||
}
|
||||
|
||||
listItem := func(gtx C, index int) D {
|
||||
for t.Parameters[index].Clicked() {
|
||||
if t.EditMode() != tracker.EditParameters || t.ParamIndex() != index {
|
||||
t.SetEditMode(tracker.EditParameters)
|
||||
t.SetParamIndex(index)
|
||||
} else {
|
||||
t.ResetParam()
|
||||
}
|
||||
}
|
||||
param, err := t.Param(index)
|
||||
if err != nil {
|
||||
return D{}
|
||||
}
|
||||
oldVal := param.Value
|
||||
paramStyle := t.ParamStyle(t.Theme, ¶m, t.Parameters[index])
|
||||
paramStyle.Focus = t.EditMode() == tracker.EditParameters && t.ParamIndex() == index
|
||||
dims := paramStyle.Layout(gtx)
|
||||
if oldVal != param.Value {
|
||||
t.SetEditMode(tracker.EditParameters)
|
||||
t.SetParamIndex(index)
|
||||
t.SetParam(param.Value)
|
||||
}
|
||||
return dims
|
||||
}
|
||||
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return t.ParameterList.Layout(gtx, numItems, listItem)
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
gtx.Constraints.Min = gtx.Constraints.Max
|
||||
return t.ParameterScrollBar.Layout(gtx, unit.Dp(10), numItems, &t.ParameterList.Position)
|
||||
}))
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutUnitFooter() layout.Widget {
|
||||
return func(gtx C) D {
|
||||
for t.ClearUnitBtn.Clicked() {
|
||||
t.SetUnitType("")
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
for t.DeleteUnitBtn.Clicked() {
|
||||
t.DeleteUnit(false)
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
deleteUnitBtnStyle := material.IconButton(t.Theme, t.DeleteUnitBtn, widgetForIcon(icons.ActionDelete))
|
||||
deleteUnitBtnStyle.Background = transparent
|
||||
deleteUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if t.CanDeleteUnit() {
|
||||
deleteUnitBtnStyle.Color = primaryColor
|
||||
} else {
|
||||
deleteUnitBtnStyle.Color = disabledTextColor
|
||||
}
|
||||
text := t.Unit().Type
|
||||
if text == "" {
|
||||
text = "Choose unit type"
|
||||
} else {
|
||||
text = strings.Title(text)
|
||||
}
|
||||
hintText := Label(text, white)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
var dims D
|
||||
if t.Unit().Type != "" {
|
||||
clearUnitBtnStyle := material.IconButton(t.Theme, t.ClearUnitBtn, widgetForIcon(icons.ContentClear))
|
||||
clearUnitBtnStyle.Color = primaryColor
|
||||
clearUnitBtnStyle.Background = transparent
|
||||
clearUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
dims = clearUnitBtnStyle.Layout(gtx)
|
||||
}
|
||||
return D{Size: image.Pt(gtx.Px(unit.Dp(48)), dims.Size.Y)}
|
||||
}),
|
||||
layout.Flexed(1, hintText),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutUnitTypeChooser(gtx C) D {
|
||||
listElem := func(gtx C, i int) D {
|
||||
for t.ChooseUnitTypeBtns[i].Clicked() {
|
||||
t.SetUnitType(tracker.UnitTypeNames[i])
|
||||
}
|
||||
labelStyle := LabelStyle{Text: tracker.UnitTypeNames[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)}
|
||||
bg := func(gtx C) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, 20))
|
||||
var color color.NRGBA
|
||||
if t.ChooseUnitTypeBtns[i].Hovered() {
|
||||
color = unitTypeListHighlightColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
leftMargin := layout.Inset{Left: unit.Dp(10)}
|
||||
return layout.Stack{Alignment: layout.W}.Layout(gtx,
|
||||
layout.Stacked(bg),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return leftMargin.Layout(gtx, labelStyle.Layout)
|
||||
}),
|
||||
layout.Expanded(t.ChooseUnitTypeBtns[i].Layout))
|
||||
}
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return t.ChooseUnitTypeList.Layout(gtx, len(tracker.UnitTypeNames), listElem)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return t.ChooseUnitScrollBar.Layout(gtx, unit.Dp(10), len(tracker.UnitTypeNames), &t.ChooseUnitTypeList.Position)
|
||||
}),
|
||||
)
|
||||
}
|
||||
47
tracker/gioui/vumeter.go
Normal file
47
tracker/gioui/vumeter.go
Normal file
@ -0,0 +1,47 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type VuMeter struct {
|
||||
Volume tracker.Volume
|
||||
Range float32
|
||||
}
|
||||
|
||||
func (v VuMeter) Layout(gtx C) D {
|
||||
defer op.Save(gtx.Ops).Load()
|
||||
gtx.Constraints.Max.Y = gtx.Px(unit.Dp(12))
|
||||
height := gtx.Px(unit.Dp(6))
|
||||
for j := 0; j < 2; j++ {
|
||||
value := v.Volume.Average[j] + v.Range
|
||||
if value > 0 {
|
||||
x := int(value/v.Range*float32(gtx.Constraints.Max.X) + 0.5)
|
||||
if x > gtx.Constraints.Max.X {
|
||||
x = gtx.Constraints.Max.X
|
||||
}
|
||||
paint.FillShape(gtx.Ops, mediumEmphasisTextColor, clip.Rect(image.Rect(0, 0, x, height)).Op())
|
||||
}
|
||||
valueMax := v.Volume.Peak[j] + v.Range
|
||||
if valueMax > 0 {
|
||||
color := white
|
||||
if valueMax >= v.Range {
|
||||
color = errorColor
|
||||
}
|
||||
x := int(valueMax/v.Range*float32(gtx.Constraints.Max.X) + 0.5)
|
||||
if x > gtx.Constraints.Max.X {
|
||||
x = gtx.Constraints.Max.X
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect(image.Rect(x-1, 0, x, height)).Op())
|
||||
}
|
||||
op.Offset(f32.Pt(0, float32(height))).Add(gtx.Ops)
|
||||
}
|
||||
return D{Size: gtx.Constraints.Max}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
package tracker
|
||||
|
||||
var gmDlsEntries = []GmDlsEntry{
|
||||
var GmDlsEntries = []GmDlsEntry{
|
||||
{Start: 140078, LoopStart: 1353, LoopLength: 91, SuggestedTranspose: 1, Name: "101BS35"},
|
||||
{Start: 141606, LoopStart: 1380, LoopLength: 43, SuggestedTranspose: -12, Name: "101BS48"},
|
||||
{Start: 143113, LoopStart: 5448, LoopLength: 563, SuggestedTranspose: 5, Name: "12STR55A"},
|
||||
|
||||
@ -10,12 +10,12 @@ type GmDlsEntry struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
var gmDlsEntryMap = make(map[compiler.SampleOffset]int)
|
||||
var GmDlsEntryMap = make(map[compiler.SampleOffset]int)
|
||||
|
||||
func init() {
|
||||
for i, e := range gmDlsEntries {
|
||||
for i, e := range GmDlsEntries {
|
||||
key := compiler.SampleOffset{Start: uint32(e.Start), LoopStart: uint16(e.LoopStart), LoopLength: uint16(e.LoopLength)}
|
||||
gmDlsEntryMap[key] = i
|
||||
GmDlsEntryMap[key] = i
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,448 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/key"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var noteMap = map[string]int{
|
||||
"Z": -12,
|
||||
"S": -11,
|
||||
"X": -10,
|
||||
"D": -9,
|
||||
"C": -8,
|
||||
"V": -7,
|
||||
"G": -6,
|
||||
"B": -5,
|
||||
"H": -4,
|
||||
"N": -3,
|
||||
"J": -2,
|
||||
"M": -1,
|
||||
",": 0,
|
||||
"L": 1,
|
||||
".": 2,
|
||||
"Q": 0,
|
||||
"2": 1,
|
||||
"W": 2,
|
||||
"3": 3,
|
||||
"E": 4,
|
||||
"R": 5,
|
||||
"5": 6,
|
||||
"T": 7,
|
||||
"6": 8,
|
||||
"Y": 9,
|
||||
"7": 10,
|
||||
"U": 11,
|
||||
"I": 12,
|
||||
"9": 13,
|
||||
"O": 14,
|
||||
"0": 15,
|
||||
"P": 16,
|
||||
}
|
||||
|
||||
var unitKeyMap = map[string]string{
|
||||
"e": "envelope",
|
||||
"o": "oscillator",
|
||||
"m": "mulp",
|
||||
"M": "mul",
|
||||
"a": "addp",
|
||||
"A": "add",
|
||||
"p": "pan",
|
||||
"S": "push",
|
||||
"P": "pop",
|
||||
"O": "out",
|
||||
"l": "loadnote",
|
||||
"L": "loadval",
|
||||
"h": "xch",
|
||||
"d": "delay",
|
||||
"D": "distort",
|
||||
"H": "hold",
|
||||
"b": "crush",
|
||||
"g": "gain",
|
||||
"i": "invgain",
|
||||
"f": "filter",
|
||||
"I": "clip",
|
||||
"E": "speed",
|
||||
"r": "compressor",
|
||||
"u": "outaux",
|
||||
"U": "aux",
|
||||
"s": "send",
|
||||
"n": "noise",
|
||||
"N": "in",
|
||||
"R": "receive",
|
||||
}
|
||||
|
||||
// KeyEvent handles incoming key events and returns true if repaint is needed.
|
||||
func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool {
|
||||
if e.State == key.Press {
|
||||
if t.InstrumentNameEditor.Focused() {
|
||||
return false
|
||||
}
|
||||
switch e.Name {
|
||||
case "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, err := yaml.Marshal(t.song)
|
||||
if err == nil {
|
||||
w.WriteClipboard(string(contents))
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
w.ReadClipboard()
|
||||
return true
|
||||
}
|
||||
case "Z":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Undo()
|
||||
return true
|
||||
}
|
||||
case "Y":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Redo()
|
||||
return true
|
||||
}
|
||||
case "N":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.LoadSong(defaultSong.Copy())
|
||||
return true
|
||||
}
|
||||
case "S":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.SaveSongFile()
|
||||
return false
|
||||
}
|
||||
case "O":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.LoadSongFile()
|
||||
return true
|
||||
}
|
||||
case "F1":
|
||||
t.EditMode = EditPatterns
|
||||
return true
|
||||
case "F2":
|
||||
t.EditMode = EditTracks
|
||||
return true
|
||||
case "F3":
|
||||
t.EditMode = EditUnits
|
||||
return true
|
||||
case "F4":
|
||||
t.EditMode = EditParameters
|
||||
return true
|
||||
case "F5":
|
||||
t.NoteTracking = true
|
||||
t.PlayPosition.Pattern = t.Cursor.Pattern
|
||||
if t.EditMode == EditPatterns {
|
||||
t.PlayPosition.Row = 0
|
||||
} else {
|
||||
t.PlayPosition.Row = t.Cursor.Row
|
||||
}
|
||||
t.PlayPosition.Row-- // TODO: we advance soon to make up for this -1, but this is not very elegant way to do it
|
||||
t.SetPlaying(true)
|
||||
return true
|
||||
case "F6":
|
||||
t.NoteTracking = false
|
||||
t.PlayPosition.Pattern = t.Cursor.Pattern
|
||||
if t.EditMode == EditPatterns {
|
||||
t.PlayPosition.Row = 0
|
||||
} else {
|
||||
t.PlayPosition.Row = t.Cursor.Row
|
||||
}
|
||||
t.PlayPosition.Row-- // TODO: we advance soon to make up for this -1, but this is not very elegant way to do it
|
||||
t.SetPlaying(true)
|
||||
return true
|
||||
case "F7":
|
||||
t.NoteTracking = false
|
||||
t.SetPlaying(true)
|
||||
return true
|
||||
case "F8":
|
||||
t.SetPlaying(false)
|
||||
return true
|
||||
case key.NameDeleteForward:
|
||||
switch t.EditMode {
|
||||
case EditPatterns:
|
||||
t.DeleteOrderRow(true)
|
||||
return true
|
||||
case EditTracks:
|
||||
t.DeleteSelection()
|
||||
return true
|
||||
case EditUnits:
|
||||
t.DeleteUnit(true)
|
||||
return true
|
||||
}
|
||||
case key.NameDeleteBackward:
|
||||
switch t.EditMode {
|
||||
case EditPatterns:
|
||||
t.DeleteOrderRow(false)
|
||||
return true
|
||||
case EditTracks:
|
||||
t.DeleteSelection()
|
||||
return true
|
||||
case EditUnits:
|
||||
t.DeleteUnit(false)
|
||||
return true
|
||||
}
|
||||
case "Space":
|
||||
t.SetPlaying(!t.Playing)
|
||||
if t.Playing {
|
||||
if !e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.NoteTracking = true
|
||||
}
|
||||
t.PlayPosition.Pattern = t.Cursor.Pattern
|
||||
if t.EditMode == EditPatterns {
|
||||
t.PlayPosition.Row = 0
|
||||
} else {
|
||||
t.PlayPosition.Row = t.Cursor.Row
|
||||
}
|
||||
t.PlayPosition.Row--
|
||||
}
|
||||
return true
|
||||
case `\`, `<`, `>`:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
return t.ChangeOctave(1)
|
||||
}
|
||||
return t.ChangeOctave(-1)
|
||||
case key.NameTab:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.EditMode = (t.EditMode - 1 + 4) % 4
|
||||
} else {
|
||||
t.EditMode = (t.EditMode + 1) % 4
|
||||
}
|
||||
return true
|
||||
case key.NameReturn:
|
||||
switch t.EditMode {
|
||||
case EditPatterns:
|
||||
t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut))
|
||||
case EditUnits:
|
||||
t.AddUnit(!e.Modifiers.Contain(key.ModShortcut))
|
||||
}
|
||||
case key.NameUpArrow:
|
||||
switch t.EditMode {
|
||||
case EditPatterns:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Cursor.SongRow = SongRow{}
|
||||
} else {
|
||||
t.Cursor.Row -= t.song.RowsPerPattern
|
||||
}
|
||||
t.NoteTracking = false
|
||||
case EditTracks:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Cursor.Row -= t.song.RowsPerPattern
|
||||
} else {
|
||||
if t.Step.Value > 0 {
|
||||
t.Cursor.Row -= t.Step.Value
|
||||
} else {
|
||||
t.Cursor.Row--
|
||||
}
|
||||
}
|
||||
t.NoteTracking = false
|
||||
case EditUnits:
|
||||
t.CurrentUnit--
|
||||
case EditParameters:
|
||||
t.CurrentParam--
|
||||
}
|
||||
t.ClampPositions()
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.Unselect()
|
||||
}
|
||||
return true
|
||||
case key.NameDownArrow:
|
||||
switch t.EditMode {
|
||||
case EditPatterns:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Cursor.Row = t.song.TotalRows() - 1
|
||||
} else {
|
||||
t.Cursor.Row += t.song.RowsPerPattern
|
||||
}
|
||||
t.NoteTracking = false
|
||||
case EditTracks:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Cursor.Row += t.song.RowsPerPattern
|
||||
} else {
|
||||
if t.Step.Value > 0 {
|
||||
t.Cursor.Row += t.Step.Value
|
||||
} else {
|
||||
t.Cursor.Row++
|
||||
}
|
||||
}
|
||||
t.NoteTracking = false
|
||||
case EditUnits:
|
||||
t.CurrentUnit++
|
||||
case EditParameters:
|
||||
t.CurrentParam++
|
||||
}
|
||||
t.ClampPositions()
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.Unselect()
|
||||
}
|
||||
return true
|
||||
case key.NameLeftArrow:
|
||||
switch t.EditMode {
|
||||
case EditPatterns:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Cursor.Track = 0
|
||||
} else {
|
||||
t.Cursor.Track--
|
||||
}
|
||||
case EditTracks:
|
||||
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Cursor.Track--
|
||||
t.CursorColumn = 1
|
||||
} else {
|
||||
t.CursorColumn--
|
||||
}
|
||||
case EditUnits:
|
||||
t.CurrentInstrument--
|
||||
case EditParameters:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetUnitParam(t.GetUnitParam() - 16)
|
||||
} else {
|
||||
t.SetUnitParam(t.GetUnitParam() - 1)
|
||||
}
|
||||
}
|
||||
t.ClampPositions()
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.Unselect()
|
||||
}
|
||||
return true
|
||||
case key.NameRightArrow:
|
||||
switch t.EditMode {
|
||||
case EditPatterns:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Cursor.Track = len(t.song.Tracks) - 1
|
||||
} else {
|
||||
t.Cursor.Track++
|
||||
}
|
||||
case EditTracks:
|
||||
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Cursor.Track++
|
||||
t.CursorColumn = 0
|
||||
} else {
|
||||
t.CursorColumn++
|
||||
}
|
||||
case EditUnits:
|
||||
t.CurrentInstrument++
|
||||
case EditParameters:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetUnitParam(t.GetUnitParam() + 16)
|
||||
} else {
|
||||
t.SetUnitParam(t.GetUnitParam() + 1)
|
||||
}
|
||||
}
|
||||
t.ClampPositions()
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.Unselect()
|
||||
}
|
||||
return true
|
||||
case "+":
|
||||
switch t.EditMode {
|
||||
case EditTracks:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AdjustSelectionPitch(12)
|
||||
} else {
|
||||
t.AdjustSelectionPitch(1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "-":
|
||||
switch t.EditMode {
|
||||
case EditTracks:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AdjustSelectionPitch(-12)
|
||||
} else {
|
||||
t.AdjustSelectionPitch(-1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
switch t.EditMode {
|
||||
case EditPatterns:
|
||||
if iv, err := strconv.Atoi(e.Name); err == nil {
|
||||
t.SetCurrentPattern(byte(iv))
|
||||
return true
|
||||
}
|
||||
if b := byte(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
|
||||
t.SetCurrentPattern(b + 10)
|
||||
return true
|
||||
}
|
||||
case EditTracks:
|
||||
if t.TrackShowHex[t.Cursor.Track] {
|
||||
if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
|
||||
t.NumberPressed(byte(iv))
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if e.Name == "A" {
|
||||
t.SetCurrentNote(0)
|
||||
return true
|
||||
}
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
n := getNoteValue(int(t.Octave.Value), val)
|
||||
t.SetCurrentNote(n)
|
||||
trk := t.Cursor.Track
|
||||
start := t.song.FirstTrackVoice(trk)
|
||||
end := start + t.song.Tracks[trk].NumVoices
|
||||
t.KeyPlaying[e.Name] = t.sequencer.Trigger(start, end, n)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
case EditUnits:
|
||||
name := e.Name
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
name = strings.ToLower(name)
|
||||
}
|
||||
if val, ok := unitKeyMap[name]; ok {
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.SetUnit(val)
|
||||
return true
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case EditParameters:
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
note := getNoteValue(int(t.Octave.Value), val)
|
||||
instr := t.CurrentInstrument
|
||||
start := t.song.FirstInstrumentVoice(instr)
|
||||
end := start + t.song.Patch.Instruments[instr].NumVoices
|
||||
t.KeyPlaying[e.Name] = t.sequencer.Trigger(start, end, note)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.State == key.Release {
|
||||
if f, ok := t.KeyPlaying[e.Name]; ok {
|
||||
f()
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
if t.EditMode == EditTracks && t.Playing && t.getCurrent() == 1 && t.NoteTracking {
|
||||
t.SetCurrentNote(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getCurrent returns the current (note) value in current pattern under the cursor
|
||||
func (t *Tracker) getCurrent() byte {
|
||||
return t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row]
|
||||
}
|
||||
|
||||
// NumberPressed handles incoming presses while in either of the hex number columns
|
||||
func (t *Tracker) NumberPressed(iv byte) {
|
||||
val := t.getCurrent()
|
||||
if t.CursorColumn == 0 {
|
||||
val = ((iv & 0xF) << 4) | (val & 0xF)
|
||||
} else if t.CursorColumn == 1 {
|
||||
val = (val & 0xF0) | (iv & 0xF)
|
||||
}
|
||||
t.SetCurrentNote(val)
|
||||
}
|
||||
939
tracker/model.go
Normal file
939
tracker/model.go
Normal file
@ -0,0 +1,939 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/compiler"
|
||||
)
|
||||
|
||||
// Model implements the mutable state for the tracker program GUI.
|
||||
//
|
||||
// Go does not have immutable slices, so there's no efficient way to guarantee
|
||||
// accidental mutations in the song. But at least the value members are
|
||||
// protected.
|
||||
type Model struct {
|
||||
song sointu.Song
|
||||
editMode EditMode
|
||||
selectionCorner SongPoint
|
||||
cursor SongPoint
|
||||
lowNibble bool
|
||||
instrIndex int
|
||||
unitIndex int
|
||||
paramIndex int
|
||||
octave int
|
||||
noteTracking bool
|
||||
usedIDs map[int]bool
|
||||
maxID int
|
||||
|
||||
prevUndoType string
|
||||
undoSkipCounter int
|
||||
undoStack []sointu.Song
|
||||
redoStack []sointu.Song
|
||||
|
||||
samplesPerRowObservers []chan<- int
|
||||
patchObservers []chan<- sointu.Patch
|
||||
scoreObservers []chan<- sointu.Score
|
||||
playingObservers []chan<- bool
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
Type ParameterType
|
||||
Name string
|
||||
Hint string
|
||||
Value int
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
|
||||
type EditMode int
|
||||
|
||||
type ParameterType int
|
||||
|
||||
const (
|
||||
EditPatterns EditMode = iota
|
||||
EditTracks
|
||||
EditUnits
|
||||
EditParameters
|
||||
)
|
||||
|
||||
const (
|
||||
IntegerParameter ParameterType = iota
|
||||
BoolParameter
|
||||
IDParameter
|
||||
)
|
||||
|
||||
const maxUndo = 256
|
||||
|
||||
func NewModel() *Model {
|
||||
ret := new(Model)
|
||||
ret.setSongNoUndo(defaultSong.Copy())
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *Model) ResetSong() {
|
||||
m.SetSong(defaultSong.Copy())
|
||||
}
|
||||
|
||||
func (m *Model) SetSong(song sointu.Song) {
|
||||
m.saveUndo("SetSong", 0)
|
||||
m.setSongNoUndo(song)
|
||||
}
|
||||
|
||||
func (m *Model) SetOctave(value int) bool {
|
||||
if value < 0 {
|
||||
value = 0
|
||||
}
|
||||
if value > 9 {
|
||||
value = 9
|
||||
}
|
||||
if m.octave == value {
|
||||
return false
|
||||
}
|
||||
m.octave = value
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Model) SetInstrument(instrument sointu.Instrument) bool {
|
||||
if len(instrument.Units) == 0 {
|
||||
return false
|
||||
}
|
||||
m.saveUndo("SetInstrument", 0)
|
||||
m.freeUnitIDs(m.song.Patch[m.instrIndex].Units)
|
||||
m.assignUnitIDs(instrument.Units)
|
||||
m.song.Patch[m.instrIndex] = instrument
|
||||
m.clampPositions()
|
||||
m.notifyPatchChange()
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Model) SetInstrIndex(value int) {
|
||||
m.instrIndex = value
|
||||
m.clampPositions()
|
||||
}
|
||||
|
||||
func (m *Model) SetInstrumentVoices(value int) {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
maxRemain := m.MaxInstrumentVoices()
|
||||
if value > maxRemain {
|
||||
value = maxRemain
|
||||
}
|
||||
if m.Instrument().NumVoices == value {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetInstrumentVoices", 10)
|
||||
m.song.Patch[m.instrIndex].NumVoices = value
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) MaxInstrumentVoices() int {
|
||||
maxRemain := 32 - m.song.Patch.NumVoices() + m.Instrument().NumVoices
|
||||
if maxRemain < 1 {
|
||||
return 1
|
||||
}
|
||||
return maxRemain
|
||||
}
|
||||
|
||||
func (m *Model) SetInstrumentName(name string) {
|
||||
name = strings.TrimSpace(name)
|
||||
if m.Instrument().Name == name {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetInstrumentName", 10)
|
||||
m.song.Patch[m.instrIndex].Name = name
|
||||
}
|
||||
|
||||
func (m *Model) SetBPM(value int) {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
if value > 999 {
|
||||
value = 999
|
||||
}
|
||||
if m.song.BPM == value {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetBPM", 100)
|
||||
m.song.BPM = value
|
||||
m.notifySamplesPerRowChange()
|
||||
}
|
||||
|
||||
func (m *Model) SetRowsPerBeat(value int) {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
if value > 32 {
|
||||
value = 32
|
||||
}
|
||||
if m.song.RowsPerBeat == value {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetRowsPerBeat", 10)
|
||||
m.song.RowsPerBeat = value
|
||||
m.notifySamplesPerRowChange()
|
||||
}
|
||||
|
||||
func (m *Model) AddTrack(after bool) {
|
||||
if !m.CanAddTrack() {
|
||||
return
|
||||
}
|
||||
m.saveUndo("AddTrack", 0)
|
||||
newTracks := make([]sointu.Track, len(m.song.Score.Tracks)+1)
|
||||
if after {
|
||||
m.cursor.Track++
|
||||
}
|
||||
copy(newTracks, m.song.Score.Tracks[:m.cursor.Track])
|
||||
copy(newTracks[m.cursor.Track+1:], m.song.Score.Tracks[m.cursor.Track:])
|
||||
newTracks[m.cursor.Track] = sointu.Track{
|
||||
NumVoices: 1,
|
||||
Patterns: [][]byte{make([]byte, m.song.Score.RowsPerPattern)},
|
||||
}
|
||||
m.song.Score.Tracks = newTracks
|
||||
m.clampPositions()
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) CanAddTrack() bool {
|
||||
return m.song.Score.NumVoices() < 32
|
||||
}
|
||||
|
||||
func (m *Model) SetTrackVoices(value int) {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
maxRemain := m.MaxTrackVoices()
|
||||
if value > maxRemain {
|
||||
value = maxRemain
|
||||
}
|
||||
if m.song.Score.Tracks[m.cursor.Track].NumVoices == value {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetTrackVoices", 10)
|
||||
m.song.Score.Tracks[m.cursor.Track].NumVoices = value
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) MaxTrackVoices() int {
|
||||
maxRemain := 32 - m.song.Score.NumVoices() + m.song.Score.Tracks[m.cursor.Track].NumVoices
|
||||
if maxRemain < 1 {
|
||||
maxRemain = 1
|
||||
}
|
||||
return maxRemain
|
||||
}
|
||||
|
||||
func (m *Model) AddInstrument(after bool) {
|
||||
if !m.CanAddInstrument() {
|
||||
return
|
||||
}
|
||||
m.saveUndo("AddInstrument", 0)
|
||||
newInstruments := make([]sointu.Instrument, len(m.song.Patch)+1)
|
||||
if after {
|
||||
m.instrIndex++
|
||||
}
|
||||
copy(newInstruments, m.song.Patch[:m.instrIndex])
|
||||
copy(newInstruments[m.instrIndex+1:], m.song.Patch[m.instrIndex:])
|
||||
newInstr := defaultInstrument.Copy()
|
||||
m.assignUnitIDs(newInstr.Units)
|
||||
newInstruments[m.instrIndex] = newInstr
|
||||
m.unitIndex = 0
|
||||
m.paramIndex = 0
|
||||
m.song.Patch = newInstruments
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) CanAddInstrument() bool {
|
||||
return m.song.Patch.NumVoices() < 32
|
||||
}
|
||||
|
||||
func (m *Model) SwapInstruments(i, j int) {
|
||||
if i < 0 || j < 0 || i >= len(m.song.Patch) || j >= len(m.song.Patch) || i == j {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SwapInstruments", 10)
|
||||
instruments := m.song.Patch
|
||||
instruments[i], instruments[j] = instruments[j], instruments[i]
|
||||
m.clampPositions()
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) DeleteInstrument(forward bool) {
|
||||
if !m.CanDeleteInstrument() {
|
||||
return
|
||||
}
|
||||
m.saveUndo("DeleteInstrument", 0)
|
||||
m.freeUnitIDs(m.song.Patch[m.instrIndex].Units)
|
||||
m.song.Patch = append(m.song.Patch[:m.instrIndex], m.song.Patch[m.instrIndex+1:]...)
|
||||
if (!forward && m.instrIndex > 0) || m.instrIndex >= len(m.song.Patch) {
|
||||
m.instrIndex--
|
||||
}
|
||||
m.clampPositions()
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) CanDeleteInstrument() bool {
|
||||
return len(m.song.Patch) > 1
|
||||
}
|
||||
|
||||
func (m *Model) Note() byte {
|
||||
trk := m.song.Score.Tracks[m.cursor.Track]
|
||||
if m.cursor.Pattern < 0 || m.cursor.Pattern >= len(trk.Order) {
|
||||
return 1
|
||||
}
|
||||
p := trk.Order[m.cursor.Pattern]
|
||||
if p < 0 || p >= len(trk.Patterns) {
|
||||
return 1
|
||||
}
|
||||
pat := trk.Patterns[p]
|
||||
if m.cursor.Row < 0 || m.cursor.Row >= len(pat) {
|
||||
return 1
|
||||
}
|
||||
return pat[m.cursor.Row]
|
||||
}
|
||||
|
||||
// SetCurrentNote sets the (note) value in current pattern under cursor to iv
|
||||
func (m *Model) SetNote(iv byte) {
|
||||
m.saveUndo("SetNote", 10)
|
||||
tracks := m.song.Score.Tracks
|
||||
order := tracks[m.cursor.Track].Order
|
||||
if m.cursor.Pattern < 0 || m.cursor.Pattern >= len(order) || m.cursor.Row < 0 {
|
||||
return
|
||||
}
|
||||
patIndex := order[m.cursor.Pattern]
|
||||
if patIndex < 0 {
|
||||
return
|
||||
}
|
||||
for len(tracks[m.cursor.Track].Patterns) <= patIndex {
|
||||
tracks[m.cursor.Track].Patterns = append(tracks[m.cursor.Track].Patterns, nil)
|
||||
}
|
||||
patterns := tracks[m.cursor.Track].Patterns
|
||||
for len(patterns[patIndex]) <= m.cursor.Row {
|
||||
patterns[patIndex] = append(patterns[patIndex], 1)
|
||||
}
|
||||
patterns[patIndex][m.cursor.Row] = iv
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) SetCurrentPattern(pat int) {
|
||||
m.saveUndo("SetCurrentPattern", 0)
|
||||
track := &m.song.Score.Tracks[m.cursor.Track]
|
||||
for len(track.Order) <= m.cursor.Pattern {
|
||||
track.Order = append(track.Order, -1)
|
||||
}
|
||||
track.Order[m.cursor.Pattern] = pat
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) SetSongLength(value int) {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
if value == m.song.Score.Length {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetSongLength", 10)
|
||||
m.song.Score.Length = value
|
||||
m.clampPositions()
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) SetRowsPerPattern(value int) {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
if value > 255 {
|
||||
value = 255
|
||||
}
|
||||
if value == m.song.Score.RowsPerPattern {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetRowsPerPattern", 10)
|
||||
m.song.Score.RowsPerPattern = value
|
||||
m.clampPositions()
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) SetUnitType(t string) {
|
||||
unit, ok := defaultUnits[t]
|
||||
if !ok { // if the type is invalid, we just set it to empty unit
|
||||
unit = sointu.Unit{Parameters: make(map[string]int)}
|
||||
} else {
|
||||
unit = unit.Copy()
|
||||
}
|
||||
if m.Unit().Type == unit.Type {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetUnitType", 0)
|
||||
oldID := m.Unit().ID
|
||||
m.Instrument().Units[m.unitIndex] = unit
|
||||
m.Instrument().Units[m.unitIndex].ID = oldID // keep the ID of the replaced unit
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) SetUnitIndex(value int) {
|
||||
m.unitIndex = value
|
||||
m.paramIndex = 0
|
||||
m.clampPositions()
|
||||
}
|
||||
|
||||
func (m *Model) AddUnit(after bool) {
|
||||
m.saveUndo("AddUnit", 10)
|
||||
newUnits := make([]sointu.Unit, len(m.Instrument().Units)+1)
|
||||
if after {
|
||||
m.unitIndex++
|
||||
}
|
||||
copy(newUnits, m.Instrument().Units[:m.unitIndex])
|
||||
copy(newUnits[m.unitIndex+1:], m.Instrument().Units[m.unitIndex:])
|
||||
m.assignUnitIDs(newUnits[m.unitIndex : m.unitIndex+1])
|
||||
m.song.Patch[m.instrIndex].Units = newUnits
|
||||
m.paramIndex = 0
|
||||
m.clampPositions()
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) AddOrderRow(after bool) {
|
||||
m.saveUndo("AddOrderRow", 10)
|
||||
if after {
|
||||
m.cursor.Pattern++
|
||||
}
|
||||
for i, trk := range m.song.Score.Tracks {
|
||||
if l := len(trk.Order); l > m.cursor.Pattern {
|
||||
newOrder := make([]int, l+1)
|
||||
copy(newOrder, trk.Order[:m.cursor.Pattern])
|
||||
copy(newOrder[m.cursor.Pattern+1:], trk.Order[m.cursor.Pattern:])
|
||||
newOrder[m.cursor.Pattern] = -1
|
||||
m.song.Score.Tracks[i].Order = newOrder
|
||||
}
|
||||
}
|
||||
m.song.Score.Length++
|
||||
m.selectionCorner = m.cursor
|
||||
m.clampPositions()
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) DeleteOrderRow(forward bool) {
|
||||
if m.song.Score.Length <= 1 {
|
||||
return
|
||||
}
|
||||
m.saveUndo("DeleteOrderRow", 0)
|
||||
for i, trk := range m.song.Score.Tracks {
|
||||
if l := len(trk.Order); l > m.cursor.Pattern {
|
||||
newOrder := make([]int, l-1)
|
||||
copy(newOrder, trk.Order[:m.cursor.Pattern])
|
||||
copy(newOrder[m.cursor.Pattern:], trk.Order[m.cursor.Pattern+1:])
|
||||
m.song.Score.Tracks[i].Order = newOrder
|
||||
}
|
||||
}
|
||||
if !forward && m.cursor.Pattern > 0 {
|
||||
m.cursor.Pattern--
|
||||
}
|
||||
m.song.Score.Length--
|
||||
m.selectionCorner = m.cursor
|
||||
m.clampPositions()
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) DeleteUnit(forward bool) {
|
||||
if !m.CanDeleteUnit() {
|
||||
return
|
||||
}
|
||||
instr := m.Instrument()
|
||||
m.saveUndo("DeleteUnit", 0)
|
||||
delete(m.usedIDs, instr.Units[m.unitIndex].ID)
|
||||
newUnits := make([]sointu.Unit, len(instr.Units)-1)
|
||||
copy(newUnits, instr.Units[:m.unitIndex])
|
||||
copy(newUnits[m.unitIndex:], instr.Units[m.unitIndex+1:])
|
||||
m.song.Patch[m.instrIndex].Units = newUnits
|
||||
if !forward && m.unitIndex > 0 {
|
||||
m.unitIndex--
|
||||
}
|
||||
m.paramIndex = 0
|
||||
m.clampPositions()
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) CanDeleteUnit() bool {
|
||||
return len(m.Instrument().Units) > 1
|
||||
}
|
||||
|
||||
func (m *Model) ResetParam() {
|
||||
p, err := m.Param(m.paramIndex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
unit := m.Unit()
|
||||
paramList, ok := sointu.UnitTypes[unit.Type]
|
||||
if !ok || m.paramIndex < 0 || m.paramIndex >= len(paramList) {
|
||||
return
|
||||
}
|
||||
paramType := paramList[m.paramIndex]
|
||||
defaultValue, ok := defaultUnits[unit.Type].Parameters[paramType.Name]
|
||||
if unit.Parameters[p.Name] == defaultValue {
|
||||
return
|
||||
}
|
||||
m.saveUndo("ResetParam", 0)
|
||||
unit.Parameters[paramType.Name] = defaultValue
|
||||
m.clampPositions()
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) SetParamIndex(value int) {
|
||||
m.paramIndex = value
|
||||
m.clampPositions()
|
||||
}
|
||||
|
||||
func (m *Model) setGmDlsEntry(index int) {
|
||||
if index < 0 || index >= len(GmDlsEntries) {
|
||||
return
|
||||
}
|
||||
entry := GmDlsEntries[index]
|
||||
unit := m.Unit()
|
||||
if unit.Type != "oscillator" || unit.Parameters["type"] != sointu.Sample {
|
||||
return
|
||||
}
|
||||
if unit.Parameters["samplestart"] == entry.Start && unit.Parameters["loopstart"] == entry.LoopStart && unit.Parameters["looplength"] == entry.LoopLength {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetGmDlsEntry", 20)
|
||||
unit.Parameters["samplestart"] = entry.Start
|
||||
unit.Parameters["loopstart"] = entry.LoopStart
|
||||
unit.Parameters["looplength"] = entry.LoopLength
|
||||
unit.Parameters["transpose"] = 64 + entry.SuggestedTranspose
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) SwapUnits(i, j int) {
|
||||
units := m.Instrument().Units
|
||||
if i < 0 || j < 0 || i >= len(units) || j >= len(units) || i == j {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SwapUnits", 10)
|
||||
units[i], units[j] = units[j], units[i]
|
||||
m.clampPositions()
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) getSelectionRange() (int, int, int, int) {
|
||||
r1 := m.cursor.Pattern*m.song.Score.RowsPerPattern + m.cursor.Row
|
||||
r2 := m.selectionCorner.Pattern*m.song.Score.RowsPerPattern + m.selectionCorner.Row
|
||||
if r2 < r1 {
|
||||
r1, r2 = r2, r1
|
||||
}
|
||||
t1 := m.cursor.Track
|
||||
t2 := m.selectionCorner.Track
|
||||
if t2 < t1 {
|
||||
t1, t2 = t2, t1
|
||||
}
|
||||
return r1, r2, t1, t2
|
||||
}
|
||||
|
||||
func (m *Model) AdjustSelectionPitch(delta int) {
|
||||
m.saveUndo("AdjustSelectionPitch", 10)
|
||||
r1, r2, t1, t2 := m.getSelectionRange()
|
||||
for c := t1; c <= t2; c++ {
|
||||
adjustedNotes := map[struct {
|
||||
Pat int
|
||||
Row int
|
||||
}]bool{}
|
||||
for r := r1; r <= r2; r++ {
|
||||
s := SongRow{Row: r}.Wrap(m.song.Score)
|
||||
if s.Pattern >= len(m.song.Score.Tracks[c].Order) {
|
||||
break
|
||||
}
|
||||
p := m.song.Score.Tracks[c].Order[s.Pattern]
|
||||
if p < 0 {
|
||||
continue
|
||||
}
|
||||
noteIndex := struct {
|
||||
Pat int
|
||||
Row int
|
||||
}{p, s.Row}
|
||||
if !adjustedNotes[noteIndex] {
|
||||
patterns := m.song.Score.Tracks[c].Patterns
|
||||
if p >= len(patterns) {
|
||||
continue
|
||||
}
|
||||
pattern := patterns[p]
|
||||
if s.Row >= len(pattern) {
|
||||
continue
|
||||
}
|
||||
if val := pattern[s.Row]; val > 1 {
|
||||
newVal := int(val) + delta
|
||||
if newVal < 2 {
|
||||
newVal = 2
|
||||
} else if newVal > 255 {
|
||||
newVal = 255
|
||||
}
|
||||
pattern[s.Row] = byte(newVal)
|
||||
}
|
||||
adjustedNotes[noteIndex] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) DeleteSelection() {
|
||||
m.saveUndo("DeleteSelection", 0)
|
||||
r1, r2, t1, t2 := m.getSelectionRange()
|
||||
for r := r1; r <= r2; r++ {
|
||||
s := SongRow{Row: r}.Wrap(m.song.Score)
|
||||
for c := t1; c <= t2; c++ {
|
||||
p := m.song.Score.Tracks[c].Order[s.Pattern]
|
||||
if p < 0 {
|
||||
continue
|
||||
}
|
||||
patterns := m.song.Score.Tracks[c].Patterns
|
||||
if p >= len(patterns) {
|
||||
continue
|
||||
}
|
||||
pattern := patterns[p]
|
||||
if s.Row >= len(pattern) {
|
||||
continue
|
||||
}
|
||||
m.song.Score.Tracks[c].Patterns[p][s.Row] = 1
|
||||
}
|
||||
}
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) DeletePatternSelection() {
|
||||
m.saveUndo("DeletePatternSelection", 0)
|
||||
r1, r2, t1, t2 := m.getSelectionRange()
|
||||
p1 := SongRow{Row: r1}.Wrap(m.song.Score).Pattern
|
||||
p2 := SongRow{Row: r2}.Wrap(m.song.Score).Pattern
|
||||
for p := p1; p <= p2; p++ {
|
||||
for c := t1; c <= t2; c++ {
|
||||
if p < len(m.song.Score.Tracks[c].Order) {
|
||||
m.song.Score.Tracks[c].Order[p] = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) SetEditMode(value EditMode) {
|
||||
m.editMode = value
|
||||
}
|
||||
|
||||
func (m *Model) Undo() {
|
||||
if !m.CanUndo() {
|
||||
return
|
||||
}
|
||||
if len(m.redoStack) >= maxUndo {
|
||||
m.redoStack = m.redoStack[1:]
|
||||
}
|
||||
m.redoStack = append(m.redoStack, m.song.Copy())
|
||||
m.setSongNoUndo(m.undoStack[len(m.undoStack)-1])
|
||||
m.undoStack = m.undoStack[:len(m.undoStack)-1]
|
||||
}
|
||||
|
||||
func (m *Model) CanUndo() bool {
|
||||
return len(m.undoStack) > 0
|
||||
}
|
||||
|
||||
func (m *Model) Redo() {
|
||||
if !m.CanRedo() {
|
||||
return
|
||||
}
|
||||
if len(m.undoStack) >= maxUndo {
|
||||
m.undoStack = m.undoStack[1:]
|
||||
}
|
||||
m.undoStack = append(m.undoStack, m.song.Copy())
|
||||
m.setSongNoUndo(m.redoStack[len(m.redoStack)-1])
|
||||
m.redoStack = m.redoStack[:len(m.redoStack)-1]
|
||||
}
|
||||
|
||||
func (m *Model) CanRedo() bool {
|
||||
return len(m.redoStack) > 0
|
||||
}
|
||||
|
||||
func (m *Model) SetNoteTracking(value bool) {
|
||||
m.noteTracking = value
|
||||
}
|
||||
|
||||
func (m *Model) NoteTracking() bool {
|
||||
return m.noteTracking
|
||||
}
|
||||
|
||||
func (m *Model) Octave() int {
|
||||
return m.octave
|
||||
}
|
||||
|
||||
func (m *Model) Song() sointu.Song {
|
||||
return m.song
|
||||
}
|
||||
|
||||
func (m *Model) EditMode() EditMode {
|
||||
return m.editMode
|
||||
}
|
||||
|
||||
func (m *Model) SelectionCorner() SongPoint {
|
||||
return m.selectionCorner
|
||||
}
|
||||
|
||||
func (m *Model) SetSelectionCorner(value SongPoint) {
|
||||
m.selectionCorner = value
|
||||
m.clampPositions()
|
||||
}
|
||||
|
||||
func (m *Model) Cursor() SongPoint {
|
||||
return m.cursor
|
||||
}
|
||||
|
||||
func (m *Model) SetCursor(value SongPoint) {
|
||||
m.cursor = value
|
||||
m.clampPositions()
|
||||
}
|
||||
|
||||
func (m *Model) LowNibble() bool {
|
||||
return m.lowNibble
|
||||
}
|
||||
|
||||
func (m *Model) SetLowNibble(value bool) {
|
||||
m.lowNibble = value
|
||||
}
|
||||
|
||||
func (m *Model) InstrIndex() int {
|
||||
return m.instrIndex
|
||||
}
|
||||
|
||||
func (m *Model) Track() sointu.Track {
|
||||
return m.song.Score.Tracks[m.cursor.Track]
|
||||
}
|
||||
|
||||
func (m *Model) Instrument() sointu.Instrument {
|
||||
return m.song.Patch[m.instrIndex]
|
||||
}
|
||||
|
||||
func (m *Model) Unit() sointu.Unit {
|
||||
return m.song.Patch[m.instrIndex].Units[m.unitIndex]
|
||||
}
|
||||
|
||||
func (m *Model) UnitIndex() int {
|
||||
return m.unitIndex
|
||||
}
|
||||
|
||||
func (m *Model) ParamIndex() int {
|
||||
return m.paramIndex
|
||||
}
|
||||
|
||||
func (m *Model) clampPositions() {
|
||||
m.cursor = m.cursor.Clamp(m.song.Score)
|
||||
m.selectionCorner = m.selectionCorner.Clamp(m.song.Score)
|
||||
if !m.Track().Effect {
|
||||
m.lowNibble = false
|
||||
}
|
||||
m.instrIndex = clamp(m.instrIndex, 0, len(m.song.Patch)-1)
|
||||
m.unitIndex = clamp(m.unitIndex, 0, len(m.Instrument().Units)-1)
|
||||
for m.paramIndex < 0 {
|
||||
if m.unitIndex == 0 {
|
||||
m.paramIndex = 0
|
||||
break
|
||||
}
|
||||
m.unitIndex--
|
||||
m.paramIndex += m.NumParams()
|
||||
}
|
||||
for n := m.NumParams(); m.paramIndex >= n; n = m.NumParams() {
|
||||
if m.unitIndex == len(m.Instrument().Units)-1 {
|
||||
m.paramIndex = n - 1
|
||||
break
|
||||
}
|
||||
m.paramIndex -= n
|
||||
m.unitIndex++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) NumParams() int {
|
||||
unit := m.Unit()
|
||||
if unit.Type == "oscillator" {
|
||||
if unit.Parameters["type"] != sointu.Sample {
|
||||
return 10
|
||||
}
|
||||
return 14
|
||||
}
|
||||
numSettableParams := 0
|
||||
for _, t := range sointu.UnitTypes[m.Unit().Type] {
|
||||
if t.CanSet {
|
||||
numSettableParams++
|
||||
}
|
||||
}
|
||||
if numSettableParams == 0 {
|
||||
numSettableParams = 1
|
||||
}
|
||||
return numSettableParams
|
||||
}
|
||||
|
||||
func (m *Model) Param(index int) (Parameter, error) {
|
||||
unit := m.Unit()
|
||||
for _, t := range sointu.UnitTypes[unit.Type] {
|
||||
if !t.CanSet {
|
||||
continue
|
||||
}
|
||||
if index != 0 {
|
||||
index--
|
||||
continue
|
||||
}
|
||||
typ := IntegerParameter
|
||||
if t.MaxValue == t.MinValue+1 {
|
||||
typ = BoolParameter
|
||||
}
|
||||
val := m.Unit().Parameters[t.Name]
|
||||
name := t.Name
|
||||
hint := m.song.Patch.ParamHintString(m.instrIndex, m.unitIndex, name)
|
||||
var text string
|
||||
if hint != "" {
|
||||
text = fmt.Sprintf("%v / %v", val, hint)
|
||||
} else {
|
||||
text = strconv.Itoa(val)
|
||||
}
|
||||
min, max := t.MinValue, t.MaxValue
|
||||
if unit.Type == "send" {
|
||||
if t.Name == "voice" {
|
||||
i, _, err := m.song.Patch.FindSendTarget(unit.Parameters["target"])
|
||||
if err == nil {
|
||||
max = m.song.Patch[i].NumVoices
|
||||
}
|
||||
} else if t.Name == "target" {
|
||||
typ = IDParameter
|
||||
}
|
||||
}
|
||||
return Parameter{Type: typ, Min: min, Max: max, Name: name, Hint: text, Value: val}, nil
|
||||
}
|
||||
if unit.Type == "oscillator" && index == 0 {
|
||||
key := compiler.SampleOffset{Start: uint32(unit.Parameters["samplestart"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])}
|
||||
val := 0
|
||||
hint := "0 / custom"
|
||||
if v, ok := GmDlsEntryMap[key]; ok {
|
||||
val = v + 1
|
||||
hint = fmt.Sprintf("%v / %v", val, GmDlsEntries[v].Name)
|
||||
}
|
||||
return Parameter{Type: IntegerParameter, Min: 0, Max: len(GmDlsEntries), Name: "sample", Hint: hint, Value: val}, nil
|
||||
}
|
||||
return Parameter{}, errors.New("invalid parameter")
|
||||
}
|
||||
|
||||
func (m *Model) SetParam(value int) {
|
||||
p, err := m.Param(m.paramIndex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if value < p.Min {
|
||||
value = p.Min
|
||||
} else if value > p.Max {
|
||||
value = p.Max
|
||||
}
|
||||
if p.Name == "sample" {
|
||||
m.setGmDlsEntry(value - 1)
|
||||
return
|
||||
}
|
||||
unit := m.Unit()
|
||||
if unit.Parameters[p.Name] == value {
|
||||
return
|
||||
}
|
||||
m.saveUndo("SetParam", 20)
|
||||
unit.Parameters[p.Name] = value
|
||||
m.clampPositions()
|
||||
m.notifyPatchChange()
|
||||
}
|
||||
|
||||
func (m *Model) AddPatchObserver(observer chan<- sointu.Patch) {
|
||||
m.patchObservers = append(m.patchObservers, observer)
|
||||
}
|
||||
|
||||
func (m *Model) AddScoreObserver(observer chan<- sointu.Score) {
|
||||
m.scoreObservers = append(m.scoreObservers, observer)
|
||||
}
|
||||
|
||||
func (m *Model) AddSamplesPerRowObserver(observer chan<- int) {
|
||||
m.samplesPerRowObservers = append(m.samplesPerRowObservers, observer)
|
||||
}
|
||||
|
||||
func (m *Model) AddPlayingObserver(observer chan<- bool) {
|
||||
m.playingObservers = append(m.playingObservers, observer)
|
||||
}
|
||||
|
||||
func (m *Model) setSongNoUndo(song sointu.Song) {
|
||||
m.song = song
|
||||
m.usedIDs = make(map[int]bool)
|
||||
m.maxID = 0
|
||||
for _, instr := range m.song.Patch {
|
||||
for _, unit := range instr.Units {
|
||||
if m.maxID < unit.ID {
|
||||
m.maxID = unit.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, instr := range m.song.Patch {
|
||||
m.assignUnitIDs(instr.Units)
|
||||
}
|
||||
m.clampPositions()
|
||||
m.notifySamplesPerRowChange()
|
||||
m.notifyPatchChange()
|
||||
m.notifyScoreChange()
|
||||
}
|
||||
|
||||
func (m *Model) notifyPatchChange() {
|
||||
for _, channel := range m.patchObservers {
|
||||
channel <- m.song.Patch.Copy()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) notifyScoreChange() {
|
||||
for _, channel := range m.scoreObservers {
|
||||
channel <- m.song.Score.Copy()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) notifySamplesPerRowChange() {
|
||||
for _, channel := range m.samplesPerRowObservers {
|
||||
channel <- m.song.SamplesPerRow()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) saveUndo(undoType string, undoSkipping int) {
|
||||
if m.prevUndoType == undoType && m.undoSkipCounter < undoSkipping {
|
||||
m.undoSkipCounter++
|
||||
return
|
||||
}
|
||||
m.prevUndoType = undoType
|
||||
m.undoSkipCounter = 0
|
||||
if len(m.undoStack) >= maxUndo {
|
||||
m.undoStack = m.undoStack[1:]
|
||||
}
|
||||
m.undoStack = append(m.undoStack, m.song.Copy())
|
||||
m.redoStack = m.redoStack[:0]
|
||||
}
|
||||
|
||||
func (m *Model) freeUnitIDs(units []sointu.Unit) {
|
||||
for _, u := range units {
|
||||
delete(m.usedIDs, u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) assignUnitIDs(units []sointu.Unit) {
|
||||
for i := range units {
|
||||
if units[i].ID == 0 || m.usedIDs[units[i].ID] {
|
||||
m.maxID++
|
||||
units[i].ID = m.maxID
|
||||
}
|
||||
m.usedIDs[units[i].ID] = true
|
||||
if m.maxID < units[i].ID {
|
||||
m.maxID = units[i].ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clamp(a, min, max int) int {
|
||||
if a < min {
|
||||
return min
|
||||
}
|
||||
if a > max {
|
||||
return max
|
||||
}
|
||||
return a
|
||||
}
|
||||
@ -19,8 +19,7 @@ var notes = []string{
|
||||
"B-",
|
||||
}
|
||||
|
||||
// valueAsNote returns the textual representation of a note value
|
||||
func valueAsNote(val byte) string {
|
||||
func NoteStr(val byte) string {
|
||||
if val == 1 {
|
||||
return "..." // hold
|
||||
}
|
||||
@ -38,7 +37,6 @@ func valueAsNote(val byte) string {
|
||||
return fmt.Sprintf("%s%d", notes[oNote], octave)
|
||||
}
|
||||
|
||||
// noteValue return the note value for a particular note and octave combination
|
||||
func getNoteValue(octave, note int) byte {
|
||||
func NoteAsValue(octave, note int) byte {
|
||||
return byte(baseNote + (octave * 12) + note)
|
||||
}
|
||||
|
||||
274
tracker/player.go
Normal file
274
tracker/player.go
Normal file
@ -0,0 +1,274 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
packedPos uint64
|
||||
|
||||
playCmds chan uint64
|
||||
|
||||
mutex sync.Mutex
|
||||
runningID uint32
|
||||
voiceNoteID []uint32
|
||||
voiceReleased []bool
|
||||
synth sointu.Synth
|
||||
patch sointu.Patch
|
||||
|
||||
synthNotNil int32
|
||||
}
|
||||
|
||||
type voiceNote struct {
|
||||
voice int
|
||||
note byte
|
||||
}
|
||||
|
||||
// Position returns the current play position (song row), and a bool indicating
|
||||
// if the player is currently playing. The function is threadsafe.
|
||||
func (p *Player) Position() (SongRow, bool) {
|
||||
packedPos := atomic.LoadUint64(&p.packedPos)
|
||||
if packedPos == math.MaxUint64 { // stopped
|
||||
return SongRow{}, false
|
||||
}
|
||||
return unpackPosition(packedPos), true
|
||||
}
|
||||
|
||||
func (p *Player) Playing() bool {
|
||||
packedPos := atomic.LoadUint64(&p.packedPos)
|
||||
if packedPos == math.MaxUint64 { // stopped
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Player) Play(position SongRow) {
|
||||
position.Row-- // we'll advance this very shortly
|
||||
p.playCmds <- packPosition(position)
|
||||
}
|
||||
|
||||
func (p *Player) Stop() {
|
||||
p.playCmds <- math.MaxUint64
|
||||
}
|
||||
|
||||
func (p *Player) Disable() {
|
||||
p.mutex.Lock()
|
||||
p.synth = nil
|
||||
atomic.StoreInt32(&p.synthNotNil, 0)
|
||||
p.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (p *Player) Enabled() bool {
|
||||
return atomic.LoadInt32(&p.synthNotNil) == 1
|
||||
}
|
||||
|
||||
func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-chan sointu.Patch, scores <-chan sointu.Score, samplesPerRows <-chan int, posChanged chan<- struct{}, outputs ...chan<- []float32) *Player {
|
||||
p := &Player{playCmds: make(chan uint64, 16)}
|
||||
go func() {
|
||||
var score sointu.Score
|
||||
buffer := make([]float32, 2048)
|
||||
buffer2 := make([]float32, 2048)
|
||||
zeros := make([]float32, 2048)
|
||||
rowTime := 0
|
||||
samplesPerRow := math.MaxInt32
|
||||
var trackIDs []uint32
|
||||
atomic.StoreUint64(&p.packedPos, math.MaxUint64)
|
||||
for {
|
||||
select {
|
||||
case <-closer:
|
||||
for _, o := range outputs {
|
||||
close(o)
|
||||
}
|
||||
return
|
||||
case patch := <-patchs:
|
||||
p.mutex.Lock()
|
||||
p.patch = patch
|
||||
if p.synth != nil {
|
||||
err := p.synth.Update(patch)
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
atomic.StoreInt32(&p.synthNotNil, 0)
|
||||
}
|
||||
} else {
|
||||
s, err := service.Compile(patch)
|
||||
if err == nil {
|
||||
p.synth = s
|
||||
atomic.StoreInt32(&p.synthNotNil, 1)
|
||||
for i := 0; i < 32; i++ {
|
||||
s.Release(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.mutex.Unlock()
|
||||
case score = <-scores:
|
||||
if row, playing := p.Position(); playing {
|
||||
atomic.StoreUint64(&p.packedPos, packPosition(row.Wrap(score)))
|
||||
}
|
||||
case samplesPerRow = <-samplesPerRows:
|
||||
case packedPos := <-p.playCmds:
|
||||
atomic.StoreUint64(&p.packedPos, packedPos)
|
||||
if packedPos == math.MaxUint64 {
|
||||
p.mutex.Lock()
|
||||
for _, id := range trackIDs {
|
||||
p.release(id)
|
||||
}
|
||||
p.mutex.Unlock()
|
||||
}
|
||||
rowTime = math.MaxInt32
|
||||
default:
|
||||
row, playing := p.Position()
|
||||
if playing && rowTime >= samplesPerRow && score.Length > 0 && score.RowsPerPattern > 0 {
|
||||
row.Row++ // advance row (this is why we subtracted one in Play())
|
||||
row = row.Wrap(score)
|
||||
atomic.StoreUint64(&p.packedPos, packPosition(row))
|
||||
select {
|
||||
case posChanged <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
p.mutex.Lock()
|
||||
lastVoice := 0
|
||||
for i, t := range score.Tracks {
|
||||
start := lastVoice
|
||||
lastVoice = start + t.NumVoices
|
||||
if row.Pattern < 0 || row.Pattern >= len(t.Order) {
|
||||
continue
|
||||
}
|
||||
o := t.Order[row.Pattern]
|
||||
if o < 0 || o >= len(t.Patterns) {
|
||||
continue
|
||||
}
|
||||
pat := t.Patterns[o]
|
||||
if row.Row < 0 || row.Row >= len(pat) {
|
||||
continue
|
||||
}
|
||||
n := pat[row.Row]
|
||||
for len(trackIDs) <= i {
|
||||
trackIDs = append(trackIDs, 0)
|
||||
}
|
||||
if n != 1 && trackIDs[i] > 0 {
|
||||
p.release(trackIDs[i])
|
||||
}
|
||||
if n > 1 && p.synth != nil {
|
||||
trackIDs[i] = p.trigger(start, lastVoice, n)
|
||||
}
|
||||
}
|
||||
p.mutex.Unlock()
|
||||
rowTime = 0
|
||||
}
|
||||
if p.synth != nil {
|
||||
renderTime := samplesPerRow - rowTime
|
||||
if !playing {
|
||||
renderTime = math.MaxInt32
|
||||
}
|
||||
p.mutex.Lock()
|
||||
rendered, timeAdvanced, err := p.synth.Render(buffer, renderTime)
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
atomic.StoreInt32(&p.synthNotNil, 0)
|
||||
}
|
||||
p.mutex.Unlock()
|
||||
rowTime += timeAdvanced
|
||||
for _, o := range outputs {
|
||||
o <- buffer[:rendered*2]
|
||||
}
|
||||
buffer2, buffer = buffer, buffer2
|
||||
} else {
|
||||
rowTime += len(zeros) / 2
|
||||
for _, o := range outputs {
|
||||
o <- zeros
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return p
|
||||
}
|
||||
|
||||
// Trigger is used to manually play a note on the sequencer when jamming. It is
|
||||
// thread-safe. It starts to play one of the voice in the range voiceStart
|
||||
// (inclusive) and voiceEnd (exclusive). It returns a id that can be called to
|
||||
// release the voice playing the note (in case the voice has not been captured
|
||||
// by someone else already).
|
||||
func (p *Player) Trigger(voiceStart, voiceEnd int, note byte) uint32 {
|
||||
if note <= 1 {
|
||||
return 0
|
||||
}
|
||||
p.mutex.Lock()
|
||||
id := p.trigger(voiceStart, voiceEnd, note)
|
||||
p.mutex.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
// Release is used to manually release a note on the player when jamming.
|
||||
// Expects an ID that was previously acquired by calling Trigger.
|
||||
func (p *Player) Release(ID uint32) {
|
||||
if ID == 0 {
|
||||
return
|
||||
}
|
||||
p.mutex.Lock()
|
||||
p.release(ID)
|
||||
p.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (p *Player) trigger(voiceStart, voiceEnd int, note byte) uint32 {
|
||||
if p.synth == nil {
|
||||
return 0
|
||||
}
|
||||
var oldestID uint32 = math.MaxUint32
|
||||
p.runningID++
|
||||
newID := p.runningID
|
||||
oldestReleased := false
|
||||
oldestVoice := 0
|
||||
for i := voiceStart; i < voiceEnd; i++ {
|
||||
for len(p.voiceReleased) <= i {
|
||||
p.voiceReleased = append(p.voiceReleased, true)
|
||||
}
|
||||
for len(p.voiceNoteID) <= i {
|
||||
p.voiceNoteID = append(p.voiceNoteID, 0)
|
||||
}
|
||||
// find a suitable voice to trigger. if the voice has been released,
|
||||
// then we prefer to trigger that over a voice that is still playing. in
|
||||
// case two voices are both playing or or both are released, we prefer
|
||||
// the older one
|
||||
id := p.voiceNoteID[i]
|
||||
isReleased := p.voiceReleased[i]
|
||||
if id < oldestID && (oldestReleased == isReleased) || (!oldestReleased && isReleased) {
|
||||
oldestVoice = i
|
||||
oldestID = id
|
||||
oldestReleased = isReleased
|
||||
}
|
||||
}
|
||||
p.voiceNoteID[oldestVoice] = newID
|
||||
p.voiceReleased[oldestVoice] = false
|
||||
if p.synth != nil {
|
||||
p.synth.Trigger(oldestVoice, note)
|
||||
}
|
||||
return newID
|
||||
}
|
||||
|
||||
func (p *Player) release(ID uint32) {
|
||||
if p.synth == nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(p.voiceNoteID); i++ {
|
||||
if p.voiceNoteID[i] == ID && !p.voiceReleased[i] {
|
||||
p.voiceReleased[i] = true
|
||||
p.synth.Release(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func packPosition(pos SongRow) uint64 {
|
||||
return (uint64(uint32(pos.Pattern)) << 32) + uint64(uint32(pos.Row))
|
||||
}
|
||||
|
||||
func unpackPosition(packedPos uint64) SongRow {
|
||||
pattern := int(int32(packedPos >> 32))
|
||||
row := int(int32(packedPos & 0xFFFFFFFF))
|
||||
return SongRow{Pattern: pattern, Row: row}
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strings"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
const rowMarkerWidth = 50
|
||||
|
||||
func (t *Tracker) layoutRowMarkers(patternRows, sequenceLength, cursorRow, cursorPattern, cursorCol, playRow, playPattern int) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.X = rowMarkerWidth
|
||||
paint.FillShape(gtx.Ops, rowMarkerSurfaceColor, clip.Rect{
|
||||
Max: gtx.Constraints.Max,
|
||||
}.Op())
|
||||
defer op.Save(gtx.Ops).Load()
|
||||
clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops)
|
||||
op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops)
|
||||
cursorSongRow := cursorPattern*patternRows + cursorRow
|
||||
playSongRow := playPattern*patternRows + playRow
|
||||
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops)
|
||||
beatMarkerDensity := t.song.RowsPerBeat
|
||||
for beatMarkerDensity <= 2 {
|
||||
beatMarkerDensity *= 2
|
||||
}
|
||||
for i := 0; i < sequenceLength; i++ {
|
||||
for j := 0; j < patternRows; j++ {
|
||||
songRow := i*patternRows + j
|
||||
if mod(songRow, beatMarkerDensity*2) == 0 {
|
||||
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
} else if mod(songRow, beatMarkerDensity) == 0 {
|
||||
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if songRow == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if j == 0 {
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", i)))
|
||||
}
|
||||
if t.EditMode == EditTracks && songRow == cursorSongRow {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
op.Offset(f32.Pt(rowMarkerWidth/2, 0)).Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)))
|
||||
op.Offset(f32.Pt(-rowMarkerWidth/2, trackRowHeight)).Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
return layout.Dimensions{Size: image.Pt(rowMarkerWidth, gtx.Constraints.Max.Y)}
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
func (t *Tracker) Run(w *app.Window) error {
|
||||
var ops op.Ops
|
||||
for {
|
||||
select {
|
||||
case <-t.refresh:
|
||||
w.Invalidate()
|
||||
case e := <-w.Events():
|
||||
switch e := e.(type) {
|
||||
case system.DestroyEvent:
|
||||
return e.Err
|
||||
case key.Event:
|
||||
if t.KeyEvent(w, e) {
|
||||
w.Invalidate()
|
||||
}
|
||||
case clipboard.Event:
|
||||
err := t.UnmarshalContent([]byte(e.Text))
|
||||
if err == nil {
|
||||
w.Invalidate()
|
||||
}
|
||||
case system.FrameEvent:
|
||||
gtx := layout.NewContext(&ops, e)
|
||||
t.Layout(gtx)
|
||||
e.Frame(gtx.Ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,271 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
// how many times the sequencer tries to fill the buffer. If the buffer is not
|
||||
// filled after this many tries, there's probably an issue with rowlength (e.g.
|
||||
// infinite BPM, rowlength = 0) or something else, so we error instead of
|
||||
// letting ReadAudio hang.
|
||||
const SEQUENCER_MAX_READ_TRIES = 1000
|
||||
|
||||
// Sequencer is a AudioSource that uses the given synth to render audio. In
|
||||
// periods of rowLength, it pulls new notes to trigger/release from the given
|
||||
// iterator. Note that the iterator should be thread safe, as the ReadAudio
|
||||
// might be called from another go routine.
|
||||
type Sequencer struct {
|
||||
validSynth int32
|
||||
closer chan struct{}
|
||||
setPatch chan sointu.Patch
|
||||
setRowLength chan int
|
||||
noteOn chan noteOnEvent
|
||||
noteOff chan uint32
|
||||
synth sointu.Synth
|
||||
voiceNoteID []uint32
|
||||
voiceReleased []bool
|
||||
idCounter uint32
|
||||
}
|
||||
|
||||
type RowNote struct {
|
||||
NumVoices int
|
||||
Note byte
|
||||
}
|
||||
|
||||
type noteOnEvent struct {
|
||||
voiceStart int
|
||||
voiceEnd int
|
||||
note byte
|
||||
id uint32
|
||||
}
|
||||
|
||||
type noteID struct {
|
||||
voice int
|
||||
id uint32
|
||||
}
|
||||
|
||||
func NewSequencer(bufferSize int, service sointu.SynthService, context sointu.AudioContext, callBack func([]float32), iterator func([]RowNote) []RowNote) *Sequencer {
|
||||
ret := &Sequencer{
|
||||
closer: make(chan struct{}),
|
||||
setPatch: make(chan sointu.Patch, 32),
|
||||
setRowLength: make(chan int, 32),
|
||||
noteOn: make(chan noteOnEvent, 32),
|
||||
noteOff: make(chan uint32, 32),
|
||||
voiceNoteID: make([]uint32, 32),
|
||||
voiceReleased: make([]bool, 32),
|
||||
}
|
||||
// the iterator is a bit unconventional in the sense that it might return
|
||||
// false to indicate that there is no row available, but might still return
|
||||
// true in future attempts if new rows become available.
|
||||
go ret.loop(bufferSize, service, context, callBack, iterator)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Sequencer) loop(bufferSize int, service sointu.SynthService, context sointu.AudioContext, callBack func([]float32), iterator func([]RowNote) []RowNote) {
|
||||
buffer := make([]float32, bufferSize)
|
||||
renderTries := 0
|
||||
audioOut := context.Output()
|
||||
defer audioOut.Close()
|
||||
rowIn := make([]RowNote, 32)
|
||||
rowLength := math.MaxInt32
|
||||
rowTimeRemaining := 0
|
||||
trackNotes := make([]uint32, 32)
|
||||
for {
|
||||
for !s.Enabled() {
|
||||
select {
|
||||
case <-s.closer:
|
||||
return
|
||||
case <-s.noteOn:
|
||||
case <-s.noteOff:
|
||||
case rowLength = <-s.setRowLength:
|
||||
case patch := <-s.setPatch:
|
||||
var err error
|
||||
s.synth, err = service.Compile(patch)
|
||||
if err == nil {
|
||||
s.enable()
|
||||
for i := range s.voiceReleased {
|
||||
s.voiceReleased[i] = true
|
||||
s.synth.Release(i)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
released := false
|
||||
for s.Enabled() {
|
||||
select {
|
||||
case <-s.closer:
|
||||
return
|
||||
case n := <-s.noteOn:
|
||||
s.trigger(n.voiceStart, n.voiceEnd, n.note, n.id)
|
||||
case n := <-s.noteOff:
|
||||
s.release(n)
|
||||
case rowLength = <-s.setRowLength:
|
||||
case patch := <-s.setPatch:
|
||||
err := s.synth.Update(patch)
|
||||
if err != nil {
|
||||
s.Disable()
|
||||
break
|
||||
}
|
||||
default:
|
||||
renderTime := rowTimeRemaining
|
||||
if rowTimeRemaining <= 0 {
|
||||
rowOut := iterator(rowIn[:0])
|
||||
if len(rowOut) > 0 {
|
||||
curVoice := 0
|
||||
for i, rn := range rowOut {
|
||||
end := curVoice + rn.NumVoices
|
||||
if rn.Note != 1 {
|
||||
s.release(trackNotes[i])
|
||||
}
|
||||
if rn.Note > 1 {
|
||||
id := s.getNewID()
|
||||
s.trigger(curVoice, end, rn.Note, id)
|
||||
trackNotes[i] = id
|
||||
}
|
||||
curVoice = end
|
||||
}
|
||||
rowTimeRemaining = rowLength
|
||||
renderTime = rowLength
|
||||
released = false
|
||||
} else {
|
||||
if !released {
|
||||
s.releaseVoiceRange(0, len(s.voiceNoteID))
|
||||
released = true
|
||||
}
|
||||
rowTimeRemaining = 0
|
||||
renderTime = math.MaxInt32
|
||||
}
|
||||
}
|
||||
rendered, timeAdvanced, err := s.synth.Render(buffer, renderTime)
|
||||
callBack(buffer)
|
||||
if err != nil {
|
||||
s.Disable()
|
||||
break
|
||||
}
|
||||
rowTimeRemaining -= timeAdvanced
|
||||
if timeAdvanced == 0 {
|
||||
renderTries++
|
||||
} else {
|
||||
renderTries = 0
|
||||
}
|
||||
if renderTries >= SEQUENCER_MAX_READ_TRIES {
|
||||
s.Disable()
|
||||
break
|
||||
}
|
||||
err = audioOut.WriteAudio(buffer[:2*rendered])
|
||||
if err != nil {
|
||||
s.Disable()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sequencer) Enabled() bool {
|
||||
return atomic.LoadInt32(&s.validSynth) == 1
|
||||
}
|
||||
|
||||
func (s *Sequencer) Disable() {
|
||||
atomic.StoreInt32(&s.validSynth, 0)
|
||||
}
|
||||
|
||||
func (s *Sequencer) SetRowLength(rowLength int) {
|
||||
s.setRowLength <- rowLength
|
||||
}
|
||||
|
||||
// Close closes the sequencer and releases all its resources
|
||||
func (s *Sequencer) Close() {
|
||||
s.closer <- struct{}{}
|
||||
}
|
||||
|
||||
// SetPatch updates the synth to match given patch
|
||||
func (s *Sequencer) SetPatch(patch sointu.Patch) {
|
||||
s.setPatch <- patch.Copy()
|
||||
}
|
||||
|
||||
// Trigger is used to manually play a note on the sequencer when jamming. It is
|
||||
// thread-safe. It starts to play one of the voice in the range voiceStart
|
||||
// (inclusive) and voiceEnd (exclusive). It returns a release function that can
|
||||
// be called to release the voice playing the note (in case the voice has not
|
||||
// been captured by someone else already). Note that Trigger will never block,
|
||||
// but calling the release function might block until the sequencer has been
|
||||
// able to assign a voice to the note.
|
||||
func (s *Sequencer) Trigger(voiceStart, voiceEnd int, note byte) func() {
|
||||
if note <= 1 {
|
||||
return func() {}
|
||||
}
|
||||
id := s.getNewID()
|
||||
e := noteOnEvent{
|
||||
voiceStart: voiceStart,
|
||||
voiceEnd: voiceEnd,
|
||||
note: note,
|
||||
id: id,
|
||||
}
|
||||
s.noteOn <- e
|
||||
return func() {
|
||||
s.noteOff <- id // now, tell the sequencer to stop it
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sequencer) getNewID() uint32 {
|
||||
return atomic.AddUint32(&s.idCounter, 1)
|
||||
}
|
||||
|
||||
func (s *Sequencer) enable() {
|
||||
atomic.StoreInt32(&s.validSynth, 1)
|
||||
}
|
||||
|
||||
func (s *Sequencer) trigger(voiceStart, voiceEnd int, note byte, newID uint32) {
|
||||
if !s.Enabled() {
|
||||
return
|
||||
}
|
||||
var oldestID uint32 = math.MaxUint32
|
||||
oldestReleased := false
|
||||
oldestVoice := 0
|
||||
for i := voiceStart; i < voiceEnd; i++ {
|
||||
// find a suitable voice to trigger. if the voice has been released,
|
||||
// then we prefer to trigger that over a voice that is still playing. in
|
||||
// case two voices are both playing or or both are released, we prefer
|
||||
// the older one
|
||||
id := s.voiceNoteID[i]
|
||||
isReleased := s.voiceReleased[i]
|
||||
if id < oldestID && (oldestReleased == isReleased) || (!oldestReleased && isReleased) {
|
||||
oldestVoice = i
|
||||
oldestID = id
|
||||
oldestReleased = isReleased
|
||||
}
|
||||
}
|
||||
s.voiceNoteID[oldestVoice] = newID
|
||||
s.voiceReleased[oldestVoice] = false
|
||||
s.synth.Trigger(oldestVoice, note)
|
||||
}
|
||||
|
||||
func (s *Sequencer) release(id uint32) {
|
||||
if !s.Enabled() {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(s.voiceNoteID); i++ {
|
||||
if s.voiceNoteID[i] == id && !s.voiceReleased[i] {
|
||||
s.voiceReleased[i] = true
|
||||
s.synth.Release(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sequencer) releaseVoiceRange(voiceStart, voiceEnd int) {
|
||||
if !s.Enabled() {
|
||||
return
|
||||
}
|
||||
for i := voiceStart; i < voiceEnd; i++ {
|
||||
if !s.voiceReleased[i] {
|
||||
s.voiceReleased[i] = true
|
||||
s.synth.Release(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,36 +17,56 @@ type SongRect struct {
|
||||
Corner2 SongPoint
|
||||
}
|
||||
|
||||
func (r *SongRow) Wrap(song sointu.Song) {
|
||||
totalRow := r.Pattern*song.RowsPerPattern + r.Row
|
||||
r.Row = mod(totalRow, song.RowsPerPattern)
|
||||
r.Pattern = mod((totalRow-r.Row)/song.RowsPerPattern, song.SequenceLength())
|
||||
func (r SongRow) AddRows(rows int) SongRow {
|
||||
return SongRow{Row: r.Row + rows, Pattern: r.Pattern}
|
||||
}
|
||||
|
||||
func (r *SongRow) Clamp(song sointu.Song) {
|
||||
totalRow := r.Pattern*song.RowsPerPattern + r.Row
|
||||
func (r SongRow) AddPatterns(patterns int) SongRow {
|
||||
return SongRow{Row: r.Row, Pattern: r.Pattern + patterns}
|
||||
}
|
||||
|
||||
func (r SongRow) Wrap(score sointu.Score) SongRow {
|
||||
totalRow := r.Pattern*score.RowsPerPattern + r.Row
|
||||
r.Row = mod(totalRow, score.RowsPerPattern)
|
||||
r.Pattern = mod((totalRow-r.Row)/score.RowsPerPattern, score.Length)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r SongRow) Clamp(score sointu.Score) SongRow {
|
||||
totalRow := r.Pattern*score.RowsPerPattern + r.Row
|
||||
if totalRow < 0 {
|
||||
totalRow = 0
|
||||
}
|
||||
if totalRow >= song.TotalRows() {
|
||||
totalRow = song.TotalRows() - 1
|
||||
if totalRow >= score.LengthInRows() {
|
||||
totalRow = score.LengthInRows() - 1
|
||||
}
|
||||
r.Row = totalRow % song.RowsPerPattern
|
||||
r.Pattern = ((totalRow - r.Row) / song.RowsPerPattern) % song.SequenceLength()
|
||||
r.Row = totalRow % score.RowsPerPattern
|
||||
r.Pattern = ((totalRow - r.Row) / score.RowsPerPattern) % score.Length
|
||||
return r
|
||||
}
|
||||
|
||||
func (p *SongPoint) Wrap(song sointu.Song) {
|
||||
p.Track = mod(p.Track, len(song.Tracks))
|
||||
p.SongRow.Wrap(song)
|
||||
func (r SongPoint) AddRows(rows int) SongPoint {
|
||||
return SongPoint{Track: r.Track, SongRow: r.SongRow.AddRows(rows)}
|
||||
}
|
||||
|
||||
func (p *SongPoint) Clamp(song sointu.Song) {
|
||||
func (r SongPoint) AddPatterns(patterns int) SongPoint {
|
||||
return SongPoint{Track: r.Track, SongRow: r.SongRow.AddPatterns(patterns)}
|
||||
}
|
||||
|
||||
func (p SongPoint) Wrap(score sointu.Score) SongPoint {
|
||||
p.Track = mod(p.Track, len(score.Tracks))
|
||||
p.SongRow = p.SongRow.Wrap(score)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p SongPoint) Clamp(score sointu.Score) SongPoint {
|
||||
if p.Track < 0 {
|
||||
p.Track = 0
|
||||
} else if l := len(song.Tracks); p.Track >= l {
|
||||
} else if l := len(score.Tracks); p.Track >= l {
|
||||
p.Track = l - 1
|
||||
}
|
||||
p.SongRow.Clamp(song)
|
||||
p.SongRow = p.SongRow.Clamp(score)
|
||||
return p
|
||||
}
|
||||
|
||||
func (r *SongRect) Contains(p SongPoint) bool {
|
||||
|
||||
@ -1,823 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/text"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type EditMode int
|
||||
|
||||
const (
|
||||
EditPatterns EditMode = iota
|
||||
EditTracks
|
||||
EditUnits
|
||||
EditParameters
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
songPlayMutex sync.RWMutex // protects song and playing
|
||||
song sointu.Song
|
||||
Playing bool
|
||||
// protects PlayPattern and PlayRow
|
||||
playRowPatMutex sync.RWMutex // protects song and playing
|
||||
PlayPosition SongRow
|
||||
EditMode EditMode
|
||||
SelectionCorner SongPoint
|
||||
Cursor SongPoint
|
||||
MenuBar []widget.Clickable
|
||||
Menus []Menu
|
||||
CursorColumn int
|
||||
CurrentInstrument int
|
||||
CurrentUnit int
|
||||
CurrentParam int
|
||||
NoteTracking bool
|
||||
Theme *material.Theme
|
||||
Octave *NumberInput
|
||||
BPM *NumberInput
|
||||
RowsPerPattern *NumberInput
|
||||
RowsPerBeat *NumberInput
|
||||
Step *NumberInput
|
||||
InstrumentVoices *NumberInput
|
||||
TrackVoices *NumberInput
|
||||
InstrumentNameEditor *widget.Editor
|
||||
NewTrackBtn *widget.Clickable
|
||||
NewInstrumentBtn *widget.Clickable
|
||||
DeleteInstrumentBtn *widget.Clickable
|
||||
AddSemitoneBtn *widget.Clickable
|
||||
SubtractSemitoneBtn *widget.Clickable
|
||||
AddOctaveBtn *widget.Clickable
|
||||
SubtractOctaveBtn *widget.Clickable
|
||||
SongLength *NumberInput
|
||||
PanicBtn *widget.Clickable
|
||||
CopyInstrumentBtn *widget.Clickable
|
||||
ParameterSliders []*widget.Float
|
||||
ParameterList *layout.List
|
||||
ParameterScrollBar *ScrollBar
|
||||
UnitDragList *DragList
|
||||
UnitScrollBar *ScrollBar
|
||||
DeleteUnitBtn *widget.Clickable
|
||||
ClearUnitBtn *widget.Clickable
|
||||
ChooseUnitTypeList *layout.List
|
||||
ChooseUnitScrollBar *ScrollBar
|
||||
ChooseUnitTypeBtns []*widget.Clickable
|
||||
AddUnitBtn *widget.Clickable
|
||||
ParameterLabelBtns []*widget.Clickable
|
||||
InstrumentDragList *DragList
|
||||
InstrumentScrollBar *ScrollBar
|
||||
TrackHexCheckBox *widget.Bool
|
||||
TrackShowHex []bool
|
||||
VuMeter VuMeter
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
StackUse []int
|
||||
KeyPlaying map[string]func()
|
||||
Alert Alert
|
||||
|
||||
sequencer *Sequencer
|
||||
refresh chan struct{}
|
||||
audioContext sointu.AudioContext
|
||||
undoStack []sointu.Song
|
||||
redoStack []sointu.Song
|
||||
}
|
||||
|
||||
func (t *Tracker) LoadSong(song sointu.Song) error {
|
||||
if err := song.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid song: %w", err)
|
||||
}
|
||||
t.songPlayMutex.Lock()
|
||||
defer t.songPlayMutex.Unlock()
|
||||
t.song = song
|
||||
t.ClampPositions()
|
||||
if t.sequencer != nil {
|
||||
t.sequencer.SetPatch(song.Patch)
|
||||
t.sequencer.SetRowLength(song.SamplesPerRow())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tracker) UnmarshalContent(bytes []byte) error {
|
||||
var instr sointu.Instrument
|
||||
if errJSON := json.Unmarshal(bytes, &instr); errJSON == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if errYaml := yaml.Unmarshal(bytes, &instr); errYaml == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
if song.BPM > 0 {
|
||||
t.LoadSong(song)
|
||||
return nil
|
||||
}
|
||||
return errors.New("was able to unmarshal a song, but the bpm was 0")
|
||||
}
|
||||
|
||||
func clamp(a, min, max int) int {
|
||||
if a < min {
|
||||
return min
|
||||
}
|
||||
if a >= max {
|
||||
return max - 1
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (t *Tracker) Close() {
|
||||
t.audioContext.Close()
|
||||
}
|
||||
|
||||
func (t *Tracker) SetPlaying(value bool) {
|
||||
t.songPlayMutex.Lock()
|
||||
defer t.songPlayMutex.Unlock()
|
||||
t.Playing = value
|
||||
}
|
||||
|
||||
func (t *Tracker) ChangeOctave(delta int) bool {
|
||||
newOctave := t.Octave.Value + delta
|
||||
if newOctave < 0 {
|
||||
newOctave = 0
|
||||
}
|
||||
if newOctave > 9 {
|
||||
newOctave = 9
|
||||
}
|
||||
if newOctave != t.Octave.Value {
|
||||
t.Octave.Value = newOctave
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) SetInstrument(instrument sointu.Instrument) bool {
|
||||
if len(instrument.Units) > 0 {
|
||||
t.SaveUndo()
|
||||
t.song.Patch.Instruments[t.CurrentInstrument] = instrument
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) SetInstrumentVoices(value int) bool {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
maxRemain := 32 - t.song.Patch.TotalVoices() + t.song.Patch.Instruments[t.CurrentInstrument].NumVoices
|
||||
if maxRemain < 1 {
|
||||
maxRemain = 1
|
||||
}
|
||||
if value > maxRemain {
|
||||
value = maxRemain
|
||||
}
|
||||
if value != int(t.song.Patch.Instruments[t.CurrentInstrument].NumVoices) {
|
||||
t.SaveUndo()
|
||||
t.song.Patch.Instruments[t.CurrentInstrument].NumVoices = value
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) SetInstrumentName(name string) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != t.song.Patch.Instruments[t.CurrentInstrument].Name {
|
||||
t.SaveUndo()
|
||||
t.song.Patch.Instruments[t.CurrentInstrument].Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) SetBPM(value int) bool {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
if value > 999 {
|
||||
value = 999
|
||||
}
|
||||
if value != int(t.song.BPM) {
|
||||
t.SaveUndo()
|
||||
t.song.BPM = value
|
||||
t.sequencer.SetRowLength(t.song.SamplesPerRow())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) SetRowsPerBeat(value int) bool {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
if value > 32 {
|
||||
value = 32
|
||||
}
|
||||
if value != int(t.song.RowsPerBeat) {
|
||||
t.SaveUndo()
|
||||
t.song.RowsPerBeat = value
|
||||
t.sequencer.SetRowLength(t.song.SamplesPerRow())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) AddTrack() {
|
||||
t.SaveUndo()
|
||||
if t.song.TotalTrackVoices() < t.song.Patch.TotalVoices() {
|
||||
seq := make([]byte, t.song.SequenceLength())
|
||||
patterns := [][]byte{make([]byte, t.song.RowsPerPattern)}
|
||||
t.song.Tracks = append(t.song.Tracks, sointu.Track{
|
||||
NumVoices: 1,
|
||||
Patterns: patterns,
|
||||
Sequence: seq,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) SetTrackVoices(value int) bool {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
maxRemain := t.song.Patch.TotalVoices() - t.song.TotalTrackVoices() + t.song.Tracks[t.Cursor.Track].NumVoices
|
||||
if maxRemain < 1 {
|
||||
maxRemain = 1
|
||||
}
|
||||
if value > maxRemain {
|
||||
value = maxRemain
|
||||
}
|
||||
if value != int(t.song.Tracks[t.Cursor.Track].NumVoices) {
|
||||
t.SaveUndo()
|
||||
t.song.Tracks[t.Cursor.Track].NumVoices = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) AddInstrument() {
|
||||
if t.song.Patch.TotalVoices() >= 32 {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
instr := make([]sointu.Instrument, len(t.song.Patch.Instruments)+1)
|
||||
copy(instr, t.song.Patch.Instruments[:t.CurrentInstrument+1])
|
||||
instr[t.CurrentInstrument+1] = defaultInstrument.Copy()
|
||||
copy(instr[t.CurrentInstrument+2:], t.song.Patch.Instruments[t.CurrentInstrument+1:])
|
||||
t.song.Patch.Instruments = instr
|
||||
t.CurrentInstrument++
|
||||
t.ClampPositions()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) SwapInstruments(i, j int) {
|
||||
if i < 0 || j < 0 || i >= len(t.song.Patch.Instruments) || j >= len(t.song.Patch.Instruments) {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
if j < i {
|
||||
i, j = j, i
|
||||
}
|
||||
startI := t.song.FirstInstrumentVoice(i)
|
||||
voicesI := t.song.Patch.Instruments[i].NumVoices
|
||||
endI := startI + voicesI
|
||||
startJ := t.song.FirstInstrumentVoice(j)
|
||||
voicesJ := t.song.Patch.Instruments[j].NumVoices
|
||||
endJ := startI + voicesJ
|
||||
for _, instr := range t.song.Patch.Instruments {
|
||||
for _, u := range instr.Units {
|
||||
if u.Type == "send" {
|
||||
v := u.Parameters["voice"]
|
||||
if v == 0 {
|
||||
continue
|
||||
}
|
||||
if v > startI && v <= endI { // voice belonged to the instrument I
|
||||
u.Parameters["voice"] = v + endJ - endI
|
||||
} else if v > startJ && v <= endJ { // voice belonged to the instrument J
|
||||
u.Parameters["voice"] = v - startJ + startI
|
||||
} else if v > endI && v <= startJ { // voice was between the two instruments
|
||||
u.Parameters["voice"] = v - voicesI + voicesJ
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
instruments := t.song.Patch.Instruments
|
||||
instruments[i], instruments[j] = instruments[j], instruments[i]
|
||||
t.ClampPositions()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) DeleteInstrument() {
|
||||
if len(t.song.Patch.Instruments) <= 1 {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
start := t.song.FirstInstrumentVoice(t.CurrentInstrument)
|
||||
numVoices := t.song.Patch.Instruments[t.CurrentInstrument].NumVoices
|
||||
end := start + numVoices
|
||||
for _, i := range t.song.Patch.Instruments {
|
||||
for _, u := range i.Units {
|
||||
if u.Type == "send" {
|
||||
if v := u.Parameters["voice"]; v > 0 {
|
||||
if v > start && v <= end {
|
||||
u.Parameters["voice"] = 0
|
||||
u.Parameters["unit"] = 63
|
||||
} else if v > end {
|
||||
u.Parameters["voice"] -= numVoices
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.song.Patch.Instruments = append(t.song.Patch.Instruments[:t.CurrentInstrument], t.song.Patch.Instruments[t.CurrentInstrument+1:]...)
|
||||
if t.CurrentInstrument >= len(t.song.Patch.Instruments) {
|
||||
t.CurrentInstrument = len(t.song.Patch.Instruments) - 1
|
||||
}
|
||||
t.ClampPositions()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
// SetCurrentNote sets the (note) value in current pattern under cursor to iv
|
||||
func (t *Tracker) SetCurrentNote(iv byte) {
|
||||
t.SaveUndo()
|
||||
t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row] = iv
|
||||
if !t.Playing || !t.NoteTracking {
|
||||
t.Cursor.Row += t.Step.Value
|
||||
t.ClampPositions()
|
||||
t.Unselect()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) SetCurrentPattern(pat byte) {
|
||||
t.SaveUndo()
|
||||
length := len(t.song.Tracks[t.Cursor.Track].Patterns)
|
||||
if int(pat) >= length {
|
||||
tail := make([][]byte, int(pat)-length+1)
|
||||
for i := range tail {
|
||||
tail[i] = make([]byte, t.song.RowsPerPattern)
|
||||
}
|
||||
t.song.Tracks[t.Cursor.Track].Patterns = append(t.song.Tracks[t.Cursor.Track].Patterns, tail...)
|
||||
}
|
||||
t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern] = pat
|
||||
if t.Step.Value > 0 && (!t.Playing || !t.NoteTracking) {
|
||||
t.Cursor.Pattern++
|
||||
t.ClampPositions()
|
||||
t.Unselect()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) SetSongLength(value int) {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
if value != t.song.SequenceLength() {
|
||||
t.SaveUndo()
|
||||
for i := range t.song.Tracks {
|
||||
seq := t.song.Tracks[i].Sequence
|
||||
if len(t.song.Tracks[i].Sequence) > value {
|
||||
t.song.Tracks[i].Sequence = t.song.Tracks[i].Sequence[:value]
|
||||
} else if len(t.song.Tracks[i].Sequence) < value {
|
||||
for k := len(t.song.Tracks[i].Sequence); k < value; k++ {
|
||||
t.song.Tracks[i].Sequence = append(seq, seq[len(seq)-1])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
t.ClampPositions()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) SetRowsPerPattern(value int) {
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
if value > 255 {
|
||||
value = 255
|
||||
}
|
||||
if value != t.song.RowsPerPattern {
|
||||
t.SaveUndo()
|
||||
for i := range t.song.Tracks {
|
||||
for j := range t.song.Tracks[i].Patterns {
|
||||
pat := t.song.Tracks[i].Patterns[j]
|
||||
if l := len(pat); l < value {
|
||||
tail := make([]byte, value-l)
|
||||
for k := range tail {
|
||||
tail[k] = 1
|
||||
}
|
||||
t.song.Tracks[i].Patterns[j] = append(pat, tail...)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.song.RowsPerPattern = value
|
||||
t.ClampPositions()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) SetUnit(typ string) {
|
||||
unit, ok := defaultUnits[typ]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if unit.Type == t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit] = unit.Copy()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) AddUnit(after bool) {
|
||||
t.SaveUndo()
|
||||
start := t.song.FirstInstrumentVoice(t.CurrentInstrument)
|
||||
end := start + t.song.Patch.Instruments[t.CurrentInstrument].NumVoices
|
||||
for ind, i := range t.song.Patch.Instruments {
|
||||
for _, u := range i.Units {
|
||||
if u.Type == "send" {
|
||||
if v := u.Parameters["voice"]; (ind == t.CurrentInstrument && v == 0) || (v > 0 && v > start && v <= end) {
|
||||
if u.Parameters["unit"] > t.CurrentUnit {
|
||||
u.Parameters["unit"]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
units := make([]sointu.Unit, len(t.song.Patch.Instruments[t.CurrentInstrument].Units)+1)
|
||||
newUnitIndex := t.CurrentUnit
|
||||
if after {
|
||||
newUnitIndex++
|
||||
}
|
||||
copy(units, t.song.Patch.Instruments[t.CurrentInstrument].Units[:newUnitIndex])
|
||||
copy(units[newUnitIndex+1:], t.song.Patch.Instruments[t.CurrentInstrument].Units[newUnitIndex:])
|
||||
t.song.Patch.Instruments[t.CurrentInstrument].Units = units
|
||||
t.CurrentUnit = newUnitIndex
|
||||
t.CurrentParam = 0
|
||||
t.ClampPositions()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) AddOrderRow(after bool) {
|
||||
t.SaveUndo()
|
||||
l := t.song.SequenceLength()
|
||||
newRowIndex := t.Cursor.Pattern
|
||||
if after {
|
||||
newRowIndex++
|
||||
}
|
||||
for i, trk := range t.song.Tracks {
|
||||
seq := make([]byte, l+1)
|
||||
copy(seq, trk.Sequence[:newRowIndex])
|
||||
copy(seq[newRowIndex+1:], trk.Sequence[newRowIndex:])
|
||||
t.song.Tracks[i].Sequence = seq
|
||||
}
|
||||
t.Cursor.Pattern = newRowIndex
|
||||
t.SelectionCorner = t.Cursor
|
||||
t.ClampPositions()
|
||||
}
|
||||
|
||||
func (t *Tracker) DeleteOrderRow(forward bool) {
|
||||
l := t.song.SequenceLength()
|
||||
if l <= 1 {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
c := t.Cursor.Pattern
|
||||
for i, trk := range t.song.Tracks {
|
||||
seq := make([]byte, l-1)
|
||||
copy(seq, trk.Sequence[:c])
|
||||
copy(seq[c:], trk.Sequence[c+1:])
|
||||
t.song.Tracks[i].Sequence = seq
|
||||
}
|
||||
if !forward {
|
||||
if t.Cursor.Pattern > 0 {
|
||||
t.Cursor.Pattern--
|
||||
}
|
||||
}
|
||||
t.SelectionCorner = t.Cursor
|
||||
t.ClampPositions()
|
||||
}
|
||||
|
||||
func (t *Tracker) ClearUnit() {
|
||||
t.SaveUndo()
|
||||
t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type = ""
|
||||
t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters = make(map[string]int)
|
||||
t.CurrentParam = 0
|
||||
t.ClampPositions()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) DeleteUnit(forward bool) {
|
||||
if len(t.song.Patch.Instruments[t.CurrentInstrument].Units) <= 1 {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
start := t.song.FirstInstrumentVoice(t.CurrentInstrument)
|
||||
end := start + t.song.Patch.Instruments[t.CurrentInstrument].NumVoices
|
||||
for ind, i := range t.song.Patch.Instruments {
|
||||
for _, u := range i.Units {
|
||||
if u.Type == "send" {
|
||||
if v := u.Parameters["voice"]; (ind == t.CurrentInstrument && v == 0) || (v > 0 && v > start && v <= end) {
|
||||
if u.Parameters["unit"] > t.CurrentUnit {
|
||||
u.Parameters["unit"]--
|
||||
} else if u.Parameters["unit"] == t.CurrentUnit {
|
||||
u.Parameters["unit"] = 63
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
units := make([]sointu.Unit, len(t.song.Patch.Instruments[t.CurrentInstrument].Units)-1)
|
||||
copy(units, t.song.Patch.Instruments[t.CurrentInstrument].Units[:t.CurrentUnit])
|
||||
copy(units[t.CurrentUnit:], t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit+1:])
|
||||
t.song.Patch.Instruments[t.CurrentInstrument].Units = units
|
||||
if !forward && t.CurrentUnit > 0 {
|
||||
t.CurrentUnit--
|
||||
}
|
||||
t.CurrentParam = 0
|
||||
t.ClampPositions()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) GetUnitParam() int {
|
||||
unit := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit]
|
||||
paramtype := sointu.UnitTypes[unit.Type][t.CurrentParam]
|
||||
return unit.Parameters[paramtype.Name]
|
||||
}
|
||||
|
||||
func (t *Tracker) SetUnitParam(value int) {
|
||||
unit := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit]
|
||||
unittype := sointu.UnitTypes[unit.Type][t.CurrentParam]
|
||||
if value < unittype.MinValue {
|
||||
value = unittype.MinValue
|
||||
} else if value > unittype.MaxValue {
|
||||
value = unittype.MaxValue
|
||||
}
|
||||
if unit.Parameters[unittype.Name] == value {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
unit.Parameters[unittype.Name] = value
|
||||
t.ClampPositions()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) SetGmDlsEntry(index int) {
|
||||
if index < 0 || index >= len(gmDlsEntries) {
|
||||
return
|
||||
}
|
||||
entry := gmDlsEntries[index]
|
||||
unit := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit]
|
||||
if unit.Type != "oscillator" || unit.Parameters["type"] != sointu.Sample {
|
||||
return
|
||||
}
|
||||
if unit.Parameters["samplestart"] == entry.Start && unit.Parameters["loopstart"] == entry.LoopStart && unit.Parameters["looplength"] == entry.LoopLength {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
unit.Parameters["samplestart"] = entry.Start
|
||||
unit.Parameters["loopstart"] = entry.LoopStart
|
||||
unit.Parameters["looplength"] = entry.LoopLength
|
||||
unit.Parameters["transpose"] = 64 + entry.SuggestedTranspose
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) SwapUnits(i, j int) {
|
||||
if i < 0 || j < 0 || i >= len(t.song.Patch.Instruments[t.CurrentInstrument].Units) || j >= len(t.song.Patch.Instruments[t.CurrentInstrument].Units) {
|
||||
return
|
||||
}
|
||||
t.SaveUndo()
|
||||
start := t.song.FirstInstrumentVoice(t.CurrentInstrument)
|
||||
end := start + t.song.Patch.Instruments[t.CurrentInstrument].NumVoices
|
||||
for ind, instr := range t.song.Patch.Instruments {
|
||||
for _, u := range instr.Units {
|
||||
if u.Type == "send" {
|
||||
if v := u.Parameters["voice"]; (ind == t.CurrentInstrument && v == 0) || (v > 0 && v > start && v <= end) {
|
||||
if u.Parameters["unit"] == i {
|
||||
u.Parameters["unit"] = j
|
||||
} else if u.Parameters["unit"] == j {
|
||||
u.Parameters["unit"] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
units := t.song.Patch.Instruments[t.CurrentInstrument].Units
|
||||
units[i], units[j] = units[j], units[i]
|
||||
t.ClampPositions()
|
||||
t.sequencer.SetPatch(t.song.Patch)
|
||||
}
|
||||
|
||||
func (t *Tracker) ClampPositions() {
|
||||
t.PlayPosition.Clamp(t.song)
|
||||
t.Cursor.Clamp(t.song)
|
||||
t.SelectionCorner.Clamp(t.song)
|
||||
if t.Cursor.Track >= len(t.TrackShowHex) || !t.TrackShowHex[t.Cursor.Track] {
|
||||
t.CursorColumn = 0
|
||||
}
|
||||
t.CurrentInstrument = clamp(t.CurrentInstrument, 0, len(t.song.Patch.Instruments))
|
||||
t.CurrentUnit = clamp(t.CurrentUnit, 0, len(t.song.Patch.Instruments[t.CurrentInstrument].Units))
|
||||
numSettableParams := 0
|
||||
for _, t := range sointu.UnitTypes[t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type] {
|
||||
if t.CanSet {
|
||||
numSettableParams++
|
||||
}
|
||||
}
|
||||
if numSettableParams == 0 {
|
||||
numSettableParams = 1
|
||||
}
|
||||
if t.CurrentParam < 0 && t.CurrentUnit > 0 {
|
||||
t.CurrentUnit--
|
||||
numSettableParams = 0
|
||||
for _, t := range sointu.UnitTypes[t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type] {
|
||||
if t.CanSet {
|
||||
numSettableParams++
|
||||
}
|
||||
}
|
||||
t.CurrentParam = numSettableParams - 1
|
||||
} else if t.CurrentParam >= numSettableParams && t.CurrentUnit < len(t.song.Patch.Instruments[t.CurrentInstrument].Units)-1 {
|
||||
t.CurrentUnit++
|
||||
t.CurrentParam = 0
|
||||
} else {
|
||||
t.CurrentParam = clamp(t.CurrentParam, 0, numSettableParams)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) getSelectionRange() (int, int, int, int) {
|
||||
r1 := t.Cursor.Pattern*t.song.RowsPerPattern + t.Cursor.Row
|
||||
r2 := t.SelectionCorner.Pattern*t.song.RowsPerPattern + t.SelectionCorner.Row
|
||||
if r2 < r1 {
|
||||
r1, r2 = r2, r1
|
||||
}
|
||||
t1 := t.Cursor.Track
|
||||
t2 := t.SelectionCorner.Track
|
||||
if t2 < t1 {
|
||||
t1, t2 = t2, t1
|
||||
}
|
||||
return r1, r2, t1, t2
|
||||
}
|
||||
|
||||
func (t *Tracker) AdjustSelectionPitch(delta int) {
|
||||
t.SaveUndo()
|
||||
r1, r2, t1, t2 := t.getSelectionRange()
|
||||
for c := t1; c <= t2; c++ {
|
||||
adjustedNotes := map[struct {
|
||||
Pat byte
|
||||
Row int
|
||||
}]bool{}
|
||||
for r := r1; r <= r2; r++ {
|
||||
s := SongRow{Row: r}
|
||||
s.Wrap(t.song)
|
||||
p := t.song.Tracks[c].Sequence[s.Pattern]
|
||||
noteIndex := struct {
|
||||
Pat byte
|
||||
Row int
|
||||
}{p, s.Row}
|
||||
if !adjustedNotes[noteIndex] {
|
||||
if val := t.song.Tracks[c].Patterns[p][s.Row]; val > 1 {
|
||||
newVal := int(val) + delta
|
||||
if newVal < 2 {
|
||||
newVal = 2
|
||||
} else if newVal > 255 {
|
||||
newVal = 255
|
||||
}
|
||||
t.song.Tracks[c].Patterns[p][s.Row] = byte(newVal)
|
||||
}
|
||||
adjustedNotes[noteIndex] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) DeleteSelection() {
|
||||
t.SaveUndo()
|
||||
r1, r2, t1, t2 := t.getSelectionRange()
|
||||
for r := r1; r <= r2; r++ {
|
||||
s := SongRow{Row: r}
|
||||
s.Wrap(t.song)
|
||||
for c := t1; c <= t2; c++ {
|
||||
p := t.song.Tracks[c].Sequence[s.Pattern]
|
||||
t.song.Tracks[c].Patterns[p][s.Row] = 1
|
||||
}
|
||||
}
|
||||
if (!t.Playing || !t.NoteTracking) && t.Step.Value > 0 && r1 == r2 {
|
||||
t.Cursor.Row += t.Step.Value
|
||||
t.ClampPositions()
|
||||
t.Unselect()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) Unselect() {
|
||||
t.SelectionCorner = t.Cursor
|
||||
}
|
||||
|
||||
func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tracker {
|
||||
t := &Tracker{
|
||||
Theme: material.NewTheme(gofont.Collection()),
|
||||
audioContext: audioContext,
|
||||
BPM: new(NumberInput),
|
||||
Octave: new(NumberInput),
|
||||
SongLength: new(NumberInput),
|
||||
RowsPerPattern: new(NumberInput),
|
||||
RowsPerBeat: new(NumberInput),
|
||||
Step: new(NumberInput),
|
||||
InstrumentVoices: new(NumberInput),
|
||||
TrackVoices: new(NumberInput),
|
||||
InstrumentNameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
|
||||
NewTrackBtn: new(widget.Clickable),
|
||||
NewInstrumentBtn: new(widget.Clickable),
|
||||
DeleteInstrumentBtn: new(widget.Clickable),
|
||||
AddSemitoneBtn: new(widget.Clickable),
|
||||
SubtractSemitoneBtn: new(widget.Clickable),
|
||||
AddOctaveBtn: new(widget.Clickable),
|
||||
SubtractOctaveBtn: new(widget.Clickable),
|
||||
AddUnitBtn: new(widget.Clickable),
|
||||
DeleteUnitBtn: new(widget.Clickable),
|
||||
ClearUnitBtn: new(widget.Clickable),
|
||||
PanicBtn: new(widget.Clickable),
|
||||
CopyInstrumentBtn: new(widget.Clickable),
|
||||
TrackHexCheckBox: new(widget.Bool),
|
||||
Menus: make([]Menu, 2),
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
UnitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}},
|
||||
UnitScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
refresh: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already
|
||||
undoStack: []sointu.Song{},
|
||||
redoStack: []sointu.Song{},
|
||||
InstrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}},
|
||||
InstrumentScrollBar: &ScrollBar{Axis: layout.Horizontal},
|
||||
ParameterList: &layout.List{Axis: layout.Vertical},
|
||||
ParameterScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
TopHorizontalSplit: new(Split),
|
||||
BottomHorizontalSplit: new(Split),
|
||||
VerticalSplit: new(Split),
|
||||
ChooseUnitTypeList: &layout.List{Axis: layout.Vertical},
|
||||
ChooseUnitScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
KeyPlaying: make(map[string]func()),
|
||||
}
|
||||
t.UnitDragList.HoverItem = -1
|
||||
t.InstrumentDragList.HoverItem = -1
|
||||
t.Octave.Value = 4
|
||||
t.VerticalSplit.Axis = layout.Vertical
|
||||
t.BottomHorizontalSplit.Ratio = -.6
|
||||
t.TopHorizontalSplit.Ratio = -.6
|
||||
t.Theme.Palette.Fg = primaryColor
|
||||
t.Theme.Palette.ContrastFg = black
|
||||
t.EditMode = EditTracks
|
||||
t.Step.Value = 1
|
||||
t.VuMeter.FallOff = 2e-8
|
||||
t.VuMeter.RangeDb = 80
|
||||
t.VuMeter.Decay = 1e-3
|
||||
for range allUnits {
|
||||
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable))
|
||||
}
|
||||
callBack := func(buf []float32) {
|
||||
t.VuMeter.Update(buf)
|
||||
select {
|
||||
case t.refresh <- struct{}{}:
|
||||
default:
|
||||
// message dropped, there's already a tick queued, so no need to queue extra
|
||||
}
|
||||
}
|
||||
t.sequencer = NewSequencer(2048, synthService, audioContext, callBack, func(row []RowNote) []RowNote {
|
||||
t.playRowPatMutex.Lock()
|
||||
if !t.Playing {
|
||||
t.playRowPatMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
t.PlayPosition.Row++
|
||||
t.PlayPosition.Wrap(t.song)
|
||||
if t.NoteTracking {
|
||||
t.Cursor.SongRow = t.PlayPosition
|
||||
t.SelectionCorner.SongRow = t.PlayPosition
|
||||
}
|
||||
for _, track := range t.song.Tracks {
|
||||
patternIndex := track.Sequence[t.PlayPosition.Pattern]
|
||||
note := track.Patterns[patternIndex][t.PlayPosition.Row]
|
||||
row = append(row, RowNote{Note: note, NumVoices: track.NumVoices})
|
||||
}
|
||||
t.playRowPatMutex.Unlock()
|
||||
select {
|
||||
case t.refresh <- struct{}{}:
|
||||
default:
|
||||
// message dropped, there's already a tick queued, so no need to queue extra
|
||||
}
|
||||
|
||||
return row
|
||||
})
|
||||
if err := t.LoadSong(defaultSong.Copy()); err != nil {
|
||||
panic(fmt.Errorf("cannot load default song: %w", err))
|
||||
}
|
||||
return t
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
package tracker
|
||||
|
||||
var undoSkip = map[string]int{
|
||||
"setNote": 10,
|
||||
}
|
||||
|
||||
const maxUndo = 256
|
||||
|
||||
func (t *Tracker) SaveUndo() {
|
||||
if len(t.undoStack) >= maxUndo {
|
||||
t.undoStack = t.undoStack[1:]
|
||||
}
|
||||
t.undoStack = append(t.undoStack, t.song.Copy())
|
||||
t.redoStack = t.redoStack[:0]
|
||||
}
|
||||
|
||||
func (t *Tracker) Undo() {
|
||||
if len(t.undoStack) > 0 {
|
||||
if len(t.redoStack) >= maxUndo {
|
||||
t.redoStack = t.redoStack[1:]
|
||||
}
|
||||
t.redoStack = append(t.redoStack, t.song.Copy())
|
||||
t.LoadSong(t.undoStack[len(t.undoStack)-1])
|
||||
t.undoStack = t.undoStack[:len(t.undoStack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) Redo() {
|
||||
if len(t.redoStack) > 0 {
|
||||
if len(t.undoStack) >= maxUndo {
|
||||
t.undoStack = t.undoStack[1:]
|
||||
}
|
||||
t.undoStack = append(t.undoStack, t.song.Copy())
|
||||
t.LoadSong(t.redoStack[len(t.redoStack)-1])
|
||||
t.redoStack = t.redoStack[:len(t.redoStack)-1]
|
||||
}
|
||||
}
|
||||
@ -1,238 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/compiler"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
func (t *Tracker) layoutUnitEditor(gtx C) D {
|
||||
editorFunc := t.layoutUnitSliders
|
||||
if t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type == "" {
|
||||
editorFunc = t.layoutUnitTypeChooser
|
||||
}
|
||||
return Surface{Gray: 24, Focus: t.EditMode == EditUnits || t.EditMode == EditParameters}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, editorFunc),
|
||||
layout.Rigid(t.layoutUnitFooter()))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutUnitSliders(gtx C) D {
|
||||
u := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit]
|
||||
ut, ok := sointu.UnitTypes[u.Type]
|
||||
if !ok {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
listElements := func(gtx C, index int) D {
|
||||
for len(t.ParameterSliders) <= index {
|
||||
t.ParameterSliders = append(t.ParameterSliders, new(widget.Float))
|
||||
}
|
||||
for len(t.ParameterLabelBtns) <= index {
|
||||
t.ParameterLabelBtns = append(t.ParameterLabelBtns, new(widget.Clickable))
|
||||
}
|
||||
for t.ParameterLabelBtns[index].Clicked() {
|
||||
if t.EditMode != EditParameters || t.CurrentParam != index {
|
||||
t.EditMode = EditParameters
|
||||
t.CurrentParam = index
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
} else {
|
||||
if index < len(ut) {
|
||||
t.SetUnitParam(defaultUnits[u.Type].Parameters[ut[index].Name])
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
params := u.Parameters
|
||||
var name string
|
||||
var value, min, max int
|
||||
var valueText string
|
||||
if u.Type == "oscillator" && index == len(ut) {
|
||||
name = "sample"
|
||||
key := compiler.SampleOffset{Start: uint32(params["samplestart"]), LoopStart: uint16(params["loopstart"]), LoopLength: uint16(params["looplength"])}
|
||||
if v, ok := gmDlsEntryMap[key]; ok {
|
||||
value = v + 1
|
||||
valueText = fmt.Sprintf("%v / %v", value, gmDlsEntries[v].Name)
|
||||
} else {
|
||||
value = 0
|
||||
valueText = "0 / custom"
|
||||
}
|
||||
min, max = 0, len(gmDlsEntries)
|
||||
} else {
|
||||
if ut[index].MaxValue < ut[index].MinValue {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
name = ut[index].Name
|
||||
if u.Type == "oscillator" && (name == "samplestart" || name == "loopstart" || name == "looplength") {
|
||||
if params["type"] != sointu.Sample {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
}
|
||||
value = params[name]
|
||||
min, max = ut[index].MinValue, ut[index].MaxValue
|
||||
if u.Type == "send" && name == "voice" {
|
||||
max = t.song.Patch.TotalVoices()
|
||||
} else if u.Type == "send" && name == "unit" { // set the maximum values depending on the send target
|
||||
instrIndex, _, _, _ := t.song.Patch.FindSendTarget(t.CurrentInstrument, t.CurrentUnit)
|
||||
if instrIndex != -1 {
|
||||
max = len(t.song.Patch.Instruments[instrIndex].Units) - 1
|
||||
}
|
||||
} else if u.Type == "send" && name == "port" { // set the maximum values depending on the send target
|
||||
instrIndex, unitIndex, _, _ := t.song.Patch.FindSendTarget(t.CurrentInstrument, t.CurrentUnit)
|
||||
if instrIndex != -1 && unitIndex != -1 {
|
||||
max = len(sointu.Ports[t.song.Patch.Instruments[instrIndex].Units[unitIndex].Type]) - 1
|
||||
}
|
||||
}
|
||||
hint := t.song.ParamHintString(t.CurrentInstrument, t.CurrentUnit, name)
|
||||
if hint != "" {
|
||||
valueText = fmt.Sprintf("%v / %v", value, hint)
|
||||
} else {
|
||||
valueText = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
if !t.ParameterSliders[index].Dragging() {
|
||||
t.ParameterSliders[index].Value = float32(value)
|
||||
}
|
||||
if max < min {
|
||||
max = min
|
||||
}
|
||||
sliderStyle := material.Slider(t.Theme, t.ParameterSliders[index], float32(min), float32(max))
|
||||
sliderStyle.Color = t.Theme.Fg
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(110))
|
||||
return layout.E.Layout(gtx, Label(name, white))
|
||||
}),
|
||||
layout.Expanded(t.ParameterLabelBtns[index].Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Px(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(40))
|
||||
if t.EditMode == EditParameters && t.CurrentParam == index {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
dims := sliderStyle.Layout(gtx)
|
||||
for sliderStyle.Float.Changed() {
|
||||
t.EditMode = EditParameters
|
||||
t.CurrentParam = index
|
||||
if u.Type == "oscillator" && name == "sample" {
|
||||
v := int(t.ParameterSliders[index].Value+0.5) - 1
|
||||
if v >= 0 {
|
||||
t.SetGmDlsEntry(v)
|
||||
}
|
||||
} else {
|
||||
t.SetUnitParam(int(t.ParameterSliders[index].Value + 0.5))
|
||||
}
|
||||
}
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(Label(valueText, white)),
|
||||
)
|
||||
}
|
||||
l := len(ut)
|
||||
if u.Type == "oscillator" && u.Parameters["type"] == sointu.Sample {
|
||||
l++
|
||||
}
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return t.ParameterList.Layout(gtx, l, listElements)
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
gtx.Constraints.Min = gtx.Constraints.Max
|
||||
return t.ParameterScrollBar.Layout(gtx, unit.Dp(10), l, &t.ParameterList.Position)
|
||||
}))
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutUnitFooter() layout.Widget {
|
||||
return func(gtx C) D {
|
||||
for t.ClearUnitBtn.Clicked() {
|
||||
t.ClearUnit()
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
for t.DeleteUnitBtn.Clicked() {
|
||||
t.DeleteUnit(false)
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
deleteUnitBtnStyle := material.IconButton(t.Theme, t.DeleteUnitBtn, widgetForIcon(icons.ActionDelete))
|
||||
deleteUnitBtnStyle.Background = transparent
|
||||
deleteUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if len(t.song.Patch.Instruments[t.CurrentInstrument].Units) > 1 {
|
||||
deleteUnitBtnStyle.Color = primaryColor
|
||||
} else {
|
||||
deleteUnitBtnStyle.Color = disabledTextColor
|
||||
}
|
||||
text := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type
|
||||
if text == "" {
|
||||
text = "Choose unit type"
|
||||
} else {
|
||||
text = strings.Title(text)
|
||||
}
|
||||
hintText := Label(text, white)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
var dims D
|
||||
if t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type != "" {
|
||||
clearUnitBtnStyle := material.IconButton(t.Theme, t.ClearUnitBtn, widgetForIcon(icons.ContentClear))
|
||||
clearUnitBtnStyle.Color = primaryColor
|
||||
clearUnitBtnStyle.Background = transparent
|
||||
clearUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
dims = clearUnitBtnStyle.Layout(gtx)
|
||||
}
|
||||
return D{Size: image.Pt(gtx.Px(unit.Dp(48)), dims.Size.Y)}
|
||||
}),
|
||||
layout.Flexed(1, hintText),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutUnitTypeChooser(gtx C) D {
|
||||
listElem := func(gtx C, i int) D {
|
||||
for t.ChooseUnitTypeBtns[i].Clicked() {
|
||||
t.SetUnit(allUnits[i])
|
||||
}
|
||||
labelStyle := LabelStyle{Text: allUnits[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)}
|
||||
bg := func(gtx C) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, 20))
|
||||
var color color.NRGBA
|
||||
if t.ChooseUnitTypeBtns[i].Hovered() {
|
||||
color = unitTypeListHighlightColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
leftMargin := layout.Inset{Left: unit.Dp(10)}
|
||||
return layout.Stack{Alignment: layout.W}.Layout(gtx,
|
||||
layout.Stacked(bg),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return leftMargin.Layout(gtx, labelStyle.Layout)
|
||||
}),
|
||||
layout.Expanded(t.ChooseUnitTypeBtns[i].Layout))
|
||||
}
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return t.ChooseUnitTypeList.Layout(gtx, len(allUnits), listElem)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return t.ChooseUnitScrollBar.Layout(gtx, unit.Dp(10), len(allUnits), &t.ChooseUnitTypeList.Position)
|
||||
}),
|
||||
)
|
||||
}
|
||||
60
tracker/vuanalyzer.go
Normal file
60
tracker/vuanalyzer.go
Normal file
@ -0,0 +1,60 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// Volume represents an average and peak volume measurement, in decibels. 0 dB =
|
||||
// signal level of +-1.
|
||||
type Volume struct {
|
||||
Average [2]float32
|
||||
Peak [2]float32
|
||||
}
|
||||
|
||||
// VuAnalyzer receives stereo from the bc channel and converts these into peak &
|
||||
// average volume measurements, and pushes Volume values into the vc channel.
|
||||
// The pushes are nonblocking so if e.g. a GUI does not have enough time to
|
||||
// process redraw the volume meter, the values is just skipped. Thus, the vc
|
||||
// chan should have a capacity of at least 1 (!).
|
||||
//
|
||||
// Internally, it first converts the signal to decibels (0 dB = +-1). Then, the
|
||||
// average volume level is computed by smoothing the decibel values with a
|
||||
// exponentially decaying average, with a time constant tau (in seconds).
|
||||
// Typical value could be 0.3 (seconds).
|
||||
//
|
||||
// Peak volume detection is similar exponential smoothing, but the time
|
||||
// constants for attack and release are different. Generally attack << release.
|
||||
// Typical values could be attack 1.5e-3 and release 1.5 (seconds)
|
||||
//
|
||||
// minVolume is just a hard limit for the vuanalyzer volumes, in decibels, just to
|
||||
// prevent negative infinities for volumes
|
||||
func VuAnalyzer(tau float64, attack float64, release float64, minVolume float32, bc <-chan []float32, vc chan<- Volume) {
|
||||
v := Volume{Average: [2]float32{minVolume, minVolume}, Peak: [2]float32{minVolume, minVolume}}
|
||||
alpha := 1 - float32(math.Exp(-1.0/(tau*44100))) // from https://en.wikipedia.org/wiki/Exponential_smoothing
|
||||
alphaAttack := 1 - float32(math.Exp(-1.0/(attack*44100)))
|
||||
alphaRelease := 1 - float32(math.Exp(-1.0/(release*44100)))
|
||||
for buffer := range bc {
|
||||
for j := 0; j < 2; j++ {
|
||||
for i := 0; i < len(buffer); i += 2 {
|
||||
sample2 := float64(buffer[i+j] * buffer[i+j])
|
||||
if math.IsNaN(sample2) {
|
||||
sample2 = float64(minVolume)
|
||||
}
|
||||
dB := float32(10 * math.Log10(float64(sample2)))
|
||||
if dB < minVolume {
|
||||
dB = minVolume
|
||||
}
|
||||
v.Average[j] += (dB - v.Average[j]) * alpha
|
||||
alphaPeak := alphaAttack
|
||||
if dB < v.Peak[j] {
|
||||
alphaPeak = alphaRelease
|
||||
}
|
||||
v.Peak[j] += (dB - v.Peak[j]) * alphaPeak
|
||||
}
|
||||
}
|
||||
select {
|
||||
case vc <- v:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type VuMeter struct {
|
||||
avg [2]float32
|
||||
max [2]float32
|
||||
speed [2]float32
|
||||
FallOff float32
|
||||
Decay float32
|
||||
RangeDb float32
|
||||
}
|
||||
|
||||
func (v *VuMeter) Update(buffer []float32) {
|
||||
for j := 0; j < 2; j++ {
|
||||
for i := 0; i < len(buffer); i += 2 {
|
||||
sample2 := buffer[i+j] * buffer[i+j]
|
||||
db := float32(10*math.Log10(float64(sample2))) + v.RangeDb
|
||||
v.speed[j] += v.FallOff
|
||||
v.max[j] -= v.speed[j]
|
||||
if v.max[j] < 0 {
|
||||
v.max[j] = 0
|
||||
}
|
||||
if v.max[j] < db {
|
||||
v.max[j] = db
|
||||
v.speed[j] = 0
|
||||
}
|
||||
v.avg[j] += (sample2 - v.avg[j]) * v.Decay
|
||||
if math.IsNaN(float64(v.avg[j])) {
|
||||
v.avg[j] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VuMeter) Reset() {
|
||||
v.avg = [2]float32{}
|
||||
v.max = [2]float32{}
|
||||
}
|
||||
|
||||
func (v *VuMeter) Layout(gtx C) D {
|
||||
defer op.Save(gtx.Ops).Load()
|
||||
gtx.Constraints.Max.Y = gtx.Px(unit.Dp(12))
|
||||
height := gtx.Px(unit.Dp(6))
|
||||
for j := 0; j < 2; j++ {
|
||||
value := float32(10*math.Log10(float64(v.avg[j]))) + v.RangeDb
|
||||
if value > 0 {
|
||||
x := int(value/v.RangeDb*float32(gtx.Constraints.Max.X) + 0.5)
|
||||
if x > gtx.Constraints.Max.X {
|
||||
x = gtx.Constraints.Max.X
|
||||
}
|
||||
paint.FillShape(gtx.Ops, mediumEmphasisTextColor, clip.Rect(image.Rect(0, 0, x, height)).Op())
|
||||
}
|
||||
valueMax := v.max[j]
|
||||
if valueMax > 0 {
|
||||
color := white
|
||||
if valueMax >= v.RangeDb {
|
||||
color = errorColor
|
||||
}
|
||||
x := int(valueMax/v.RangeDb*float32(gtx.Constraints.Max.X) + 0.5)
|
||||
if x > gtx.Constraints.Max.X {
|
||||
x = gtx.Constraints.Max.X
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect(image.Rect(x-1, 0, x, height)).Op())
|
||||
}
|
||||
op.Offset(f32.Pt(0, float32(height))).Add(gtx.Ops)
|
||||
}
|
||||
return D{Size: gtx.Constraints.Max}
|
||||
}
|
||||
Reference in New Issue
Block a user