feat: input midi velocity into a separate track (includes many structural changes)

This commit is contained in:
qm210
2024-11-22 02:38:57 +01:00
parent 4169356845
commit 4eebfd9f48
23 changed files with 531 additions and 177 deletions

View File

@ -44,6 +44,14 @@ type (
TipArea component.TipArea
Bool tracker.Bool
}
MenuClickable struct {
Clickable Clickable
menu Menu
Selected tracker.OptionalInt
TipArea component.TipArea
Tooltip component.Tooltip
}
)
func NewActionClickable(a tracker.Action) *ActionClickable {
@ -136,7 +144,10 @@ func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) Butt
ret := Button(th, &b.Clickable, text)
ret.Background = transparent
ret.Inset = layout.UniformInset(unit.Dp(6))
if b.Bool.Value() {
if !b.Bool.Enabled() {
ret.Color = disabledTextColor
ret.Background = transparent
} else if b.Bool.Value() {
ret.Color = th.Palette.ContrastFg
ret.Background = th.Palette.Fg
} else {
@ -287,6 +298,7 @@ type ButtonStyle struct {
Inset layout.Inset
Button *Clickable
shaper *text.Shaper
Hidden bool
}
type ButtonLayoutStyle struct {
@ -351,6 +363,9 @@ func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
CornerRadius: b.CornerRadius,
Button: b.Button,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
if b.Hidden {
return layout.Dimensions{}
}
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
colMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: b.Color}.Add(gtx.Ops)

View File

@ -10,6 +10,9 @@ var iconCache = map[*byte]*widget.Icon{}
// widgetForIcon returns a widget for IconVG data, but caching the results
func widgetForIcon(icon []byte) *widget.Icon {
if icon == nil {
return nil
}
if widget, ok := iconCache[&icon[0]]; ok {
return widget
}

View File

@ -46,5 +46,17 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
}
func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget {
return LabelStyle{Text: str, Color: color, ShadeColor: black, Font: labelDefaultFont, FontSize: labelDefaultFontSize, Alignment: layout.W, Shaper: shaper}.Layout
return SizedLabel(str, color, shaper, labelDefaultFontSize)
}
func SizedLabel(str string, color color.NRGBA, shaper *text.Shaper, fontSize unit.Sp) layout.Widget {
return LabelStyle{
Text: str,
Color: color,
ShadeColor: black,
Font: labelDefaultFont,
FontSize: fontSize,
Alignment: layout.W,
Shaper: shaper,
}.Layout
}

View File

@ -12,8 +12,6 @@ import (
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
)
@ -103,12 +101,12 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
}
icon := widgetForIcon(item.IconBytes)
iconColor := m.IconColor
if !item.Doer.Allowed() {
iconColor = mediumEmphasisTextColor
}
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor, Shaper: m.Shaper}
if !item.Doer.Allowed() {
// note: might be a bug in gioui, but for iconColor = mediumEmphasisTextColor
// this does not render the icon at all. other colors seem to work fine.
iconColor = disabledTextColor
textLabel.Color = mediumEmphasisTextColor
}
shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor, Shaper: m.Shaper}
@ -116,13 +114,18 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return iconInset.Layout(gtx, func(gtx C) D {
p := gtx.Dp(unit.Dp(m.IconSize))
p := gtx.Dp(m.IconSize)
gtx.Constraints.Min = image.Pt(p, p)
if icon == nil {
return D{Size: gtx.Constraints.Min}
}
return icon.Layout(gtx, iconColor)
})
}),
layout.Rigid(textLabel.Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, 1)} }),
layout.Flexed(1, func(gtx C) D {
return D{Size: image.Pt(gtx.Constraints.Max.X, 1)}
}),
layout.Rigid(func(gtx C) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}),
@ -168,14 +171,14 @@ func PopupMenu(menu *Menu, shaper *text.Shaper) MenuStyle {
}
}
func (tr *Tracker) layoutMenu(gtx C, title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
func (tr *Tracker) layoutMenu(gtx C, title string, clickable *Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
for clickable.Clicked(gtx) {
menu.Visible = true
}
m := PopupMenu(menu, tr.Theme.Shaper)
return func(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
titleBtn := material.Button(tr.Theme, clickable, title)
titleBtn := Button(tr.Theme, clickable, title)
titleBtn.Color = white
titleBtn.Background = transparent
titleBtn.CornerRadius = unit.Dp(0)

View File

@ -2,6 +2,7 @@ package gioui
import (
"fmt"
"gioui.org/x/component"
"image"
"image/color"
"strconv"
@ -64,6 +65,7 @@ type NoteEditor struct {
EffectBtn *BoolClickable
UniqueBtn *BoolClickable
TrackMidiInBtn *BoolClickable
TrackForMidiVelIn *MenuClickable
scrollTable *ScrollTable
eventFilters []event.Filter
@ -88,6 +90,7 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
EffectBtn: NewBoolClickable(model.Effect().Bool()),
UniqueBtn: NewBoolClickable(model.UniquePatterns().Bool()),
TrackMidiInBtn: NewBoolClickable(model.TrackMidiIn().Bool()),
TrackForMidiVelIn: &MenuClickable{Selected: model.TrackForMidiVelIn().OptionalInt()},
scrollTable: NewScrollTable(
model.Notes().Table(),
model.Tracks().List(),
@ -158,14 +161,12 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
in := layout.UniformInset(unit.Dp(1))
voiceUpDown := func(gtx C) D {
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
return in.Layout(gtx, numStyle.Layout)
}
voiceUpDown := NumericUpDownPadded(t.Theme, te.TrackVoices, "Number of voices for this track", 1)
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex")
uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
midiInBtnStyle := ToggleButton(gtx, t.Theme, te.TrackMidiInBtn, "MIDI")
midiInBtnStyle.Hidden = !t.HasAnyMidiInput()
trackForMidiVelInSelector := te.layoutMidiVelInTrackSelector(t, " vel:")
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
layout.Rigid(addSemitoneBtnStyle.Layout),
@ -176,16 +177,62 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
layout.Rigid(effectBtnStyle.Layout),
layout.Rigid(uniqueBtnStyle.Layout),
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
layout.Rigid(voiceUpDown),
layout.Rigid(voiceUpDown.Layout),
layout.Rigid(splitTrackBtnStyle.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(midiInBtnStyle.Layout),
layout.Rigid(trackForMidiVelInSelector),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(deleteTrackBtnStyle.Layout),
layout.Rigid(newTrackBtnStyle.Layout))
})
}
func (te *NoteEditor) layoutMidiVelInTrackSelector(t *Tracker, label string) func(gtx C) D {
if !t.HasAnyMidiInput() {
return layout.Spacer{}.Layout
}
tracks := t.Model.Tracks().List()
trackItems := make([]MenuItem, tracks.Count()+1)
trackForMidiVelIn := t.Model.TrackForMidiVelIn()
offText := "\u2014off\u2014"
currentText := offText
for i := range trackItems {
trackItems[i] = MenuItem{
Text: offText,
Doer: tracker.Check(
func() { trackForMidiVelIn.OptionalInt().Set(i-1, i > 0) },
func() bool { return t.Model.CanUseTrackForMidiVelInput(i - 1) },
),
}
if i > 0 {
trackItems[i].Text = fmt.Sprintf("%d %s", i-1, t.Model.TrackTitle(i-1))
}
if trackForMidiVelIn.OptionalInt().Equals(i-1, i > 0) {
trackItems[i].IconBytes = icons.NavigationChevronRight
if trackForMidiVelIn.IsValid() {
currentText = trackItems[i].Text
}
}
}
return func(gtx C) D {
tooltip := component.PlatformTooltip(t.Theme, "Record MIDI VEL into chosen track. This can not be one of the selected tracks (where MIDI Notes go).")
return te.TrackForMidiVelIn.TipArea.Layout(gtx, tooltip, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(SizedLabel(label, white, t.Theme.Shaper, unit.Sp(12))),
layout.Rigid(t.layoutMenu(gtx,
currentText,
&te.TrackForMidiVelIn.Clickable,
&te.TrackForMidiVelIn.menu,
unit.Dp(200),
trackItems...,
)),
)
})
}
}
const baseNote = 24
var notes = []string{
@ -287,17 +334,20 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
c = cursorColor
}
if hasTrackMidiIn {
c = cursorForTrackMidiInColor
c = trackMidiInCurrentColor
}
te.paintColumnCell(gtx, x, t, c)
te.paintColumnCell(gtx, x, t, c, hasTrackMidiIn)
}
// draw the corresponding "fake cursors" for instrument-track-groups (for polyphony)
if hasTrackMidiIn {
if hasTrackMidiIn && y == cursor.Y {
for _, trackIndex := range t.Model.TracksWithSameInstrumentAsCurrent() {
if x == trackIndex && y == cursor.Y {
te.paintColumnCell(gtx, x, t, cursorNeighborForTrackMidiInColor)
if x == trackIndex {
te.paintColumnCell(gtx, x, t, trackMidiInAdditionalColor, hasTrackMidiIn)
}
}
if t.Model.TrackForMidiVelIn().Equals(x) {
te.paintColumnCell(gtx, x, t, trackMidiVelInColor, hasTrackMidiIn)
}
}
// draw the pattern marker
@ -334,10 +384,10 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
return table.Layout(gtx)
}
func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA) {
func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA, ignoreEffect bool) {
cw := gtx.Constraints.Min.X
cx := 0
if t.Model.Notes().Effect(x) {
if t.Model.Notes().Effect(x) && !ignoreEffect {
cw /= 2
if t.Model.Notes().LowNibble() {
cx += cw
@ -406,21 +456,3 @@ func (te *NoteEditor) finishNoteInsert(t *Tracker, note byte, keyName key.Name)
t.KeyPlaying[keyName] = t.TrackNoteOn(trk, note)
}
}
func (te *NoteEditor) HandleMidiInput(t *Tracker) {
inputDeactivated := !t.Model.TrackMidiIn().Value()
if inputDeactivated {
return
}
te.scrollTable.Table.SetCursor2(te.scrollTable.Table.Cursor())
remaining := t.Model.CountNextTracksForCurrentInstrument()
for i, note := range t.MidiNotePlaying {
t.Model.Notes().Table().Set(note)
te.scrollTable.Table.MoveCursor(1, 0)
te.scrollTable.EnsureCursorVisible()
if i >= remaining {
break
}
}
te.scrollTable.Table.SetCursor(te.scrollTable.Table.Cursor2())
}

View File

@ -48,7 +48,9 @@ type NumericUpDownStyle struct {
Tooltip component.Tooltip
Width unit.Dp
Height unit.Dp
Padding unit.Dp
shaper text.Shaper
Hidden bool
}
func NewNumberInput(v tracker.Int) *NumberInput {
@ -56,6 +58,10 @@ func NewNumberInput(v tracker.Int) *NumberInput {
}
func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) NumericUpDownStyle {
return NumericUpDownPadded(th, number, tooltip, 0)
}
func NumericUpDownPadded(th *material.Theme, number *NumberInput, tooltip string, padding int) NumericUpDownStyle {
bgColor := th.Palette.Fg
bgColor.R /= 4
bgColor.G /= 4
@ -74,11 +80,22 @@ func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) Nume
Tooltip: Tooltip(th, tooltip),
Width: unit.Dp(70),
Height: unit.Dp(20),
Padding: unit.Dp(padding),
shaper: *th.Shaper,
}
}
func (s *NumericUpDownStyle) Layout(gtx C) D {
if s.Hidden {
return D{}
}
if s.Padding <= 0 {
return s.layoutWithTooltip(gtx)
}
return layout.UniformInset(s.Padding).Layout(gtx, s.layoutWithTooltip)
}
func (s *NumericUpDownStyle) layoutWithTooltip(gtx C) D {
if s.Tooltip.Text.Text != "" {
return s.NumberInput.tipArea.Layout(gtx, s.Tooltip, s.actualLayout)
}

View File

@ -7,14 +7,13 @@ import (
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/version"
"golang.org/x/exp/shiny/materialdesign/icons"
)
type SongPanel struct {
MenuBar []widget.Clickable
MenuBar []Clickable
Menus []Menu
BPM *NumberInput
RowsPerPattern *NumberInput
@ -57,7 +56,7 @@ type SongPanel struct {
func NewSongPanel(model *tracker.Model) *SongPanel {
ret := &SongPanel{
MenuBar: make([]widget.Clickable, 3),
MenuBar: make([]Clickable, 3),
Menus: make([]Menu, 3),
BPM: NewNumberInput(model.BPM().Int()),
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),

View File

@ -60,8 +60,10 @@ var activeLightSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255}
var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12}
var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16}
var cursorForTrackMidiInColor = color.NRGBA{R: 255, G: 100, B: 140, A: 48}
var cursorNeighborForTrackMidiInColor = color.NRGBA{R: 255, G: 100, B: 140, A: 24}
var trackMidiInCurrentColor = color.NRGBA{R: 255, G: 100, B: 140, A: 48}
var trackMidiInAdditionalColor = withScaledAlpha(trackMidiInCurrentColor, 0.7)
var trackMidiVelInColor = withScaledAlpha(trackMidiInCurrentColor, 0.3)
var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255}
@ -75,3 +77,13 @@ var dialogBgColor = color.NRGBA{R: 0, G: 0, B: 0, A: 224}
var paramIsSendTargetColor = color.NRGBA{R: 120, G: 120, B: 210, A: 255}
var paramValueInvalidColor = color.NRGBA{R: 120, G: 120, B: 120, A: 190}
func withScaledAlpha(c color.NRGBA, factor float32) color.NRGBA {
A := factor * float32(c.A)
return color.NRGBA{
R: c.R,
G: c.G,
B: c.B,
A: uint8(A),
}
}

