Skip to content
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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Dragging mouse to select rectangles in the tables
- The standalone tracker can open a MIDI port for receiving MIDI notes
([#166][i166])
- The note editor has a button to allow entering notes by MIDI. Polyphony is
supported if there are tracks available. ([#170][i170])
- Direct Midi Input: The note editor has a button to allow entering notes
by MIDI (i.e. while not recording). ([#170][i170])
- Polyphony is supported if there are tracks available.
- The velocity of the last MIDI input note can be sent to another track
(selector next to the MIDI button in the note editor), optionally.
- Units can have comments, to make it easier to distinguish between units of
same type within an instrument. These comments are also shown when choosing
the send target. ([#114][i114])
Expand Down
2 changes: 1 addition & 1 deletion cmd/sointu-track/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func main() {

trackerUi := gioui.NewTracker(model)
audioCloser := audioContext.Play(func(buf sointu.AudioBuffer) error {
player.Process(buf, midiContext, trackerUi)
player.Process(buf, midiContext)
Copy link
Contributor Author

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

return nil
})

Expand Down
2 changes: 1 addition & 1 deletion cmd/sointu-vsti/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
recoveryFile = filepath.Join(configDir, "sointu", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes))
}
broker := tracker.NewBroker()
model := tracker.NewModel(broker, cmd.MainSynther, NullMIDIContext{}, recoveryFile)

Check failure on line 78 in cmd/sointu-vsti/main.go

View workflow job for this annotation

GitHub Actions / binaries (ubuntu-latest, /home/runner/nasm/nasm, sointu-vsti.so, -buildmode=c-shared -tags=plugin...

cannot use NullMIDIContext{} (value of type NullMIDIContext) as tracker.MIDIContext value in argument to tracker.NewModel: NullMIDIContext does not implement tracker.MIDIContext (missing method SetPlayerConstraints)
player := tracker.NewPlayer(broker, cmd.MainSynther)
detector := tracker.NewDetector(broker)
go detector.Run()
Expand Down Expand Up @@ -109,7 +109,7 @@
buf = append(buf, make(sointu.AudioBuffer, out.Frames-len(buf))...)
}
buf = buf[:out.Frames]
player.Process(buf, &context, nil)
player.Process(buf, &context)

Check failure on line 112 in cmd/sointu-vsti/main.go

View workflow job for this annotation

GitHub Actions / binaries (ubuntu-latest, /home/runner/nasm/nasm, sointu-vsti.so, -buildmode=c-shared -tags=plugin...

cannot use &context (value of type *VSTIProcessContext) as tracker.PlayerProcessContext value in argument to player.Process: *VSTIProcessContext does not implement tracker.PlayerProcessContext (missing method Constraints)
for i := 0; i < out.Frames; i++ {
left[i], right[i] = buf[i][0], buf[i][1]
}
Expand Down
13 changes: 8 additions & 5 deletions tracker/bool.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,15 @@ func (m *Follow) Value() bool { return m.follow }
func (m *Follow) setValue(val bool) { m.follow = val }
func (m *Follow) Enabled() bool { return true }

// TrackMidiIn (Midi Input for notes in the tracks)
// Midi Input for notes in the tracks

func (m *TrackMidiIn) Bool() Bool { return Bool{m} }
func (m *TrackMidiIn) Value() bool { return m.trackMidiIn }
func (m *TrackMidiIn) setValue(val bool) { m.trackMidiIn = val }
func (m *TrackMidiIn) Enabled() bool { return m.MIDI.HasDeviceOpen() }
func (m *TrackMidiIn) Bool() Bool { return Bool{m} }
func (m *TrackMidiIn) Value() bool { return m.trackMidiIn }
func (m *TrackMidiIn) setValue(val bool) {
m.trackMidiIn = val
((*Model)(m)).updatePlayerConstraints()
}
func (m *TrackMidiIn) Enabled() bool { return m.MIDI.HasDeviceOpen() }

// Effect methods

Expand Down
45 changes: 40 additions & 5 deletions tracker/derived.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package tracker

import (
"fmt"
"github.com/vsariola/sointu"
"iter"
"slices"

"github.com/vsariola/sointu"
)

/*
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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() {
Expand Down Expand Up @@ -165,6 +192,7 @@ func (m *Model) updateDerivedScoreData() {
},
)
}
m.updatePlayerConstraints()
}

func (m *Model) updateDerivedPatchData() {
Expand Down Expand Up @@ -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] {
Expand Down
17 changes: 16 additions & 1 deletion tracker/gioui/buttons.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

@qm210 qm210 Nov 22, 2024

Choose a reason for hiding this comment

The 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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -287,6 +298,7 @@ type ButtonStyle struct {
Inset layout.Inset
Button *Clickable
shaper *text.Shaper
Hidden bool
}

type ButtonLayoutStyle struct {
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Expand Down
3 changes: 3 additions & 0 deletions tracker/gioui/iconcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
Expand Down
14 changes: 13 additions & 1 deletion tracker/gioui/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
21 changes: 12 additions & 9 deletions tracker/gioui/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}),
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Expand Down
Loading
Loading