Skip to content

Commit

Permalink
feat: midi note input for the tracker
Browse files Browse the repository at this point in the history
  • Loading branch information
qm210 authored and vsariola committed Oct 22, 2024
1 parent 216cde2 commit 8dfadac
Show file tree
Hide file tree
Showing 17 changed files with 298 additions and 62 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ build/
# Project specific
old/

# VS Code
# IDEs
.vscode/
.idea/

# project specific
# this is autogenerated from bridge.go.in
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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])
- 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
26 changes: 11 additions & 15 deletions cmd/sointu-track/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,17 @@ import (
"runtime/pprof"

"gioui.org/app"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/cmd"
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/tracker/gioui"
"github.com/vsariola/sointu/tracker/gomidi"
)

type PlayerAudioSource struct {
*tracker.Player
playerProcessContext tracker.PlayerProcessContext
}

func (p *PlayerAudioSource) ReadAudio(buf sointu.AudioBuffer) error {
p.Player.Process(buf, p.playerProcessContext)
return nil
}

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
var defaultMidiInput = flag.String("midi-input", "", "connect MIDI input to matching device name")
var firstMidiInput = flag.Bool("first-midi-input", false, "connect MIDI input to first device found")

func main() {
flag.Parse()
Expand All @@ -54,19 +45,24 @@ func main() {
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery")
}
midiContext := gomidi.NewContext()
defer midiContext.Close()
midiContext.TryToOpenBy(*defaultMidiInput, *firstMidiInput)
model, player := tracker.NewModelPlayer(cmd.MainSynther, midiContext, recoveryFile)
defer model.MIDI.Close()

if a := flag.Args(); len(a) > 0 {
f, err := os.Open(a[0])
if err == nil {
model.ReadSong(f)
}
f.Close()
}
tracker := gioui.NewTracker(model)
audioCloser := audioContext.Play(&PlayerAudioSource{player, midiContext})

trackerUi := gioui.NewTracker(model)
processor := tracker.NewProcessor(player, midiContext, trackerUi)
audioCloser := audioContext.Play(processor)

go func() {
tracker.Main()
trackerUi.Main()
audioCloser.Close()
if *cpuprofile != "" {
pprof.StopCPUProfile()
Expand Down
4 changes: 3 additions & 1 deletion cmd/sointu-vsti/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func (m NullMIDIContext) InputDevices(yield func(tracker.MIDIDevice) bool) {}

func (m NullMIDIContext) Close() {}

func (m NullMIDIContext) HasDeviceOpen() bool { return false }

func (c *VSTIProcessContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
for c.eventIndex < len(c.events) {
ev := c.events[c.eventIndex]
Expand Down Expand Up @@ -101,7 +103,7 @@ func init() {
buf = append(buf, make(sointu.AudioBuffer, out.Frames-len(buf))...)
}
buf = buf[:out.Frames]
player.Process(buf, &context)
player.Process(buf, &context, nil)
for i := 0; i < out.Frames; i++ {
left[i], right[i] = buf[i][0], buf[i][1]
}
Expand Down
30 changes: 30 additions & 0 deletions song.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sointu

import (
"errors"
"iter"
)

type (
Expand Down Expand Up @@ -331,3 +332,32 @@ func TotalVoices[T any, S ~[]T, P NumVoicerPointer[T]](slice S) (ret int) {
}
return
}

func (s *Song) InstrumentForTrack(trackIndex int) (int, bool) {
voiceIndex := s.Score.FirstVoiceForTrack(trackIndex)
instrument, err := s.Patch.InstrumentForVoice(voiceIndex)
return instrument, err == nil
}

func (s *Song) AllTracksWithSameInstrument(trackIndex int) iter.Seq[int] {
return func(yield func(int) bool) {

currentInstrument, currentExists := s.InstrumentForTrack(trackIndex)
if !currentExists {
return
}

for i := 0; i < len(s.Score.Tracks); i++ {
instrument, exists := s.InstrumentForTrack(i)
if !exists {
return
}
if instrument != currentInstrument {
continue
}
if !yield(i) {
return
}
}
}
}
8 changes: 4 additions & 4 deletions tracker/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,12 +475,12 @@ func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFl
func (m *Model) ExportInt16() Action { return Allow(func() { m.dialog = ExportInt16Explorer }) }
func (m *Model) SelectMidiInput(item MIDIDevice) Action {
return Allow(func() {
if err := item.Open(); err != nil {
message := fmt.Sprintf("Could not open MIDI device: %s", item)
m.Alerts().Add(message, Error)
} else {
if err := item.Open(); err == nil {
message := fmt.Sprintf("Opened MIDI device: %s", item)
m.Alerts().Add(message, Info)
} else {
message := fmt.Sprintf("Could not open MIDI device: %s", item)
m.Alerts().Add(message, Error)
}
})
}
Expand Down
9 changes: 9 additions & 0 deletions tracker/bool.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type (
Playing Model
InstrEnlarged Model
Effect Model
TrackMidiIn Model
CommentExpanded Model
Follow Model
UnitSearching Model
Expand Down Expand Up @@ -44,6 +45,7 @@ func (m *Model) IsRecording() *IsRecording { return (*IsRecording)(m) }
func (m *Model) Playing() *Playing { return (*Playing)(m) }
func (m *Model) InstrEnlarged() *InstrEnlarged { return (*InstrEnlarged)(m) }
func (m *Model) Effect() *Effect { return (*Effect)(m) }
func (m *Model) TrackMidiIn() *TrackMidiIn { return (*TrackMidiIn)(m) }
func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) }
func (m *Model) Follow() *Follow { return (*Follow)(m) }
func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
Expand Down Expand Up @@ -110,6 +112,13 @@ 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)

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() }

// Effect methods

func (m *Effect) Bool() Bool { return Bool{m} }
Expand Down
95 changes: 72 additions & 23 deletions tracker/gioui/note_editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gioui
import (
"fmt"
"image"
"image/color"
"strconv"
"strings"

Expand Down Expand Up @@ -62,6 +63,7 @@ type NoteEditor struct {
NoteOffBtn *ActionClickable
EffectBtn *BoolClickable
UniqueBtn *BoolClickable
TrackMidiInBtn *BoolClickable

scrollTable *ScrollTable
eventFilters []event.Filter
Expand All @@ -85,6 +87,7 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
NoteOffBtn: NewActionClickable(model.EditNoteOff()),
EffectBtn: NewBoolClickable(model.Effect().Bool()),
UniqueBtn: NewBoolClickable(model.UniquePatterns().Bool()),
TrackMidiInBtn: NewBoolClickable(model.TrackMidiIn().Bool()),
scrollTable: NewScrollTable(
model.Notes().Table(),
model.Tracks().List(),
Expand Down Expand Up @@ -162,6 +165,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
}
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")
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),
Expand All @@ -175,6 +179,8 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
layout.Rigid(voiceUpDown),
layout.Rigid(splitTrackBtnStyle.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(midiInBtnStyle.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(deleteTrackBtnStyle.Layout),
layout.Rigid(newTrackBtnStyle.Layout))
})
Expand Down Expand Up @@ -217,7 +223,13 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
h := gtx.Dp(unit.Dp(trackColTitleHeight))
title := ((*tracker.Order)(t.Model)).Title(i)
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
LabelStyle{
Alignment: layout.N,
Text: title,
FontSize: unit.Sp(12),
Color: mediumEmphasisTextColor,
Shaper: t.Theme.Shaper,
}.Layout(gtx)
return D{Size: image.Pt(pxWidth, h)}
}

Expand Down Expand Up @@ -253,8 +265,10 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
return D{Size: image.Pt(w, pxHeight)}
}

drawSelection := te.scrollTable.Table.Cursor() != te.scrollTable.Table.Cursor2()
cursor := te.scrollTable.Table.Cursor()
drawSelection := cursor != te.scrollTable.Table.Cursor2()
selection := te.scrollTable.Table.Range()
hasTrackMidiIn := te.TrackMidiInBtn.Bool.Value()

cell := func(gtx C, x, y int) D {
// draw the background, to indicate selection
Expand All @@ -268,21 +282,25 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
}
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
// draw the cursor
if point == te.scrollTable.Table.Cursor() {
cw := gtx.Constraints.Min.X
cx := 0
if t.Model.Notes().Effect(x) {
cw /= 2
if t.Model.Notes().LowNibble() {
cx += cw
}
}
if point == cursor {
c := inactiveSelectionColor
if te.scrollTable.Focused() {
c = cursorColor
}
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(cx, 0), Max: image.Pt(cx+cw, gtx.Constraints.Min.Y)}.Op())
if hasTrackMidiIn {
c = cursorForTrackMidiInColor
}
te.paintColumnCell(gtx, x, t, c)
}
// draw the corresponding "fake cursors" for instrument-track-groups (for polyphony)
if hasTrackMidiIn {
for trackIndex := range ((*tracker.Order)(t.Model)).TrackIndicesForCurrentInstrument() {
if x == trackIndex && y == cursor.Y {
te.paintColumnCell(gtx, x, t, cursorNeighborForTrackMidiInColor)
}
}
}