View File

@ -35,7 +35,6 @@ type (
BottomHorizontalSplit *Split
VerticalSplit *Split
KeyPlaying map[key.Name]tracker.NoteID
MidiNotePlaying []byte
PopupAlert *PopupAlert
SaveChangesDialog *Dialog
@ -78,7 +77,6 @@ func NewTracker(model *tracker.Model) *Tracker {
VerticalSplit: &Split{Axis: layout.Vertical},
KeyPlaying: make(map[key.Name]tracker.NoteID),
MidiNotePlaying: make([]byte, 0, 32),
SaveChangesDialog: NewDialog(model.SaveSong(), model.DiscardSong(), model.Cancel()),
WaveTypeDialog: NewDialog(model.ExportInt16(), model.ExportFloat(), model.Cancel()),
InstrumentEditor: NewInstrumentEditor(model),
@ -103,7 +101,10 @@ func (t *Tracker) Main() {
titleFooter := ""
w := new(app.Window)
w.Option(app.Title("Sointu Tracker"))
w.Option(app.Size(unit.Dp(800), unit.Dp(600)))
w.Option(
app.Size(unit.Dp(800), unit.Dp(600)),
app.Fullscreen.Option(),
)
t.InstrumentEditor.Focus()
recoveryTicker := time.NewTicker(time.Second * 30)
t.Explorer = explorer.NewExplorer(w)
@ -302,45 +303,9 @@ func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
)
}
/// Event Handling (for UI updates when playing etc.)
func (t *Tracker) ProcessMessage(msg interface{}) {
switch msg.(type) {
case tracker.StartPlayMsg:
fmt.Println("Tracker received StartPlayMsg")
case tracker.RecordingMsg:
fmt.Println("Tracker received RecordingMsg")
default:
break
}
}
func (t *Tracker) ProcessEvent(event tracker.MIDINoteEvent) {
// MIDINoteEvent can be only NoteOn / NoteOff, i.e. its On field
if event.On {
t.addToMidiNotePlaying(event.Note)
} else {
t.removeFromMidiNotePlaying(event.Note)
}
t.TrackEditor.HandleMidiInput(t)
}
func (t *Tracker) addToMidiNotePlaying(note byte) {
for _, n := range t.MidiNotePlaying {
if n == note {
return
}
}
t.MidiNotePlaying = append(t.MidiNotePlaying, note)
}
func (t *Tracker) removeFromMidiNotePlaying(note byte) {
for i, n := range t.MidiNotePlaying {
if n == note {
t.MidiNotePlaying = append(
t.MidiNotePlaying[:i],
t.MidiNotePlaying[i+1:]...,
)
}
func (t *Tracker) HasAnyMidiInput() bool {
for _ = range t.Model.MIDI.InputDevices {
return true
}
return false
}

View File

@ -32,7 +32,7 @@ type UnitEditor struct {
CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable
SelectTypeBtn *Clickable
commentEditor *Editor
caser cases.Caser
@ -47,7 +47,7 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable),
SelectTypeBtn: new(Clickable),
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
sliderList: NewDragList(m.Params().List(), layout.Vertical),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
@ -236,9 +236,9 @@ func (pe *UnitEditor) command(e key.Event, t *Tracker) {
type ParameterWidget struct {
floatWidget widget.Float
boolWidget widget.Bool
instrBtn widget.Clickable
instrBtn Clickable
instrMenu Menu
unitBtn widget.Clickable
unitBtn Clickable
unitMenu Menu
Parameter tracker.Parameter
tipArea component.TipArea
@ -332,7 +332,6 @@ func (p ParameterStyle) Layout(gtx C) D {
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
instrItems := make([]MenuItem, p.tracker.Instruments().Count())
for i := range instrItems {
i := i
name, _, _, _ := p.tracker.Instruments().Item(i)
instrItems[i].Text = name
instrItems[i].IconBytes = icons.NavigationChevronRight