audio!!!
This commit is contained in:
parent
d24aed44f5
commit
6592b13c6f
15
README.md
15
README.md
@ -17,14 +17,20 @@ reasonably shouldn't, let me know (or fix it and then let me know!)
|
||||
|
||||
- [x] joining a channel
|
||||
- [x] handling everyone's state
|
||||
- [ ] any of the actual audio stuff
|
||||
- [ ] noise suppression with rnnoise
|
||||
- [x] any of the actual audio stuff
|
||||
- [x] noise suppression with rnnoise
|
||||
- [ ] chat with more than a singular other person, probably
|
||||
|
||||
## building
|
||||
hubbub requires go 1.23 to build. it might run on earlier versions, but i think
|
||||
`slices` mainlined in 1.23 and i can't be bothered to check. run `go build` and
|
||||
copy the binary wherever.
|
||||
|
||||
you also need to have the following headers:
|
||||
- rnnoise
|
||||
- portaudio
|
||||
- opus
|
||||
|
||||
## usage
|
||||
hubbub needs a TOML config file, either passed using `--config` or at
|
||||
`.config/hubbub/config.toml`:
|
||||
@ -40,8 +46,9 @@ note that the nick is different than the username: if you're already logged onto
|
||||
irc you won't be able to use that nick. you can also pass a different server or
|
||||
port but you really don't have a reason to.
|
||||
|
||||
i'm writing this part about audio way in advance. i'll probably use alsa? either
|
||||
alsa or pipewire if i can be bothered with pipewire.
|
||||
for audio, this uses portaudio, so as long as you have it installed and you have
|
||||
either pipewire or alsa or jack or pulseaudio, it'll work. JACK might spit out
|
||||
some ugly stuff to stderr on launch, i'm working on fixing that.
|
||||
|
||||
here's the controls:
|
||||
```
|
||||
|
||||
140
audio/audio.go
Normal file
140
audio/audio.go
Normal file
@ -0,0 +1,140 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"math"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ergochat/irc-go/ircevent"
|
||||
"github.com/gordonklaus/portaudio"
|
||||
"gopkg.in/hraban/opus.v2"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo pkg-config: rnnoise
|
||||
#include <rnnoise.h>
|
||||
|
||||
DenoiseState *rn_setup() {
|
||||
return rnnoise_create(NULL);
|
||||
}
|
||||
|
||||
void rn_cleanup(DenoiseState *st) {
|
||||
rnnoise_destroy(st);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const sampleRate = 24000
|
||||
const channels = 1
|
||||
|
||||
type Audio struct {
|
||||
Muted bool
|
||||
Deafened bool
|
||||
OutBuffer chan string
|
||||
}
|
||||
|
||||
func NewAudio() Audio {
|
||||
return Audio{
|
||||
Muted: false,
|
||||
Deafened: false,
|
||||
OutBuffer: make(chan string),
|
||||
}
|
||||
}
|
||||
|
||||
func (au *Audio) ProcessInput(conn *ircevent.Connection, channel string) error {
|
||||
st := C.rn_setup()
|
||||
defer C.rn_cleanup(st)
|
||||
|
||||
enc, err := opus.NewEncoder(sampleRate, channels, opus.AppVoIP)
|
||||
if err != nil {
|
||||
log.Printf("error creating opus encoder: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
dec, err := opus.NewDecoder(sampleRate, channels)
|
||||
if err != nil {
|
||||
log.Printf("error creating opus decoder: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
portaudio.Initialize()
|
||||
defer portaudio.Terminate()
|
||||
inChan := make(chan []float32, 8)
|
||||
stream, err := portaudio.OpenDefaultStream(channels, channels, sampleRate, 480, func(in, out []float32) {
|
||||
if !au.Muted {
|
||||
frame := make([]float32, len(in))
|
||||
copy(frame, in)
|
||||
|
||||
select {
|
||||
case inChan <- frame:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
empty := make([]float32, len(out))
|
||||
if !au.Deafened {
|
||||
select {
|
||||
case str := <-au.OutBuffer:
|
||||
raw, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if _, err = dec.DecodeFloat32(raw, out); err != nil {
|
||||
break
|
||||
}
|
||||
default:
|
||||
copy(out, empty)
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("error opening input stream: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer stream.Close()
|
||||
if err = stream.Start(); err != nil {
|
||||
log.Printf("error starting input stream: %v", err)
|
||||
return err
|
||||
}
|
||||
defer stream.Stop()
|
||||
|
||||
data := make([]byte, 480)
|
||||
debounce := 0
|
||||
for frame := range inChan {
|
||||
C.rnnoise_process_frame(
|
||||
st,
|
||||
(*C.float)(unsafe.Pointer(&frame[0])),
|
||||
(*C.float)(unsafe.Pointer(&frame[0])),
|
||||
)
|
||||
|
||||
if rms(frame) < 0.02 {
|
||||
debounce++
|
||||
if debounce >= 10 {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
debounce = 0
|
||||
}
|
||||
|
||||
n, err := enc.EncodeFloat32(frame, data)
|
||||
if err != nil {
|
||||
log.Printf("error encoding opus data: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
str := base64.StdEncoding.EncodeToString(data[:n])
|
||||
conn.Privmsg(channel, str)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rms(frame []float32) float64 {
|
||||
var sum float32
|
||||
for _, s := range frame {
|
||||
sum += s * s
|
||||
}
|
||||
return math.Sqrt(float64(sum / float32(len(frame))))
|
||||
}
|
||||
2
go.mod
2
go.mod
@ -23,6 +23,7 @@ require (
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@ -33,6 +34,7 @@ require (
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/ergochat/irc-go v0.4.0
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/gordonklaus/portaudio v0.0.0-20250206071425-98a94950218b
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@ -22,6 +22,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gordonklaus/portaudio v0.0.0-20250206071425-98a94950218b h1:WEuQWBxelOGHA6z9lABqaMLMrfwVyMdN3UgRLT+YUPo=
|
||||
github.com/gordonklaus/portaudio v0.0.0-20250206071425-98a94950218b/go.mod h1:esZFQEUwqC+l76f2R8bIWSwXMaPbp79PppwZ1eJhFco=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@ -80,3 +82,5 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM=
|
||||
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g=
|
||||
|
||||
28
main.go
28
main.go
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.tilde.town/nbsp/hubbub/audio"
|
||||
"git.tilde.town/nbsp/hubbub/config"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@ -58,6 +59,7 @@ type model struct {
|
||||
mute bool
|
||||
deafen bool
|
||||
conn *Conn
|
||||
au *audio.Audio
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
@ -80,6 +82,8 @@ func (m *model) setMute(is bool) {
|
||||
un = "UN"
|
||||
}
|
||||
m.conn.Privmsg(m.channel, un+"MUTE")
|
||||
|
||||
m.au.Muted = m.mute || m.deafen
|
||||
}
|
||||
func (m *model) setDeafen(is bool) {
|
||||
m.deafen = is
|
||||
@ -91,6 +95,9 @@ func (m *model) setDeafen(is bool) {
|
||||
un = "UN"
|
||||
}
|
||||
m.conn.Privmsg(m.channel, un+"DEAFEN")
|
||||
|
||||
m.au.Muted = m.mute || m.deafen
|
||||
m.au.Deafened = m.deafen
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
@ -117,6 +124,7 @@ func (m model) Init() tea.Cmd {
|
||||
u.isMuted = false
|
||||
default:
|
||||
u.lastSpoke = time.Now()
|
||||
m.au.OutBuffer <- message
|
||||
}
|
||||
|
||||
m.users[e.Nick()] = u
|
||||
@ -221,19 +229,19 @@ func (m model) View() (s string) {
|
||||
func start(c *cli.Context) error {
|
||||
config, err := config.LoadConfigFile(c.String("config"))
|
||||
if err != nil {
|
||||
log.Fatalf("unable to read config file: %v", err)
|
||||
log.Printf("unable to read config file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := os.ReadFile(os.ExpandEnv(strings.Replace(config.Privkey, "~", "$HOME", 1)))
|
||||
if err != nil {
|
||||
log.Fatalf("unable to read private key: %v", err)
|
||||
log.Printf("unable to read private key: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(key)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to parse private key: %v", err)
|
||||
log.Printf("unable to parse private key: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -245,7 +253,7 @@ func start(c *cli.Context) error {
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // XXX
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("unable to connect to ssh: %v", err)
|
||||
log.Printf("unable to connect to ssh: %v", err)
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
@ -260,14 +268,24 @@ func start(c *cli.Context) error {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := conn.Connect(); err != nil {
|
||||
log.Fatalf("unable to connect to irc: %v", err)
|
||||
log.Printf("unable to connect to irc: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
au := audio.NewAudio()
|
||||
go func() {
|
||||
if err := au.ProcessInput(&conn.Connection, config.Channel); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
p := tea.NewProgram(model{
|
||||
channel: config.Channel,
|
||||
nick: config.Nick,
|
||||
conn: conn,
|
||||
au: &au,
|
||||
users: map[string]user{},
|
||||
notifications: []notification{},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user