// draw the pattern marker
rpp := max(t.RowsPerPattern().Value(), 1)
pat := y / rpp
Expand Down Expand Up @@ -317,6 +335,18 @@ 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) {
cw := gtx.Constraints.Min.X
cx := 0
if t.Model.Notes().Effect(x) {
cw /= 2
if t.Model.Notes().LowNibble() {
cx += cw
}
}
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(cx, 0), Max: image.Pt(cx+cw, gtx.Constraints.Min.Y)}.Op())
}

func mod(x, d int) int {
x = x % d
if x >= 0 {
Expand All @@ -338,7 +368,7 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
if nibbleValue, err := strconv.ParseInt(string(e.Name), 16, 8); err == nil {
t.Model.Notes().FillNibble(byte(nibbleValue), t.Model.Notes().LowNibble())
n = t.Model.Notes().Value(te.scrollTable.Table.Cursor())
goto validNote
te.finishNoteInsert(t, n, e.Name)
}
} else {
action, ok := keyBindingMap[e]
Expand All @@ -347,11 +377,7 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
}
if action == "NoteOff" {
t.Model.Notes().Table().Fill(0)
if step := t.Model.Step().Value(); step > 0 {
te.scrollTable.Table.MoveCursor(0, step)
te.scrollTable.Table.SetCursor2(te.scrollTable.Table.Cursor())
}
te.scrollTable.EnsureCursorVisible()
te.finishNoteInsert(t, 0, "")
return
}
if action[:4] == "Note" {
Expand All @@ -361,20 +387,43 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
}
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val-12)
t.Model.Notes().Table().Fill(int(n))
goto validNote
te.finishNoteInsert(t, n, e.Name)
}
}
return
validNote:
}

