mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
The linker flags -static -static-libgcc -static-libstdc++ tell mingw to link statically; otherwise gcc_s_seh-1, stdc++-6 and winpthread-1 are needed. Fixes #188.
194 lines
4.9 KiB
Go
194 lines
4.9 KiB
Go
package gomidi
|
|
|
|
// These cgo linker flags tell mingw to link gcc_s_seh-1, stdc++-6 and
|
|
// winpthread-1 statically; otherwise they are needed as DLLs
|
|
|
|
// #cgo windows LDFLAGS: -static -static-libgcc -static-libstdc++
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/vsariola/sointu/tracker"
|
|
"gitlab.com/gomidi/midi/v2"
|
|
"gitlab.com/gomidi/midi/v2/drivers"
|
|
"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
|
)
|
|
|
|
type (
|
|
RTMIDIContext struct {
|
|
driver *rtmididrv.Driver
|
|
currentIn drivers.In
|
|
events chan timestampedMsg
|
|
eventsBuf []timestampedMsg
|
|
eventIndex int
|
|
startFrame int
|
|
startFrameSet bool
|
|
}
|
|
|
|
RTMIDIDevice struct {
|
|
context *RTMIDIContext
|
|
in drivers.In
|
|
}
|
|
|
|
timestampedMsg struct {
|
|
frame int
|
|
msg midi.Message
|
|
}
|
|
)
|
|
|
|
func (m *RTMIDIContext) InputDevices(yield func(tracker.MIDIDevice) bool) {
|
|
if m.driver == nil {
|
|
return
|
|
}
|
|
ins, err := m.driver.Ins()
|
|
if err != nil {
|
|
return
|
|
}
|
|
for i := 0; i < len(ins); i++ {
|
|
device := RTMIDIDevice{context: m, in: ins[i]}
|
|
if !yield(device) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Open the driver.
|
|
func NewContext() *RTMIDIContext {
|
|
m := RTMIDIContext{events: make(chan timestampedMsg, 1024)}
|
|
// there's not much we can do if this fails, so just use m.driver = nil to
|
|
// indicate no driver available
|
|
m.driver, _ = rtmididrv.New()
|
|
return &m
|
|
}
|
|
|
|
// Open an input device while closing the currently open if necessary.
|
|
func (m RTMIDIDevice) Open() error {
|
|
if m.context.currentIn == m.in {
|
|
return nil
|
|
}
|
|
if m.context.driver == nil {
|
|
return errors.New("no driver available")
|
|
}
|
|
if m.context.HasDeviceOpen() {
|
|
m.context.currentIn.Close()
|
|
}
|
|
m.context.currentIn = m.in
|
|
err := m.in.Open()
|
|
if err != nil {
|
|
m.context.currentIn = nil
|
|
return fmt.Errorf("opening MIDI input failed: %W", err)
|
|
}
|
|
_, err = midi.ListenTo(m.in, m.context.HandleMessage)
|
|
if err != nil {
|
|
m.in.Close()
|
|
m.context.currentIn = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d RTMIDIDevice) String() string {
|
|
return d.in.String()
|
|
}
|
|
|
|
func (m *RTMIDIContext) HandleMessage(msg midi.Message, timestampms int32) {
|
|
select {
|
|
case m.events <- timestampedMsg{frame: int(int64(timestampms) * 44100 / 1000), msg: msg}: // if the channel is full, just drop the message
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (c *RTMIDIContext) NextEvent(frame int) (event tracker.MIDINoteEvent, ok bool) {
|
|
F:
|
|
for {
|
|
select {
|
|
case msg := <-c.events:
|
|
c.eventsBuf = append(c.eventsBuf, msg)
|
|
if !c.startFrameSet {
|
|
c.startFrame = msg.frame
|
|
c.startFrameSet = true
|
|
}
|
|
default:
|
|
break F
|
|
}
|
|
}
|
|
if c.eventIndex > 0 { // an event was consumed, check how badly we need to adjust the timing
|
|
delta := frame + c.startFrame - c.eventsBuf[c.eventIndex-1].frame
|
|
// delta should never be a negative number, because the renderer does
|
|
// not consume an event until current frame is past the frame of the
|
|
// event. However, if it's been a while since we consumed event, delta
|
|
// may by *positive* i.e. we consume the event too late. So adjust the
|
|
// internal clock in that case.
|
|
c.startFrame -= delta / 5 // adjust the start frame towards the consumed event
|
|
}
|
|
for c.eventIndex < len(c.eventsBuf) {
|
|
var channel uint8
|
|
var velocity uint8
|
|
var key uint8
|
|
m := c.eventsBuf[c.eventIndex]
|
|
f := m.frame - c.startFrame
|
|
c.eventIndex++
|
|
if m.msg.GetNoteOn(&channel, &key, &velocity) {
|
|
return tracker.MIDINoteEvent{Frame: f, On: true, Channel: int(channel), Note: key}, true
|
|
} else if m.msg.GetNoteOff(&channel, &key, &velocity) {
|
|
return tracker.MIDINoteEvent{Frame: f, On: false, Channel: int(channel), Note: key}, true
|
|
}
|
|
}
|
|
c.eventIndex = len(c.eventsBuf) + 1
|
|
return tracker.MIDINoteEvent{}, false
|
|
}
|
|
|
|
func (c *RTMIDIContext) FinishBlock(frame int) {
|
|
c.startFrame += frame
|
|
if c.eventIndex > 0 {
|
|
copy(c.eventsBuf, c.eventsBuf[c.eventIndex-1:])
|
|
c.eventsBuf = c.eventsBuf[:len(c.eventsBuf)-c.eventIndex+1]
|
|
if len(c.eventsBuf) > 0 {
|
|
// Events were not consumed this round; adjust the start frame
|
|
// towards the future events. What this does is that it tries to
|
|
// render the events at the same time as they were received here
|
|
// delta will be always a negative number
|
|
delta := c.startFrame - c.eventsBuf[0].frame
|
|
c.startFrame -= delta / 5
|
|
}
|
|
}
|
|
c.eventIndex = 0
|
|
}
|
|
|
|
func (c *RTMIDIContext) BPM() (bpm float64, ok bool) {
|
|
return 0, false
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|