From ab0678aa3b0717ba5d9d2bc57757e475da5b6776 Mon Sep 17 00:00:00 2001 From: mapleafgo Date: Wed, 6 Nov 2024 15:44:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=85=E6=9C=BA=E5=8A=9F=E8=83=BD=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/middleware/bgm.go | 38 +++- internal/middleware/sound_start.go | 8 +- internal/middleware/ticker.go | 4 +- internal/routes/background.go | 93 ---------- internal/routes/wait.go | 179 +++++++++++++++++++ internal/schema/bg.go | 29 --- internal/schema/wait.go | 29 +++ internal/server.go | 14 +- pkg/audio/bgm.go | 31 ++++ {internal/common => pkg/audio}/link_audio.go | 30 +++- pkg/relay/relay.go | 4 +- pkg/tts/aliyun.go | 2 + pkg/video/paly.go | 1 + readme.md | 6 +- 14 files changed, 317 insertions(+), 151 deletions(-) delete mode 100644 internal/routes/background.go create mode 100644 internal/routes/wait.go delete mode 100644 internal/schema/bg.go create mode 100644 internal/schema/wait.go create mode 100644 pkg/audio/bgm.go rename {internal/common => pkg/audio}/link_audio.go (68%) create mode 100644 pkg/video/paly.go diff --git a/internal/middleware/bgm.go b/internal/middleware/bgm.go index ee95c01..62da888 100644 --- a/internal/middleware/bgm.go +++ b/internal/middleware/bgm.go @@ -1,19 +1,51 @@ package middleware import ( - "game-driver/internal/common" "game-driver/internal/schema" "game-driver/leaf" "game-driver/pkg/audio" + "github.com/gopxl/beep/v2/speaker" + "log" + "sync" ) func PlayBgm() leaf.HandlerFunc { return func(c *leaf.Context) { pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey) - bgm := common.LinkAudio(pm.BGM) + bgm := audio.LinkAudio(pm.BGM) if bgm != nil { - go audio.PlayMP3(c, bgm) + // 等待组 + var wait sync.WaitGroup + defer wait.Wait() + + // 结束信号通道 + a := make(chan struct{}) + // 发送结束信号 + defer close(a) + + go func() { + // 等待结束 + wait.Add(1) + defer wait.Done() + + ctrl, closer := audio.PlayBgmMP3(bgm) + defer closer() + if ctrl == nil { + log.Println("播放背景音乐失败") + return + } + + select { + case <-a: + { + speaker.Lock() + ctrl.Streamer = nil + speaker.Unlock() + return + } + } + }() } c.Next() diff --git a/internal/middleware/sound_start.go b/internal/middleware/sound_start.go index 37c7c48..1300f25 100644 --- a/internal/middleware/sound_start.go +++ b/internal/middleware/sound_start.go @@ -7,17 +7,17 @@ import ( ) // SoundStart 开始词播报 -func SoundStart(t *tts.AliTTS) leaf.HandlerFunc { +func SoundStart() leaf.HandlerFunc { return func(c *leaf.Context) { pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey) - t.Sound(pm.TTS.Start) + tts.DefaultTTS.Sound(pm.TTS.Start) defer func() { switch leaf.Value[leaf.EndType](c, leaf.EndKey) { case leaf.EndTimer: - t.Sound(pm.TTS.End) + tts.DefaultTTS.Sound(pm.TTS.End) case leaf.EndStop: - t.Sound(pm.TTS.Stop) + tts.DefaultTTS.Sound(pm.TTS.Stop) } }() diff --git a/internal/middleware/ticker.go b/internal/middleware/ticker.go index bfcecb6..e08c436 100644 --- a/internal/middleware/ticker.go +++ b/internal/middleware/ticker.go @@ -8,7 +8,7 @@ import ( "time" ) -func TickerAction(t *tts.AliTTS) leaf.HandlerFunc { +func TickerAction() leaf.HandlerFunc { return func(c *leaf.Context) { pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey) @@ -50,7 +50,7 @@ func TickerAction(t *tts.AliTTS) leaf.HandlerFunc { //TODO: 屏幕打印 } if to, ok := ttsMap[s]; ok { - t.Sound(to.Value) + tts.DefaultTTS.Sound(to.Value) } } } diff --git a/internal/routes/background.go b/internal/routes/background.go deleted file mode 100644 index 063df56..0000000 --- a/internal/routes/background.go +++ /dev/null @@ -1,93 +0,0 @@ -package routes - -import ( - "game-driver/internal/common" - "game-driver/internal/middleware" - "game-driver/internal/schema" - "game-driver/leaf" - "game-driver/pkg/audio" - "github.com/gopxl/beep/v2" - "github.com/gopxl/beep/v2/mp3" - "github.com/gopxl/beep/v2/speaker" - "log" - "time" -) - -func timerAction(timestamp int64) <-chan struct{} { - a := make(chan struct{}) - - go func() { - if timestamp == 0 { - close(a) - } else { - <-time.After(time.Until(time.Unix(timestamp, 0))) - close(a) - } - }() - - return a -} - -func BackgroundAction(c *leaf.Context) { - payload := leaf.Value[*schema.BackgroundModel](c, middleware.PayloadJSONKey) - - if payload.Start != 0 && payload.End != 0 && time.Unix(payload.Start, 0).After(time.Unix(payload.End, 0)) { - log.Println("开始时间大于结束时间") - return - } - - if payload.End != 0 { - cancel := leaf.WithDeadline(c, time.Unix(payload.End, 0)) - defer cancel() - } - - select { - case <-c.Done(): - case <-timerAction(payload.Start): - go audioAction(c, payload.Items[0], payload.TimeModel) - } -} - -func audioAction(c *leaf.Context, item schema.BackgroundItemModel, root schema.TimeModel) { - if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) { - log.Println("开始时间小于根任务开始时间") - return - } - - select { - case <-c.Done(): - case <-timerAction(item.Start): - { - log.Println("开始执行后台任务") - data := common.LinkAudio(item.Data) - - streamer, format, err := mp3.Decode(data) - if err != nil { - return - } - defer streamer.Close() - - s := beep.Resample(4, format.SampleRate, audio.DefaultSampleRate, streamer) - - ctrl := &beep.Ctrl{Streamer: s} - done := make(chan struct{}) - speaker.Play(beep.Seq(ctrl, beep.Callback(func() { - close(done) - }))) - - for { - select { - case <-done: - return - case <-c.Done(): - { - speaker.Lock() - ctrl.Streamer = nil - speaker.Unlock() - } - } - } - - } - } -} diff --git a/internal/routes/wait.go b/internal/routes/wait.go new file mode 100644 index 0000000..dc9abc7 --- /dev/null +++ b/internal/routes/wait.go @@ -0,0 +1,179 @@ +package routes + +import ( + "game-driver/internal/middleware" + "game-driver/internal/schema" + "game-driver/leaf" + "game-driver/pkg/audio" + "game-driver/pkg/relay" + "game-driver/pkg/tts" + "github.com/gopxl/beep/v2/speaker" + "log" + "sync" + "time" +) + +func timerAction(timestamp int64) <-chan struct{} { + a := make(chan struct{}) + + go func() { + if timestamp == 0 { + close(a) + } else { + <-time.After(time.Until(time.Unix(timestamp, 0))) + close(a) + } + }() + + return a +} + +func WaitAction(c *leaf.Context) { + payload := leaf.Value[*schema.WaitModel](c, middleware.PayloadJSONKey) + + if payload.Start != 0 && payload.End != 0 && time.Unix(payload.Start, 0).After(time.Unix(payload.End, 0)) { + log.Println("开始时间大于结束时间") + return + } + + if payload.End != 0 { + cancel := leaf.WithDeadline(c, time.Unix(payload.End, 0)) + defer cancel() + } + + select { + case <-c.Done(): + case <-timerAction(payload.Start): + // 等待组 + var wait sync.WaitGroup + defer wait.Wait() + for _, item := range payload.Items { + switch item.Type { + case schema.WaitAudio: + // 执行音乐播放 + go func() { + wait.Add(1) + defer wait.Done() + audioAction(c, item, payload.TimeModel) + }() + case schema.WaitTTS: + // 执行TTS播放 + go func() { + wait.Add(1) + defer wait.Done() + ttsAction(c, item, payload.TimeModel) + }() + case schema.WaitRelay: + // 执行继电器供电 + go func() { + wait.Add(1) + defer wait.Done() + relayAction(c, item, payload.TimeModel) + }() + case schema.WaitVideo: + case schema.WaitWeb: + default: + log.Printf("不支持的类型: %d\n", item.Type) + } + } + } +} + +func audioAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeModel) { + if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) { + log.Println("开始时间小于根任务开始时间") + return + } + + if item.End != 0 { + cancel := leaf.WithDeadline(c, time.Unix(item.End, 0)) + defer cancel() + } + + select { + case <-c.Done(): + case <-timerAction(item.Start): + { + log.Println("开始执行后台任务") + data := audio.LinkAudio(item.Data) + + ctrl, closer := audio.PlayBgmMP3(data) + defer closer() + if ctrl == nil { + log.Println("播放背景音乐失败") + return + } + + select { + case <-c.Done(): + { + speaker.Lock() + ctrl.Streamer = nil + speaker.Unlock() + } + } + } + } +} + +func ttsAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeModel) { + if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) { + log.Println("开始时间小于根任务开始时间") + return + } + + if item.End != 0 { + cancel := leaf.WithDeadline(c, time.Unix(item.End, 0)) + defer cancel() + } + + reader, err := tts.DefaultTTS.Get(item.Data) + if err != nil { + log.Println("语音合成异常: ", err) + return + } + + select { + case <-c.Done(): + case <-timerAction(item.Start): + { + for { + audio.PlayWav(c, reader) + select { + case <-c.Done(): + return + case <-time.After(time.Duration(item.Interval) * time.Second): + } + } + } + } +} + +func relayAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeModel) { + if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) { + log.Println("开始时间小于根任务开始时间") + return + } + + if item.End != 0 { + cancel := leaf.WithDeadline(c, time.Unix(item.End, 0)) + defer cancel() + } + + device, err := relay.New(item.Data, nil) + if err != nil { + log.Println("继电器初始化异常: ", err) + return + } + defer device.Close() + + select { + case <-c.Done(): + case <-timerAction(item.Start): + { + device.On(1) + <-c.Done() + device.Off(1) + } + } +} diff --git a/internal/schema/bg.go b/internal/schema/bg.go deleted file mode 100644 index ca56664..0000000 --- a/internal/schema/bg.go +++ /dev/null @@ -1,29 +0,0 @@ -package schema - -type ItemType int - -const ( - TYPE_AUDIO ItemType = iota - TYPE_VIDEO - TYPE_WEB - TYPE_TTS - TYPE_RELAY -) - -type TimeModel struct { - Start int64 - End int64 -} - -type BackgroundItemModel struct { - TimeModel - Interval int64 - Type ItemType - Data string -} - -type BackgroundModel struct { - JsonModel - TimeModel - Items []BackgroundItemModel -} diff --git a/internal/schema/wait.go b/internal/schema/wait.go new file mode 100644 index 0000000..1cee38f --- /dev/null +++ b/internal/schema/wait.go @@ -0,0 +1,29 @@ +package schema + +type WaitType int + +const ( + WaitAudio WaitType = iota + WaitVideo + WaitWeb + WaitTTS + WaitRelay +) + +type TimeModel struct { + Start int64 + End int64 +} + +type WaitItemModel struct { + TimeModel + Type WaitType + Data string + Interval int64 +} + +type WaitModel struct { + JsonModel + TimeModel + Items []WaitItemModel +} diff --git a/internal/server.go b/internal/server.go index 1fcdc29..98786ad 100644 --- a/internal/server.go +++ b/internal/server.go @@ -96,7 +96,7 @@ func Run() { } // 构建语音合成对象 - t := tts.New(ctx, config.C.Aliyun) + tts.DefaultTTS = tts.New(ctx, config.C.Aliyun) // 构建继电器对象 //r, err := relay.New(config.C.Relay, func(msg string) { @@ -117,10 +117,10 @@ func Run() { middleware.PayloadJSON[schema.PlayModal](), middleware.DeviceLock(device), middleware.EmergencyStop(common.GlobalStopper), - middleware.SoundStart(t), + middleware.SoundStart(), middleware.RelayMaster(nil), middleware.TimeoutOver(config.C.Game.MaxTimeout), - middleware.TickerAction(t), + middleware.TickerAction(), middleware.PlayBgm(), func(c *leaf.Context) { log.Println("接收到启动消息: ", string(c.Payload)) @@ -130,11 +130,11 @@ func Run() { } }, ) - // 处理后台线程报文 - router.RegisterHandler(topicPrefix+"bg", - middleware.PayloadJSON[schema.BackgroundModel](), + // 处理待机线程报文 + router.RegisterHandler(topicPrefix+"wait", + middleware.PayloadJSON[schema.WaitModel](), middleware.EmergencyStop(common.GlobalBgStopper), - routes.BackgroundAction, + routes.WaitAction, ) // 处理指令 router.RegisterHandler(topicPrefix+"command", routes.Command(device)) diff --git a/pkg/audio/bgm.go b/pkg/audio/bgm.go new file mode 100644 index 0000000..884deeb --- /dev/null +++ b/pkg/audio/bgm.go @@ -0,0 +1,31 @@ +package audio + +import ( + "github.com/gopxl/beep/v2" + "github.com/gopxl/beep/v2/mp3" + "github.com/gopxl/beep/v2/speaker" + "io" + "log" +) + +func PlayBgmMP3(r io.ReadCloser, opts ...beep.LoopOption) (*beep.Ctrl, func() error) { + streamer, format, err := mp3.Decode(r) + if err != nil { + return nil, func() error { return nil } + } + + loop2, err := beep.Loop2(streamer, opts...) + if err != nil { + log.Println("循环播放异常: ", err) + return nil, streamer.Close + } + + s := beep.Resample(4, format.SampleRate, DefaultSampleRate, loop2) + + ctrl := &beep.Ctrl{Streamer: s} + speaker.Play(beep.Seq(ctrl, beep.Callback(func() { + streamer.Close() + }))) + + return ctrl, streamer.Close +} diff --git a/internal/common/link_audio.go b/pkg/audio/link_audio.go similarity index 68% rename from internal/common/link_audio.go rename to pkg/audio/link_audio.go index 8af793d..3f48930 100644 --- a/internal/common/link_audio.go +++ b/pkg/audio/link_audio.go @@ -1,4 +1,4 @@ -package common +package audio import ( "bytes" @@ -10,6 +10,24 @@ import ( "strings" ) +type reader struct { + bytes.Reader + io.Closer +} + +func (a *reader) Seek(offset int64, whence int) (int64, error) { + return a.Reader.Seek(offset, whence) +} + +func toSeeker(src io.ReadCloser) io.ReadCloser { + buf := &bytes.Buffer{} + _, _ = io.Copy(buf, src) + return &reader{ + Reader: *bytes.NewReader(buf.Bytes()), + Closer: src, + } +} + func open(u string) io.ReadCloser { p, _ := strings.CutPrefix(u, "file://") f, e := os.Open(p) @@ -29,13 +47,6 @@ func get(u string) io.ReadCloser { return resp.Body } -func toBuffer(b io.ReadCloser) io.ReadCloser { - data := &bytes.Buffer{} - _, _ = data.ReadFrom(b) - defer b.Close() - return io.NopCloser(data) -} - func LinkAudio(link string) (bgm io.ReadCloser) { u, err := url.Parse(link) if err != nil { @@ -47,8 +58,9 @@ func LinkAudio(link string) (bgm io.ReadCloser) { bgm = get(u.String()) } else { log.Printf("不支持的音频文件协议: %v\n", u.String()) + return } - bgm = toBuffer(bgm) + bgm = toSeeker(bgm) } return } diff --git a/pkg/relay/relay.go b/pkg/relay/relay.go index 3302c8c..452f0d4 100644 --- a/pkg/relay/relay.go +++ b/pkg/relay/relay.go @@ -37,7 +37,9 @@ func New(portName string, reader func(msg string)) (*Device, error) { for { r := bufio.NewReader(port) line, _, _ := r.ReadLine() - reader(string(line)) + if reader != nil { + reader(string(line)) + } } }() return &Device{port: port}, nil diff --git a/pkg/tts/aliyun.go b/pkg/tts/aliyun.go index af00bc8..922345b 100644 --- a/pkg/tts/aliyun.go +++ b/pkg/tts/aliyun.go @@ -26,6 +26,8 @@ type result struct { Error error } +var DefaultTTS = &AliTTS{} + // onTaskFailed 识别过程中的错误处理回调参数 func (tts *AliTTS) onTaskFailed(text string, param interface{}) { p, _ := param.(*result) diff --git a/pkg/video/paly.go b/pkg/video/paly.go new file mode 100644 index 0000000..2c9ffe7 --- /dev/null +++ b/pkg/video/paly.go @@ -0,0 +1 @@ +package video diff --git a/readme.md b/readme.md index cabe838..869e01b 100644 --- a/readme.md +++ b/readme.md @@ -117,9 +117,9 @@ status > 设备接收到该指令会立即向 `device/${location}/${point}/status` 发送一次当前状态 -## 4. 后台执行 +## 4. 待机执行 -Topic: `server/${location}/${point}/bg` +Topic: `server/${location}/${point}/wait` Payload: @@ -148,4 +148,4 @@ Payload: } ``` -> 同一个类型的后台任务只能有一个,当有新的任务到达时会覆盖之前的任务 +> 同一个类型的待机任务只能有一个,当有新的任务到达时会覆盖之前的任务