Support for stereo mic.

This commit is contained in:
Storm Dragon
2026-02-09 22:33:17 -05:00
parent cfbefd3f7d
commit e3b6eac2a0
2 changed files with 536 additions and 389 deletions

View File

@@ -1,35 +1,35 @@
package gumble package gumble
import ( import (
"time" "time"
) )
const ( const (
// AudioSampleRate is the audio sample rate (in hertz) for incoming and // AudioSampleRate is the audio sample rate (in hertz) for incoming and
// outgoing audio. // outgoing audio.
AudioSampleRate = 48000 AudioSampleRate = 48000
// AudioDefaultInterval is the default interval that audio packets are sent // AudioDefaultInterval is the default interval that audio packets are sent
// at. // at.
AudioDefaultInterval = 10 * time.Millisecond AudioDefaultInterval = 10 * time.Millisecond
// AudioMonoChannels is the number of channels used for voice transmission // AudioMonoChannels is the number of channels used for voice transmission
AudioMonoChannels = 1 AudioMonoChannels = 1
// AudioChannels is the number of channels used for playback // AudioChannels is the number of channels used for playback
AudioChannels = 2 AudioChannels = 2
// AudioDefaultFrameSize is the number of audio frames that should be sent in // AudioDefaultFrameSize is the number of audio frames that should be sent in
// a 10ms window (mono samples) // a 10ms window (mono samples)
AudioDefaultFrameSize = AudioSampleRate / 100 AudioDefaultFrameSize = AudioSampleRate / 100
// AudioMaximumFrameSize is the maximum audio frame size from another user // AudioMaximumFrameSize is the maximum audio frame size from another user
// that will be processed (accounting for stereo) // that will be processed (accounting for stereo)
AudioMaximumFrameSize = (AudioSampleRate / 1000 * 60) * AudioChannels AudioMaximumFrameSize = (AudioSampleRate / 1000 * 60) * AudioChannels
// AudioDefaultDataBytes is the default number of bytes that an audio frame // AudioDefaultDataBytes is the default number of bytes that an audio frame
// can use. // can use.
AudioDefaultDataBytes = 40 AudioDefaultDataBytes = 40
) )
// AudioListener is the interface that must be implemented by types wishing to // AudioListener is the interface that must be implemented by types wishing to
@@ -39,52 +39,55 @@ const (
// implementer's responsibility to continuously process AudioStreamEvent.C // implementer's responsibility to continuously process AudioStreamEvent.C
// until it is closed. // until it is closed.
type AudioListener interface { type AudioListener interface {
OnAudioStream(e *AudioStreamEvent) OnAudioStream(e *AudioStreamEvent)
} }
// AudioStreamEvent is event that is passed to AudioListener.OnAudioStream. // AudioStreamEvent is event that is passed to AudioListener.OnAudioStream.
type AudioStreamEvent struct { type AudioStreamEvent struct {
Client *Client Client *Client
User *User User *User
C <-chan *AudioPacket C <-chan *AudioPacket
} }
// AudioBuffer is a slice of PCM audio samples. // AudioBuffer is a slice of PCM audio samples.
type AudioBuffer []int16 type AudioBuffer []int16
func (a AudioBuffer) writeAudio(client *Client, seq int64, final bool) error { func (a AudioBuffer) writeAudio(client *Client, seq int64, final bool) error {
// Choose encoder based on whether stereo is enabled // Choose encoder based on whether buffer size indicates stereo or mono
encoder := client.AudioEncoder encoder := client.AudioEncoder
if client.IsStereoEncoderEnabled() && client.AudioEncoderStereo != nil { frameSize := client.Config.AudioFrameSize()
encoder = client.AudioEncoderStereo if len(a) == frameSize*AudioChannels && client.AudioEncoderStereo != nil {
} encoder = client.AudioEncoderStereo
if encoder == nil { } else if client.IsStereoEncoderEnabled() && client.AudioEncoderStereo != nil {
return nil encoder = client.AudioEncoderStereo
} }
dataBytes := client.Config.AudioDataBytes if encoder == nil {
raw, err := encoder.Encode(a, len(a), dataBytes) return nil
if final { }
defer encoder.Reset() dataBytes := client.Config.AudioDataBytes
} raw, err := encoder.Encode(a, len(a), dataBytes)
if err != nil { if final {
return err defer encoder.Reset()
} }
if err != nil {
return err
}
var targetID byte var targetID byte
if target := client.VoiceTarget; target != nil { if target := client.VoiceTarget; target != nil {
targetID = byte(target.ID) targetID = byte(target.ID)
} }
return client.Conn.WriteAudio(byte(4), targetID, seq, final, raw, nil, nil, nil) return client.Conn.WriteAudio(byte(4), targetID, seq, final, raw, nil, nil, nil)
} }
// AudioPacket contains incoming audio samples and information. // AudioPacket contains incoming audio samples and information.
type AudioPacket struct { type AudioPacket struct {
Client *Client Client *Client
Sender *User Sender *User
Target *VoiceTarget Target *VoiceTarget
AudioBuffer AudioBuffer
HasPosition bool HasPosition bool
X, Y, Z float32 X, Y, Z float32
} }

View File

@@ -1,442 +1,586 @@
package gumbleopenal package gumbleopenal
import ( import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"os/exec" "os/exec"
"time" "time"
"git.stormux.org/storm/barnard/audio" "git.stormux.org/storm/barnard/audio"
"git.stormux.org/storm/barnard/gumble/gumble" "git.stormux.org/storm/barnard/gumble/go-openal/openal"
"git.stormux.org/storm/barnard/gumble/go-openal/openal" "git.stormux.org/storm/barnard/gumble/gumble"
"git.stormux.org/storm/barnard/noise"
) )
// NoiseProcessor interface for noise suppression // NoiseProcessor interface for noise suppression
type NoiseProcessor interface { type NoiseProcessor interface {
ProcessSamples(samples []int16) ProcessSamples(samples []int16)
IsEnabled() bool IsEnabled() bool
} }
// EffectsProcessor interface for voice effects // EffectsProcessor interface for voice effects
type EffectsProcessor interface { type EffectsProcessor interface {
ProcessSamples(samples []int16) ProcessSamples(samples []int16)
IsEnabled() bool IsEnabled() bool
} }
// FilePlayer interface for file playback // FilePlayer interface for file playback
type FilePlayer interface { type FilePlayer interface {
GetAudioFrame() []int16 GetAudioFrame() []int16
IsPlaying() bool IsPlaying() bool
} }
const ( const (
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4) maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
) )
var ( var (
ErrState = errors.New("gumbleopenal: invalid state") ErrState = errors.New("gumbleopenal: invalid state")
ErrMic = errors.New("gumbleopenal: microphone disconnected or misconfigured") ErrMic = errors.New("gumbleopenal: microphone disconnected or misconfigured")
ErrInputDevice = errors.New("gumbleopenal: invalid input device or parameters") ErrInputDevice = errors.New("gumbleopenal: invalid input device or parameters")
ErrOutputDevice = errors.New("gumbleopenal: invalid output device or parameters") ErrOutputDevice = errors.New("gumbleopenal: invalid output device or parameters")
) )
func beep() { func beep() {
cmd := exec.Command("beep") cmd := exec.Command("beep")
cmdout, err := cmd.Output() cmdout, err := cmd.Output()
if err != nil { if err != nil {
panic(err) panic(err)
} }
if cmdout != nil { if cmdout != nil {
} }
} }
type Stream struct { type Stream struct {
client *gumble.Client client *gumble.Client
link gumble.Detacher link gumble.Detacher
deviceSource *openal.CaptureDevice deviceSource *openal.CaptureDevice
sourceFrameSize int sourceFormat openal.Format
micVolume float32 sourceChannels int
sourceStop chan bool sourceFrameSize int
micVolume float32
sourceStop chan bool
deviceSink *openal.Device deviceSink *openal.Device
contextSink *openal.Context contextSink *openal.Context
noiseProcessor NoiseProcessor noiseProcessor NoiseProcessor
micAGC *audio.AGC noiseProcessorRight NoiseProcessor
effectsProcessor EffectsProcessor micAGC *audio.AGC
filePlayer FilePlayer micAGCRight *audio.AGC
effectsProcessor EffectsProcessor
effectsProcessorRight EffectsProcessor
filePlayer FilePlayer
} }
func New(client *gumble.Client, inputDevice *string, outputDevice *string, test bool) (*Stream, error) { func New(client *gumble.Client, inputDevice *string, outputDevice *string, test bool) (*Stream, error) {
frmsz := 480 frmsz := 480
if !test { if !test {
frmsz = client.Config.AudioFrameSize() frmsz = client.Config.AudioFrameSize()
} }
// Always use mono for input device inputFormat := openal.FormatStereo16
idev := openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, openal.FormatMono16, uint32(frmsz)) sourceChannels := 2
if idev == nil { idev := openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, inputFormat, uint32(frmsz))
return nil, ErrInputDevice if idev == nil {
} inputFormat = openal.FormatMono16
sourceChannels = 1
idev = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, inputFormat, uint32(frmsz))
}
if idev == nil {
return nil, ErrInputDevice
}
odev := openal.OpenDevice(*outputDevice) odev := openal.OpenDevice(*outputDevice)
if odev == nil { if odev == nil {
idev.CaptureCloseDevice() idev.CaptureCloseDevice()
return nil, ErrOutputDevice return nil, ErrOutputDevice
} }
if test { if test {
idev.CaptureCloseDevice() idev.CaptureCloseDevice()
odev.CloseDevice() odev.CloseDevice()
return nil, nil return nil, nil
} }
s := &Stream{ s := &Stream{
client: client, client: client,
sourceFrameSize: frmsz, sourceFormat: inputFormat,
micVolume: 1.0, sourceChannels: sourceChannels,
micAGC: audio.NewAGC(), // Always enable AGC for outgoing mic sourceFrameSize: frmsz,
} micVolume: 1.0,
micAGC: audio.NewAGC(), // Always enable AGC for outgoing mic
}
if sourceChannels == 2 {
s.micAGCRight = audio.NewAGC()
}
s.deviceSource = idev s.deviceSource = idev
if s.deviceSource == nil { if s.deviceSource == nil {
return nil, ErrInputDevice return nil, ErrInputDevice
} }
s.deviceSink = odev s.deviceSink = odev
if s.deviceSink == nil { if s.deviceSink == nil {
return nil, ErrOutputDevice return nil, ErrOutputDevice
} }
s.contextSink = s.deviceSink.CreateContext() s.contextSink = s.deviceSink.CreateContext()
if s.contextSink == nil { if s.contextSink == nil {
s.Destroy() s.Destroy()
return nil, ErrOutputDevice return nil, ErrOutputDevice
} }
s.contextSink.Activate() s.contextSink.Activate()
return s, nil return s, nil
} }
func (s *Stream) AttachStream(client *gumble.Client) { func (s *Stream) AttachStream(client *gumble.Client) {
s.link = client.Config.AttachAudio(s) s.link = client.Config.AttachAudio(s)
} }
func (s *Stream) SetNoiseProcessor(np NoiseProcessor) { func (s *Stream) SetNoiseProcessor(np NoiseProcessor) {
s.noiseProcessor = np s.noiseProcessor = np
s.noiseProcessorRight = cloneNoiseProcessor(np)
} }
func (s *Stream) SetEffectsProcessor(ep EffectsProcessor) { func (s *Stream) SetEffectsProcessor(ep EffectsProcessor) {
s.effectsProcessor = ep s.effectsProcessor = ep
s.effectsProcessorRight = cloneEffectsProcessor(ep)
} }
func (s *Stream) GetEffectsProcessor() EffectsProcessor { func (s *Stream) GetEffectsProcessor() EffectsProcessor {
return s.effectsProcessor return s.effectsProcessor
} }
func (s *Stream) SetFilePlayer(fp FilePlayer) { func (s *Stream) SetFilePlayer(fp FilePlayer) {
s.filePlayer = fp s.filePlayer = fp
} }
func (s *Stream) GetFilePlayer() FilePlayer { func (s *Stream) GetFilePlayer() FilePlayer {
return s.filePlayer return s.filePlayer
} }
func (s *Stream) Destroy() { func (s *Stream) Destroy() {
if s.link != nil { if s.link != nil {
s.link.Detach() s.link.Detach()
} }
if s.deviceSource != nil { if s.deviceSource != nil {
s.StopSource() s.StopSource()
s.deviceSource.CaptureCloseDevice() s.deviceSource.CaptureCloseDevice()
s.deviceSource = nil s.deviceSource = nil
} }
if s.deviceSink != nil { if s.deviceSink != nil {
s.contextSink.Destroy() s.contextSink.Destroy()
s.deviceSink.CloseDevice() s.deviceSink.CloseDevice()
s.contextSink = nil s.contextSink = nil
s.deviceSink = nil s.deviceSink = nil
} }
} }
func (s *Stream) StartSource(inputDevice *string) error { func (s *Stream) StartSource(inputDevice *string) error {
if s.sourceStop != nil { if s.sourceStop != nil {
return ErrState return ErrState
} }
if s.deviceSource == nil { if s.deviceSource == nil {
return ErrMic return ErrMic
} }
s.deviceSource.CaptureStart() s.deviceSource.CaptureStart()
s.sourceStop = make(chan bool) s.sourceStop = make(chan bool)
go s.sourceRoutine(inputDevice) go s.sourceRoutine(inputDevice)
return nil return nil
} }
func (s *Stream) StopSource() error { func (s *Stream) StopSource() error {
if s.deviceSource == nil { if s.deviceSource == nil {
return ErrMic return ErrMic
} }
s.deviceSource.CaptureStop() s.deviceSource.CaptureStop()
if s.sourceStop == nil { if s.sourceStop == nil {
return ErrState return ErrState
} }
close(s.sourceStop) close(s.sourceStop)
s.sourceStop = nil s.sourceStop = nil
return nil return nil
} }
func (s *Stream) GetMicVolume() float32 { func (s *Stream) GetMicVolume() float32 {
return s.micVolume return s.micVolume
} }
func (s *Stream) SetMicVolume(change float32, relative bool) { func (s *Stream) SetMicVolume(change float32, relative bool) {
var val float32 var val float32
if relative { if relative {
val = s.GetMicVolume() + change val = s.GetMicVolume() + change
} else { } else {
val = change val = change
} }
if val >= 1 { if val >= 1 {
val = 1.0 val = 1.0
} }
if val <= 0 { if val <= 0 {
val = 0 val = 0
} }
s.micVolume = val s.micVolume = val
} }
func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
go func(e *gumble.AudioStreamEvent) { go func(e *gumble.AudioStreamEvent) {
var source = openal.NewSource() var source = openal.NewSource()
e.User.AudioSource = &source e.User.AudioSource = &source
// Set initial gain based on volume and mute state
if e.User.LocallyMuted {
e.User.AudioSource.SetGain(0)
} else {
e.User.AudioSource.SetGain(e.User.Volume)
}
bufferCount := e.Client.Config.Buffers // Set initial gain based on volume and mute state
if bufferCount < 64 { if e.User.LocallyMuted {
bufferCount = 64 e.User.AudioSource.SetGain(0)
} } else {
emptyBufs := openal.NewBuffers(bufferCount) e.User.AudioSource.SetGain(e.User.Volume)
}
reclaim := func() {
if n := source.BuffersProcessed(); n > 0 {
reclaimedBufs := make(openal.Buffers, n)
source.UnqueueBuffers(reclaimedBufs)
emptyBufs = append(emptyBufs, reclaimedBufs...)
}
}
var raw [maxBufferSize]byte bufferCount := e.Client.Config.Buffers
if bufferCount < 64 {
for packet := range e.C { bufferCount = 64
// Skip processing if user is locally muted }
if e.User.LocallyMuted { emptyBufs := openal.NewBuffers(bufferCount)
continue
}
var boost uint16 = uint16(1) reclaim := func() {
samples := len(packet.AudioBuffer) if n := source.BuffersProcessed(); n > 0 {
if samples > cap(raw)/2 { reclaimedBufs := make(openal.Buffers, n)
continue source.UnqueueBuffers(reclaimedBufs)
} emptyBufs = append(emptyBufs, reclaimedBufs...)
}
boost = e.User.Boost }
// Check if sample count suggests stereo data var raw [maxBufferSize]byte
isStereo := samples > gumble.AudioDefaultFrameSize && samples%2 == 0
format := openal.FormatMono16
if isStereo {
format = openal.FormatStereo16
samples = samples / 2
}
rawPtr := 0 for packet := range e.C {
if isStereo { // Skip processing if user is locally muted
// Process stereo samples as pairs if e.User.LocallyMuted {
for i := 0; i < samples*2; i += 2 { continue
// Process left channel with saturation protection }
sample := packet.AudioBuffer[i]
if boost > 1 {
boosted := int32(sample) * int32(boost)
if boosted > 32767 {
sample = 32767
} else if boosted < -32767 {
sample = -32767
} else {
sample = int16(boosted)
}
}
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
rawPtr += 2
// Process right channel with saturation protection var boost uint16 = uint16(1)
sample = packet.AudioBuffer[i+1] samples := len(packet.AudioBuffer)
if boost > 1 { if samples > cap(raw)/2 {
boosted := int32(sample) * int32(boost) continue
if boosted > 32767 { }
sample = 32767
} else if boosted < -32767 {
sample = -32767
} else {
sample = int16(boosted)
}
}
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
rawPtr += 2
}
} else {
// Process mono samples with saturation protection
for i := 0; i < samples; i++ {
sample := packet.AudioBuffer[i]
if boost > 1 {
boosted := int32(sample) * int32(boost)
if boosted > 32767 {
sample = 32767
} else if boosted < -32767 {
sample = -32767
} else {
sample = int16(boosted)
}
}
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
rawPtr += 2
}
}
reclaim() boost = e.User.Boost
if len(emptyBufs) == 0 {
continue
}
last := len(emptyBufs) - 1 // Check if sample count suggests stereo data
buffer := emptyBufs[last] isStereo := samples > gumble.AudioDefaultFrameSize && samples%2 == 0
emptyBufs = emptyBufs[:last] format := openal.FormatMono16
if isStereo {
format = openal.FormatStereo16
samples = samples / 2
}
buffer.SetData(format, raw[:rawPtr], gumble.AudioSampleRate) rawPtr := 0
source.QueueBuffer(buffer) if isStereo {
// Process stereo samples as pairs
for i := 0; i < samples*2; i += 2 {
// Process left channel with saturation protection
sample := packet.AudioBuffer[i]
if boost > 1 {
boosted := int32(sample) * int32(boost)
if boosted > 32767 {
sample = 32767
} else if boosted < -32767 {
sample = -32767
} else {
sample = int16(boosted)
}
}
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
rawPtr += 2
if source.State() != openal.Playing { // Process right channel with saturation protection
source.Play() sample = packet.AudioBuffer[i+1]
} if boost > 1 {
} boosted := int32(sample) * int32(boost)
reclaim() if boosted > 32767 {
emptyBufs.Delete() sample = 32767
source.Delete() } else if boosted < -32767 {
}(e) sample = -32767
} else {
sample = int16(boosted)
}
}
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
rawPtr += 2
}
} else {
// Process mono samples with saturation protection
for i := 0; i < samples; i++ {
sample := packet.AudioBuffer[i]
if boost > 1 {
boosted := int32(sample) * int32(boost)
if boosted > 32767 {
sample = 32767
} else if boosted < -32767 {
sample = -32767
} else {
sample = int16(boosted)
}
}
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]
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) { func (s *Stream) sourceRoutine(inputDevice *string) {
interval := s.client.Config.AudioInterval interval := s.client.Config.AudioInterval
frameSize := s.client.Config.AudioFrameSize() frameSize := s.client.Config.AudioFrameSize()
if frameSize != s.sourceFrameSize { if frameSize != s.sourceFrameSize {
s.deviceSource.CaptureCloseDevice() s.deviceSource.CaptureCloseDevice()
s.sourceFrameSize = frameSize s.sourceFrameSize = frameSize
// Always use mono for input s.deviceSource = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, s.sourceFormat, uint32(s.sourceFrameSize))
s.deviceSource = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, openal.FormatMono16, uint32(s.sourceFrameSize)) if s.deviceSource == nil && s.sourceFormat == openal.FormatStereo16 {
} s.sourceFormat = openal.FormatMono16
s.sourceChannels = 1
s.deviceSource = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, s.sourceFormat, uint32(s.sourceFrameSize))
}
}
if s.deviceSource == nil {
return
}
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
stop := s.sourceStop stop := s.sourceStop
outgoing := s.client.AudioOutgoing() outgoing := s.client.AudioOutgoing()
defer close(outgoing) defer close(outgoing)
for { for {
select { select {
case <-stop: case <-stop:
return return
case <-ticker.C: case <-ticker.C:
// Initialize buffer with silence sampleCount := frameSize * s.sourceChannels
int16Buffer := make([]int16, frameSize) int16Buffer := make([]int16, sampleCount)
// Capture microphone if available // Capture microphone if available
hasMicInput := false hasMicInput := false
buff := s.deviceSource.CaptureSamples(uint32(frameSize)) buff := s.deviceSource.CaptureSamples(uint32(frameSize))
if len(buff) == frameSize*2 { if len(buff) == sampleCount*2 {
hasMicInput = true hasMicInput = true
for i := range int16Buffer { for i := 0; i < sampleCount; i++ {
sample := int16(binary.LittleEndian.Uint16(buff[i*2:])) sample := int16(binary.LittleEndian.Uint16(buff[i*2:]))
if s.micVolume != 1.0 { if s.micVolume != 1.0 {
sample = int16(float32(sample) * s.micVolume) sample = int16(float32(sample) * s.micVolume)
} }
int16Buffer[i] = sample int16Buffer[i] = sample
} }
// Apply noise suppression if available and enabled if s.sourceChannels == 1 {
if s.noiseProcessor != nil && s.noiseProcessor.IsEnabled() { s.processMonoSamples(int16Buffer)
s.noiseProcessor.ProcessSamples(int16Buffer) } else {
} s.processStereoSamples(int16Buffer, frameSize)
}
}
// Apply AGC to outgoing microphone audio (always enabled) // Mix with or use file audio if playing
if s.micAGC != nil { hasFileAudio := false
s.micAGC.ProcessSamples(int16Buffer) var outputBuffer []int16
}
// Apply voice effects if available and enabled if s.filePlayer != nil && s.filePlayer.IsPlaying() {
if s.effectsProcessor != nil && s.effectsProcessor.IsEnabled() { fileAudio := s.filePlayer.GetAudioFrame()
s.effectsProcessor.ProcessSamples(int16Buffer) if fileAudio != nil && len(fileAudio) > 0 {
} hasFileAudio = true
} // File audio is stereo - send as stereo when file is playing
// Create stereo buffer (frameSize * 2 channels)
outputBuffer = make([]int16, frameSize*2)
// Mix with or use file audio if playing if hasMicInput {
hasFileAudio := false if s.sourceChannels == 2 {
var outputBuffer []int16 // Mix stereo mic with stereo file
for i := 0; i < frameSize; i++ {
idx := i * 2
if idx+1 < len(fileAudio) {
left := int32(int16Buffer[idx]) + int32(fileAudio[idx])
if left > 32767 {
left = 32767
} else if left < -32768 {
left = -32768
}
outputBuffer[idx] = int16(left)
if s.filePlayer != nil && s.filePlayer.IsPlaying() { right := int32(int16Buffer[idx+1]) + int32(fileAudio[idx+1])
fileAudio := s.filePlayer.GetAudioFrame() if right > 32767 {
if fileAudio != nil && len(fileAudio) > 0 { right = 32767
hasFileAudio = true } else if right < -32768 {
// File audio is stereo - send as stereo when file is playing right = -32768
// Create stereo buffer (frameSize * 2 channels) }
outputBuffer = make([]int16, frameSize*2) outputBuffer[idx+1] = int16(right)
}
}
} else {
// Mix mono mic with stereo file
for i := 0; i < frameSize; i++ {
idx := i * 2
if idx+1 < len(fileAudio) {
left := int32(int16Buffer[i]) + int32(fileAudio[idx])
if left > 32767 {
left = 32767
} else if left < -32768 {
left = -32768
}
outputBuffer[idx] = int16(left)
if hasMicInput { right := int32(int16Buffer[i]) + int32(fileAudio[idx+1])
// Mix mono mic with stereo file if right > 32767 {
for i := 0; i < frameSize; i++ { right = 32767
if i*2+1 < len(fileAudio) { } else if right < -32768 {
// Left channel: mic + file left right = -32768
left := int32(int16Buffer[i]) + int32(fileAudio[i*2]) }
if left > 32767 { outputBuffer[idx+1] = int16(right)
left = 32767 }
} else if left < -32768 { }
left = -32768 }
} } else {
outputBuffer[i*2] = int16(left) // Use file audio only (already stereo)
copy(outputBuffer, fileAudio[:frameSize*2])
}
}
}
// Right channel: mic + file right // Determine what to send
right := int32(int16Buffer[i]) + int32(fileAudio[i*2+1]) if hasFileAudio {
if right > 32767 { // Send stereo buffer when file is playing
right = 32767 outgoing <- gumble.AudioBuffer(outputBuffer)
} else if right < -32768 { } else if hasMicInput {
right = -32768 // Send mic when no file is playing
} outgoing <- gumble.AudioBuffer(int16Buffer)
outputBuffer[i*2+1] = int16(right) }
} }
} }
} else { }
// Use file audio only (already stereo)
copy(outputBuffer, fileAudio[:frameSize*2]) func (s *Stream) processMonoSamples(samples []int16) {
} s.processChannel(samples, s.noiseProcessor, s.micAGC, s.effectsProcessor)
} }
}
func (s *Stream) processStereoSamples(samples []int16, frameSize int) {
// Determine what to send if frameSize == 0 || len(samples) < frameSize*2 {
if hasFileAudio { return
// Send stereo buffer when file is playing }
outgoing <- gumble.AudioBuffer(outputBuffer)
} else if hasMicInput { s.ensureStereoProcessors()
// Send mono mic when no file is playing s.syncStereoProcessors()
outgoing <- gumble.AudioBuffer(int16Buffer)
} left := make([]int16, frameSize)
} right := make([]int16, frameSize)
}
for i := 0; i < frameSize; i++ {
idx := i * 2
left[i] = samples[idx]
right[i] = samples[idx+1]
}
s.processChannel(left, s.noiseProcessor, s.micAGC, s.effectsProcessor)
s.processChannel(right, s.noiseProcessorRight, s.micAGCRight, s.effectsProcessorRight)
for i := 0; i < frameSize; i++ {
idx := i * 2
samples[idx] = left[i]
samples[idx+1] = right[i]
}
}
func (s *Stream) processChannel(samples []int16, noiseProcessor NoiseProcessor, micAGC *audio.AGC, effectsProcessor EffectsProcessor) {
if noiseProcessor != nil && noiseProcessor.IsEnabled() {
noiseProcessor.ProcessSamples(samples)
}
if micAGC != nil {
micAGC.ProcessSamples(samples)
}
if effectsProcessor != nil && effectsProcessor.IsEnabled() {
effectsProcessor.ProcessSamples(samples)
}
}
func (s *Stream) ensureStereoProcessors() {
if s.micAGCRight == nil {
s.micAGCRight = audio.NewAGC()
}
if s.noiseProcessorRight == nil {
s.noiseProcessorRight = cloneNoiseProcessor(s.noiseProcessor)
}
if s.effectsProcessorRight == nil {
s.effectsProcessorRight = cloneEffectsProcessor(s.effectsProcessor)
}
}
func (s *Stream) syncStereoProcessors() {
leftSuppressor, leftOk := s.noiseProcessor.(*noise.Suppressor)
rightSuppressor, rightOk := s.noiseProcessorRight.(*noise.Suppressor)
if leftOk && rightOk {
if leftSuppressor.IsEnabled() != rightSuppressor.IsEnabled() {
rightSuppressor.SetEnabled(leftSuppressor.IsEnabled())
}
if leftSuppressor.GetThreshold() != rightSuppressor.GetThreshold() {
rightSuppressor.SetThreshold(leftSuppressor.GetThreshold())
}
}
leftEffects, leftOk := s.effectsProcessor.(*audio.EffectsProcessor)
rightEffects, rightOk := s.effectsProcessorRight.(*audio.EffectsProcessor)
if leftOk && rightOk {
if leftEffects.IsEnabled() != rightEffects.IsEnabled() {
rightEffects.SetEnabled(leftEffects.IsEnabled())
}
if leftEffects.GetCurrentEffect() != rightEffects.GetCurrentEffect() {
rightEffects.SetEffect(leftEffects.GetCurrentEffect())
}
}
}
func cloneNoiseProcessor(np NoiseProcessor) NoiseProcessor {
if np == nil {
return nil
}
if suppressor, ok := np.(*noise.Suppressor); ok {
clone := noise.NewSuppressor()
clone.SetEnabled(suppressor.IsEnabled())
clone.SetThreshold(suppressor.GetThreshold())
return clone
}
return nil
}
func cloneEffectsProcessor(ep EffectsProcessor) EffectsProcessor {
if ep == nil {
return nil
}
if processor, ok := ep.(*audio.EffectsProcessor); ok {
clone := audio.NewEffectsProcessor(gumble.AudioSampleRate)
clone.SetEnabled(processor.IsEnabled())
clone.SetEffect(processor.GetCurrentEffect())
return clone
}
return nil
} }