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] joining a channel
|
||||||
- [x] handling everyone's state
|
- [x] handling everyone's state
|
||||||
- [ ] any of the actual audio stuff
|
- [x] any of the actual audio stuff
|
||||||
- [ ] noise suppression with rnnoise
|
- [x] noise suppression with rnnoise
|
||||||
|
- [ ] chat with more than a singular other person, probably
|
||||||
|
|
||||||
## building
|
## building
|
||||||
hubbub requires go 1.23 to build. it might run on earlier versions, but i think
|
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
|
`slices` mainlined in 1.23 and i can't be bothered to check. run `go build` and
|
||||||
copy the binary wherever.
|
copy the binary wherever.
|
||||||
|
|
||||||
|
you also need to have the following headers:
|
||||||
|
- rnnoise
|
||||||
|
- portaudio
|
||||||
|
- opus
|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
hubbub needs a TOML config file, either passed using `--config` or at
|
hubbub needs a TOML config file, either passed using `--config` or at
|
||||||
`.config/hubbub/config.toml`:
|
`.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
|
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.
|
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
|
for audio, this uses portaudio, so as long as you have it installed and you have
|
||||||
alsa or pipewire if i can be bothered with pipewire.
|
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:
|
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/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -33,6 +34,7 @@ require (
|
|||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/ergochat/irc-go v0.4.0
|
github.com/ergochat/irc-go v0.4.0
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
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/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // 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/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 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
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=
|
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 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
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/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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.tilde.town/nbsp/hubbub/audio"
|
||||||
"git.tilde.town/nbsp/hubbub/config"
|
"git.tilde.town/nbsp/hubbub/config"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
@ -58,6 +59,7 @@ type model struct {
|
|||||||
mute bool
|
mute bool
|
||||||
deafen bool
|
deafen bool
|
||||||
conn *Conn
|
conn *Conn
|
||||||
|
au *audio.Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
@ -80,6 +82,8 @@ func (m *model) setMute(is bool) {
|
|||||||
un = "UN"
|
un = "UN"
|
||||||
}
|
}
|
||||||
m.conn.Privmsg(m.channel, un+"MUTE")
|
m.conn.Privmsg(m.channel, un+"MUTE")
|
||||||
|
|
||||||
|
m.au.Muted = m.mute || m.deafen
|
||||||
}
|
}
|
||||||
func (m *model) setDeafen(is bool) {
|
func (m *model) setDeafen(is bool) {
|
||||||
m.deafen = is
|
m.deafen = is
|
||||||
@ -91,6 +95,9 @@ func (m *model) setDeafen(is bool) {
|
|||||||
un = "UN"
|
un = "UN"
|
||||||
}
|
}
|
||||||
m.conn.Privmsg(m.channel, un+"DEAFEN")
|
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 {
|
func (m model) Init() tea.Cmd {
|
||||||
@ -117,6 +124,7 @@ func (m model) Init() tea.Cmd {
|
|||||||
u.isMuted = false
|
u.isMuted = false
|
||||||
default:
|
default:
|
||||||
u.lastSpoke = time.Now()
|
u.lastSpoke = time.Now()
|
||||||
|
m.au.OutBuffer <- message
|
||||||
}
|
}
|
||||||
|
|
||||||
m.users[e.Nick()] = u
|
m.users[e.Nick()] = u
|
||||||
@ -221,19 +229,19 @@ func (m model) View() (s string) {
|
|||||||
func start(c *cli.Context) error {
|
func start(c *cli.Context) error {
|
||||||
config, err := config.LoadConfigFile(c.String("config"))
|
config, err := config.LoadConfigFile(c.String("config"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("unable to read config file: %v", err)
|
log.Printf("unable to read config file: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := os.ReadFile(os.ExpandEnv(strings.Replace(config.Privkey, "~", "$HOME", 1)))
|
key, err := os.ReadFile(os.ExpandEnv(strings.Replace(config.Privkey, "~", "$HOME", 1)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("unable to read private key: %v", err)
|
log.Printf("unable to read private key: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
signer, err := ssh.ParsePrivateKey(key)
|
signer, err := ssh.ParsePrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("unable to parse private key: %v", err)
|
log.Printf("unable to parse private key: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +253,7 @@ func start(c *cli.Context) error {
|
|||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // XXX
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // XXX
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("unable to connect to ssh: %v", err)
|
log.Printf("unable to connect to ssh: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
@ -260,14 +268,24 @@ func start(c *cli.Context) error {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.Connect(); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
au := audio.NewAudio()
|
||||||
|
go func() {
|
||||||
|
if err := au.ProcessInput(&conn.Connection, config.Channel); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
p := tea.NewProgram(model{
|
p := tea.NewProgram(model{
|
||||||
channel: config.Channel,
|
channel: config.Channel,
|
||||||
nick: config.Nick,
|
nick: config.Nick,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
|
au: &au,
|
||||||
users: map[string]user{},
|
users: map[string]user{},
|
||||||
notifications: []notification{},
|
notifications: []notification{},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user