199 lines
4.7 KiB
Go
199 lines
4.7 KiB
Go
// git.auengun.net/GregoryDosh/automidically
|
|
// Copyright (C) 2020 GregoryDosh
|
|
|
|
package configurator
|
|
|
|
import (
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.auengun.net/GregoryDosh/automidically/internal/midi"
|
|
"git.auengun.net/GregoryDosh/automidically/internal/mixer"
|
|
"git.auengun.net/GregoryDosh/automidically/internal/pulseaudio"
|
|
"git.auengun.net/GregoryDosh/automidically/internal/shell"
|
|
"github.com/bep/debounce"
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/sirupsen/logrus"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var log = logrus.WithField("module", "configurator")
|
|
|
|
type MappingOptions struct {
|
|
Mixer []mixer.Mapping `yaml:"mixer,omitempty"`
|
|
Shell []shell.Mapping `yaml:"shell,omitempty"`
|
|
}
|
|
|
|
type Configurator struct {
|
|
filename string
|
|
EchoMIDIEvents bool `yaml:"echoMIDIEvents"`
|
|
Mapping MappingOptions `yaml:"mapping,omitempty"`
|
|
MIDIDevice *midi.Device
|
|
MIDIDeviceName string `yaml:"midiDevicename"`
|
|
PulseAudio *pulseaudio.PulseAudio
|
|
reloadConfig chan bool
|
|
sync.Mutex
|
|
}
|
|
|
|
func (c *Configurator) updateConfigFromDiskLoop() {
|
|
log.Trace("Enter updateConfigFromDiskLoop")
|
|
defer log.Trace("Exit updateConfigFromDiskLoop")
|
|
|
|
d := debounce.New(time.Second * 1)
|
|
|
|
// Filewatch to reload config when the source on the disk changes.
|
|
fileWatcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer fileWatcher.Close()
|
|
if err := fileWatcher.Add(c.filename); err != nil {
|
|
log.Fatalf("%s %s", c.filename, err)
|
|
}
|
|
|
|
// Filewatcher events, and manual refresh loop
|
|
for {
|
|
select {
|
|
case <-c.reloadConfig:
|
|
d(c.readConfigFromDiskAndInit)
|
|
case event, ok := <-fileWatcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
|
c.reloadConfig <- true
|
|
}
|
|
case err, ok := <-fileWatcher.Errors:
|
|
if !ok {
|
|
log.Error(err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Configurator) readConfigFromDiskAndInit() {
|
|
log.Trace("Enter readConfigFromDiskAndInit")
|
|
defer log.Trace("Exit readConfigFromDiskAndInit")
|
|
log.Infof("reading %s from disk", c.filename)
|
|
|
|
f, err := os.ReadFile(c.filename)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return
|
|
}
|
|
|
|
// Using an anonymous struct so we don't overwrite existing data
|
|
// without locking and so that we don't lock or cleanup unnessarily
|
|
// if it's not needed since we could have a bad config.
|
|
newMapping := struct {
|
|
Mapping MappingOptions `yaml:"mapping"`
|
|
MIDIDeviceName string `yaml:"midiDevicename"`
|
|
EchoMIDIEvents bool `yaml:"echoMIDIEvents"`
|
|
}{}
|
|
if err := yaml.Unmarshal(f, &newMapping); err != nil {
|
|
log.Errorf("unable to parse new config: %s", err)
|
|
return
|
|
}
|
|
|
|
// Lock the configuration, do cleanup on the soon to be replaced configs
|
|
// then replace the old with the new and call any initialization routines.
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
|
|
// Midi Device Cleanup & Initialiation
|
|
if !strings.EqualFold(newMapping.MIDIDeviceName, c.MIDIDeviceName) {
|
|
if c.MIDIDeviceName != "" {
|
|
log.Trace("MIDI device name changed")
|
|
}
|
|
if c.MIDIDevice != nil {
|
|
if err := c.MIDIDevice.Cleanup(); err != nil {
|
|
log.Error(err)
|
|
}
|
|
}
|
|
c.MIDIDeviceName = newMapping.MIDIDeviceName
|
|
c.MIDIDevice = midi.New(c.MIDIDeviceName)
|
|
}
|
|
|
|
mappingChanged := false
|
|
|
|
// Mixer
|
|
for _, mapping := range newMapping.Mapping.Mixer {
|
|
if err := mapping.Validate(); err != nil {
|
|
log.Errorf("unable to parse new config: %s", err)
|
|
return
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(c.Mapping.Mixer, newMapping.Mapping.Mixer) {
|
|
mappingChanged = true
|
|
log.Debug("detected new mixer mappings")
|
|
c.Mapping.Mixer = newMapping.Mapping.Mixer
|
|
}
|
|
|
|
// Shell
|
|
if !reflect.DeepEqual(c.Mapping.Shell, newMapping.Mapping.Shell) {
|
|
mappingChanged = true
|
|
log.Debug("detected new shell mappings")
|
|
c.Mapping.Shell = newMapping.Mapping.Shell
|
|
}
|
|
|
|
if c.MIDIDevice != nil {
|
|
c.MIDIDevice.SetMessageCallback(c.midiMessageCallback)
|
|
}
|
|
|
|
// EchoMIDIEvents
|
|
c.EchoMIDIEvents = newMapping.EchoMIDIEvents
|
|
|
|
log.Debug("completed configuration reload")
|
|
if mappingChanged {
|
|
log.Tracef("%+v", c.Mapping)
|
|
}
|
|
}
|
|
|
|
func (c *Configurator) midiMessageCallback(cc int, v int) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if c.EchoMIDIEvents {
|
|
log.WithFields(logrus.Fields{
|
|
"CC": cc,
|
|
"Value": v,
|
|
}).Info()
|
|
}
|
|
for _, m := range c.Mapping.Mixer {
|
|
if m.Cc == cc {
|
|
go func(m mixer.Mapping) {
|
|
c.PulseAudio.HandleMIDIMessage(&m, cc, v)
|
|
}(m)
|
|
return
|
|
}
|
|
}
|
|
for _, m := range c.Mapping.Shell {
|
|
if m.Cc == cc {
|
|
go func(m shell.Mapping) {
|
|
m.HandleMIDIMessage(cc, v)
|
|
}(m)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func New(filename string) *Configurator {
|
|
pa, err := pulseaudio.New()
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
|
|
c := &Configurator{
|
|
filename: filename,
|
|
reloadConfig: make(chan bool, 1),
|
|
PulseAudio: pa,
|
|
}
|
|
|
|
go c.updateConfigFromDiskLoop()
|
|
c.reloadConfig <- true
|
|
|
|
return c
|
|
}
|