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:
vsariola
2021-02-23 23:55:42 +02:00
parent fd1d018e82
commit adcf3ebce8
155 changed files with 5520 additions and 4914 deletions

View File

@ -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)
}

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image/color"

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image"

View File

@ -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

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"log"

View File

@ -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
View 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)
}

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image"

View File

@ -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)
},
)
}

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image"

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"fmt"

205
tracker/gioui/parameter.go Normal file
View 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)
}
}
}*/

View File

@ -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))
}

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image/color"

View 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
View 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()
}

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image"

View File

@ -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),
)
}

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image"

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image/color"

View File

@ -1,4 +1,4 @@
package tracker
package gioui
import (
"image/color"

View File

@ -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
View 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
View 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, &param, 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
View 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}
}

View File

@ -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"},

View File

@ -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
}
}

View File

@ -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
View 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
}

View File

@ -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
View 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}
}

View File

@ -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)}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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]
}
}

View File

@ -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
View 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:
}
}
}

View File

@ -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}
}