Added /file and /stop commands.
This commit is contained in:
@@ -2,9 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
|
||||
"git.stormux.org/storm/barnard/audio"
|
||||
"git.stormux.org/storm/barnard/config"
|
||||
"git.stormux.org/storm/barnard/fileplayback"
|
||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
|
||||
"git.stormux.org/storm/barnard/noise"
|
||||
@@ -51,6 +53,10 @@ type Barnard struct {
|
||||
|
||||
// Added for voice effects
|
||||
VoiceEffects *audio.EffectsProcessor
|
||||
|
||||
// Added for file playback
|
||||
FileStream *fileplayback.Player
|
||||
FileStreamMutex sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Barnard) StopTransmission() {
|
||||
|
||||
17
client.go
17
client.go
@@ -5,9 +5,11 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/barnard/fileplayback"
|
||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
|
||||
"git.stormux.org/storm/barnard/gumble/gumbleutil"
|
||||
"git.stormux.org/storm/barnard/gumble/opus"
|
||||
)
|
||||
|
||||
func (b *Barnard) start() {
|
||||
@@ -51,6 +53,21 @@ func (b *Barnard) connect(reconnect bool) bool {
|
||||
b.Stream.AttachStream(b.Client)
|
||||
b.Stream.SetNoiseProcessor(b.NoiseSuppressor)
|
||||
b.Stream.SetEffectsProcessor(b.VoiceEffects)
|
||||
|
||||
// Initialize stereo encoder for file playback
|
||||
b.Client.AudioEncoderStereo = opus.NewStereoEncoder()
|
||||
|
||||
// Initialize file player
|
||||
b.FileStreamMutex.Lock()
|
||||
b.FileStream = fileplayback.New(b.Client)
|
||||
b.FileStream.SetErrorFunc(func(err error) {
|
||||
// Disable stereo when file finishes or errors
|
||||
b.Client.DisableStereoEncoder()
|
||||
b.AddOutputLine(fmt.Sprintf("File playback: %s", err.Error()))
|
||||
})
|
||||
b.Stream.SetFilePlayer(b.FileStream)
|
||||
b.FileStreamMutex.Unlock()
|
||||
|
||||
b.Connected = true
|
||||
return true
|
||||
}
|
||||
|
||||
241
fileplayback/player.go
Normal file
241
fileplayback/player.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package fileplayback
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||
"git.stormux.org/storm/barnard/gumble/go-openal/openal"
|
||||
)
|
||||
|
||||
// Player handles file playback and mixing with microphone audio
|
||||
type Player struct {
|
||||
client *gumble.Client
|
||||
filename string
|
||||
audioChan chan gumble.AudioBuffer
|
||||
stopChan chan struct{}
|
||||
mutex sync.Mutex
|
||||
playing bool
|
||||
errorFunc func(error)
|
||||
|
||||
// Local playback
|
||||
localSource *openal.Source
|
||||
localBuffers openal.Buffers
|
||||
}
|
||||
|
||||
// New creates a new file player
|
||||
func New(client *gumble.Client) *Player {
|
||||
return &Player{
|
||||
client: client,
|
||||
audioChan: make(chan gumble.AudioBuffer, 100),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// SetErrorFunc sets the error callback function
|
||||
func (p *Player) SetErrorFunc(f func(error)) {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
p.errorFunc = f
|
||||
}
|
||||
|
||||
func (p *Player) reportError(err error) {
|
||||
p.mutex.Lock()
|
||||
errorFunc := p.errorFunc
|
||||
p.mutex.Unlock()
|
||||
|
||||
if errorFunc != nil {
|
||||
errorFunc(err)
|
||||
}
|
||||
}
|
||||
|
||||
// PlayFile starts playing a file
|
||||
func (p *Player) PlayFile(filename string) error {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
if p.playing {
|
||||
return errors.New("file already playing")
|
||||
}
|
||||
|
||||
p.filename = filename
|
||||
|
||||
// Initialize local playback
|
||||
source := openal.NewSource()
|
||||
p.localSource = &source
|
||||
p.localSource.SetGain(1.0)
|
||||
|
||||
// Create buffers for local playback
|
||||
p.localBuffers = openal.NewBuffers(64)
|
||||
|
||||
// Start the file reading goroutine
|
||||
p.playing = true
|
||||
p.stopChan = make(chan struct{})
|
||||
go p.readFileAudio()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the currently playing file
|
||||
func (p *Player) Stop() error {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
if !p.playing {
|
||||
return errors.New("no file playing")
|
||||
}
|
||||
|
||||
close(p.stopChan)
|
||||
p.playing = false
|
||||
|
||||
// Clean up local playback
|
||||
if p.localSource != nil {
|
||||
p.localSource.Stop()
|
||||
p.localSource.Delete()
|
||||
p.localSource = nil
|
||||
}
|
||||
if p.localBuffers != nil {
|
||||
p.localBuffers.Delete()
|
||||
p.localBuffers = nil
|
||||
}
|
||||
|
||||
// Drain the audio channel
|
||||
for len(p.audioChan) > 0 {
|
||||
<-p.audioChan
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPlaying returns true if a file is currently playing
|
||||
func (p *Player) IsPlaying() bool {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
return p.playing
|
||||
}
|
||||
|
||||
// GetAudioFrame returns the next audio frame from the file, or nil if no file is playing
|
||||
func (p *Player) GetAudioFrame() []int16 {
|
||||
select {
|
||||
case frame := <-p.audioChan:
|
||||
return []int16(frame)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// playLocalAudio plays audio through the local OpenAL source
|
||||
func (p *Player) playLocalAudio(data []byte) {
|
||||
if p.localSource == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Reclaim processed buffers
|
||||
if n := p.localSource.BuffersProcessed(); n > 0 {
|
||||
reclaimedBufs := make(openal.Buffers, n)
|
||||
p.localSource.UnqueueBuffers(reclaimedBufs)
|
||||
p.localBuffers = append(p.localBuffers, reclaimedBufs...)
|
||||
}
|
||||
|
||||
// If we have available buffers, queue more audio
|
||||
if len(p.localBuffers) > 0 {
|
||||
buffer := p.localBuffers[len(p.localBuffers)-1]
|
||||
p.localBuffers = p.localBuffers[:len(p.localBuffers)-1]
|
||||
|
||||
// Set buffer data as stereo
|
||||
buffer.SetData(openal.FormatStereo16, data, gumble.AudioSampleRate)
|
||||
p.localSource.QueueBuffer(buffer)
|
||||
|
||||
// Start playing if not already
|
||||
if p.localSource.State() != openal.Playing {
|
||||
p.localSource.Play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readFileAudio reads audio from the file via ffmpeg
|
||||
func (p *Player) readFileAudio() {
|
||||
interval := p.client.Config.AudioInterval
|
||||
frameSize := p.client.Config.AudioFrameSize()
|
||||
|
||||
// Use stereo output from ffmpeg to preserve stereo files
|
||||
// Add -loglevel error to suppress info messages
|
||||
args := []string{"-loglevel", "error", "-i", p.filename}
|
||||
args = append(args, "-ac", "2", "-ar", strconv.Itoa(gumble.AudioSampleRate), "-f", "s16le", "-")
|
||||
|
||||
cmd := exec.Command("ffmpeg", args...)
|
||||
pipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
p.mutex.Lock()
|
||||
p.playing = false
|
||||
p.mutex.Unlock()
|
||||
p.reportError(errors.New("failed to create ffmpeg pipe: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
p.mutex.Lock()
|
||||
p.playing = false
|
||||
p.mutex.Unlock()
|
||||
p.reportError(errors.New("failed to start ffmpeg: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Stereo has 2 channels, so we need twice the buffer size
|
||||
byteBuffer := make([]byte, frameSize*2*2) // frameSize * 2 channels * 2 bytes per sample
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.stopChan:
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
return
|
||||
case <-ticker.C:
|
||||
n, err := io.ReadFull(pipe, byteBuffer)
|
||||
if err != nil || n != len(byteBuffer) {
|
||||
// File finished playing
|
||||
p.mutex.Lock()
|
||||
p.playing = false
|
||||
// Clean up local playback
|
||||
if p.localSource != nil {
|
||||
p.localSource.Stop()
|
||||
p.localSource.Delete()
|
||||
p.localSource = nil
|
||||
}
|
||||
if p.localBuffers != nil {
|
||||
p.localBuffers.Delete()
|
||||
p.localBuffers = nil
|
||||
}
|
||||
p.mutex.Unlock()
|
||||
cmd.Wait()
|
||||
// Notify that file finished
|
||||
p.reportError(errors.New("file playback finished"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert stereo bytes to int16 buffer
|
||||
int16Buffer := make([]int16, frameSize*2) // stereo
|
||||
for i := 0; i < len(int16Buffer); i++ {
|
||||
int16Buffer[i] = int16(binary.LittleEndian.Uint16(byteBuffer[i*2 : (i+1)*2]))
|
||||
}
|
||||
|
||||
// Play locally through OpenAL
|
||||
p.playLocalAudio(byteBuffer[:n])
|
||||
|
||||
// Send to channel (non-blocking)
|
||||
select {
|
||||
case p.audioChan <- gumble.AudioBuffer(int16Buffer):
|
||||
default:
|
||||
// Channel full, skip this frame
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,11 @@ type AudioStreamEvent struct {
|
||||
type AudioBuffer []int16
|
||||
|
||||
func (a AudioBuffer) writeAudio(client *Client, seq int64, final bool) error {
|
||||
// Choose encoder based on whether stereo is enabled
|
||||
encoder := client.AudioEncoder
|
||||
if client.IsStereoEncoderEnabled() && client.AudioEncoderStereo != nil {
|
||||
encoder = client.AudioEncoderStereo
|
||||
}
|
||||
if encoder == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -59,8 +59,10 @@ type Client struct {
|
||||
ContextActions ContextActions
|
||||
|
||||
// The audio encoder used when sending audio to the server.
|
||||
AudioEncoder AudioEncoder
|
||||
audioCodec AudioCodec
|
||||
AudioEncoder AudioEncoder
|
||||
AudioEncoderStereo AudioEncoder
|
||||
audioCodec AudioCodec
|
||||
useStereoEncoder bool
|
||||
// To whom transmitted audio will be sent. The VoiceTarget must have already
|
||||
// been sent to the server for targeting to work correctly. Setting to nil
|
||||
// will disable voice targeting (i.e. switch back to regular speaking).
|
||||
@@ -287,3 +289,24 @@ func (c *Client) Do(f func()) {
|
||||
func (c *Client) Send(message Message) {
|
||||
message.writeMessage(c)
|
||||
}
|
||||
|
||||
// EnableStereoEncoder switches to stereo encoding for file playback.
|
||||
func (c *Client) EnableStereoEncoder() {
|
||||
c.volatile.Lock()
|
||||
defer c.volatile.Unlock()
|
||||
c.useStereoEncoder = true
|
||||
}
|
||||
|
||||
// DisableStereoEncoder switches back to mono encoding for voice.
|
||||
func (c *Client) DisableStereoEncoder() {
|
||||
c.volatile.Lock()
|
||||
defer c.volatile.Unlock()
|
||||
c.useStereoEncoder = false
|
||||
}
|
||||
|
||||
// IsStereoEncoderEnabled returns true if stereo encoding is currently active.
|
||||
func (c *Client) IsStereoEncoderEnabled() bool {
|
||||
c.volatile.RLock()
|
||||
defer c.volatile.RUnlock()
|
||||
return c.useStereoEncoder
|
||||
}
|
||||
|
||||
@@ -1073,6 +1073,9 @@ func (c *Client) handleCodecVersion(buffer []byte) error {
|
||||
c.volatile.Lock()
|
||||
|
||||
c.AudioEncoder = codec.NewEncoder()
|
||||
// Also create a stereo encoder for file playback
|
||||
// Import the opus package to get NewStereoEncoder
|
||||
c.AudioEncoderStereo = nil // Will be set when needed
|
||||
|
||||
c.volatile.Unlock()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ type EffectsProcessor interface {
|
||||
IsEnabled() bool
|
||||
}
|
||||
|
||||
// FilePlayer interface for file playback
|
||||
type FilePlayer interface {
|
||||
GetAudioFrame() []int16
|
||||
IsPlaying() bool
|
||||
}
|
||||
|
||||
const (
|
||||
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
|
||||
)
|
||||
@@ -59,6 +65,7 @@ type Stream struct {
|
||||
noiseProcessor NoiseProcessor
|
||||
micAGC *audio.AGC
|
||||
effectsProcessor EffectsProcessor
|
||||
filePlayer FilePlayer
|
||||
}
|
||||
|
||||
func New(client *gumble.Client, inputDevice *string, outputDevice *string, test bool) (*Stream, error) {
|
||||
@@ -127,6 +134,14 @@ func (s *Stream) GetEffectsProcessor() EffectsProcessor {
|
||||
return s.effectsProcessor
|
||||
}
|
||||
|
||||
func (s *Stream) SetFilePlayer(fp FilePlayer) {
|
||||
s.filePlayer = fp
|
||||
}
|
||||
|
||||
func (s *Stream) GetFilePlayer() FilePlayer {
|
||||
return s.filePlayer
|
||||
}
|
||||
|
||||
|
||||
func (s *Stream) Destroy() {
|
||||
if s.link != nil {
|
||||
@@ -340,35 +355,88 @@ func (s *Stream) sourceRoutine(inputDevice *string) {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
buff := s.deviceSource.CaptureSamples(uint32(frameSize))
|
||||
if len(buff) != frameSize*2 {
|
||||
continue
|
||||
}
|
||||
// Initialize buffer with silence
|
||||
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)
|
||||
|
||||
// Capture microphone if available
|
||||
hasMicInput := false
|
||||
buff := s.deviceSource.CaptureSamples(uint32(frameSize))
|
||||
if len(buff) == frameSize*2 {
|
||||
hasMicInput = true
|
||||
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
|
||||
}
|
||||
|
||||
// Apply noise suppression if available and enabled
|
||||
if s.noiseProcessor != nil && s.noiseProcessor.IsEnabled() {
|
||||
s.noiseProcessor.ProcessSamples(int16Buffer)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
int16Buffer[i] = sample
|
||||
}
|
||||
|
||||
// Apply noise suppression if available and enabled
|
||||
if s.noiseProcessor != nil && s.noiseProcessor.IsEnabled() {
|
||||
s.noiseProcessor.ProcessSamples(int16Buffer)
|
||||
// Mix with or use file audio if playing
|
||||
hasFileAudio := false
|
||||
var outputBuffer []int16
|
||||
|
||||
if s.filePlayer != nil && s.filePlayer.IsPlaying() {
|
||||
fileAudio := s.filePlayer.GetAudioFrame()
|
||||
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)
|
||||
|
||||
if hasMicInput {
|
||||
// Mix mono mic with stereo file
|
||||
for i := 0; i < frameSize; i++ {
|
||||
if i*2+1 < len(fileAudio) {
|
||||
// Left channel: mic + file left
|
||||
left := int32(int16Buffer[i]) + int32(fileAudio[i*2])
|
||||
if left > 32767 {
|
||||
left = 32767
|
||||
} else if left < -32768 {
|
||||
left = -32768
|
||||
}
|
||||
outputBuffer[i*2] = int16(left)
|
||||
|
||||
// Right channel: mic + file right
|
||||
right := int32(int16Buffer[i]) + int32(fileAudio[i*2+1])
|
||||
if right > 32767 {
|
||||
right = 32767
|
||||
} else if right < -32768 {
|
||||
right = -32768
|
||||
}
|
||||
outputBuffer[i*2+1] = int16(right)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use file audio only (already stereo)
|
||||
copy(outputBuffer, fileAudio[:frameSize*2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply AGC to outgoing microphone audio (always enabled)
|
||||
if s.micAGC != nil {
|
||||
s.micAGC.ProcessSamples(int16Buffer)
|
||||
// Determine what to send
|
||||
if hasFileAudio {
|
||||
// Send stereo buffer when file is playing
|
||||
outgoing <- gumble.AudioBuffer(outputBuffer)
|
||||
} else if hasMicInput {
|
||||
// Send mono mic when no file is playing
|
||||
outgoing <- gumble.AudioBuffer(int16Buffer)
|
||||
}
|
||||
|
||||
// Apply voice effects if available and enabled
|
||||
if s.effectsProcessor != nil && s.effectsProcessor.IsEnabled() {
|
||||
s.effectsProcessor.ProcessSamples(int16Buffer)
|
||||
}
|
||||
|
||||
outgoing <- gumble.AudioBuffer(int16Buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,16 @@ func (*generator) NewEncoder() gumble.AudioEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
// NewStereoEncoder creates a stereo encoder for file playback
|
||||
func NewStereoEncoder() gumble.AudioEncoder {
|
||||
// Create stereo encoder for file playback
|
||||
e, _ := opus.NewEncoder(gumble.AudioSampleRate, gumble.AudioChannels, opus.AppAudio)
|
||||
e.SetBitrateToMax()
|
||||
return &Encoder{
|
||||
e,
|
||||
}
|
||||
}
|
||||
|
||||
func (*generator) NewDecoder() gumble.AudioDecoder {
|
||||
// Create decoder with stereo support
|
||||
d, _ := opus.NewDecoder(gumble.AudioSampleRate, gumble.AudioChannels)
|
||||
|
||||
137
ui.go
137
ui.go
@@ -2,7 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/nsf/termbox-go"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||
"git.stormux.org/storm/barnard/uiterm"
|
||||
"github.com/kennygrant/sanitize"
|
||||
"github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -158,6 +159,101 @@ func (b *Barnard) CommandNoiseSuppressionToggle(ui *uiterm.Ui, cmd string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Barnard) CommandPlayFile(ui *uiterm.Ui, cmd string) {
|
||||
// cmd contains just the filename part (everything after "/file ")
|
||||
filename := strings.TrimSpace(cmd)
|
||||
if filename == "" {
|
||||
b.AddOutputLine("Usage: /file <filename or URL>")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a URL
|
||||
isURL := strings.HasPrefix(filename, "http://") ||
|
||||
strings.HasPrefix(filename, "https://") ||
|
||||
strings.HasPrefix(filename, "ftp://") ||
|
||||
strings.HasPrefix(filename, "rtmp://")
|
||||
|
||||
if !isURL {
|
||||
// Expand ~ to home directory for local files
|
||||
if strings.HasPrefix(filename, "~") {
|
||||
homeDir := os.Getenv("HOME")
|
||||
filename = strings.Replace(filename, "~", homeDir, 1)
|
||||
}
|
||||
|
||||
// Check if local file exists
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
b.AddOutputLine(fmt.Sprintf("File not found: %s", filename))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !b.Connected {
|
||||
b.AddOutputLine("Not connected to server")
|
||||
return
|
||||
}
|
||||
|
||||
b.FileStreamMutex.Lock()
|
||||
defer b.FileStreamMutex.Unlock()
|
||||
|
||||
if b.FileStream != nil && b.FileStream.IsPlaying() {
|
||||
b.AddOutputLine("Already playing a file. Use /stop first.")
|
||||
return
|
||||
}
|
||||
|
||||
err := b.FileStream.PlayFile(filename)
|
||||
if err != nil {
|
||||
b.AddOutputLine(fmt.Sprintf("Error playing file: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Enable stereo encoder for file playback
|
||||
b.Client.EnableStereoEncoder()
|
||||
|
||||
// Auto-start transmission if not already transmitting
|
||||
if !b.Tx {
|
||||
err := b.Stream.StartSource(b.UserConfig.GetInputDevice())
|
||||
if err != nil {
|
||||
b.AddOutputLine(fmt.Sprintf("Error starting transmission: %s", err.Error()))
|
||||
b.FileStream.Stop()
|
||||
b.Client.DisableStereoEncoder()
|
||||
return
|
||||
}
|
||||
b.Tx = true
|
||||
b.UpdateGeneralStatus(" File ", true)
|
||||
}
|
||||
|
||||
if isURL {
|
||||
b.AddOutputLine(fmt.Sprintf("Streaming: %s (stereo)", filename))
|
||||
} else {
|
||||
b.AddOutputLine(fmt.Sprintf("Playing: %s (stereo)", filename))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Barnard) CommandStopFile(ui *uiterm.Ui, cmd string) {
|
||||
b.FileStreamMutex.Lock()
|
||||
defer b.FileStreamMutex.Unlock()
|
||||
|
||||
if b.FileStream == nil || !b.FileStream.IsPlaying() {
|
||||
b.AddOutputLine("No file playing")
|
||||
return
|
||||
}
|
||||
|
||||
err := b.FileStream.Stop()
|
||||
if err != nil {
|
||||
b.AddOutputLine(fmt.Sprintf("Error stopping file: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Disable stereo encoder when file stops
|
||||
b.Client.DisableStereoEncoder()
|
||||
|
||||
b.AddOutputLine("File playback stopped")
|
||||
|
||||
// Note: We keep transmission active even after file stops
|
||||
// User can manually stop with talk key or it will stop when they're done talking
|
||||
b.UpdateGeneralStatus(" Idle ", false)
|
||||
}
|
||||
|
||||
|
||||
func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) {
|
||||
if b.Tx && val == 1 {
|
||||
@@ -257,6 +353,43 @@ func (b *Barnard) OnTextInput(ui *uiterm.Ui, textbox *uiterm.Textbox, text strin
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a command (starts with /)
|
||||
if strings.HasPrefix(text, "/") {
|
||||
// Remove the leading slash and process as command
|
||||
cmdText := strings.TrimPrefix(text, "/")
|
||||
parts := strings.SplitN(cmdText, " ", 2)
|
||||
cmdName := parts[0]
|
||||
cmdArgs := ""
|
||||
if len(parts) > 1 {
|
||||
cmdArgs = parts[1]
|
||||
}
|
||||
|
||||
// Handle built-in commands
|
||||
switch cmdName {
|
||||
case "file":
|
||||
b.CommandPlayFile(ui, cmdArgs)
|
||||
case "stop":
|
||||
b.CommandStopFile(ui, cmdArgs)
|
||||
case "exit":
|
||||
b.CommandExit(ui, cmdArgs)
|
||||
case "status":
|
||||
b.CommandStatus(ui, cmdArgs)
|
||||
case "noise":
|
||||
b.CommandNoiseSuppressionToggle(ui, cmdArgs)
|
||||
case "micup":
|
||||
b.CommandMicUp(ui, cmdArgs)
|
||||
case "micdown":
|
||||
b.CommandMicDown(ui, cmdArgs)
|
||||
case "toggle", "talk":
|
||||
b.CommandTalk(ui, cmdArgs)
|
||||
default:
|
||||
b.AddOutputLine(fmt.Sprintf("Unknown command: /%s", cmdName))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Not a command, send as chat message
|
||||
if b.Client != nil && b.Client.Self != nil {
|
||||
if b.selectedUser != nil {
|
||||
b.selectedUser.Send(text)
|
||||
@@ -325,6 +458,8 @@ func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
|
||||
b.Ui.AddCommandListener(b.CommandExit, "exit")
|
||||
b.Ui.AddCommandListener(b.CommandStatus, "status")
|
||||
b.Ui.AddCommandListener(b.CommandNoiseSuppressionToggle, "noise")
|
||||
b.Ui.AddCommandListener(b.CommandPlayFile, "file")
|
||||
b.Ui.AddCommandListener(b.CommandStopFile, "stop")
|
||||
b.Ui.AddKeyListener(b.OnFocusPress, b.Hotkeys.SwitchViews)
|
||||
b.Ui.AddKeyListener(b.OnVoiceToggle, b.Hotkeys.Talk)
|
||||
b.Ui.AddKeyListener(b.OnTimestampToggle, b.Hotkeys.ToggleTimestamps)
|
||||
|
||||
Reference in New Issue
Block a user