func (te *NoteEditor) finishNoteInsert(t *Tracker, note byte, keyName key.Name) {
if step := t.Model.Step().Value(); step > 0 {
te.scrollTable.Table.MoveCursor(0, step)
te.scrollTable.Table.SetCursor2(te.scrollTable.Table.Cursor())
}
te.scrollTable.EnsureCursorVisible()
if _, ok := t.KeyPlaying[e.Name]; !ok {

if keyName == "" {
return
}
if _, ok := t.KeyPlaying[keyName]; !ok {
trk := te.scrollTable.Table.Cursor().X
t.KeyPlaying[e.Name] = t.TrackNoteOn(trk, n)
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 := (*tracker.Order)(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())
}

/*
Expand Down
12 changes: 9 additions & 3 deletions tracker/gioui/songpanel.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,17 @@ func (t *SongPanel) layoutMenuBar(gtx C, tr *Tracker) D {
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))

return layout.Flex{Axis: layout.Horizontal, Alignment: layout.End}.Layout(gtx,
menuLayouts := []layout.FlexChild{
layout.Rigid(tr.layoutMenu(gtx, "File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200), t.fileMenuItems...)),
layout.Rigid(tr.layoutMenu(gtx, "Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200), t.editMenuItems...)),
layout.Rigid(tr.layoutMenu(gtx, "MIDI", &t.MenuBar[2], &t.Menus[2], unit.Dp(200), t.midiMenuItems...)),
)
}
if len(t.midiMenuItems) > 0 {
menuLayouts = append(
menuLayouts,
layout.Rigid(tr.layoutMenu(gtx, "MIDI", &t.MenuBar[2], &t.Menus[2], unit.Dp(200), t.midiMenuItems...)),
)
}
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.End}.Layout(gtx, menuLayouts...)
}

func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
Expand Down
Loading

0 comments on commit 8dfadac

Please sign in to comment.