mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-18 21:14:31 -04:00
feat(tracker): add support for a MIDI controller to the standalone tracker
Closes #132.
This commit is contained in:
committed by
5684185+vsariola@users.noreply.github.com
parent
9779beee99
commit
577265b250
@ -1,6 +1,7 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
@ -444,6 +445,14 @@ func (m *Model) Cancel() Action { return Allow(func() { m.dialog = NoDialog
|
||||
func (m *Model) Export() Action { return Allow(func() { m.dialog = Export }) }
|
||||
func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFloatExplorer }) }
|
||||
func (m *Model) ExportInt16() Action { return Allow(func() { m.dialog = ExportInt16Explorer }) }
|
||||
func (m *Model) SelectMidiInput(item MIDIDevicer) Action {
|
||||
return Allow(func() {
|
||||
if !m.MIDI.OpenInputDevice(item) {
|
||||
message := fmt.Sprintf("Could not open MIDI device %s\n", item)
|
||||
m.Alerts().Add(message, Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) completeAction(checkSave bool) {
|
||||
if checkSave && m.d.ChangedSinceSave {
|
||||
|
@ -48,12 +48,15 @@ type SongPanel struct {
|
||||
followOnHint, followOffHint string
|
||||
panicHint string
|
||||
loopOffHint, loopOnHint string
|
||||
|
||||
// Midi menu items
|
||||
midiMenuItems []MenuItem
|
||||
}
|
||||
|
||||
func NewSongPanel(model *tracker.Model) *SongPanel {
|
||||
ret := &SongPanel{
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
Menus: make([]Menu, 2),
|
||||
MenuBar: make([]widget.Clickable, 3),
|
||||
Menus: make([]Menu, 3),
|
||||
BPM: NewNumberInput(model.BPM().Int()),
|
||||
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),
|
||||
RowsPerBeat: NewNumberInput(model.RowsPerBeat().Int()),
|
||||
@ -81,6 +84,13 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
|
||||
{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: keyActionMap["Redo"], Doer: model.Redo()},
|
||||
{IconBytes: icons.ImageCrop, Text: "Remove unused data", ShortcutText: keyActionMap["RemoveUnused"], Doer: model.RemoveUnused()},
|
||||
}
|
||||
for input := range model.MIDI.ListInputDevices() {
|
||||
ret.midiMenuItems = append(ret.midiMenuItems, MenuItem{
|
||||
IconBytes: icons.ImageControlPoint,
|
||||
Text: input.String(),
|
||||
Doer: model.SelectMidiInput(input),
|
||||
})
|
||||
}
|
||||
ret.rewindHint = makeHint("Rewind", "\n(%s)", "PlaySongStartUnfollow")
|
||||
ret.playHint = makeHint("Play", " (%s)", "PlayCurrentPosUnfollow")
|
||||
ret.stopHint = makeHint("Stop", " (%s)", "StopPlaying")
|
||||
@ -91,6 +101,7 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
|
||||
ret.followOffHint = makeHint("Follow off", " (%s)", "FollowToggle")
|
||||
ret.loopOffHint = makeHint("Loop off", " (%s)", "LoopToggle")
|
||||
ret.loopOnHint = makeHint("Loop on", " (%s)", "LoopToggle")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
@ -112,6 +123,7 @@ func (t *SongPanel) layoutMenuBar(gtx C, tr *Tracker) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.End}.Layout(gtx,
|
||||
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...)),
|
||||
)
|
||||
}
|
||||
|
||||
|
115
tracker/gomidi/midi.go
Normal file
115
tracker/gomidi/midi.go
Normal file
@ -0,0 +1,115 @@
|
||||
package gomidi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gitlab.com/gomidi/midi/v2"
|
||||
"gitlab.com/gomidi/midi/v2/drivers"
|
||||
"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
||||
)
|
||||
|
||||
type (
|
||||
MIDIContext struct {
|
||||
driver *rtmididrv.Driver
|
||||
inputAvailable bool
|
||||
driverAvailable bool
|
||||
currentIn MIDIDevicer
|
||||
events chan midi.Message
|
||||
}
|
||||
MIDIDevicer drivers.In
|
||||
)
|
||||
|
||||
func (m *MIDIContext) ListInputDevices() <-chan tracker.MIDIDevicer {
|
||||
|
||||
ins, err := m.driver.Ins()
|
||||
channel := make(chan tracker.MIDIDevicer, len(ins))
|
||||
if err != nil {
|
||||
m.driver.Close()
|
||||
m.driverAvailable = false
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
for i := 0; i < len(ins); i++ {
|
||||
channel <- ins[i].(MIDIDevicer)
|
||||
}
|
||||
close(channel)
|
||||
}()
|
||||
return channel
|
||||
}
|
||||
|
||||
// Open the driver.
|
||||
func CreateContext() *MIDIContext {
|
||||
m := MIDIContext{}
|
||||
var err error
|
||||
m.driver, err = rtmididrv.New()
|
||||
m.driverAvailable = err == nil
|
||||
if m.driverAvailable {
|
||||
m.events = make(chan midi.Message)
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
||||
// Open an input device while closing the currently open if necessary.
|
||||
func (m *MIDIContext) OpenInputDevice(in tracker.MIDIDevicer) bool {
|
||||
fmt.Printf("Opening midi device %s\n.", in)
|
||||
if m.driverAvailable {
|
||||
if m.currentIn == in {
|
||||
return false
|
||||
}
|
||||
if m.inputAvailable && m.currentIn.IsOpen() {
|
||||
m.currentIn.Close()
|
||||
}
|
||||
m.currentIn = in.(MIDIDevicer)
|
||||
m.currentIn.Open()
|
||||
_, err := midi.ListenTo(m.currentIn, m.HandleMessage)
|
||||
if err != nil {
|
||||
m.inputAvailable = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MIDIContext) HandleMessage(msg midi.Message, timestampms int32) {
|
||||
go func() {
|
||||
m.events <- msg
|
||||
time.Sleep(time.Nanosecond)
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *MIDIContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
|
||||
select {
|
||||
case msg := <-c.events:
|
||||
{
|
||||
var channel uint8
|
||||
var velocity uint8
|
||||
var key uint8
|
||||
var controller uint8
|
||||
var value uint8
|
||||
if msg.GetNoteOn(&channel, &key, &velocity) {
|
||||
return tracker.MIDINoteEvent{Frame: 0, On: true, Channel: int(channel), Note: key}, true
|
||||
} else if msg.GetNoteOff(&channel, &key, &velocity) {
|
||||
return tracker.MIDINoteEvent{Frame: 0, On: false, Channel: int(channel), Note: key}, true
|
||||
} else if msg.GetControlChange(&channel, &controller, &value) {
|
||||
fmt.Printf("CC @ Channel: %d, Controller: %d, Value: %d\n", channel, controller, value)
|
||||
} else {
|
||||
fmt.Printf("Unhandled MIDI message: %s\n", msg)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Note (@LeStahL): This empty select case is needed to make the implementation non-blocking.
|
||||
}
|
||||
return tracker.MIDINoteEvent{}, false
|
||||
}
|
||||
|
||||
func (c *MIDIContext) BPM() (bpm float64, ok bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (c *MIDIContext) DestroyContext() {
|
||||
close(c.events)
|
||||
c.currentIn.Close()
|
||||
c.driver.Close()
|
||||
}
|
@ -75,6 +75,8 @@ type (
|
||||
|
||||
PlayerMessages chan PlayerMsg
|
||||
modelMessages chan<- interface{}
|
||||
|
||||
MIDI MIDIContexter
|
||||
}
|
||||
|
||||
// Cursor identifies a row and a track in a song score.
|
||||
@ -120,6 +122,18 @@ type (
|
||||
ChangeType int
|
||||
|
||||
Dialog int
|
||||
|
||||
MIDIContexter interface {
|
||||
ListInputDevices() <-chan MIDIDevicer
|
||||
OpenInputDevice(item MIDIDevicer) bool
|
||||
DestroyContext()
|
||||
BPM() (bpm float64, ok bool)
|
||||
NextEvent() (event MIDINoteEvent, ok bool)
|
||||
}
|
||||
|
||||
MIDIDevicer interface {
|
||||
String() string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
|
Reference in New Issue
Block a user