mirror of
https://github.com/vsariola/sointu.git
synced 2026-09-10 02:07:01 -04:00
feat!: implement vsti, along with various refactorings and api changes for it
The RPC and sync library mechanisms were removed for now; they never really worked and contained several obvious bugs. Need to consider if syncs are useful at all during the compose time, or just used during intro.
This commit is contained in:
@@ -10,7 +10,6 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
@@ -84,7 +83,6 @@ func (t *Tracker) loadSong(filename string) {
|
||||
}
|
||||
t.SetSong(song)
|
||||
t.SetFilePath(filename)
|
||||
t.window.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", filename)))
|
||||
t.ClearUndoHistory()
|
||||
t.SetChangedSinceSave(false)
|
||||
}
|
||||
@@ -107,7 +105,6 @@ func (t *Tracker) saveSong(filename string) bool {
|
||||
}
|
||||
ioutil.WriteFile(filename, contents, 0644)
|
||||
t.SetFilePath(filename)
|
||||
t.window.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", filename)))
|
||||
t.SetChangedSinceSave(false)
|
||||
return true
|
||||
}
|
||||
@@ -117,7 +114,7 @@ func (t *Tracker) exportWav(filename string, pcm16 bool) {
|
||||
if extension == "" {
|
||||
filename = filename + ".wav"
|
||||
}
|
||||
data, _, err := sointu.Play(t.synthService, t.Song(), true) // render the song to calculate its length
|
||||
data, err := sointu.Play(t.synthService, t.Song(), true) // render the song to calculate its length
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error rendering the song during export: %v", err), Error, time.Second*3)
|
||||
return
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -20,12 +19,14 @@ import (
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/eventx"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type InstrumentEditor struct {
|
||||
newInstrumentBtn *widget.Clickable
|
||||
enlargeBtn *widget.Clickable
|
||||
deleteInstrumentBtn *widget.Clickable
|
||||
copyInstrumentBtn *widget.Clickable
|
||||
saveInstrumentBtn *widget.Clickable
|
||||
@@ -45,11 +46,13 @@ type InstrumentEditor struct {
|
||||
tag bool
|
||||
wasFocused bool
|
||||
commentExpanded bool
|
||||
voiceStates [vm.MAX_VOICES]float32
|
||||
}
|
||||
|
||||
func NewInstrumentEditor() *InstrumentEditor {
|
||||
return &InstrumentEditor{
|
||||
newInstrumentBtn: new(widget.Clickable),
|
||||
enlargeBtn: new(widget.Clickable),
|
||||
deleteInstrumentBtn: new(widget.Clickable),
|
||||
copyInstrumentBtn: new(widget.Clickable),
|
||||
saveInstrumentBtn: new(widget.Clickable),
|
||||
@@ -97,10 +100,31 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
pointer.InputOp{Tag: &ie.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
|
||||
var icon []byte
|
||||
if t.InstrEnlarged() {
|
||||
icon = icons.NavigationFullscreenExit
|
||||
} else {
|
||||
icon = icons.NavigationFullscreen
|
||||
}
|
||||
fullscreenBtnStyle := IconButton(t.Theme, ie.enlargeBtn, icon, true)
|
||||
for ie.enlargeBtn.Clicked() {
|
||||
t.SetInstrEnlarged(!t.InstrEnlarged())
|
||||
}
|
||||
for ie.newInstrumentBtn.Clicked() {
|
||||
t.AddInstrument(true)
|
||||
}
|
||||
btnStyle := IconButton(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, t.CanAddInstrument())
|
||||
octave := func(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
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))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetOctave(t.OctaveNumberInput.Value)
|
||||
return dims
|
||||
}
|
||||
newBtnStyle := IconButton(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, t.CanAddInstrument())
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{}.Layout(
|
||||
@@ -116,7 +140,19 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, btnStyle.Layout)
|
||||
inset := layout.UniformInset(unit.Dp(6))
|
||||
return inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("OCT:", white)),
|
||||
layout.Rigid(octave),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, fullscreenBtnStyle.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, newBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
@@ -205,10 +241,12 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||
t.Alert.Update("Instrument copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
}
|
||||
for ie.deleteInstrumentBtn.Clicked() && t.ModalDialog == nil {
|
||||
dialogStyle := ConfirmDialog(t.Theme, ie.confirmInstrDelete, "Are you sure you want to delete this instrument?")
|
||||
ie.confirmInstrDelete.Visible = true
|
||||
t.ModalDialog = dialogStyle.Layout
|
||||
for ie.deleteInstrumentBtn.Clicked() {
|
||||
if t.CanDeleteInstrument() {
|
||||
dialogStyle := ConfirmDialog(t.Theme, ie.confirmInstrDelete, "Are you sure you want to delete this instrument?")
|
||||
ie.confirmInstrDelete.Visible = true
|
||||
t.ModalDialog = dialogStyle.Layout
|
||||
}
|
||||
}
|
||||
for ie.confirmInstrDelete.BtnOk.Clicked() {
|
||||
t.DeleteInstrument(false)
|
||||
@@ -236,14 +274,10 @@ func (ie *InstrumentEditor) layoutInstrumentNames(gtx C, t *Tracker) D {
|
||||
grabhandle.Text = ":::"
|
||||
}
|
||||
label := func(gtx C) D {
|
||||
c := 0.0
|
||||
c := float32(0.0)
|
||||
voice := t.Song().Patch.FirstVoiceForInstrument(i)
|
||||
for j := 0; j < t.Song().Patch[i].NumVoices; j++ {
|
||||
released, event := t.player.VoiceState(voice)
|
||||
vc := math.Exp(-float64(event)/15000) * .5
|
||||
if !released {
|
||||
vc += .5
|
||||
}
|
||||
vc := ie.voiceStates[voice]
|
||||
if c < vc {
|
||||
c = vc
|
||||
}
|
||||
|
||||
+26
-18
@@ -3,6 +3,7 @@ package gioui
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/key"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -44,7 +45,7 @@ var noteMap = map[string]int{
|
||||
}
|
||||
|
||||
// KeyEvent handles incoming key events and returns true if repaint is needed.
|
||||
func (t *Tracker) KeyEvent(e key.Event) bool {
|
||||
func (t *Tracker) KeyEvent(e key.Event, window *app.Window) bool {
|
||||
if e.State == key.Press {
|
||||
if t.OpenSongDialog.Visible ||
|
||||
t.SaveSongDialog.Visible ||
|
||||
@@ -58,14 +59,14 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, err := yaml.Marshal(t.Song())
|
||||
if err == nil {
|
||||
t.window.WriteClipboard(string(contents))
|
||||
window.WriteClipboard(string(contents))
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.window.ReadClipboard()
|
||||
window.ReadClipboard()
|
||||
return true
|
||||
}
|
||||
case "Z":
|
||||
@@ -111,7 +112,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
|
||||
if t.OrderEditor.Focused() {
|
||||
startRow.Row = 0
|
||||
}
|
||||
t.player.Play(startRow)
|
||||
t.PlayFromPosition(startRow)
|
||||
return true
|
||||
case "F6":
|
||||
t.SetNoteTracking(false)
|
||||
@@ -119,19 +120,18 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
|
||||
if t.OrderEditor.Focused() {
|
||||
startRow.Row = 0
|
||||
}
|
||||
t.player.Play(startRow)
|
||||
t.PlayFromPosition(startRow)
|
||||
return true
|
||||
case "F8":
|
||||
t.player.Stop()
|
||||
t.SetPlaying(false)
|
||||
return true
|
||||
case "Space":
|
||||
_, playing := t.player.Position()
|
||||
if !playing {
|
||||
if !t.Playing() && !t.InstrEnlarged() {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().SongRow
|
||||
t.player.Play(startRow)
|
||||
t.PlayFromPosition(startRow)
|
||||
} else {
|
||||
t.player.Stop()
|
||||
t.SetPlaying(false)
|
||||
}
|
||||
case `\`, `<`, `>`:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
@@ -147,7 +147,11 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
|
||||
case t.TrackEditor.Focused():
|
||||
t.OrderEditor.Focus()
|
||||
case t.InstrumentEditor.Focused():
|
||||
t.TrackEditor.Focus()
|
||||
if t.InstrEnlarged() {
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
} else {
|
||||
t.TrackEditor.Focus()
|
||||
}
|
||||
default:
|
||||
t.InstrumentEditor.Focus()
|
||||
}
|
||||
@@ -160,7 +164,11 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
|
||||
case t.InstrumentEditor.Focused():
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
default:
|
||||
t.OrderEditor.Focus()
|
||||
if t.InstrEnlarged() {
|
||||
t.InstrumentEditor.Focus()
|
||||
} else {
|
||||
t.OrderEditor.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,18 +196,18 @@ func (t *Tracker) JammingPressed(e key.Event) {
|
||||
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)
|
||||
noteID := tracker.NoteIDInstr(instr, n)
|
||||
t.NoteOn(noteID)
|
||||
t.KeyPlaying[e.Name] = noteID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) JammingReleased(e key.Event) {
|
||||
if ID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.player.Release(ID)
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.NoteOff(noteID)
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
if _, playing := t.player.Position(); t.TrackEditor.focused && playing && t.Note() == 1 && t.NoteTracking() {
|
||||
if t.TrackEditor.focused && t.Playing() && t.Note() == 1 && t.NoteTracking() {
|
||||
t.SetNote(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
@@ -15,9 +14,13 @@ type D = layout.Dimensions
|
||||
|
||||
func (t *Tracker) Layout(gtx layout.Context) {
|
||||
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
t.layoutTop,
|
||||
t.layoutBottom)
|
||||
if t.InstrEnlarged() {
|
||||
t.layoutTop(gtx)
|
||||
} else {
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
t.layoutTop,
|
||||
t.layoutBottom)
|
||||
}
|
||||
t.Alert.Layout(gtx)
|
||||
dstyle := ConfirmDialog(t.Theme, t.ConfirmSongDialog, "Do you want to save your changes to the song? Your changes will be lost if you don't save them.")
|
||||
dstyle.ShowAlt = true
|
||||
@@ -114,7 +117,6 @@ func (t *Tracker) NewSong(forced bool) {
|
||||
}
|
||||
t.ResetSong()
|
||||
t.SetFilePath("")
|
||||
t.window.Option(app.Title("Sointu Tracker"))
|
||||
t.ClearUndoHistory()
|
||||
t.SetChangedSinceSave(false)
|
||||
}
|
||||
|
||||
@@ -73,20 +73,19 @@ func (oe *OrderEditor) doLayout(gtx C, t *Tracker) D {
|
||||
t.DeleteOrderRow(e.Name == key.NameDeleteForward)
|
||||
} else {
|
||||
t.DeletePatternSelection()
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
case "Space":
|
||||
_, playing := t.player.Position()
|
||||
if !playing {
|
||||
if !t.Playing() {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().SongRow
|
||||
startRow.Row = 0
|
||||
t.player.Play(startRow)
|
||||
t.PlayFromPosition(startRow)
|
||||
} else {
|
||||
t.player.Stop()
|
||||
t.SetPlaying(false)
|
||||
}
|
||||
case key.NameReturn:
|
||||
t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut))
|
||||
@@ -143,14 +142,14 @@ func (oe *OrderEditor) doLayout(gtx C, t *Tracker) D {
|
||||
}
|
||||
if iv, err := strconv.Atoi(e.Name); err == nil {
|
||||
t.SetCurrentPattern(iv)
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
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 {
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
@@ -202,7 +201,7 @@ func (oe *OrderEditor) doLayout(gtx C, t *Tracker) D {
|
||||
gtx.Constraints.Max.Y -= patternCellHeight
|
||||
gtx.Constraints.Min.Y -= patternCellHeight
|
||||
element := func(gtx C, j int) D {
|
||||
if playPos, ok := t.player.Position(); ok && j == playPos.Pattern {
|
||||
if playPos := t.PlayPosition(); t.Playing() && 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)
|
||||
|
||||
@@ -24,7 +24,7 @@ func (t *Tracker) layoutRowMarkers(gtx C) D {
|
||||
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()
|
||||
playPos := t.PlayPosition()
|
||||
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
|
||||
@@ -39,7 +39,7 @@ func (t *Tracker) layoutRowMarkers(gtx C) D {
|
||||
} 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 {
|
||||
if t.Playing() && songRow == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if j == 0 {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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 := <-t.errorChannel:
|
||||
t.Alert.Update(e.Error(), Error, time.Second*5)
|
||||
w.Invalidate()
|
||||
case e := <-w.Events():
|
||||
switch e := e.(type) {
|
||||
case system.DestroyEvent:
|
||||
if !t.Quit(false) {
|
||||
// TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog
|
||||
w = app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
}
|
||||
case key.Event:
|
||||
if t.KeyEvent(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)
|
||||
}
|
||||
}
|
||||
if t.quitted {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Main(audioContext sointu.AudioContext, synthService sointu.SynthService, syncChannel chan<- []float32) {
|
||||
go func() {
|
||||
w := app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
t := New(audioContext, synthService, syncChannel, w)
|
||||
defer t.Close()
|
||||
if err := t.Run(w); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
app.Main()
|
||||
}
|
||||
+34
-13
@@ -3,7 +3,6 @@ package gioui
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"gioui.org/f32"
|
||||
@@ -19,6 +18,22 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const shortcutKey = "Ctrl+"
|
||||
|
||||
var fileMenuItems []MenuItem = []MenuItem{
|
||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N"},
|
||||
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song As..."},
|
||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav..."},
|
||||
}
|
||||
|
||||
func init() {
|
||||
if CAN_QUIT {
|
||||
fileMenuItems = append(fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit"})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutSongPanel(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(t.layoutMenuBar),
|
||||
@@ -87,18 +102,9 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
|
||||
clickedItem, hasClicked = t.Menus[1].Clicked()
|
||||
}
|
||||
|
||||
shortcutKey := "Ctrl+"
|
||||
if runtime.GOOS == "darwin" {
|
||||
shortcutKey = "Cmd+"
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(t.layoutMenu("File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200),
|
||||
MenuItem{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N"},
|
||||
MenuItem{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
|
||||
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
|
||||
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song As..."},
|
||||
MenuItem{IconBytes: icons.ImageAudiotrack, Text: "Export Wav..."},
|
||||
MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit"},
|
||||
fileMenuItems...,
|
||||
)),
|
||||
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200),
|
||||
MenuItem{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Disabled: !t.CanUndo()},
|
||||
@@ -116,14 +122,25 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
|
||||
var panicBtnStyle material.ButtonStyle
|
||||
if t.player.Enabled() {
|
||||
if !t.Panic() {
|
||||
panicBtnStyle = LowEmphasisButton(t.Theme, t.PanicBtn, "Panic")
|
||||
} else {
|
||||
panicBtnStyle = HighEmphasisButton(t.Theme, t.PanicBtn, "Panic")
|
||||
}
|
||||
|
||||
for t.PanicBtn.Clicked() {
|
||||
t.player.Disable()
|
||||
t.SetPanic(!t.Panic())
|
||||
}
|
||||
|
||||
var recordBtnStyle material.ButtonStyle
|
||||
if !t.Recording() {
|
||||
recordBtnStyle = LowEmphasisButton(t.Theme, t.RecordBtn, "Record")
|
||||
} else {
|
||||
recordBtnStyle = HighEmphasisButton(t.Theme, t.RecordBtn, "Record")
|
||||
}
|
||||
|
||||
for t.RecordBtn.Clicked() {
|
||||
t.SetRecording(!t.Recording())
|
||||
}
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
@@ -200,6 +217,10 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return panicBtnStyle.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return recordBtnStyle.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(VuMeter{Volume: t.lastVolume, Range: 100}.Layout),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions
|
||||
switch e.Name {
|
||||
case key.NameDeleteForward, key.NameDeleteBackward:
|
||||
t.DeleteSelection()
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
@@ -159,14 +159,14 @@ func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions
|
||||
t.SetNote(n)
|
||||
step = true
|
||||
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)
|
||||
noteID := tracker.NoteIDTrack(trk, n)
|
||||
t.NoteOn(noteID)
|
||||
t.KeyPlaying[e.Name] = noteID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if step && !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
if step && !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions
|
||||
|
||||
for te.NoteOffBtn.Clicked() {
|
||||
t.SetNote(0)
|
||||
if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 {
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
@@ -230,18 +230,9 @@ func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions
|
||||
noteOffBtnStyle := LowEmphasisButton(t.Theme, te.NoteOffBtn, "Note Off")
|
||||
deleteTrackBtnStyle := IconButton(t.Theme, te.DeleteTrackBtn, icons.ActionDelete, t.CanDeleteTrack())
|
||||
newTrackBtnStyle := IconButton(t.Theme, te.NewTrackBtn, icons.ContentAdd, t.CanAddTrack())
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
octave := func(gtx C) D {
|
||||
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))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetOctave(t.OctaveNumberInput.Value)
|
||||
return dims
|
||||
}
|
||||
n := t.Song().Score.Tracks[t.Cursor().Track].NumVoices
|
||||
te.TrackVoices.Value = n
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
voiceUpDown := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, 1, t.MaxTrackVoices())
|
||||
gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20))
|
||||
@@ -251,8 +242,6 @@ func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions
|
||||
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)),
|
||||
layout.Rigid(octave),
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Px(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(subtractSemitoneBtnStyle.Layout),
|
||||
|
||||
+81
-41
@@ -4,10 +4,16 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu"
|
||||
@@ -33,12 +39,13 @@ type Tracker struct {
|
||||
InstrumentVoices *NumberInput
|
||||
SongLength *NumberInput
|
||||
PanicBtn *widget.Clickable
|
||||
RecordBtn *widget.Clickable
|
||||
AddUnitBtn *widget.Clickable
|
||||
TrackHexCheckBox *widget.Bool
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
KeyPlaying map[string]uint32
|
||||
KeyPlaying map[string]tracker.NoteID
|
||||
Alert Alert
|
||||
ConfirmSongDialog *Dialog
|
||||
WaveTypeDialog *Dialog
|
||||
@@ -48,22 +55,17 @@ type Tracker struct {
|
||||
SaveInstrumentDialog *FileDialog
|
||||
ExportWavDialog *FileDialog
|
||||
ConfirmSongActionType int
|
||||
window *app.Window
|
||||
ModalDialog layout.Widget
|
||||
InstrumentEditor *InstrumentEditor
|
||||
OrderEditor *OrderEditor
|
||||
TrackEditor *TrackEditor
|
||||
|
||||
lastVolume tracker.Volume
|
||||
volumeChan chan tracker.Volume
|
||||
|
||||
wavFilePath string
|
||||
player *tracker.Player
|
||||
refresh chan struct{}
|
||||
playerCloser chan struct{}
|
||||
errorChannel chan error
|
||||
quitted bool
|
||||
audioContext sointu.AudioContext
|
||||
synthService sointu.SynthService
|
||||
|
||||
*tracker.Model
|
||||
@@ -94,15 +96,9 @@ func (t *Tracker) UnmarshalContent(bytes []byte) error {
|
||||
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, syncChannel chan<- []float32, window *app.Window) *Tracker {
|
||||
func NewTracker(model *tracker.Model, synthService sointu.SynthService) *Tracker {
|
||||
t := &Tracker{
|
||||
Theme: material.NewTheme(gofont.Collection()),
|
||||
audioContext: audioContext,
|
||||
BPM: new(NumberInput),
|
||||
OctaveNumberInput: &NumberInput{Value: 4},
|
||||
SongLength: new(NumberInput),
|
||||
@@ -112,6 +108,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn
|
||||
InstrumentVoices: new(NumberInput),
|
||||
|
||||
PanicBtn: new(widget.Clickable),
|
||||
RecordBtn: new(widget.Clickable),
|
||||
TrackHexCheckBox: new(widget.Bool),
|
||||
Menus: make([]Menu, 2),
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
@@ -121,9 +118,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn
|
||||
BottomHorizontalSplit: &Split{Ratio: -.6},
|
||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||
|
||||
KeyPlaying: make(map[string]uint32),
|
||||
volumeChan: make(chan tracker.Volume, 1),
|
||||
playerCloser: make(chan struct{}),
|
||||
KeyPlaying: make(map[string]tracker.NoteID),
|
||||
ConfirmSongDialog: new(Dialog),
|
||||
WaveTypeDialog: new(Dialog),
|
||||
OpenSongDialog: NewFileDialog(),
|
||||
@@ -136,40 +131,85 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn
|
||||
|
||||
ExportWavDialog: NewFileDialog(),
|
||||
errorChannel: make(chan error, 32),
|
||||
window: window,
|
||||
synthService: synthService,
|
||||
Model: model,
|
||||
}
|
||||
t.Model = tracker.NewModel()
|
||||
vuBufferObserver := make(chan []float32)
|
||||
go tracker.VuAnalyzer(0.3, 1e-4, 1, -100, 20, vuBufferObserver, t.volumeChan, t.errorChannel)
|
||||
t.Theme.Palette.Fg = primaryColor
|
||||
t.Theme.Palette.ContrastFg = black
|
||||
t.TrackEditor.Focus()
|
||||
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, syncChannel, audioChannel, vuBufferObserver)
|
||||
audioOut := audioContext.Output()
|
||||
go func() {
|
||||
for buf := range audioChannel {
|
||||
audioOut.WriteAudio(buf)
|
||||
}
|
||||
}()
|
||||
t.ResetSong()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmQuit
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return false
|
||||
func (t *Tracker) Main() {
|
||||
titleFooter := ""
|
||||
w := app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
var ops op.Ops
|
||||
mainloop:
|
||||
for {
|
||||
if pos, playing := t.PlayPosition(), t.Playing(); t.NoteTracking() && playing {
|
||||
cursor := t.Cursor()
|
||||
cursor.SongRow = pos
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
}
|
||||
if titleFooter != t.FilePath() {
|
||||
titleFooter = t.FilePath()
|
||||
if titleFooter != "" {
|
||||
w.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", titleFooter)))
|
||||
} else {
|
||||
w.Option(app.Title(fmt.Sprintf("Sointu Tracker")))
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-t.refresh:
|
||||
w.Invalidate()
|
||||
case e := <-t.errorChannel:
|
||||
t.Alert.Update(e.Error(), Error, time.Second*5)
|
||||
w.Invalidate()
|
||||
case e := <-t.PlayerMessages:
|
||||
if err, ok := e.Inner.(tracker.PlayerCrashMessage); ok {
|
||||
t.Alert.Update(err.Error(), Error, time.Second*3)
|
||||
}
|
||||
if err, ok := e.Inner.(tracker.PlayerVolumeErrorMessage); ok {
|
||||
t.Alert.Update(err.Error(), Warning, time.Second*3)
|
||||
}
|
||||
t.lastVolume = e.Volume
|
||||
t.InstrumentEditor.voiceStates = e.VoiceStates
|
||||
t.ProcessPlayerMessage(e)
|
||||
w.Invalidate()
|
||||
case e := <-w.Events():
|
||||
switch e := e.(type) {
|
||||
case system.DestroyEvent:
|
||||
if !t.Quit(false) {
|
||||
// TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog
|
||||
w = app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
}
|
||||
case key.Event:
|
||||
if t.KeyEvent(e, w) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
if t.quitted {
|
||||
break mainloop
|
||||
}
|
||||
}
|
||||
t.quitted = true
|
||||
return true
|
||||
w.Close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build !plugin
|
||||
|
||||
package gioui
|
||||
|
||||
const CAN_QUIT = true
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmQuit
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return false
|
||||
}
|
||||
t.quitted = true
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build plugin
|
||||
|
||||
package gioui
|
||||
|
||||
const CAN_QUIT = false
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
t.quitted = forced
|
||||
return forced
|
||||
}
|
||||
Reference in New Issue
Block a user