install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
## Default wake words
|
||||
|
||||
- alexa_v0.1
|
||||
- hey_jarvis_v0.1
|
||||
- hey_mycroft_v0.1
|
||||
- hey_rhasspy_v0.1
|
||||
- ok_nabu_v0.1
|
||||
|
||||
## Useful Links
|
||||
|
||||
- https://github.com/rhasspy/wyoming-satellite
|
||||
- https://github.com/rhasspy/wyoming-openwakeword
|
||||
- https://github.com/fwartner/home-assistant-wakewords-collection
|
||||
- https://github.com/esphome/micro-wake-word-models/tree/main?tab=readme-ov-file
|
||||
@@ -0,0 +1,99 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
conn net.Conn
|
||||
rd *bufio.Reader
|
||||
}
|
||||
|
||||
func DialAPI(address string) (*API, error) {
|
||||
conn, err := net.DialTimeout("tcp", address, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewAPI(conn), nil
|
||||
}
|
||||
|
||||
const Version = "1.5.4"
|
||||
|
||||
func NewAPI(conn net.Conn) *API {
|
||||
return &API{conn: conn, rd: bufio.NewReader(conn)}
|
||||
}
|
||||
|
||||
func (w *API) WriteEvent(evt *Event) (err error) {
|
||||
hdr := EventHeader{
|
||||
Type: evt.Type,
|
||||
Version: Version,
|
||||
DataLength: len(evt.Data),
|
||||
PayloadLength: len(evt.Payload),
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf = append(buf, '\n')
|
||||
buf = append(buf, evt.Data...)
|
||||
buf = append(buf, evt.Payload...)
|
||||
|
||||
_, err = w.conn.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *API) ReadEvent() (*Event, error) {
|
||||
data, err := w.rd.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var hdr EventHeader
|
||||
if err = json.Unmarshal(data, &hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
evt := Event{Type: hdr.Type}
|
||||
|
||||
if hdr.DataLength > 0 {
|
||||
data = make([]byte, hdr.DataLength)
|
||||
if _, err = io.ReadFull(w.rd, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
evt.Data = string(data)
|
||||
}
|
||||
|
||||
if hdr.PayloadLength > 0 {
|
||||
evt.Payload = make([]byte, hdr.PayloadLength)
|
||||
if _, err = io.ReadFull(w.rd, evt.Payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &evt, nil
|
||||
}
|
||||
|
||||
func (w *API) Close() error {
|
||||
return w.conn.Close()
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Type string
|
||||
Data string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
type EventHeader struct {
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
DataLength int `json:"data_length,omitempty"`
|
||||
PayloadLength int `json:"payload_length,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Backchannel struct {
|
||||
core.Connection
|
||||
api *API
|
||||
}
|
||||
|
||||
func newBackchannel(conn net.Conn) *Backchannel {
|
||||
return &Backchannel{
|
||||
core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "wyoming",
|
||||
Medias: []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCML, ClockRate: 22050},
|
||||
},
|
||||
},
|
||||
},
|
||||
Transport: conn,
|
||||
},
|
||||
NewAPI(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (b *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
sender := core.NewSender(media, codec)
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
ts := time.Now().Nanosecond()
|
||||
evt := &Event{
|
||||
Type: "audio-chunk",
|
||||
Data: fmt.Sprintf(`{"rate":22050,"width":2,"channels":1,"timestamp":%d}`, ts),
|
||||
Payload: pkt.Payload,
|
||||
}
|
||||
_ = b.api.WriteEvent(evt)
|
||||
}
|
||||
sender.HandleRTP(track)
|
||||
b.Senders = append(b.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backchannel) Start() error {
|
||||
for {
|
||||
if _, err := b.api.ReadEvent(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/expr"
|
||||
"github.com/AlexxIT/go2rtc/pkg/wav"
|
||||
)
|
||||
|
||||
type env struct {
|
||||
*satellite
|
||||
Type string
|
||||
Data string
|
||||
}
|
||||
|
||||
func (s *satellite) handleEvent(evt *Event) {
|
||||
switch evt.Type {
|
||||
case "describe":
|
||||
// {"asr": [], "tts": [], "handle": [], "intent": [], "wake": [], "satellite": {"name": "my satellite", "attribution": {"name": "", "url": ""}, "installed": true, "description": "my satellite", "version": "1.4.1", "area": null, "snd_format": null}}
|
||||
data := fmt.Sprintf(`{"satellite":{"name":%q,"attribution":{"name":"go2rtc","url":"https://github.com/AlexxIT/go2rtc"},"installed":true}}`, s.srv.Name)
|
||||
s.WriteEvent("info", data)
|
||||
case "run-satellite":
|
||||
s.Detect()
|
||||
case "pause-satellite":
|
||||
s.Stop()
|
||||
case "detect": // WAKE_WORD_START {"names": null}
|
||||
case "detection": // WAKE_WORD_END {"name": "ok_nabu_v0.1", "timestamp": 17580, "speaker": null}
|
||||
case "transcribe": // STT_START {"language": "en"}
|
||||
case "voice-started": // STT_VAD_START {"timestamp": 1160}
|
||||
case "voice-stopped": // STT_VAD_END {"timestamp": 2470}
|
||||
s.Pause()
|
||||
case "transcript": // STT_END {"text": "how are you"}
|
||||
case "synthesize": // TTS_START {"text": "Sorry, I couldn't understand that", "voice": {"language": "en"}}
|
||||
case "audio-start": // TTS_END {"rate": 22050, "width": 2, "channels": 1, "timestamp": 0}
|
||||
case "audio-chunk": // {"rate": 22050, "width": 2, "channels": 1, "timestamp": 0}
|
||||
case "audio-stop": // {"timestamp": 2.880000000000002}
|
||||
// run async because PlayAudio takes some time
|
||||
go func() {
|
||||
s.PlayAudio()
|
||||
s.WriteEvent("played")
|
||||
s.Detect()
|
||||
}()
|
||||
case "error":
|
||||
s.Detect()
|
||||
case "internal-run":
|
||||
s.WriteEvent("run-pipeline", `{"start_stage":"wake","end_stage":"tts"}`)
|
||||
s.Stream()
|
||||
case "internal-detection":
|
||||
s.WriteEvent("run-pipeline", `{"start_stage":"asr","end_stage":"tts"}`)
|
||||
s.Stream()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *satellite) handleScript(evt *Event) {
|
||||
var script string
|
||||
if s.srv.Event != nil {
|
||||
script = s.srv.Event[evt.Type]
|
||||
}
|
||||
|
||||
s.srv.Trace("event=%s data=%s payload size=%d", evt.Type, evt.Data, len(evt.Payload))
|
||||
|
||||
if script == "" {
|
||||
s.handleEvent(evt)
|
||||
return
|
||||
}
|
||||
|
||||
// run async because script can have sleeps
|
||||
go func() {
|
||||
e := &env{satellite: s, Type: evt.Type, Data: evt.Data}
|
||||
if res, err := expr.Eval(script, e); err != nil {
|
||||
s.srv.Trace("event=%s expr error=%s", evt.Type, err)
|
||||
s.handleEvent(evt)
|
||||
} else {
|
||||
s.srv.Trace("event=%s expr result=%v", evt.Type, res)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *satellite) Detect() bool {
|
||||
return s.setMicState(stateWaitVAD)
|
||||
}
|
||||
|
||||
func (s *satellite) Stream() bool {
|
||||
return s.setMicState(stateActive)
|
||||
}
|
||||
|
||||
func (s *satellite) Pause() bool {
|
||||
return s.setMicState(stateIdle)
|
||||
}
|
||||
|
||||
func (s *satellite) Stop() bool {
|
||||
s.micStop()
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *satellite) WriteEvent(args ...string) bool {
|
||||
if len(args) == 0 {
|
||||
return false
|
||||
}
|
||||
evt := &Event{Type: args[0]}
|
||||
if len(args) > 1 {
|
||||
evt.Data = args[1]
|
||||
}
|
||||
if err := s.api.WriteEvent(evt); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *satellite) PlayAudio() bool {
|
||||
return s.playAudio(sndCodec, bytes.NewReader(s.sndAudio))
|
||||
}
|
||||
|
||||
func (s *satellite) PlayFile(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
codec, err := wav.ReadHeader(f)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.playAudio(codec, f)
|
||||
}
|
||||
|
||||
func (e *env) Sleep(s string) bool {
|
||||
d, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
time.Sleep(d)
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (s *Server) HandleMic(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
var closed core.Waiter
|
||||
var timestamp int
|
||||
|
||||
api := NewAPI(conn)
|
||||
mic := newMicConsumer(func(chunk []byte) {
|
||||
data := fmt.Sprintf(`{"rate":16000,"width":2,"channels":1,"timestamp":%d}`, timestamp)
|
||||
evt := &Event{Type: "audio-chunk", Data: data, Payload: chunk}
|
||||
if err := api.WriteEvent(evt); err != nil {
|
||||
closed.Done(nil)
|
||||
}
|
||||
|
||||
timestamp += len(chunk) / 2
|
||||
})
|
||||
mic.RemoteAddr = api.conn.RemoteAddr().String()
|
||||
|
||||
if err := s.MicHandler(mic); err != nil {
|
||||
s.Error("mic error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = closed.Wait()
|
||||
_ = mic.Stop()
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
core.Connection
|
||||
api *API
|
||||
}
|
||||
|
||||
func newProducer(conn net.Conn) *Producer {
|
||||
return &Producer{
|
||||
core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "wyoming",
|
||||
Medias: []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCML, ClockRate: 16000},
|
||||
},
|
||||
},
|
||||
},
|
||||
Transport: conn,
|
||||
},
|
||||
NewAPI(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Producer) Start() error {
|
||||
var seq uint16
|
||||
var ts uint32
|
||||
|
||||
for {
|
||||
evt, err := p.api.ReadEvent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if evt.Type != "audio-chunk" {
|
||||
continue
|
||||
}
|
||||
|
||||
p.Recv += len(evt.Payload)
|
||||
|
||||
pkt := &core.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: seq,
|
||||
Timestamp: ts,
|
||||
},
|
||||
Payload: evt.Payload,
|
||||
}
|
||||
p.Receivers[0].WriteRTP(pkt)
|
||||
|
||||
seq++
|
||||
ts += uint32(len(evt.Payload) / 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm/s16le"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Name string
|
||||
Event map[string]string
|
||||
|
||||
VADThreshold int16
|
||||
WakeURI string
|
||||
|
||||
MicHandler func(cons core.Consumer) error
|
||||
SndHandler func(prod core.Producer) error
|
||||
|
||||
Trace func(format string, v ...any)
|
||||
Error func(format string, v ...any)
|
||||
}
|
||||
|
||||
func (s *Server) Serve(l net.Listener) error {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go s.Handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Handle(conn net.Conn) {
|
||||
api := NewAPI(conn)
|
||||
sat := newSatellite(api, s)
|
||||
defer sat.Close()
|
||||
|
||||
for {
|
||||
evt, err := api.ReadEvent()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch evt.Type {
|
||||
case "ping": // {"text": null}
|
||||
_ = api.WriteEvent(&Event{Type: "pong", Data: evt.Data})
|
||||
case "audio-start": // TTS_END {"rate": 22050, "width": 2, "channels": 1, "timestamp": 0}
|
||||
sat.sndAudio = sat.sndAudio[:0]
|
||||
case "audio-chunk": // {"rate": 22050, "width": 2, "channels": 1, "timestamp": 0}
|
||||
sat.sndAudio = append(sat.sndAudio, evt.Payload...)
|
||||
default:
|
||||
sat.handleScript(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// states like http.ConnState
|
||||
const (
|
||||
stateError = -2
|
||||
stateClosed = -1
|
||||
stateNew = 0
|
||||
stateIdle = 1
|
||||
stateWaitVAD = 2 // aka wait VAD
|
||||
stateWaitWakeWord = 3
|
||||
stateActive = 4
|
||||
)
|
||||
|
||||
type satellite struct {
|
||||
api *API
|
||||
srv *Server
|
||||
|
||||
micState int8
|
||||
micTS int
|
||||
micMu sync.Mutex
|
||||
sndAudio []byte
|
||||
|
||||
mic *micConsumer
|
||||
wake *WakeWord
|
||||
}
|
||||
|
||||
func newSatellite(api *API, srv *Server) *satellite {
|
||||
sat := &satellite{api: api, srv: srv}
|
||||
return sat
|
||||
}
|
||||
|
||||
func (s *satellite) Close() error {
|
||||
s.Stop()
|
||||
return s.api.Close()
|
||||
}
|
||||
|
||||
const wakeTimeout = 5 * 2 * 16000 // 5 seconds
|
||||
|
||||
func (s *satellite) setMicState(state int8) bool {
|
||||
s.micMu.Lock()
|
||||
defer s.micMu.Unlock()
|
||||
|
||||
if s.micState == stateNew {
|
||||
s.mic = newMicConsumer(s.onMicChunk)
|
||||
s.mic.RemoteAddr = s.api.conn.RemoteAddr().String()
|
||||
if err := s.srv.MicHandler(s.mic); err != nil {
|
||||
s.micState = stateError
|
||||
s.srv.Error("can't get mic: %w", err)
|
||||
_ = s.api.Close()
|
||||
} else {
|
||||
s.micState = stateIdle
|
||||
}
|
||||
}
|
||||
|
||||
if s.micState < stateIdle {
|
||||
return false
|
||||
}
|
||||
|
||||
s.micState = state
|
||||
s.micTS = 0
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *satellite) micStop() {
|
||||
s.micMu.Lock()
|
||||
|
||||
s.micState = stateClosed
|
||||
if s.mic != nil {
|
||||
_ = s.mic.Stop()
|
||||
s.mic = nil
|
||||
}
|
||||
if s.wake != nil {
|
||||
_ = s.wake.Close()
|
||||
s.wake = nil
|
||||
}
|
||||
|
||||
s.micMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *satellite) onMicChunk(chunk []byte) {
|
||||
s.micMu.Lock()
|
||||
defer s.micMu.Unlock()
|
||||
|
||||
if s.micState == stateIdle {
|
||||
return
|
||||
}
|
||||
|
||||
if s.micState == stateWaitVAD {
|
||||
// tests show that values over 1000 are most likely speech
|
||||
if s.srv.VADThreshold == 0 || s16le.PeaksRMS(chunk) > s.srv.VADThreshold {
|
||||
if s.wake == nil && s.srv.WakeURI != "" {
|
||||
s.wake, _ = DialWakeWord(s.srv.WakeURI)
|
||||
}
|
||||
if s.wake == nil {
|
||||
// some problems with wake word - redirect to HA
|
||||
s.micState = stateIdle
|
||||
go s.handleScript(&Event{Type: "internal-run"})
|
||||
} else {
|
||||
s.micState = stateWaitWakeWord
|
||||
}
|
||||
s.micTS = 0
|
||||
}
|
||||
}
|
||||
|
||||
if s.micState == stateWaitWakeWord {
|
||||
if s.wake.Detection != "" {
|
||||
// check if wake word detected
|
||||
s.micState = stateIdle
|
||||
go s.handleScript(&Event{Type: "internal-detection", Data: `{"name":"` + s.wake.Detection + `"}`})
|
||||
} else if err := s.wake.WriteChunk(chunk); err != nil {
|
||||
// wake word service failed
|
||||
s.micState = stateWaitVAD
|
||||
_ = s.wake.Close()
|
||||
s.wake = nil
|
||||
} else if s.micTS > wakeTimeout {
|
||||
// wake word detection timeout
|
||||
s.micState = stateWaitVAD
|
||||
}
|
||||
} else if s.wake != nil {
|
||||
_ = s.wake.Close()
|
||||
s.wake = nil
|
||||
}
|
||||
|
||||
if s.micState == stateActive {
|
||||
data := fmt.Sprintf(`{"rate":16000,"width":2,"channels":1,"timestamp":%d}`, s.micTS)
|
||||
evt := &Event{Type: "audio-chunk", Data: data, Payload: chunk}
|
||||
_ = s.api.WriteEvent(evt)
|
||||
}
|
||||
|
||||
s.micTS += len(chunk) / 2
|
||||
}
|
||||
|
||||
func (s *satellite) playAudio(codec *core.Codec, rd io.Reader) bool {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
prod := pcm.OpenSync(codec, rd)
|
||||
prod.OnClose(cancel)
|
||||
|
||||
if err := s.srv.SndHandler(prod); err != nil {
|
||||
return false
|
||||
} else {
|
||||
<-ctx.Done()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
type micConsumer struct {
|
||||
core.Connection
|
||||
onData func(chunk []byte)
|
||||
onClose func()
|
||||
}
|
||||
|
||||
func newMicConsumer(onData func(chunk []byte)) *micConsumer {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: pcm.ConsumerCodecs(),
|
||||
},
|
||||
}
|
||||
|
||||
return &micConsumer{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "wyoming",
|
||||
Protocol: "tcp",
|
||||
Medias: medias,
|
||||
},
|
||||
onData: onData,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *micConsumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
src := track.Codec
|
||||
dst := &core.Codec{
|
||||
Name: core.CodecPCML,
|
||||
ClockRate: 16000,
|
||||
Channels: 1,
|
||||
}
|
||||
sender := core.NewSender(media, dst)
|
||||
sender.Handler = pcm.TranscodeHandler(dst, src,
|
||||
repack(func(packet *core.Packet) {
|
||||
c.onData(packet.Payload)
|
||||
}),
|
||||
)
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *micConsumer) Stop() error {
|
||||
if c.onClose != nil {
|
||||
c.onClose()
|
||||
}
|
||||
return c.Connection.Stop()
|
||||
}
|
||||
|
||||
func repack(handler core.HandlerFunc) core.HandlerFunc {
|
||||
const PacketSize = 2 * 16000 / 50 // 20ms
|
||||
|
||||
var buf []byte
|
||||
|
||||
return func(pkt *rtp.Packet) {
|
||||
buf = append(buf, pkt.Payload...)
|
||||
|
||||
for len(buf) >= PacketSize {
|
||||
pkt = &core.Packet{Payload: buf[:PacketSize]}
|
||||
buf = buf[PacketSize:]
|
||||
handler(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
)
|
||||
|
||||
func (s *Server) HandleSnd(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
var snd []byte
|
||||
|
||||
api := NewAPI(conn)
|
||||
for {
|
||||
evt, err := api.ReadEvent()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.Trace("event: %s data: %s payload: %d", evt.Type, evt.Data, len(evt.Payload))
|
||||
|
||||
switch evt.Type {
|
||||
case "audio-start":
|
||||
snd = snd[:0]
|
||||
case "audio-chunk":
|
||||
snd = append(snd, evt.Payload...)
|
||||
case "audio-stop":
|
||||
prod := pcm.OpenSync(sndCodec, bytes.NewReader(snd))
|
||||
if err = s.SndHandler(prod); err != nil {
|
||||
s.Error("snd error: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sndCodec = &core.Codec{Name: core.CodecPCML, ClockRate: 22050}
|
||||
@@ -0,0 +1,120 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type WakeWord struct {
|
||||
*API
|
||||
names []string
|
||||
send int
|
||||
|
||||
Detection string
|
||||
}
|
||||
|
||||
func DialWakeWord(rawURL string) (*WakeWord, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api, err := DialAPI(u.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := u.Query()["name"]
|
||||
if len(names) == 0 {
|
||||
names = []string{"ok_nabu_v0.1"}
|
||||
}
|
||||
|
||||
wake := &WakeWord{API: api, names: names}
|
||||
if err = wake.Start(); err != nil {
|
||||
_ = wake.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go wake.handle()
|
||||
return wake, nil
|
||||
}
|
||||
|
||||
func (w *WakeWord) handle() {
|
||||
defer w.Close()
|
||||
|
||||
for {
|
||||
evt, err := w.ReadEvent()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if evt.Type == "detection" {
|
||||
var data struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err = json.Unmarshal([]byte(evt.Data), &data); err != nil {
|
||||
return
|
||||
}
|
||||
w.Detection = data.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//func (w *WakeWord) Describe() error {
|
||||
// if err := w.WriteEvent(&Event{Type: "describe"}); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// evt, err := w.ReadEvent()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// var info struct {
|
||||
// Wake []struct {
|
||||
// Models []struct {
|
||||
// Name string `json:"name"`
|
||||
// } `json:"models"`
|
||||
// } `json:"wake"`
|
||||
// }
|
||||
// if err = json.Unmarshal(evt.Data, &info); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
//}
|
||||
|
||||
func (w *WakeWord) Start() error {
|
||||
msg := struct {
|
||||
Names []string `json:"names"`
|
||||
}{
|
||||
Names: w.names,
|
||||
}
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
evt := &Event{Type: "detect", Data: string(data)}
|
||||
if err := w.WriteEvent(evt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evt = &Event{Type: "audio-start", Data: audioData(0)}
|
||||
return w.WriteEvent(evt)
|
||||
}
|
||||
|
||||
func (w *WakeWord) Close() error {
|
||||
return w.conn.Close()
|
||||
}
|
||||
|
||||
func (w *WakeWord) WriteChunk(payload []byte) error {
|
||||
evt := &Event{Type: "audio-chunk", Data: audioData(w.send), Payload: payload}
|
||||
w.send += len(payload)
|
||||
return w.WriteEvent(evt)
|
||||
}
|
||||
|
||||
func audioData(send int) string {
|
||||
// timestamp in ms = send / 2 * 1000 / 16000 = send / 32
|
||||
return fmt.Sprintf(`{"rate":16000,"width":2,"channels":1,"timestamp":%d}`, send/32)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func Dial(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Query().Get("backchannel") != "1" {
|
||||
return newProducer(conn), nil
|
||||
} else {
|
||||
return newBackchannel(conn), nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user