mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
feat: input midi velocity into a separate track (includes many structural changes)
This commit is contained in:
parent
4169356845
commit
4eebfd9f48
@ -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
|
- Dragging mouse to select rectangles in the tables
|
||||||
- The standalone tracker can open a MIDI port for receiving MIDI notes
|
- The standalone tracker can open a MIDI port for receiving MIDI notes
|
||||||
([#166][i166])
|
([#166][i166])
|
||||||
- The note editor has a button to allow entering notes by MIDI. Polyphony is
|
- Direct Midi Input: The note editor has a button to allow entering notes
|
||||||
supported if there are tracks available. ([#170][i170])
|
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
|
- 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
|
same type within an instrument. These comments are also shown when choosing
|
||||||
the send target. ([#114][i114])
|
the send target. ([#114][i114])
|
||||||
|
@ -64,7 +64,7 @@ func main() {
|
|||||||
|
|
||||||
trackerUi := gioui.NewTracker(model)
|
trackerUi := gioui.NewTracker(model)
|
||||||
audioCloser := audioContext.Play(func(buf sointu.AudioBuffer) error {
|
audioCloser := audioContext.Play(func(buf sointu.AudioBuffer) error {
|
||||||
player.Process(buf, midiContext, trackerUi)
|
player.Process(buf, midiContext)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -112,12 +112,15 @@ func (m *Follow) Value() bool { return m.follow }
|
|||||||
func (m *Follow) setValue(val bool) { m.follow = val }
|
func (m *Follow) setValue(val bool) { m.follow = val }
|
||||||
func (m *Follow) Enabled() bool { return true }
|
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) Bool() Bool { return Bool{m} }
|
||||||
func (m *TrackMidiIn) Value() bool { return m.trackMidiIn }
|
func (m *TrackMidiIn) Value() bool { return m.trackMidiIn }
|
||||||
func (m *TrackMidiIn) setValue(val bool) { m.trackMidiIn = val }
|
func (m *TrackMidiIn) setValue(val bool) {
|
||||||
func (m *TrackMidiIn) Enabled() bool { return m.MIDI.HasDeviceOpen() }
|
m.trackMidiIn = val
|
||||||
|
((*Model)(m)).updatePlayerConstraints()
|
||||||
|
}
|
||||||
|
func (m *TrackMidiIn) Enabled() bool { return m.MIDI.HasDeviceOpen() }
|
||||||
|
|
||||||
// Effect methods
|
// Effect methods
|
||||||
|
|
||||||
|
@ -2,9 +2,10 @@ package tracker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
"iter"
|
"iter"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -115,17 +116,17 @@ func (m *Model) PatternUnique(t, p int) bool {
|
|||||||
// public getters with further model information
|
// public getters with further model information
|
||||||
|
|
||||||
func (m *Model) TracksWithSameInstrumentAsCurrent() []int {
|
func (m *Model) TracksWithSameInstrumentAsCurrent() []int {
|
||||||
currentTrack := m.d.Cursor.Track
|
d, ok := m.currentDerivedForTrack()
|
||||||
if currentTrack > len(m.derived.forTrack) {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return m.derived.forTrack[currentTrack].tracksWithSameInstrument
|
return d.tracksWithSameInstrument
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) CountNextTracksForCurrentInstrument() int {
|
func (m *Model) CountNextTracksForCurrentInstrument() int {
|
||||||
currentTrack := m.d.Cursor.Track
|
currentTrack := m.d.Cursor.Track
|
||||||
count := 0
|
count := 0
|
||||||
for t := range m.TracksWithSameInstrumentAsCurrent() {
|
for _, t := range m.TracksWithSameInstrumentAsCurrent() {
|
||||||
if t > currentTrack {
|
if t > currentTrack {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@ -133,6 +134,32 @@ func (m *Model) CountNextTracksForCurrentInstrument() int {
|
|||||||
return 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],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// init / update methods
|
||||||
|
|
||||||
func (m *Model) initDerivedData() {
|
func (m *Model) initDerivedData() {
|
||||||
@ -165,6 +192,7 @@ func (m *Model) updateDerivedScoreData() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
m.updatePlayerConstraints()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) updateDerivedPatchData() {
|
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...
|
// internals...
|
||||||
|
|
||||||
func (m *Model) collectSendSources(unit sointu.Unit, paramName string) iter.Seq[sendSourceData] {
|
func (m *Model) collectSendSources(unit sointu.Unit, paramName string) iter.Seq[sendSourceData] {
|
||||||
|
@ -44,6 +44,14 @@ type (
|
|||||||
TipArea component.TipArea
|
TipArea component.TipArea
|
||||||
Bool tracker.Bool
|
Bool tracker.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MenuClickable struct {
|
||||||
|
Clickable Clickable
|
||||||
|
menu Menu
|
||||||
|
Selected tracker.OptionalInt
|
||||||
|
TipArea component.TipArea
|
||||||
|
Tooltip component.Tooltip
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewActionClickable(a tracker.Action) *ActionClickable {
|
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 := Button(th, &b.Clickable, text)
|
||||||
ret.Background = transparent
|
ret.Background = transparent
|
||||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
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.Color = th.Palette.ContrastFg
|
||||||
ret.Background = th.Palette.Fg
|
ret.Background = th.Palette.Fg
|
||||||
} else {
|
} else {
|
||||||
@ -287,6 +298,7 @@ type ButtonStyle struct {
|
|||||||
Inset layout.Inset
|
Inset layout.Inset
|
||||||
Button *Clickable
|
Button *Clickable
|
||||||
shaper *text.Shaper
|
shaper *text.Shaper
|
||||||
|
Hidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ButtonLayoutStyle struct {
|
type ButtonLayoutStyle struct {
|
||||||
@ -351,6 +363,9 @@ func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
CornerRadius: b.CornerRadius,
|
CornerRadius: b.CornerRadius,
|
||||||
Button: b.Button,
|
Button: b.Button,
|
||||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
}.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 {
|
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
colMacro := op.Record(gtx.Ops)
|
colMacro := op.Record(gtx.Ops)
|
||||||
paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
|
paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
|
||||||
|
@ -10,6 +10,9 @@ var iconCache = map[*byte]*widget.Icon{}
|
|||||||
|
|
||||||
// widgetForIcon returns a widget for IconVG data, but caching the results
|
// widgetForIcon returns a widget for IconVG data, but caching the results
|
||||||
func widgetForIcon(icon []byte) *widget.Icon {
|
func widgetForIcon(icon []byte) *widget.Icon {
|
||||||
|
if icon == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if widget, ok := iconCache[&icon[0]]; ok {
|
if widget, ok := iconCache[&icon[0]]; ok {
|
||||||
return widget
|
return widget
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
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
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,6 @@ import (
|
|||||||
"gioui.org/op/paint"
|
"gioui.org/op/paint"
|
||||||
"gioui.org/text"
|
"gioui.org/text"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget"
|
|
||||||
"gioui.org/widget/material"
|
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -103,12 +101,12 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
|||||||
}
|
}
|
||||||
icon := widgetForIcon(item.IconBytes)
|
icon := widgetForIcon(item.IconBytes)
|
||||||
iconColor := m.IconColor
|
iconColor := m.IconColor
|
||||||
if !item.Doer.Allowed() {
|
|
||||||
iconColor = mediumEmphasisTextColor
|
|
||||||
}
|
|
||||||
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
|
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}
|
textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor, Shaper: m.Shaper}
|
||||||
if !item.Doer.Allowed() {
|
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
|
textLabel.Color = mediumEmphasisTextColor
|
||||||
}
|
}
|
||||||
shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor, Shaper: m.Shaper}
|
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,
|
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return iconInset.Layout(gtx, 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)
|
gtx.Constraints.Min = image.Pt(p, p)
|
||||||
|
if icon == nil {
|
||||||
|
return D{Size: gtx.Constraints.Min}
|
||||||
|
}
|
||||||
return icon.Layout(gtx, iconColor)
|
return icon.Layout(gtx, iconColor)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Rigid(textLabel.Layout),
|
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 {
|
layout.Rigid(func(gtx C) D {
|
||||||
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
|
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) {
|
for clickable.Clicked(gtx) {
|
||||||
menu.Visible = true
|
menu.Visible = true
|
||||||
}
|
}
|
||||||
m := PopupMenu(menu, tr.Theme.Shaper)
|
m := PopupMenu(menu, tr.Theme.Shaper)
|
||||||
return func(gtx C) D {
|
return func(gtx C) D {
|
||||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
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.Color = white
|
||||||
titleBtn.Background = transparent
|
titleBtn.Background = transparent
|
||||||
titleBtn.CornerRadius = unit.Dp(0)
|
titleBtn.CornerRadius = unit.Dp(0)
|
||||||
|
@ -2,6 +2,7 @@ package gioui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"gioui.org/x/component"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -64,6 +65,7 @@ type NoteEditor struct {
|
|||||||
EffectBtn *BoolClickable
|
EffectBtn *BoolClickable
|
||||||
UniqueBtn *BoolClickable
|
UniqueBtn *BoolClickable
|
||||||
TrackMidiInBtn *BoolClickable
|
TrackMidiInBtn *BoolClickable
|
||||||
|
TrackForMidiVelIn *MenuClickable
|
||||||
|
|
||||||
scrollTable *ScrollTable
|
scrollTable *ScrollTable
|
||||||
eventFilters []event.Filter
|
eventFilters []event.Filter
|
||||||
@ -88,6 +90,7 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
|||||||
EffectBtn: NewBoolClickable(model.Effect().Bool()),
|
EffectBtn: NewBoolClickable(model.Effect().Bool()),
|
||||||
UniqueBtn: NewBoolClickable(model.UniquePatterns().Bool()),
|
UniqueBtn: NewBoolClickable(model.UniquePatterns().Bool()),
|
||||||
TrackMidiInBtn: NewBoolClickable(model.TrackMidiIn().Bool()),
|
TrackMidiInBtn: NewBoolClickable(model.TrackMidiIn().Bool()),
|
||||||
|
TrackForMidiVelIn: &MenuClickable{Selected: model.TrackForMidiVelIn().OptionalInt()},
|
||||||
scrollTable: NewScrollTable(
|
scrollTable: NewScrollTable(
|
||||||
model.Notes().Table(),
|
model.Notes().Table(),
|
||||||
model.Tracks().List(),
|
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)
|
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
|
||||||
splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
|
splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
|
||||||
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
|
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
|
||||||
in := layout.UniformInset(unit.Dp(1))
|
voiceUpDown := NumericUpDownPadded(t.Theme, te.TrackVoices, "Number of voices for this track", 1)
|
||||||
voiceUpDown := func(gtx C) D {
|
|
||||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
|
|
||||||
return in.Layout(gtx, numStyle.Layout)
|
|
||||||
}
|
|
||||||
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex")
|
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex")
|
||||||
uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
|
uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
|
||||||
midiInBtnStyle := ToggleButton(gtx, t.Theme, te.TrackMidiInBtn, "MIDI")
|
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,
|
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(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||||
@ -176,16 +177,62 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
|||||||
layout.Rigid(effectBtnStyle.Layout),
|
layout.Rigid(effectBtnStyle.Layout),
|
||||||
layout.Rigid(uniqueBtnStyle.Layout),
|
layout.Rigid(uniqueBtnStyle.Layout),
|
||||||
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
|
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
|
||||||
layout.Rigid(voiceUpDown),
|
layout.Rigid(voiceUpDown.Layout),
|
||||||
layout.Rigid(splitTrackBtnStyle.Layout),
|
layout.Rigid(splitTrackBtnStyle.Layout),
|
||||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||||
layout.Rigid(midiInBtnStyle.Layout),
|
layout.Rigid(midiInBtnStyle.Layout),
|
||||||
|
layout.Rigid(trackForMidiVelInSelector),
|
||||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||||
layout.Rigid(deleteTrackBtnStyle.Layout),
|
layout.Rigid(deleteTrackBtnStyle.Layout),
|
||||||
layout.Rigid(newTrackBtnStyle.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
|
const baseNote = 24
|
||||||
|
|
||||||
var notes = []string{
|
var notes = []string{
|
||||||
@ -287,17 +334,20 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
c = cursorColor
|
c = cursorColor
|
||||||
}
|
}
|
||||||
if hasTrackMidiIn {
|
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)
|
// draw the corresponding "fake cursors" for instrument-track-groups (for polyphony)
|
||||||
if hasTrackMidiIn {
|
if hasTrackMidiIn && y == cursor.Y {
|
||||||
for _, trackIndex := range t.Model.TracksWithSameInstrumentAsCurrent() {
|
for _, trackIndex := range t.Model.TracksWithSameInstrumentAsCurrent() {
|
||||||
if x == trackIndex && y == cursor.Y {
|
if x == trackIndex {
|
||||||
te.paintColumnCell(gtx, x, t, cursorNeighborForTrackMidiInColor)
|
te.paintColumnCell(gtx, x, t, trackMidiInAdditionalColor, hasTrackMidiIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if t.Model.TrackForMidiVelIn().Equals(x) {
|
||||||
|
te.paintColumnCell(gtx, x, t, trackMidiVelInColor, hasTrackMidiIn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw the pattern marker
|
// draw the pattern marker
|
||||||
@ -334,10 +384,10 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
return table.Layout(gtx)
|
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
|
cw := gtx.Constraints.Min.X
|
||||||
cx := 0
|
cx := 0
|
||||||
if t.Model.Notes().Effect(x) {
|
if t.Model.Notes().Effect(x) && !ignoreEffect {
|
||||||
cw /= 2
|
cw /= 2
|
||||||
if t.Model.Notes().LowNibble() {
|
if t.Model.Notes().LowNibble() {
|
||||||
cx += cw
|
cx += cw
|
||||||
@ -406,21 +456,3 @@ func (te *NoteEditor) finishNoteInsert(t *Tracker, note byte, keyName key.Name)
|
|||||||
t.KeyPlaying[keyName] = t.TrackNoteOn(trk, note)
|
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())
|
|
||||||
}
|
|
||||||
|
@ -48,7 +48,9 @@ type NumericUpDownStyle struct {
|
|||||||
Tooltip component.Tooltip
|
Tooltip component.Tooltip
|
||||||
Width unit.Dp
|
Width unit.Dp
|
||||||
Height unit.Dp
|
Height unit.Dp
|
||||||
|
Padding unit.Dp
|
||||||
shaper text.Shaper
|
shaper text.Shaper
|
||||||
|
Hidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNumberInput(v tracker.Int) *NumberInput {
|
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 {
|
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 := th.Palette.Fg
|
||||||
bgColor.R /= 4
|
bgColor.R /= 4
|
||||||
bgColor.G /= 4
|
bgColor.G /= 4
|
||||||
@ -74,11 +80,22 @@ func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) Nume
|
|||||||
Tooltip: Tooltip(th, tooltip),
|
Tooltip: Tooltip(th, tooltip),
|
||||||
Width: unit.Dp(70),
|
Width: unit.Dp(70),
|
||||||
Height: unit.Dp(20),
|
Height: unit.Dp(20),
|
||||||
|
Padding: unit.Dp(padding),
|
||||||
shaper: *th.Shaper,
|
shaper: *th.Shaper,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NumericUpDownStyle) Layout(gtx C) D {
|
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 != "" {
|
if s.Tooltip.Text.Text != "" {
|
||||||
return s.NumberInput.tipArea.Layout(gtx, s.Tooltip, s.actualLayout)
|
return s.NumberInput.tipArea.Layout(gtx, s.Tooltip, s.actualLayout)
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,13 @@ import (
|
|||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
"gioui.org/op/paint"
|
"gioui.org/op/paint"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget"
|
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
"github.com/vsariola/sointu/version"
|
"github.com/vsariola/sointu/version"
|
||||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SongPanel struct {
|
type SongPanel struct {
|
||||||
MenuBar []widget.Clickable
|
MenuBar []Clickable
|
||||||
Menus []Menu
|
Menus []Menu
|
||||||
BPM *NumberInput
|
BPM *NumberInput
|
||||||
RowsPerPattern *NumberInput
|
RowsPerPattern *NumberInput
|
||||||
@ -57,7 +56,7 @@ type SongPanel struct {
|
|||||||
|
|
||||||
func NewSongPanel(model *tracker.Model) *SongPanel {
|
func NewSongPanel(model *tracker.Model) *SongPanel {
|
||||||
ret := &SongPanel{
|
ret := &SongPanel{
|
||||||
MenuBar: make([]widget.Clickable, 3),
|
MenuBar: make([]Clickable, 3),
|
||||||
Menus: make([]Menu, 3),
|
Menus: make([]Menu, 3),
|
||||||
BPM: NewNumberInput(model.BPM().Int()),
|
BPM: NewNumberInput(model.BPM().Int()),
|
||||||
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),
|
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),
|
||||||
|
@ -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 cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
|
||||||
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12}
|
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 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}
|
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 paramIsSendTargetColor = color.NRGBA{R: 120, G: 120, B: 210, A: 255}
|
||||||
var paramValueInvalidColor = color.NRGBA{R: 120, G: 120, B: 120, A: 190}
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -35,7 +35,6 @@ type (
|
|||||||
BottomHorizontalSplit *Split
|
BottomHorizontalSplit *Split
|
||||||
VerticalSplit *Split
|
VerticalSplit *Split
|
||||||
KeyPlaying map[key.Name]tracker.NoteID
|
KeyPlaying map[key.Name]tracker.NoteID
|
||||||
MidiNotePlaying []byte
|
|
||||||
PopupAlert *PopupAlert
|
PopupAlert *PopupAlert
|
||||||
|
|
||||||
SaveChangesDialog *Dialog
|
SaveChangesDialog *Dialog
|
||||||
@ -78,7 +77,6 @@ func NewTracker(model *tracker.Model) *Tracker {
|
|||||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||||
|
|
||||||
KeyPlaying: make(map[key.Name]tracker.NoteID),
|
KeyPlaying: make(map[key.Name]tracker.NoteID),
|
||||||
MidiNotePlaying: make([]byte, 0, 32),
|
|
||||||
SaveChangesDialog: NewDialog(model.SaveSong(), model.DiscardSong(), model.Cancel()),
|
SaveChangesDialog: NewDialog(model.SaveSong(), model.DiscardSong(), model.Cancel()),
|
||||||
WaveTypeDialog: NewDialog(model.ExportInt16(), model.ExportFloat(), model.Cancel()),
|
WaveTypeDialog: NewDialog(model.ExportInt16(), model.ExportFloat(), model.Cancel()),
|
||||||
InstrumentEditor: NewInstrumentEditor(model),
|
InstrumentEditor: NewInstrumentEditor(model),
|
||||||
@ -103,7 +101,10 @@ func (t *Tracker) Main() {
|
|||||||
titleFooter := ""
|
titleFooter := ""
|
||||||
w := new(app.Window)
|
w := new(app.Window)
|
||||||
w.Option(app.Title("Sointu Tracker"))
|
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()
|
t.InstrumentEditor.Focus()
|
||||||
recoveryTicker := time.NewTicker(time.Second * 30)
|
recoveryTicker := time.NewTicker(time.Second * 30)
|
||||||
t.Explorer = explorer.NewExplorer(w)
|
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) HasAnyMidiInput() bool {
|
||||||
|
for _ = range t.Model.MIDI.InputDevices {
|
||||||
func (t *Tracker) ProcessMessage(msg interface{}) {
|
return true
|
||||||
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:]...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ type UnitEditor struct {
|
|||||||
CopyUnitBtn *TipClickable
|
CopyUnitBtn *TipClickable
|
||||||
ClearUnitBtn *ActionClickable
|
ClearUnitBtn *ActionClickable
|
||||||
DisableUnitBtn *BoolClickable
|
DisableUnitBtn *BoolClickable
|
||||||
SelectTypeBtn *widget.Clickable
|
SelectTypeBtn *Clickable
|
||||||
commentEditor *Editor
|
commentEditor *Editor
|
||||||
caser cases.Caser
|
caser cases.Caser
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
|||||||
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
|
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
|
||||||
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
|
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
|
||||||
CopyUnitBtn: new(TipClickable),
|
CopyUnitBtn: new(TipClickable),
|
||||||
SelectTypeBtn: new(widget.Clickable),
|
SelectTypeBtn: new(Clickable),
|
||||||
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
|
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
|
||||||
sliderList: NewDragList(m.Params().List(), layout.Vertical),
|
sliderList: NewDragList(m.Params().List(), layout.Vertical),
|
||||||
searchList: NewDragList(m.SearchResults().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 {
|
type ParameterWidget struct {
|
||||||
floatWidget widget.Float
|
floatWidget widget.Float
|
||||||
boolWidget widget.Bool
|
boolWidget widget.Bool
|
||||||
instrBtn widget.Clickable
|
instrBtn Clickable
|
||||||
instrMenu Menu
|
instrMenu Menu
|
||||||
unitBtn widget.Clickable
|
unitBtn Clickable
|
||||||
unitMenu Menu
|
unitMenu Menu
|
||||||
Parameter tracker.Parameter
|
Parameter tracker.Parameter
|
||||||
tipArea component.TipArea
|
tipArea component.TipArea
|
||||||
@ -332,7 +332,6 @@ func (p ParameterStyle) Layout(gtx C) D {
|
|||||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||||
instrItems := make([]MenuItem, p.tracker.Instruments().Count())
|
instrItems := make([]MenuItem, p.tracker.Instruments().Count())
|
||||||
for i := range instrItems {
|
for i := range instrItems {
|
||||||
i := i
|
|
||||||
name, _, _, _ := p.tracker.Instruments().Item(i)
|
name, _, _, _ := p.tracker.Instruments().Item(i)
|
||||||
instrItems[i].Text = name
|
instrItems[i].Text = name
|
||||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||||
|
@ -13,13 +13,19 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
RTMIDIContext struct {
|
RTMIDIContext struct {
|
||||||
driver *rtmididrv.Driver
|
driver *rtmididrv.Driver
|
||||||
currentIn drivers.In
|
currentIn drivers.In
|
||||||
events chan timestampedMsg
|
inputDevices []RTMIDIDevice
|
||||||
eventsBuf []timestampedMsg
|
devicesInitialized bool
|
||||||
eventIndex int
|
events chan timestampedMsg
|
||||||
startFrame int
|
eventsBuf []timestampedMsg
|
||||||
startFrameSet bool
|
eventIndex int
|
||||||
|
startFrame int
|
||||||
|
startFrameSet bool
|
||||||
|
|
||||||
|
// qm210: this is my current solution for passing model information to the player
|
||||||
|
// I do not completely love this, but improve at your own peril.
|
||||||
|
currentConstraints tracker.PlayerProcessConstraints
|
||||||
}
|
}
|
||||||
|
|
||||||
RTMIDIDevice struct {
|
RTMIDIDevice struct {
|
||||||
@ -34,6 +40,22 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (m *RTMIDIContext) InputDevices(yield func(tracker.MIDIDevice) bool) {
|
func (m *RTMIDIContext) InputDevices(yield func(tracker.MIDIDevice) bool) {
|
||||||
|
if m.devicesInitialized {
|
||||||
|
m.yieldCachedInputDevices(yield)
|
||||||
|
} else {
|
||||||
|
m.initInputDevices(yield)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RTMIDIContext) yieldCachedInputDevices(yield func(tracker.MIDIDevice) bool) {
|
||||||
|
for _, device := range m.inputDevices {
|
||||||
|
if !yield(device) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *RTMIDIContext) initInputDevices(yield func(tracker.MIDIDevice) bool) {
|
||||||
if m.driver == nil {
|
if m.driver == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -43,10 +65,12 @@ func (m *RTMIDIContext) InputDevices(yield func(tracker.MIDIDevice) bool) {
|
|||||||
}
|
}
|
||||||
for i := 0; i < len(ins); i++ {
|
for i := 0; i < len(ins); i++ {
|
||||||
device := RTMIDIDevice{context: m, in: ins[i]}
|
device := RTMIDIDevice{context: m, in: ins[i]}
|
||||||
|
m.inputDevices = append(m.inputDevices, device)
|
||||||
if !yield(device) {
|
if !yield(device) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.devicesInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the driver.
|
// Open the driver.
|
||||||
@ -87,6 +111,37 @@ func (d RTMIDIDevice) String() string {
|
|||||||
return d.in.String()
|
return d.in.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *RTMIDIContext) Close() {
|
||||||
|
if c.driver == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.currentIn != nil && c.currentIn.IsOpen() {
|
||||||
|
c.currentIn.Close()
|
||||||
|
}
|
||||||
|
c.driver.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RTMIDIContext) HasDeviceOpen() bool {
|
||||||
|
return c.currentIn != nil && c.currentIn.IsOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RTMIDIContext) TryToOpenBy(namePrefix string, takeFirst bool) {
|
||||||
|
if namePrefix == "" && !takeFirst {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for input := range c.InputDevices {
|
||||||
|
if takeFirst || strings.HasPrefix(input.String(), namePrefix) {
|
||||||
|
input.Open()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if takeFirst {
|
||||||
|
fmt.Errorf("Could not find any MIDI Input.\n")
|
||||||
|
} else {
|
||||||
|
fmt.Errorf("Could not find any default MIDI Input starting with \"%s\".\n", namePrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *RTMIDIContext) HandleMessage(msg midi.Message, timestampms int32) {
|
func (m *RTMIDIContext) HandleMessage(msg midi.Message, timestampms int32) {
|
||||||
select {
|
select {
|
||||||
case m.events <- timestampedMsg{frame: int(int64(timestampms) * 44100 / 1000), msg: msg}: // if the channel is full, just drop the message
|
case m.events <- timestampedMsg{frame: int(int64(timestampms) * 44100 / 1000), msg: msg}: // if the channel is full, just drop the message
|
||||||
@ -124,10 +179,16 @@ F:
|
|||||||
m := c.eventsBuf[c.eventIndex]
|
m := c.eventsBuf[c.eventIndex]
|
||||||
f := m.frame - c.startFrame
|
f := m.frame - c.startFrame
|
||||||
c.eventIndex++
|
c.eventIndex++
|
||||||
if m.msg.GetNoteOn(&channel, &key, &velocity) {
|
isNoteOn := m.msg.GetNoteOn(&channel, &key, &velocity)
|
||||||
return tracker.MIDINoteEvent{Frame: f, On: true, Channel: int(channel), Note: key}, true
|
isNoteOff := !isNoteOn && m.msg.GetNoteOff(&channel, &key, &velocity)
|
||||||
} else if m.msg.GetNoteOff(&channel, &key, &velocity) {
|
if isNoteOn || isNoteOff {
|
||||||
return tracker.MIDINoteEvent{Frame: f, On: false, Channel: int(channel), Note: key}, true
|
return tracker.MIDINoteEvent{
|
||||||
|
Frame: f,
|
||||||
|
On: isNoteOn,
|
||||||
|
Channel: int(channel),
|
||||||
|
Note: key,
|
||||||
|
Velocity: velocity,
|
||||||
|
}, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.eventIndex = len(c.eventsBuf) + 1
|
c.eventIndex = len(c.eventsBuf) + 1
|
||||||
@ -155,33 +216,10 @@ func (c *RTMIDIContext) BPM() (bpm float64, ok bool) {
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RTMIDIContext) Close() {
|
func (c *RTMIDIContext) Constraints() tracker.PlayerProcessConstraints {
|
||||||
if c.driver == nil {
|
return c.currentConstraints
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.currentIn != nil && c.currentIn.IsOpen() {
|
|
||||||
c.currentIn.Close()
|
|
||||||
}
|
|
||||||
c.driver.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RTMIDIContext) HasDeviceOpen() bool {
|
func (c *RTMIDIContext) SetPlayerConstraints(constraints tracker.PlayerProcessConstraints) {
|
||||||
return c.currentIn != nil && c.currentIn.IsOpen()
|
c.currentConstraints = constraints
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RTMIDIContext) TryToOpenBy(namePrefix string, takeFirst bool) {
|
|
||||||
if namePrefix == "" && !takeFirst {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for input := range c.InputDevices {
|
|
||||||
if takeFirst || strings.HasPrefix(input.String(), namePrefix) {
|
|
||||||
input.Open()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if takeFirst {
|
|
||||||
fmt.Errorf("Could not find any MIDI Input.\n")
|
|
||||||
} else {
|
|
||||||
fmt.Errorf("Could not find any default MIDI Input starting with \"%s\".\n", namePrefix)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@ func (v Int) Add(delta int) (ok bool) {
|
|||||||
func (v Int) Set(value int) (ok bool) {
|
func (v Int) Set(value int) (ok bool) {
|
||||||
r := v.Range()
|
r := v.Range()
|
||||||
value = v.Range().Clamp(value)
|
value = v.Range().Clamp(value)
|
||||||
|
// qm210: Question: how can the Min/Max checks even be true after the preceding Clamp() ?
|
||||||
if value == v.Value() || value < r.Min || value > r.Max {
|
if value == v.Value() || value < r.Min || value > r.Max {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -428,7 +428,7 @@ func (v *Tracks) Selected2() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *Tracks) SetSelected(value int) {
|
func (v *Tracks) SetSelected(value int) {
|
||||||
v.d.Cursor.Track = max(min(value, v.Count()-1), 0)
|
(*Model)(v).ChangeTrack(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Tracks) SetSelected2(value int) {
|
func (v *Tracks) SetSelected2(value int) {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
|
"github.com/vsariola/sointu/tracker/types"
|
||||||
"github.com/vsariola/sointu/vm"
|
"github.com/vsariola/sointu/vm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,8 +80,9 @@ type (
|
|||||||
|
|
||||||
broker *Broker
|
broker *Broker
|
||||||
|
|
||||||
MIDI MIDIContext
|
MIDI MIDIContext
|
||||||
trackMidiIn bool
|
trackMidiIn bool
|
||||||
|
trackForMidiVelIn types.OptionalInteger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor identifies a row and a track in a song score.
|
// Cursor identifies a row and a track in a song score.
|
||||||
@ -131,6 +133,8 @@ type (
|
|||||||
InputDevices(yield func(MIDIDevice) bool)
|
InputDevices(yield func(MIDIDevice) bool)
|
||||||
Close()
|
Close()
|
||||||
HasDeviceOpen() bool
|
HasDeviceOpen() bool
|
||||||
|
|
||||||
|
SetPlayerConstraints(PlayerProcessConstraints)
|
||||||
}
|
}
|
||||||
|
|
||||||
MIDIDevice interface {
|
MIDIDevice interface {
|
||||||
@ -383,6 +387,8 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
m.playing = e.bool
|
m.playing = e.bool
|
||||||
case *sointu.AudioBuffer:
|
case *sointu.AudioBuffer:
|
||||||
m.signalAnalyzer.ProcessAudioBuffer(e)
|
m.signalAnalyzer.ProcessAudioBuffer(e)
|
||||||
|
case TrackInput:
|
||||||
|
m.applyTrackInput(e)
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -390,6 +396,11 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
func (m *Model) SignalAnalyzer() *ScopeModel { return m.signalAnalyzer }
|
func (m *Model) SignalAnalyzer() *ScopeModel { return m.signalAnalyzer }
|
||||||
func (m *Model) Broker() *Broker { return m.broker }
|
func (m *Model) Broker() *Broker { return m.broker }
|
||||||
|
|
||||||
|
func (m *Model) ChangeTrack(track int) {
|
||||||
|
m.d.Cursor.Track = max(min(track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||||
|
m.updatePlayerConstraints()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) TrackNoteOn(track int, note byte) (id NoteID) {
|
func (m *Model) TrackNoteOn(track int, note byte) (id NoteID) {
|
||||||
id = NoteID{IsInstr: false, Track: track, Note: note, model: m}
|
id = NoteID{IsInstr: false, Track: track, Note: note, model: m}
|
||||||
trySend(m.broker.ToPlayer, any(NoteOnMsg{id}))
|
trySend(m.broker.ToPlayer, any(NoteOnMsg{id}))
|
||||||
@ -560,3 +571,20 @@ func clamp(a, min, max int) int {
|
|||||||
}
|
}
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) applyTrackInput(trackInput TrackInput) {
|
||||||
|
c := Point{m.d.Cursor.Track, m.d.Cursor.SongPos.PatternRow}
|
||||||
|
availableIndices := m.CountNextTracksForCurrentInstrument()
|
||||||
|
for i, note := range trackInput.Notes {
|
||||||
|
m.Notes().SetValue(c, note)
|
||||||
|
if i >= availableIndices {
|
||||||
|
// only use same-instruments-tracks to the right of the c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.X++
|
||||||
|
}
|
||||||
|
if velTrackIndex, useVel := m.trackForMidiVelIn.Unpack(); useVel {
|
||||||
|
c.X = velTrackIndex
|
||||||
|
m.Notes().SetValue(c, trackInput.Velocity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
|
"github.com/vsariola/sointu/tracker/types"
|
||||||
"github.com/vsariola/sointu/vm"
|
"github.com/vsariola/sointu/vm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,6 +24,12 @@ func (NullContext) BPM() (bpm float64, ok bool) {
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (NullContext) MaxPolyphony() types.OptionalInteger {
|
||||||
|
return types.NewEmptyOptionalInteger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (NullContext) SetMaxPolyphony(v types.OptionalInteger) {}
|
||||||
|
|
||||||
func (NullContext) InputDevices(yield func(tracker.MIDIDevice) bool) {}
|
func (NullContext) InputDevices(yield func(tracker.MIDIDevice) bool) {}
|
||||||
|
|
||||||
func (NullContext) HasDeviceOpen() bool { return false }
|
func (NullContext) HasDeviceOpen() bool { return false }
|
||||||
@ -277,7 +284,7 @@ func FuzzModel(f *testing.F) {
|
|||||||
break loop
|
break loop
|
||||||
default:
|
default:
|
||||||
ctx := NullContext{}
|
ctx := NullContext{}
|
||||||
player.Process(buf, ctx, nil)
|
player.Process(buf, ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
81
tracker/optional_int.go
Normal file
81
tracker/optional_int.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import "github.com/vsariola/sointu/tracker/types"
|
||||||
|
|
||||||
|
type (
|
||||||
|
// OptionalInt tries to follow the same convention as e.g. Int{...} or Bool{...}
|
||||||
|
// Do not confuse with types.OptionalInteger, which you might use as a model,
|
||||||
|
// but don't necessarily have to.
|
||||||
|
OptionalInt struct {
|
||||||
|
optionalIntData
|
||||||
|
}
|
||||||
|
|
||||||
|
optionalIntData interface {
|
||||||
|
Unpack() (int, bool)
|
||||||
|
Value() int
|
||||||
|
Range() intRange
|
||||||
|
|
||||||
|
setValue(int)
|
||||||
|
unsetValue()
|
||||||
|
change(kind string) func()
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackMidiVelIn Model
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v OptionalInt) Set(value int, present bool) (ok bool) {
|
||||||
|
if !present {
|
||||||
|
v.unsetValue()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// TODO: can we deduplicate this by referencing Int{...}.Set(value) ?
|
||||||
|
r := v.Range()
|
||||||
|
if v.Equals(value, present) || value < r.Min || value > r.Max {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer v.change("Set")()
|
||||||
|
v.setValue(value)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v OptionalInt) Equals(value int, present bool) bool {
|
||||||
|
oldValue, oldPresent := v.Unpack()
|
||||||
|
return value == oldValue && present == oldPresent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model methods
|
||||||
|
|
||||||
|
func (m *Model) TrackForMidiVelIn() *TrackMidiVelIn { return (*TrackMidiVelIn)(m) }
|
||||||
|
|
||||||
|
// TrackForMidiVelIn - to record Velocity in the track with given number (-1 = off)
|
||||||
|
|
||||||
|
func (m *TrackMidiVelIn) OptionalInt() OptionalInt { return OptionalInt{m} }
|
||||||
|
func (m *TrackMidiVelIn) Range() intRange { return intRange{0, len(m.d.Song.Score.Tracks) - 1} }
|
||||||
|
func (m *TrackMidiVelIn) change(string) func() { return func() {} }
|
||||||
|
|
||||||
|
func (m *TrackMidiVelIn) setValue(val int) {
|
||||||
|
m.trackForMidiVelIn = types.NewOptionalInteger(val, val >= 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TrackMidiVelIn) unsetValue() {
|
||||||
|
m.trackForMidiVelIn = types.NewEmptyOptionalInteger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TrackMidiVelIn) Unpack() (int, bool) {
|
||||||
|
return m.trackForMidiVelIn.Unpack()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TrackMidiVelIn) Value() int {
|
||||||
|
return m.trackForMidiVelIn.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TrackMidiVelIn) IsValid() bool {
|
||||||
|
if m.trackForMidiVelIn.Empty() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return (*Model)(m).CanUseTrackForMidiVelInput(m.trackForMidiVelIn.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TrackMidiVelIn) Equals(value int) bool {
|
||||||
|
return m.trackForMidiVelIn.Equals(value)
|
||||||
|
}
|
@ -3,6 +3,7 @@ package tracker
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
"github.com/vsariola/sointu/vm"
|
"github.com/vsariola/sointu/vm"
|
||||||
@ -24,8 +25,9 @@ type (
|
|||||||
voices [vm.MAX_VOICES]voice
|
voices [vm.MAX_VOICES]voice
|
||||||
loop Loop
|
loop Loop
|
||||||
|
|
||||||
recState recState // is the recording off; are we waiting for a note; or are we recording
|
recState recState // is the recording off; are we waiting for a note; or are we recording
|
||||||
recording Recording // the recorded MIDI events and BPM
|
recording Recording // the recorded MIDI events and BPM
|
||||||
|
trackInput TrackInput // for events that are played when not recording
|
||||||
|
|
||||||
synther sointu.Synther // the synther used to create new synths
|
synther sointu.Synther // the synther used to create new synths
|
||||||
broker *Broker // the broker used to communicate with different parts of the tracker
|
broker *Broker // the broker used to communicate with different parts of the tracker
|
||||||
@ -37,21 +39,32 @@ type (
|
|||||||
NextEvent(frame int) (event MIDINoteEvent, ok bool)
|
NextEvent(frame int) (event MIDINoteEvent, ok bool)
|
||||||
FinishBlock(frame int)
|
FinishBlock(frame int)
|
||||||
BPM() (bpm float64, ok bool)
|
BPM() (bpm float64, ok bool)
|
||||||
|
|
||||||
|
Constraints() PlayerProcessConstraints
|
||||||
}
|
}
|
||||||
|
|
||||||
EventProcessor interface {
|
PlayerProcessConstraints struct {
|
||||||
ProcessMessage(msg interface{})
|
IsConstrained bool
|
||||||
ProcessEvent(event MIDINoteEvent)
|
MaxPolyphony int
|
||||||
|
InstrumentIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
// MIDINoteEvent is a MIDI event triggering or releasing a note. In
|
// MIDINoteEvent is a MIDI event triggering or releasing a note. In
|
||||||
// processing, the Frame is relative to the start of the current buffer. In
|
// processing, the Frame is relative to the start of the current buffer. In
|
||||||
// a Recording, the Frame is relative to the start of the recording.
|
// a Recording, the Frame is relative to the start of the recording.
|
||||||
MIDINoteEvent struct {
|
MIDINoteEvent struct {
|
||||||
Frame int
|
Frame int
|
||||||
On bool
|
On bool
|
||||||
Channel int
|
Channel int
|
||||||
Note byte
|
Note byte
|
||||||
|
Velocity byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackInput is used for the midi-into-track-input, when not recording
|
||||||
|
// For now, there is only one Velocity for all Notes. This might evolve.
|
||||||
|
TrackInput struct {
|
||||||
|
Notes []byte
|
||||||
|
Velocity byte
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -85,8 +98,10 @@ func NewPlayer(broker *Broker, synther sointu.Synther) *Player {
|
|||||||
// model. context tells the player which MIDI events happen during the current
|
// model. context tells the player which MIDI events happen during the current
|
||||||
// buffer. It is used to trigger and release notes during processing. The
|
// buffer. It is used to trigger and release notes during processing. The
|
||||||
// context is also used to get the current BPM from the host.
|
// context is also used to get the current BPM from the host.
|
||||||
func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext, ui EventProcessor) {
|
func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext) {
|
||||||
p.processMessages(context, ui)
|
p.processMessages(context)
|
||||||
|
constraints := context.Constraints()
|
||||||
|
_ = constraints
|
||||||
|
|
||||||
frame := 0
|
frame := 0
|
||||||
midi, midiOk := context.NextEvent(frame)
|
midi, midiOk := context.NextEvent(frame)
|
||||||
@ -106,15 +121,7 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
|||||||
midiTotalFrame.Frame = p.recording.TotalFrames - len(buffer)
|
midiTotalFrame.Frame = p.recording.TotalFrames - len(buffer)
|
||||||
p.recording.Events = append(p.recording.Events, midiTotalFrame)
|
p.recording.Events = append(p.recording.Events, midiTotalFrame)
|
||||||
}
|
}
|
||||||
if midi.On {
|
p.handleMidiInput(midi, constraints)
|
||||||
p.triggerInstrument(midi.Channel, midi.Note)
|
|
||||||
} else {
|
|
||||||
p.releaseInstrument(midi.Channel, midi.Note)
|
|
||||||
}
|
|
||||||
if ui != nil {
|
|
||||||
ui.ProcessEvent(midi)
|
|
||||||
}
|
|
||||||
|
|
||||||
midi, midiOk = context.NextEvent(frame)
|
midi, midiOk = context.NextEvent(frame)
|
||||||
}
|
}
|
||||||
framesUntilMidi := len(buffer)
|
framesUntilMidi := len(buffer)
|
||||||
@ -184,6 +191,50 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
|||||||
p.SendAlert("PlayerCrash", fmt.Sprintf("synth did not fill the audio buffer even with %d render calls", numRenderTries), Error)
|
p.SendAlert("PlayerCrash", fmt.Sprintf("synth did not fill the audio buffer even with %d render calls", numRenderTries), Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Player) handleMidiInput(midi MIDINoteEvent, constraints PlayerProcessConstraints) {
|
||||||
|
instrIndex := midi.Channel
|
||||||
|
if constraints.IsConstrained {
|
||||||
|
instrIndex = constraints.InstrumentIndex
|
||||||
|
}
|
||||||
|
if midi.On {
|
||||||
|
p.triggerInstrument(instrIndex, midi.Note)
|
||||||
|
if p.addTrackInput(midi, constraints) {
|
||||||
|
trySend(p.broker.ToModel, MsgToModel{Data: p.trackInput})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.releaseInstrument(instrIndex, midi.Note)
|
||||||
|
p.removeTrackInput(midi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) addTrackInput(midi MIDINoteEvent, c PlayerProcessConstraints) (changed bool) {
|
||||||
|
if c.IsConstrained {
|
||||||
|
if len(p.trackInput.Notes) == c.MaxPolyphony {
|
||||||
|
return false
|
||||||
|
} else if len(p.trackInput.Notes) > c.MaxPolyphony {
|
||||||
|
p.trackInput.Notes = p.trackInput.Notes[:c.MaxPolyphony]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains(p.trackInput.Notes, midi.Note) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
p.trackInput.Notes = append(p.trackInput.Notes, midi.Note)
|
||||||
|
p.trackInput.Velocity = midi.Velocity
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) removeTrackInput(midi MIDINoteEvent) {
|
||||||
|
for i, n := range p.trackInput.Notes {
|
||||||
|
if n == midi.Note {
|
||||||
|
p.trackInput.Notes = append(
|
||||||
|
p.trackInput.Notes[:i],
|
||||||
|
p.trackInput.Notes[i+1:]...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Player) advanceRow() {
|
func (p *Player) advanceRow() {
|
||||||
if p.song.Score.Length == 0 || p.song.Score.RowsPerPattern == 0 {
|
if p.song.Score.Length == 0 || p.song.Score.RowsPerPattern == 0 {
|
||||||
return
|
return
|
||||||
@ -220,7 +271,7 @@ func (p *Player) advanceRow() {
|
|||||||
p.rowtime = 0
|
p.rowtime = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) processMessages(context PlayerProcessContext, uiProcessor EventProcessor) {
|
func (p *Player) processMessages(context PlayerProcessContext) {
|
||||||
loop:
|
loop:
|
||||||
for { // process new message
|
for { // process new message
|
||||||
select {
|
select {
|
||||||
@ -295,9 +346,6 @@ loop:
|
|||||||
default:
|
default:
|
||||||
// ignore unknown messages
|
// ignore unknown messages
|
||||||
}
|
}
|
||||||
if uiProcessor != nil {
|
|
||||||
uiProcessor.ProcessMessage(msg)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
@ -346,7 +394,13 @@ func (p *Player) compileOrUpdateSynth() {
|
|||||||
|
|
||||||
// all sendTargets from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock
|
// all sendTargets from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock
|
||||||
func (p *Player) send(message interface{}) {
|
func (p *Player) send(message interface{}) {
|
||||||
trySend(p.broker.ToModel, MsgToModel{HasPanicPosLevels: true, Panic: p.synth == nil, SongPosition: p.songPos, VoiceLevels: p.voiceLevels, Data: message})
|
trySend(p.broker.ToModel, MsgToModel{
|
||||||
|
HasPanicPosLevels: true,
|
||||||
|
Panic: p.synth == nil,
|
||||||
|
SongPosition: p.songPos,
|
||||||
|
VoiceLevels: p.voiceLevels,
|
||||||
|
Data: message,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) triggerInstrument(instrument int, note byte) {
|
func (p *Player) triggerInstrument(instrument int, note byte) {
|
||||||
|
@ -173,7 +173,7 @@ func (m *Order) Cursor2() Point {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Order) SetCursor(p Point) {
|
func (m *Order) SetCursor(p Point) {
|
||||||
m.d.Cursor.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0)
|
(*Model)(m).ChangeTrack(p.X)
|
||||||
y := max(min(p.Y, m.d.Song.Score.Length-1), 0)
|
y := max(min(p.Y, m.d.Song.Score.Length-1), 0)
|
||||||
if y != m.d.Cursor.OrderRow {
|
if y != m.d.Cursor.OrderRow {
|
||||||
m.follow = false
|
m.follow = false
|
||||||
@ -385,7 +385,7 @@ func (m *Notes) Cursor2() Point {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *Notes) SetCursor(p Point) {
|
func (v *Notes) SetCursor(p Point) {
|
||||||
v.d.Cursor.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0)
|
(*Model)(v).ChangeTrack(p.X)
|
||||||
newPos := v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y})
|
newPos := v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y})
|
||||||
if newPos != v.d.Cursor.SongPos {
|
if newPos != v.d.Cursor.SongPos {
|
||||||
v.follow = false
|
v.follow = false
|
||||||
|
47
tracker/types/optional.go
Normal file
47
tracker/types/optional.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type (
|
||||||
|
// OptionalInteger is the simple struct, not to be confused with tracker.OptionalInt.
|
||||||
|
// It implements the tracker.optionalIntData interface, without needing to know so.
|
||||||
|
OptionalInteger struct {
|
||||||
|
value int
|
||||||
|
exists bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewOptionalInteger(value int, exists bool) OptionalInteger {
|
||||||
|
return OptionalInteger{value, exists}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOptionalIntegerOf(value int) OptionalInteger {
|
||||||
|
return OptionalInteger{
|
||||||
|
value: value,
|
||||||
|
exists: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmptyOptionalInteger() OptionalInteger {
|
||||||
|
// could also just use OptionalInteger{}
|
||||||
|
return OptionalInteger{
|
||||||
|
exists: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i OptionalInteger) Unpack() (int, bool) {
|
||||||
|
return i.value, i.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i OptionalInteger) Value() int {
|
||||||
|
if !i.exists {
|
||||||
|
panic("Access value of empty OptionalInteger")
|
||||||
|
}
|
||||||
|
return i.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i OptionalInteger) Empty() bool {
|
||||||
|
return !i.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i OptionalInteger) Equals(value int) bool {
|
||||||
|
return i.exists && i.value == value
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user