-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: input midi velocity into a separate track #182
base: master
Are you sure you want to change the base?
Changes from all commits
4eebfd9
0534ec4
ad690c7
49a259c
20a1769
cae7ad3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,10 @@ package tracker | |
|
||
import ( | ||
"fmt" | ||
"github.com/vsariola/sointu" | ||
"iter" | ||
"slices" | ||
|
||
"github.com/vsariola/sointu" | ||
) | ||
|
||
/* | ||
|
@@ -115,24 +116,50 @@ func (m *Model) PatternUnique(t, p int) bool { | |
// public getters with further model information | ||
|
||
func (m *Model) TracksWithSameInstrumentAsCurrent() []int { | ||
currentTrack := m.d.Cursor.Track | ||
if currentTrack > len(m.derived.forTrack) { | ||
d, ok := m.currentDerivedForTrack() | ||
if !ok { | ||
return nil | ||
} | ||
return m.derived.forTrack[currentTrack].tracksWithSameInstrument | ||
return d.tracksWithSameInstrument | ||
} | ||
|
||
func (m *Model) CountNextTracksForCurrentInstrument() int { | ||
currentTrack := m.d.Cursor.Track | ||
count := 0 | ||
for t := range m.TracksWithSameInstrumentAsCurrent() { | ||
for _, t := range m.TracksWithSameInstrumentAsCurrent() { | ||
if t > currentTrack { | ||
count++ | ||
} | ||
} | ||
return count | ||
} | ||
|
||
func (m *Model) CanUseTrackForMidiVelInput(trackIndex int) bool { | ||
// makes no sense to record velocity into tracks where notes get recorded | ||
tracksForMidiNoteInput := m.TracksWithSameInstrumentAsCurrent() | ||
return !slices.Contains(tracksForMidiNoteInput, trackIndex) | ||
} | ||
|
||
func (m *Model) CurrentPlayerConstraints() PlayerProcessConstraints { | ||
d, ok := m.currentDerivedForTrack() | ||
if !ok { | ||
return PlayerProcessConstraints{IsConstrained: false} | ||
} | ||
return PlayerProcessConstraints{ | ||
IsConstrained: m.trackMidiIn, | ||
MaxPolyphony: len(d.tracksWithSameInstrument), | ||
InstrumentIndex: d.instrumentRange[0], | ||
} | ||
} | ||
Comment on lines
+143
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PlayerProcessConstraints are a new concept - depending on what some settings in the Model are, the Player, via the PlayerProcessContext, might need to take care of some constraining conditions |
||
|
||
func (m *Model) currentDerivedForTrack() (derivedForTrack, bool) { | ||
currentTrack := m.d.Cursor.Track | ||
if currentTrack > len(m.derived.forTrack) { | ||
return derivedForTrack{}, false | ||
} | ||
return m.derived.forTrack[currentTrack], true | ||
} | ||
|
||
// init / update methods | ||
|
||
func (m *Model) initDerivedData() { | ||
|
@@ -165,6 +192,7 @@ func (m *Model) updateDerivedScoreData() { | |
}, | ||
) | ||
} | ||
m.updatePlayerConstraints() | ||
} | ||
|
||
func (m *Model) updateDerivedPatchData() { | ||
|
@@ -194,6 +222,13 @@ func (m *Model) updateDerivedParameterData(unit sointu.Unit) { | |
} | ||
} | ||
|
||
// updatePlayerConstraints() is different from the other derived methods, | ||
// it needs to be called after any model change that could affect the player. | ||
// for this, it reads derivedForTrack, which is why it lives here for now. | ||
func (m *Model) updatePlayerConstraints() { | ||
m.MIDI.SetPlayerConstraints(m.CurrentPlayerConstraints()) | ||
} | ||
|
||
// internals... | ||
|
||
func (m *Model) collectSendSources(unit sointu.Unit, paramName string) iter.Seq[sendSourceData] { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
Comment on lines
+47
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the new "when putting MIDI into tracks, you can, optionally, select another track where the velocity byte goes" feature required a new type of button (that opens a popup menu) |
||
) | ||
|
||
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{} | ||
} | ||
Comment on lines
+366
to
+368
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also; I considered it better UX to make the MIDI button hidden if there is no input device available (this is checked once on startup, so if I plug a device in later, I need to restart the tracker - might still be better than checking all devices every single frame) |
||
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { | ||
colMacro := op.Record(gtx.Ops) | ||
paint.ColorOp{Color: b.Color}.Add(gtx.Ops) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
Comment on lines
+13
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Menu Items were forced to have an icon until now |
||
if widget, ok := iconCache[&icon[0]]; ok { | ||
return widget | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,26 +101,31 @@ 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} | ||
shortcutInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(12), Bottom: unit.Dp(2), Top: unit.Dp(2)} | ||
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 { | ||
Comment on lines
-171
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this corresponds to the change we introduced with the buttons-that-do-not-react-to-spacebar; we have our own Clickables which were not referenced consistently yet (probably still not everywhere, but progress is done in small steps ;) ) |
||
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) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this third argument was a relic from my old solution, which is why you introduced the broker. ergo, I got rid of it