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

@@ -53,9 +53,12 @@ type AudioStreamEvent struct {
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()
if len(a) == frameSize*AudioChannels && client.AudioEncoderStereo != nil {
encoder = client.AudioEncoderStereo
} else if client.IsStereoEncoderEnabled() && client.AudioEncoderStereo != nil {
encoder = client.AudioEncoderStereo encoder = client.AudioEncoderStereo
} }
if encoder == nil { if encoder == nil {

View File

@@ -7,8 +7,9 @@ import (
"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
@@ -55,6 +56,8 @@ type Stream struct {
link gumble.Detacher link gumble.Detacher
deviceSource *openal.CaptureDevice deviceSource *openal.CaptureDevice
sourceFormat openal.Format
sourceChannels int
sourceFrameSize int sourceFrameSize int
micVolume float32 micVolume float32
sourceStop chan bool sourceStop chan bool
@@ -63,8 +66,11 @@ type Stream struct {
contextSink *openal.Context contextSink *openal.Context
noiseProcessor NoiseProcessor noiseProcessor NoiseProcessor
noiseProcessorRight NoiseProcessor
micAGC *audio.AGC micAGC *audio.AGC
micAGCRight *audio.AGC
effectsProcessor EffectsProcessor effectsProcessor EffectsProcessor
effectsProcessorRight EffectsProcessor
filePlayer FilePlayer filePlayer FilePlayer
} }
@@ -74,8 +80,14 @@ func New(client *gumble.Client, inputDevice *string, outputDevice *string, 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
idev := openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, inputFormat, uint32(frmsz))
if idev == nil {
inputFormat = openal.FormatMono16
sourceChannels = 1
idev = openal.CaptureOpenDevice(*inputDevice, gumble.AudioSampleRate, inputFormat, uint32(frmsz))
}
if idev == nil { if idev == nil {
return nil, ErrInputDevice return nil, ErrInputDevice
} }
@@ -94,10 +106,15 @@ func New(client *gumble.Client, inputDevice *string, outputDevice *string, test
s := &Stream{ s := &Stream{
client: client, client: client,
sourceFormat: inputFormat,
sourceChannels: sourceChannels,
sourceFrameSize: frmsz, sourceFrameSize: frmsz,
micVolume: 1.0, micVolume: 1.0,
micAGC: audio.NewAGC(), // Always enable AGC for outgoing mic 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 {
@@ -124,10 +141,12 @@ func (s *Stream) AttachStream(client *gumble.Client) {
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 {
@@ -142,7 +161,6 @@ 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()
@@ -338,8 +356,15 @@ func (s *Stream) sourceRoutine(inputDevice *string) {
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)
@@ -355,15 +380,15 @@ func (s *Stream) sourceRoutine(inputDevice *string) {
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)
@@ -371,19 +396,10 @@ func (s *Stream) sourceRoutine(inputDevice *string) {
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)
if s.micAGC != nil {
s.micAGC.ProcessSamples(int16Buffer)
}
// Apply voice effects if available and enabled
if s.effectsProcessor != nil && s.effectsProcessor.IsEnabled() {
s.effectsProcessor.ProcessSamples(int16Buffer)
} }
} }
@@ -400,26 +416,49 @@ func (s *Stream) sourceRoutine(inputDevice *string) {
outputBuffer = make([]int16, frameSize*2) outputBuffer = make([]int16, frameSize*2)
if hasMicInput { if hasMicInput {
// Mix mono mic with stereo file if s.sourceChannels == 2 {
// Mix stereo mic with stereo file
for i := 0; i < frameSize; i++ { for i := 0; i < frameSize; i++ {
if i*2+1 < len(fileAudio) { idx := i * 2
// Left channel: mic + file left if idx+1 < len(fileAudio) {
left := int32(int16Buffer[i]) + int32(fileAudio[i*2]) left := int32(int16Buffer[idx]) + int32(fileAudio[idx])
if left > 32767 { if left > 32767 {
left = 32767 left = 32767
} else if left < -32768 { } else if left < -32768 {
left = -32768 left = -32768
} }
outputBuffer[i*2] = int16(left) outputBuffer[idx] = int16(left)
// Right channel: mic + file right right := int32(int16Buffer[idx+1]) + int32(fileAudio[idx+1])
right := int32(int16Buffer[i]) + int32(fileAudio[i*2+1])
if right > 32767 { if right > 32767 {
right = 32767 right = 32767
} else if right < -32768 { } else if right < -32768 {
right = -32768 right = -32768
} }
outputBuffer[i*2+1] = int16(right) 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)
right := int32(int16Buffer[i]) + int32(fileAudio[idx+1])
if right > 32767 {
right = 32767
} else if right < -32768 {
right = -32768
}
outputBuffer[idx+1] = int16(right)
}
} }
} }
} else { } else {
@@ -434,9 +473,114 @@ func (s *Stream) sourceRoutine(inputDevice *string) {
// Send stereo buffer when file is playing // Send stereo buffer when file is playing
outgoing <- gumble.AudioBuffer(outputBuffer) outgoing <- gumble.AudioBuffer(outputBuffer)
} else if hasMicInput { } else if hasMicInput {
// Send mono mic when no file is playing // Send mic when no file is playing
outgoing <- gumble.AudioBuffer(int16Buffer) outgoing <- gumble.AudioBuffer(int16Buffer)
} }
} }
} }
} }
func (s *Stream) processMonoSamples(samples []int16) {
s.processChannel(samples, s.noiseProcessor, s.micAGC, s.effectsProcessor)
}
func (s *Stream) processStereoSamples(samples []int16, frameSize int) {
if frameSize == 0 || len(samples) < frameSize*2 {
return
}
s.ensureStereoProcessors()
s.syncStereoProcessors()
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
}