// // dolmetschctl // // // Copyright (C) 2019 Christian Pointner // // This file is part of dolmetschctl. // // dolmetschctl is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // dolmetschctl is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with dolmetschctl. If not, see . // package main import ( "container/list" "fmt" "log" "time" "spreadspace.org/dolmetschctl/pkg/mixer" "spreadspace.org/dolmetschctl/pkg/types" ) type MixerChannelState struct { level mixer.FaderLevel mute mixer.Mute } type MixerChannel struct { num mixer.Channel target MixerChannelState current MixerChannelState } type MixerChannels struct { original MixerChannel interpreter MixerChannel } type setLanguageReq struct { lang types.Language resCh chan error } type getLanguageReq struct { resCh chan types.Language } type getLanguagesReq struct { resCh chan []types.Language } type setOriginal2InterpreterRatioReq struct { ratio float32 resCh chan error } type getOriginal2InterpreterRatioReq struct { resCh chan float32 } type getStateRes struct { types.FullState } type getStateReq struct { resCh chan getStateRes } type subscribeStateReq struct { ch chan<- types.FullState resCh chan error } type StateMachine struct { mixer *mixer.Mixer setLanguageCh chan setLanguageReq getLanguageCh chan getLanguageReq getLanguagesCh chan getLanguagesReq setOriginal2InterpreterRatioCh chan setOriginal2InterpreterRatioReq getOriginal2InterpreterRatioCh chan getOriginal2InterpreterRatioReq getStateCh chan getStateReq subscribeStateCh chan subscribeStateReq quitCh chan bool exitedCh chan struct{} mixerEventCh chan mixer.Event languages map[types.Language]*MixerChannels channel2lang map[mixer.Channel]types.Language state types.State original2InterpreterRatio float32 language types.Language stateSubscribers list.List } func (sm *StateMachine) publishState() { var next *list.Element for sub := sm.stateSubscribers.Front(); sub != nil; sub = next { next = sub.Next() ch, ok := (sub.Value).(chan<- types.FullState) if !ok { panic(fmt.Sprintf("statemachine: subscriber list element value has wrong type: %T", sub.Value)) } select { case ch <- types.FullState{sm.state, sm.original2InterpreterRatio, sm.language}: default: // subscriber is not responding... // log.Printf("statemachine: removing subscriber '%v', because it is not responding", ch) close(ch) sm.stateSubscribers.Remove(sub) } } } func (sm *StateMachine) subscribeState(out chan<- types.FullState) error { // log.Printf("statemachine: %v subscribed to state changes", out) sm.stateSubscribers.PushBack(out) return nil } func (sm *StateMachine) handleMixerEvent(ev mixer.Event) { lang, exists := sm.channel2lang[ev.Channel] if !exists { // TODO: make this panic? log.Printf("got mixer-event for unknown channel: %s", ev) return } mcs, exists := sm.languages[lang] if !exists { panic(fmt.Sprintf("channel2lang map contains unknown language entries!")) } var mc *MixerChannel switch ev.Channel { case mcs.original.num: mc = &mcs.original case mcs.interpreter.num: mc = &mcs.interpreter default: panic(fmt.Sprintf("channel2lang points to language that does not use the channel!")) } switch ev.Type { case mixer.EventFaderChange: mc.current.level = ev.Level case mixer.EventMute: mc.current.mute = ev.Mute } } // make sure that our state and the mixer are in sync func (sm *StateMachine) initMixer() { for _, mcs := range sm.languages { sm.mixer.SetLevel(mcs.original.num, mixer.FaderLevel0db-1) sm.mixer.SetLevel(mcs.original.num, mixer.FaderLevel0db) mcs.original.target.level = mixer.FaderLevel0db sm.mixer.SetLevel(mcs.interpreter.num, mixer.FaderLevelOff+1) sm.mixer.SetLevel(mcs.interpreter.num, mixer.FaderLevelOff) mcs.interpreter.target.level = mixer.FaderLevelOff } sm.language = "" sm.state = types.StateSettled } func (sm *StateMachine) getLanguages() (langs []types.Language) { langs = append(langs, "none") for lang, _ := range sm.languages { langs = append(langs, lang) } return } // the "current language" is what is currently spoken on stage func (sm *StateMachine) setLanguage(l types.Language) error { if l == "none" { l = "" } if l != "" { if _, exists := sm.languages[l]; !exists { return fmt.Errorf("language '%s' does not exist", l) } } sm.language = l log.Printf("new target language: '%s'", sm.language) return nil } func (sm *StateMachine) setOriginal2InterpreterRatio(r float32) error { if r < 0.0 || r > 1.0 { return fmt.Errorf("original-interpreter ratio '%1.3f' is invalid, must be between 0.0 and 1.0", r) } sm.original2InterpreterRatio = r log.Printf("new original-interpreter ratio: '%1.3f'", r) return nil } func calcNextLevel(target, current mixer.FaderLevel) mixer.FaderLevel { next := target if current != mixer.FaderLevelUnknown { if next > current { next = current + 1 } else { next = current - 1 } } return next } func (sm *StateMachine) reconcile(ticker bool) { for lang, mcs := range sm.languages { if sm.language == "" || lang == sm.language || mcs.interpreter.current.mute == mixer.MuteMuted { mcs.original.target.level = mixer.FaderLevel0db mcs.interpreter.target.level = mixer.FaderLevelOff } else { mcs.original.target.level = mixer.FaderLevel(float32(mixer.FaderLevel0db) * sm.original2InterpreterRatio) if mcs.original.target.level > mixer.FaderLevelMax { mcs.original.target.level = mixer.FaderLevelMax } mcs.interpreter.target.level = mixer.FaderLevel0db } } if sm.state != types.StateSettled && !ticker { return } sm.state = types.StateSettled for _, mcs := range sm.languages { if mcs.original.target.level != mcs.original.current.level { sm.mixer.SetLevel(mcs.original.num, calcNextLevel(mcs.original.target.level, mcs.original.current.level)) sm.state = types.StateSettling } if mcs.interpreter.target.level != mcs.interpreter.current.level { sm.mixer.SetLevel(mcs.interpreter.num, calcNextLevel(mcs.interpreter.target.level, mcs.interpreter.current.level)) sm.state = types.StateSettling } } } func (sm *StateMachine) run() { defer close(sm.exitedCh) sm.initMixer() t := time.NewTicker(10 * time.Millisecond) for { oldState := sm.state oldOriginal2InterpreterRadio := sm.original2InterpreterRatio oldLanguage := sm.language select { case <-t.C: if sm.state == types.StateSettling { sm.reconcile(true) } case req := <-sm.setLanguageCh: req.resCh <- sm.setLanguage(req.lang) sm.reconcile(false) case req := <-sm.getLanguageCh: req.resCh <- sm.language case req := <-sm.getLanguagesCh: req.resCh <- sm.getLanguages() case req := <-sm.setOriginal2InterpreterRatioCh: req.resCh <- sm.setOriginal2InterpreterRatio(req.ratio) sm.reconcile(false) case req := <-sm.getOriginal2InterpreterRatioCh: req.resCh <- sm.original2InterpreterRatio case req := <-sm.getStateCh: req.resCh <- getStateRes{types.FullState{sm.state, sm.original2InterpreterRatio, sm.language}} case req := <-sm.subscribeStateCh: req.resCh <- sm.subscribeState(req.ch) case ev := <-sm.mixerEventCh: sm.handleMixerEvent(ev) sm.reconcile(false) case <-sm.quitCh: return } if oldState != sm.state || oldOriginal2InterpreterRadio != sm.original2InterpreterRatio || oldLanguage != sm.language { sm.publishState() } } } func NewStateMachine(m *mixer.Mixer) (*StateMachine, error) { sm := &StateMachine{mixer: m} sm.setLanguageCh = make(chan setLanguageReq, 10) sm.getLanguageCh = make(chan getLanguageReq, 10) sm.getLanguagesCh = make(chan getLanguagesReq, 10) sm.setOriginal2InterpreterRatioCh = make(chan setOriginal2InterpreterRatioReq, 10) sm.getOriginal2InterpreterRatioCh = make(chan getOriginal2InterpreterRatioReq, 10) sm.getStateCh = make(chan getStateReq, 10) sm.subscribeStateCh = make(chan subscribeStateReq, 10) sm.quitCh = make(chan bool, 1) sm.exitedCh = make(chan struct{}) sm.mixerEventCh = make(chan mixer.Event, 1000) sm.languages = make(map[types.Language]*MixerChannels) sm.channel2lang = make(map[mixer.Channel]types.Language) sm.state = types.StateNew sm.language = "" return sm, nil } // TODO: currently we can only deal with 2 languages... func (sm *StateMachine) AddLanguage(name types.Language, original, interpreter mixer.Channel) error { if sm.state != types.StateNew { return fmt.Errorf("adding languages is only allowed during startup") } if name == "none" { return fmt.Errorf("language 'none' is reserved") } if _, exists := sm.languages[name]; exists { return fmt.Errorf("language '%s' already exists", name) } for ch := range []mixer.Channel{original, interpreter} { if _, exists := sm.languages[name]; exists { return fmt.Errorf("mixer channel %v is already in use by language '%s'", ch, name) } } unknown := MixerChannelState{level: mixer.FaderLevelUnknown, mute: mixer.MuteUnknown} chOriginal := MixerChannel{num: original, current: unknown, target: unknown} chInterpreter := MixerChannel{num: interpreter, current: unknown, target: unknown} sm.languages[name] = &MixerChannels{chOriginal, chInterpreter} sm.channel2lang[original] = name sm.channel2lang[interpreter] = name sm.mixer.Subscribe(original, sm.mixerEventCh) sm.mixer.Subscribe(interpreter, sm.mixerEventCh) sm.original2InterpreterRatio = 0.5 // TODO: hardcoded value return nil } func (sm *StateMachine) Start() { go sm.run() } func (sm *StateMachine) SetLanguage(l types.Language) error { resCh := make(chan error) sm.setLanguageCh <- setLanguageReq{l, resCh} return <-resCh } func (sm *StateMachine) GetLanguage() types.Language { resCh := make(chan types.Language) sm.getLanguageCh <- getLanguageReq{resCh} return <-resCh } func (sm *StateMachine) GetLanguages() []types.Language { resCh := make(chan []types.Language) sm.getLanguagesCh <- getLanguagesReq{resCh} return <-resCh } func (sm *StateMachine) SetOriginal2InterpreterRatio(r float32) error { resCh := make(chan error) sm.setOriginal2InterpreterRatioCh <- setOriginal2InterpreterRatioReq{r, resCh} return <-resCh } func (sm *StateMachine) GetOriginal2InterpreterRatio() float32 { resCh := make(chan float32) sm.getOriginal2InterpreterRatioCh <- getOriginal2InterpreterRatioReq{resCh} return <-resCh } func (sm *StateMachine) GetState() (types.State, float32, types.Language) { resCh := make(chan getStateRes) sm.getStateCh <- getStateReq{resCh} res := <-resCh return res.State, res.Ratio, res.Language } func (sm *StateMachine) SubscribeState(out chan<- types.FullState) error { resCh := make(chan error) sm.subscribeStateCh <- subscribeStateReq{out, resCh} return <-resCh } func (sm *StateMachine) Shutdown() { select { case sm.quitCh <- true: default: } <-sm.exitedCh }