Support for stereo mic.
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user