301 lines
8.0 KiB
Go
301 lines
8.0 KiB
Go
|
package gumbleopenal
|
||
|
|
||
|
import (
|
||
|
"encoding/binary"
|
||
|
"errors"
|
||
|
"os/exec"
|
||
|
"time"
|
||
|
|
||
|
"git.2mb.codes/~cmb/barnard/gumble/gumble"
|
||
|
"git.2mb.codes/~cmb/go-openal/openal"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
ErrState = errors.New("gumbleopenal: invalid state")
|
||
|
ErrMic = errors.New("gumbleopenal: microphone disconnected or misconfigured")
|
||
|
ErrInputDevice = errors.New("gumbleopenal: invalid input device or parameters")
|
||
|
ErrOutputDevice = errors.New("gumbleopenal: invalid output device or parameters")
|
||
|
)
|
||
|
|
||
|
func beep() {
|
||
|
cmd := exec.Command("beep")
|
||
|
cmdout, err := cmd.Output()
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
if cmdout != nil {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type Stream struct {
|
||
|
client *gumble.Client
|
||
|
link gumble.Detacher
|
||
|
|
||
|
deviceSource *openal.CaptureDevice
|
||
|
sourceFrameSize int
|
||
|
micVolume float32
|
||
|
sourceStop chan bool
|
||
|
|
||
|
deviceSink *openal.Device
|
||
|
contextSink *openal.Context
|
||
|
}
|
||
|
|
||
|
func New(client *gumble.Client, inputDevice *string, outputDevice *string, test bool) (*Stream, error) {
|
||
|
frmsz := 480
|
||
|
if !test {
|
||
|
frmsz = client.Config.AudioFrameSize()
|
||
|
}
|
||
|
|
||
|
// Always use mono for input device
|
||
|
idev := openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, openal.FormatMono16, uint32(frmsz))
|
||
|
if idev == nil {
|
||
|
return nil, ErrInputDevice
|
||
|
}
|
||
|
|
||
|
odev := openal.OpenDevice(*outputDevice)
|
||
|
if odev == nil {
|
||
|
idev.CaptureCloseDevice()
|
||
|
return nil, ErrOutputDevice
|
||
|
}
|
||
|
|
||
|
if test {
|
||
|
idev.CaptureCloseDevice()
|
||
|
odev.CloseDevice()
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
s := &Stream{
|
||
|
client: client,
|
||
|
sourceFrameSize: frmsz,
|
||
|
micVolume: 1.0,
|
||
|
}
|
||
|
|
||
|
s.deviceSource = idev
|
||
|
if s.deviceSource == nil {
|
||
|
return nil, ErrInputDevice
|
||
|
}
|
||
|
|
||
|
s.deviceSink = odev
|
||
|
if s.deviceSink == nil {
|
||
|
return nil, ErrOutputDevice
|
||
|
}
|
||
|
s.contextSink = s.deviceSink.CreateContext()
|
||
|
if s.contextSink == nil {
|
||
|
s.Destroy()
|
||
|
return nil, ErrOutputDevice
|
||
|
}
|
||
|
s.contextSink.Activate()
|
||
|
|
||
|
return s, nil
|
||
|
}
|
||
|
|
||
|
func (s *Stream) AttachStream(client *gumble.Client) {
|
||
|
s.link = client.Config.AttachAudio(s)
|
||
|
}
|
||
|
|
||
|
func (s *Stream) Destroy() {
|
||
|
if s.link != nil {
|
||
|
s.link.Detach()
|
||
|
}
|
||
|
if s.deviceSource != nil {
|
||
|
s.StopSource()
|
||
|
s.deviceSource.CaptureCloseDevice()
|
||
|
s.deviceSource = nil
|
||
|
}
|
||
|
if s.deviceSink != nil {
|
||
|
s.contextSink.Destroy()
|
||
|
s.deviceSink.CloseDevice()
|
||
|
s.contextSink = nil
|
||
|
s.deviceSink = nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Stream) StartSource(inputDevice *string) error {
|
||
|
if s.sourceStop != nil {
|
||
|
return ErrState
|
||
|
}
|
||
|
if s.deviceSource == nil {
|
||
|
return ErrMic
|
||
|
}
|
||
|
s.deviceSource.CaptureStart()
|
||
|
s.sourceStop = make(chan bool)
|
||
|
go s.sourceRoutine(inputDevice)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *Stream) StopSource() error {
|
||
|
if s.deviceSource == nil {
|
||
|
return ErrMic
|
||
|
}
|
||
|
s.deviceSource.CaptureStop()
|
||
|
if s.sourceStop == nil {
|
||
|
return ErrState
|
||
|
}
|
||
|
close(s.sourceStop)
|
||
|
s.sourceStop = nil
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *Stream) GetMicVolume() float32 {
|
||
|
return s.micVolume
|
||
|
}
|
||
|
|
||
|
func (s *Stream) SetMicVolume(change float32, relative bool) {
|
||
|
var val float32
|
||
|
if relative {
|
||
|
val = s.GetMicVolume() + change
|
||
|
} else {
|
||
|
val = change
|
||
|
}
|
||
|
if val >= 1 {
|
||
|
val = 1.0
|
||
|
}
|
||
|
if val <= 0 {
|
||
|
val = 0
|
||
|
}
|
||
|
s.micVolume = val
|
||
|
}
|
||
|
|
||
|
func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||
|
go func(e *gumble.AudioStreamEvent) {
|
||
|
var source = openal.NewSource()
|
||
|
e.User.AudioSource = &source
|
||
|
e.User.AudioSource.SetGain(e.User.Volume)
|
||
|
|
||
|
bufferCount := e.Client.Config.Buffers
|
||
|
if bufferCount < 64 {
|
||
|
bufferCount = 64
|
||
|
}
|
||
|
emptyBufs := openal.NewBuffers(bufferCount)
|
||
|
|
||
|
reclaim := func() {
|
||
|
if n := source.BuffersProcessed(); n > 0 {
|
||
|
reclaimedBufs := make(openal.Buffers, n)
|
||
|
source.UnqueueBuffers(reclaimedBufs)
|
||
|
emptyBufs = append(emptyBufs, reclaimedBufs...)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var raw [maxBufferSize]byte
|
||
|
|
||
|
for packet := range e.C {
|
||
|
var boost uint16 = uint16(1)
|
||
|
samples := len(packet.AudioBuffer)
|
||
|
if samples > cap(raw)/2 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
boost = e.User.Boost
|
||
|
|
||
|
// Check if sample count suggests stereo data
|
||
|
// If it's not a multiple of 2, it must be mono
|
||
|
// If it's more than standard frameSize, it's likely stereo
|
||
|
isStereo := samples > gumble.AudioDefaultFrameSize && samples%2 == 0
|
||
|
format := openal.FormatMono16
|
||
|
if isStereo {
|
||
|
format = openal.FormatStereo16
|
||
|
// Adjust samples to represent stereo frame count
|
||
|
samples = samples / 2
|
||
|
}
|
||
|
|
||
|
// Process samples
|
||
|
rawPtr := 0
|
||
|
if isStereo {
|
||
|
// Process stereo samples as pairs
|
||
|
for i := 0; i < samples*2; i += 2 {
|
||
|
// Process left channel
|
||
|
sample := packet.AudioBuffer[i]
|
||
|
if boost > 1 {
|
||
|
sample = int16((int32(sample) * int32(boost)))
|
||
|
}
|
||
|
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
||
|
rawPtr += 2
|
||
|
|
||
|
// Process right channel
|
||
|
sample = packet.AudioBuffer[i+1]
|
||
|
if boost > 1 {
|
||
|
sample = int16((int32(sample) * int32(boost)))
|
||
|
}
|
||
|
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
||
|
rawPtr += 2
|
||
|
}
|
||
|
} else {
|
||
|
// Process mono samples
|
||
|
for i := 0; i < samples; i++ {
|
||
|
sample := packet.AudioBuffer[i]
|
||
|
if boost > 1 {
|
||
|
sample = int16((int32(sample) * int32(boost)))
|
||
|
}
|
||
|
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
||
|
rawPtr += 2
|
||
|
}
|
||
|
}
|
||
|
|
||
|
reclaim()
|
||
|
if len(emptyBufs) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
last := len(emptyBufs) - 1
|
||
|
buffer := emptyBufs[last]
|
||
|
emptyBufs = emptyBufs[:last]
|
||
|
|
||
|
// Set buffer data with correct format
|
||
|
buffer.SetData(format, raw[:rawPtr], gumble.AudioSampleRate)
|
||
|
source.QueueBuffer(buffer)
|
||
|
|
||
|
if source.State() != openal.Playing {
|
||
|
source.Play()
|
||
|
}
|
||
|
}
|
||
|
reclaim()
|
||
|
emptyBufs.Delete()
|
||
|
source.Delete()
|
||
|
}(e)
|
||
|
}
|
||
|
|
||
|
func (s *Stream) sourceRoutine(inputDevice *string) {
|
||
|
interval := s.client.Config.AudioInterval
|
||
|
frameSize := s.client.Config.AudioFrameSize()
|
||
|
|
||
|
if frameSize != s.sourceFrameSize {
|
||
|
s.deviceSource.CaptureCloseDevice()
|
||
|
s.sourceFrameSize = frameSize
|
||
|
// Always use mono for input
|
||
|
s.deviceSource = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, openal.FormatMono16, uint32(s.sourceFrameSize))
|
||
|
}
|
||
|
|
||
|
ticker := time.NewTicker(interval)
|
||
|
defer ticker.Stop()
|
||
|
|
||
|
stop := s.sourceStop
|
||
|
|
||
|
outgoing := s.client.AudioOutgoing()
|
||
|
defer close(outgoing)
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case <-stop:
|
||
|
return
|
||
|
case <-ticker.C:
|
||
|
buff := s.deviceSource.CaptureSamples(uint32(frameSize))
|
||
|
if len(buff) != frameSize*2 {
|
||
|
continue
|
||
|
}
|
||
|
int16Buffer := make([]int16, frameSize)
|
||
|
for i := range int16Buffer {
|
||
|
sample := int16(binary.LittleEndian.Uint16(buff[i*2:]))
|
||
|
if s.micVolume != 1.0 {
|
||
|
sample = int16(float32(sample) * s.micVolume)
|
||
|
}
|
||
|
int16Buffer[i] = sample
|
||
|
}
|
||
|
outgoing <- gumble.AudioBuffer(int16Buffer)
|
||
|
}
|
||
|
}
|
||
|
}
|