Merge recording feature
This commit is contained in:
@@ -77,8 +77,27 @@ Current Commands:
|
|||||||
* toggle: Toggle your transmission state.
|
* toggle: Toggle your transmission state.
|
||||||
* talk: Synonym for toggle.
|
* talk: Synonym for toggle.
|
||||||
* noise: Toggle noise suppression on/off for microphone input.
|
* noise: Toggle noise suppression on/off for microphone input.
|
||||||
|
* record: Toggle recording. You may also use `record start` or `record stop`.
|
||||||
* exit: Exit Barnard, just like when you press your quit key.
|
* exit: Exit Barnard, just like when you press your quit key.
|
||||||
|
|
||||||
|
## Recording
|
||||||
|
|
||||||
|
Barnard can record a single mixed file containing the audio you hear and the audio you transmit.
|
||||||
|
Recording uses the Mumble recording state so other users are notified by the server.
|
||||||
|
If the server reports that recording is not allowed, Barnard refuses to start and prints a clear message.
|
||||||
|
|
||||||
|
Recordings are saved in `~/Audio` by default. The directory is created if it does not exist.
|
||||||
|
The default file name is `barnard-recording-YYYYMMDD-HHMMSS.flac`.
|
||||||
|
|
||||||
|
Recording settings in `~/.barnard.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
recordingformat = "flac"
|
||||||
|
recordingdirectory = "~/Audio"
|
||||||
|
```
|
||||||
|
|
||||||
|
`recordingformat` may be `flac` or `opus`. Barnard uses `ffmpeg` to encode recordings.
|
||||||
|
|
||||||
## Event Notification
|
## Event Notification
|
||||||
|
|
||||||
You can use the notifycommand parameter in your config file to run a program on certain events.
|
You can use the notifycommand parameter in your config file to run a program on certain events.
|
||||||
@@ -225,6 +244,7 @@ After running the command above, `barnard` will be compiled as `$(go env GOPATH)
|
|||||||
- <kbd>F1</kbd>: toggle voice transmission
|
- <kbd>F1</kbd>: toggle voice transmission
|
||||||
- <kbd>F9</kbd>: toggle noise suppression
|
- <kbd>F9</kbd>: toggle noise suppression
|
||||||
- <kbd>F12</kbd>: cycle through voice effects
|
- <kbd>F12</kbd>: cycle through voice effects
|
||||||
|
- <kbd>Ctrl+R</kbd>: toggle recording
|
||||||
- <kbd>Ctrl+L</kbd>: clear chat log
|
- <kbd>Ctrl+L</kbd>: clear chat log
|
||||||
- <kbd>Tab</kbd>: toggle focus between chat and user tree
|
- <kbd>Tab</kbd>: toggle focus between chat and user tree
|
||||||
- <kbd>Page Up</kbd>: scroll chat up
|
- <kbd>Page Up</kbd>: scroll chat up
|
||||||
|
|||||||
+141
-134
@@ -1,167 +1,174 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/audio"
|
"git.stormux.org/storm/barnard/audio"
|
||||||
"git.stormux.org/storm/barnard/config"
|
"git.stormux.org/storm/barnard/config"
|
||||||
"git.stormux.org/storm/barnard/fileplayback"
|
"git.stormux.org/storm/barnard/fileplayback"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
|
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
|
||||||
"git.stormux.org/storm/barnard/noise"
|
"git.stormux.org/storm/barnard/noise"
|
||||||
"git.stormux.org/storm/barnard/uiterm"
|
"git.stormux.org/storm/barnard/recording"
|
||||||
|
"git.stormux.org/storm/barnard/uiterm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TreeItem struct {
|
type TreeItem struct {
|
||||||
User *gumble.User
|
User *gumble.User
|
||||||
Channel *gumble.Channel
|
Channel *gumble.Channel
|
||||||
}
|
}
|
||||||
|
|
||||||
type Barnard struct {
|
type Barnard struct {
|
||||||
Config *gumble.Config
|
Config *gumble.Config
|
||||||
UserConfig *config.Config
|
UserConfig *config.Config
|
||||||
Hotkeys *config.Hotkeys
|
Hotkeys *config.Hotkeys
|
||||||
Client *gumble.Client
|
Client *gumble.Client
|
||||||
|
|
||||||
Address string
|
Address string
|
||||||
TLSConfig tls.Config
|
TLSConfig tls.Config
|
||||||
|
|
||||||
Stream *gumbleopenal.Stream
|
Stream *gumbleopenal.Stream
|
||||||
Tx bool
|
Tx bool
|
||||||
Connected bool
|
Connected bool
|
||||||
|
|
||||||
Ui *uiterm.Ui
|
Ui *uiterm.Ui
|
||||||
UiOutput uiterm.Textview
|
UiOutput uiterm.Textview
|
||||||
UiInput uiterm.Textbox
|
UiInput uiterm.Textbox
|
||||||
UiStatus uiterm.Label
|
UiStatus uiterm.Label
|
||||||
UiTree uiterm.Tree
|
UiTree uiterm.Tree
|
||||||
UiInputStatus uiterm.Label
|
UiInputStatus uiterm.Label
|
||||||
SelectedChannel *gumble.Channel
|
SelectedChannel *gumble.Channel
|
||||||
selectedUser *gumble.User
|
selectedUser *gumble.User
|
||||||
|
|
||||||
notifyChannel chan []string
|
notifyChannel chan []string
|
||||||
|
|
||||||
exitStatus int
|
exitStatus int
|
||||||
exitMessage string
|
exitMessage string
|
||||||
|
|
||||||
// Added for channel muting
|
// Added for channel muting
|
||||||
MutedChannels map[uint32]bool
|
MutedChannels map[uint32]bool
|
||||||
|
|
||||||
// Added for noise suppression
|
// Added for noise suppression
|
||||||
NoiseSuppressor *noise.Suppressor
|
NoiseSuppressor *noise.Suppressor
|
||||||
|
|
||||||
// Added for voice effects
|
// Added for voice effects
|
||||||
VoiceEffects *audio.EffectsProcessor
|
VoiceEffects *audio.EffectsProcessor
|
||||||
|
|
||||||
// Added for file playback
|
// Added for file playback
|
||||||
FileStream *fileplayback.Player
|
FileStream *fileplayback.Player
|
||||||
FileStreamMutex sync.Mutex
|
FileStreamMutex sync.Mutex
|
||||||
|
|
||||||
|
// Added for recording
|
||||||
|
RecordingMutex sync.Mutex
|
||||||
|
Recorder *recording.Recorder
|
||||||
|
recordingStarting bool
|
||||||
|
recordingAllowed *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) StopTransmission() {
|
func (b *Barnard) StopTransmission() {
|
||||||
if b.Tx {
|
if b.Tx {
|
||||||
b.Notify("micdown", "me", "")
|
b.Notify("micdown", "me", "")
|
||||||
b.Tx = false
|
b.Tx = false
|
||||||
b.UpdateGeneralStatus(" Idle ", false)
|
b.UpdateGeneralStatus(" Idle ", false)
|
||||||
b.Stream.StopSource()
|
b.Stream.StopSource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) TreeItemCharacter(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, ch rune) {
|
func (b *Barnard) TreeItemCharacter(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, ch rune) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, key uiterm.Key) {
|
func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, key uiterm.Key) {
|
||||||
treeItem := item.(TreeItem)
|
treeItem := item.(TreeItem)
|
||||||
if key == uiterm.KeyEnter {
|
if key == uiterm.KeyEnter {
|
||||||
if treeItem.Channel != nil {
|
if treeItem.Channel != nil {
|
||||||
b.Client.Self.Move(treeItem.Channel)
|
b.Client.Self.Move(treeItem.Channel)
|
||||||
b.SetSelectedUser(nil)
|
b.SetSelectedUser(nil)
|
||||||
b.GotoChat()
|
b.GotoChat()
|
||||||
}
|
}
|
||||||
if treeItem.User != nil {
|
if treeItem.User != nil {
|
||||||
if b.selectedUser == treeItem.User {
|
if b.selectedUser == treeItem.User {
|
||||||
b.SetSelectedUser(nil)
|
b.SetSelectedUser(nil)
|
||||||
b.GotoChat()
|
b.GotoChat()
|
||||||
} else {
|
} else {
|
||||||
b.SetSelectedUser(treeItem.User)
|
b.SetSelectedUser(treeItem.User)
|
||||||
b.GotoChat()
|
b.GotoChat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mute toggle
|
// Handle mute toggle
|
||||||
if treeItem.Channel != nil {
|
if treeItem.Channel != nil {
|
||||||
if key == *b.Hotkeys.MuteToggle {
|
if key == *b.Hotkeys.MuteToggle {
|
||||||
// Determine new channel mute state
|
// Determine new channel mute state
|
||||||
channelWillBeMuted := !b.MutedChannels[treeItem.Channel.ID]
|
channelWillBeMuted := !b.MutedChannels[treeItem.Channel.ID]
|
||||||
|
|
||||||
// Set all users in channel to the same mute state
|
|
||||||
users := makeUsersArray(treeItem.Channel.Users)
|
|
||||||
for _, u := range users {
|
|
||||||
// Explicitly set user mute state to match channel state
|
|
||||||
if channelWillBeMuted && !u.LocallyMuted {
|
|
||||||
b.UserConfig.ToggleMute(u)
|
|
||||||
} else if !channelWillBeMuted && u.LocallyMuted {
|
|
||||||
b.UserConfig.ToggleMute(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.AudioSource != nil {
|
|
||||||
if u.LocallyMuted {
|
|
||||||
u.AudioSource.SetGain(0)
|
|
||||||
} else {
|
|
||||||
u.AudioSource.SetGain(u.Volume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update channel mute state
|
// Set all users in channel to the same mute state
|
||||||
if channelWillBeMuted {
|
users := makeUsersArray(treeItem.Channel.Users)
|
||||||
b.MutedChannels[treeItem.Channel.ID] = true
|
for _, u := range users {
|
||||||
// If this is the current channel, stop transmission
|
// Explicitly set user mute state to match channel state
|
||||||
if b.Client.Self.Channel.ID == treeItem.Channel.ID && b.Tx {
|
if channelWillBeMuted && !u.LocallyMuted {
|
||||||
b.StopTransmission()
|
b.UserConfig.ToggleMute(u)
|
||||||
}
|
} else if !channelWillBeMuted && u.LocallyMuted {
|
||||||
} else {
|
b.UserConfig.ToggleMute(u)
|
||||||
delete(b.MutedChannels, treeItem.Channel.ID)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
b.UiTree.Rebuild()
|
if u.AudioSource != nil {
|
||||||
b.Ui.Refresh()
|
if u.LocallyMuted {
|
||||||
}
|
u.AudioSource.SetGain(0)
|
||||||
if key == *b.Hotkeys.VolumeDown {
|
} else {
|
||||||
b.changeVolume(makeUsersArray(treeItem.Channel.Users), -0.1)
|
u.AudioSource.SetGain(u.Volume)
|
||||||
}
|
}
|
||||||
if key == *b.Hotkeys.VolumeUp {
|
}
|
||||||
b.changeVolume(makeUsersArray(treeItem.Channel.Users), 0.1)
|
}
|
||||||
}
|
|
||||||
if key == *b.Hotkeys.VolumeReset {
|
|
||||||
b.resetVolume(makeUsersArray(treeItem.Channel.Users))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if treeItem.User != nil {
|
// Update channel mute state
|
||||||
if key == *b.Hotkeys.MuteToggle {
|
if channelWillBeMuted {
|
||||||
// Toggle mute for single user
|
b.MutedChannels[treeItem.Channel.ID] = true
|
||||||
b.UserConfig.ToggleMute(treeItem.User)
|
// If this is the current channel, stop transmission
|
||||||
if treeItem.User.AudioSource != nil {
|
if b.Client.Self.Channel.ID == treeItem.Channel.ID && b.Tx {
|
||||||
if treeItem.User.LocallyMuted {
|
b.StopTransmission()
|
||||||
treeItem.User.AudioSource.SetGain(0)
|
}
|
||||||
} else {
|
} else {
|
||||||
treeItem.User.AudioSource.SetGain(treeItem.User.Volume)
|
delete(b.MutedChannels, treeItem.Channel.ID)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
b.UiTree.Rebuild()
|
b.UiTree.Rebuild()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
if key == *b.Hotkeys.VolumeDown {
|
if key == *b.Hotkeys.VolumeDown {
|
||||||
b.changeVolume([]*gumble.User{treeItem.User}, -0.1)
|
b.changeVolume(makeUsersArray(treeItem.Channel.Users), -0.1)
|
||||||
}
|
}
|
||||||
if key == *b.Hotkeys.VolumeUp {
|
if key == *b.Hotkeys.VolumeUp {
|
||||||
b.changeVolume([]*gumble.User{treeItem.User}, 0.1)
|
b.changeVolume(makeUsersArray(treeItem.Channel.Users), 0.1)
|
||||||
}
|
}
|
||||||
if key == *b.Hotkeys.VolumeReset {
|
if key == *b.Hotkeys.VolumeReset {
|
||||||
b.resetVolume([]*gumble.User{treeItem.User})
|
b.resetVolume(makeUsersArray(treeItem.Channel.Users))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if treeItem.User != nil {
|
||||||
|
if key == *b.Hotkeys.MuteToggle {
|
||||||
|
// Toggle mute for single user
|
||||||
|
b.UserConfig.ToggleMute(treeItem.User)
|
||||||
|
if treeItem.User.AudioSource != nil {
|
||||||
|
if treeItem.User.LocallyMuted {
|
||||||
|
treeItem.User.AudioSource.SetGain(0)
|
||||||
|
} else {
|
||||||
|
treeItem.User.AudioSource.SetGain(treeItem.User.Volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
if key == *b.Hotkeys.VolumeDown {
|
||||||
|
b.changeVolume([]*gumble.User{treeItem.User}, -0.1)
|
||||||
|
}
|
||||||
|
if key == *b.Hotkeys.VolumeUp {
|
||||||
|
b.changeVolume([]*gumble.User{treeItem.User}, 0.1)
|
||||||
|
}
|
||||||
|
if key == *b.Hotkeys.VolumeReset {
|
||||||
|
b.resetVolume([]*gumble.User{treeItem.User})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,246 +1,261 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/fileplayback"
|
"git.stormux.org/storm/barnard/fileplayback"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
|
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumbleutil"
|
"git.stormux.org/storm/barnard/gumble/gumbleutil"
|
||||||
"git.stormux.org/storm/barnard/gumble/opus"
|
"git.stormux.org/storm/barnard/gumble/opus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Barnard) start() {
|
func (b *Barnard) start() {
|
||||||
b.Config.Attach(gumbleutil.AutoBitrate)
|
b.Config.Attach(gumbleutil.AutoBitrate)
|
||||||
b.Config.Attach(b)
|
b.Config.Attach(b)
|
||||||
b.Config.Address = b.Address
|
b.Config.Address = b.Address
|
||||||
// test Audio
|
// test Audio
|
||||||
_, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), true)
|
_, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.exitWithError(err)
|
b.exitWithError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//connect, not reconnect
|
//connect, not reconnect
|
||||||
b.connect(false)
|
b.connect(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) exitWithError(err error) {
|
func (b *Barnard) exitWithError(err error) {
|
||||||
b.Ui.Close()
|
b.Ui.Close()
|
||||||
b.exitStatus = 1
|
b.exitStatus = 1
|
||||||
b.exitMessage = err.Error()
|
b.exitMessage = err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) connect(reconnect bool) bool {
|
func (b *Barnard) connect(reconnect bool) bool {
|
||||||
var err error
|
var err error
|
||||||
_, err = gumble.DialWithDialer(new(net.Dialer), b.Config, &b.TLSConfig)
|
_, err = gumble.DialWithDialer(new(net.Dialer), b.Config, &b.TLSConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if reconnect {
|
if reconnect {
|
||||||
b.Log(err.Error())
|
b.Log(err.Error())
|
||||||
} else {
|
} else {
|
||||||
b.exitWithError(err)
|
b.exitWithError(err)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
stream, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), false)
|
stream, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.exitWithError(err)
|
b.exitWithError(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
b.Stream = stream
|
b.Stream = stream
|
||||||
b.Stream.AttachStream(b.Client)
|
b.Stream.AttachStream(b.Client)
|
||||||
b.Stream.SetNoiseProcessor(b.NoiseSuppressor)
|
b.Stream.SetNoiseProcessor(b.NoiseSuppressor)
|
||||||
b.Stream.SetEffectsProcessor(b.VoiceEffects)
|
b.Stream.SetEffectsProcessor(b.VoiceEffects)
|
||||||
|
|
||||||
// Initialize stereo encoder for file playback
|
// Initialize stereo encoder for file playback
|
||||||
b.Client.AudioEncoderStereo = opus.NewStereoEncoder()
|
b.Client.AudioEncoderStereo = opus.NewStereoEncoder()
|
||||||
|
|
||||||
// Initialize file player
|
// Initialize file player
|
||||||
b.FileStreamMutex.Lock()
|
b.FileStreamMutex.Lock()
|
||||||
b.FileStream = fileplayback.New(b.Client)
|
b.FileStream = fileplayback.New(b.Client)
|
||||||
b.FileStream.SetErrorFunc(func(err error) {
|
b.FileStream.SetErrorFunc(func(err error) {
|
||||||
// Disable stereo when file finishes or errors
|
// Disable stereo when file finishes or errors
|
||||||
b.Client.DisableStereoEncoder()
|
b.Client.DisableStereoEncoder()
|
||||||
b.AddOutputLine(fmt.Sprintf("File playback: %s", err.Error()))
|
b.AddOutputLine(fmt.Sprintf("File playback: %s", err.Error()))
|
||||||
})
|
})
|
||||||
b.Stream.SetFilePlayer(b.FileStream)
|
b.Stream.SetFilePlayer(b.FileStream)
|
||||||
b.FileStreamMutex.Unlock()
|
b.FileStreamMutex.Unlock()
|
||||||
|
|
||||||
b.Connected = true
|
b.Connected = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
|
func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
|
||||||
b.Client = e.Client
|
b.Client = e.Client
|
||||||
|
|
||||||
// Reset muted channels state on connect
|
// Reset muted channels state on connect
|
||||||
b.MutedChannels = make(map[uint32]bool)
|
b.MutedChannels = make(map[uint32]bool)
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
b.recordingAllowed = nil
|
||||||
|
b.recordingStarting = false
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
|
||||||
b.Ui.SetActive(uiViewInput)
|
b.Ui.SetActive(uiViewInput)
|
||||||
b.UiTree.Rebuild()
|
b.UiTree.Rebuild()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
|
|
||||||
for _, u := range b.Client.Users {
|
for _, u := range b.Client.Users {
|
||||||
b.UserConfig.UpdateUser(u)
|
b.UserConfig.UpdateUser(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Client.Self.Channel.Name))
|
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Client.Self.Channel.Name))
|
||||||
b.AddOutputLine(fmt.Sprintf("Connected to %s", b.Client.Conn.RemoteAddr()))
|
b.AddOutputLine(fmt.Sprintf("Connected to %s", b.Client.Conn.RemoteAddr()))
|
||||||
wmsg := ""
|
wmsg := ""
|
||||||
if e.WelcomeMessage != nil {
|
if e.WelcomeMessage != nil {
|
||||||
wmsg = esc(*e.WelcomeMessage)
|
wmsg = esc(*e.WelcomeMessage)
|
||||||
}
|
}
|
||||||
b.Notify("connect", "me", wmsg)
|
b.Notify("connect", "me", wmsg)
|
||||||
if wmsg != "" {
|
if wmsg != "" {
|
||||||
b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg))
|
b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg))
|
||||||
}
|
}
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
|
func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
|
||||||
var reason string
|
var reason string
|
||||||
switch e.Type {
|
switch e.Type {
|
||||||
case gumble.DisconnectError:
|
case gumble.DisconnectError:
|
||||||
reason = "connection error"
|
reason = "connection error"
|
||||||
}
|
case gumble.DisconnectKicked:
|
||||||
b.Notify("disconnect", "me", reason)
|
reason = "kicked"
|
||||||
if reason == "" {
|
case gumble.DisconnectBanned:
|
||||||
b.AddOutputLine("Disconnected")
|
reason = "banned"
|
||||||
} else {
|
}
|
||||||
b.AddOutputLine("Disconnected: " + reason)
|
if e.String != "" {
|
||||||
}
|
reason = e.String
|
||||||
b.Tx = false
|
}
|
||||||
b.Connected = false
|
b.stopRecordingForDisconnect()
|
||||||
b.UiTree.Rebuild()
|
b.Notify("disconnect", "me", reason)
|
||||||
b.Ui.Refresh()
|
if reason == "" {
|
||||||
go b.reconnectGoroutine()
|
b.AddOutputLine("Disconnected")
|
||||||
|
} else {
|
||||||
|
b.AddOutputLine("Disconnected: " + reason)
|
||||||
|
}
|
||||||
|
b.Tx = false
|
||||||
|
b.Connected = false
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
go b.reconnectGoroutine()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) reconnectGoroutine() {
|
func (b *Barnard) reconnectGoroutine() {
|
||||||
for {
|
for {
|
||||||
res := b.connect(true)
|
res := b.connect(true)
|
||||||
if res == true {
|
if res == true {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) Log(s string) {
|
func (b *Barnard) Log(s string) {
|
||||||
b.AddOutputMessage(nil, s)
|
b.AddOutputMessage(nil, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnTextMessage(e *gumble.TextMessageEvent) {
|
func (b *Barnard) OnTextMessage(e *gumble.TextMessageEvent) {
|
||||||
var public = false
|
var public = false
|
||||||
for _, c := range e.Channels {
|
for _, c := range e.Channels {
|
||||||
if c.Name == b.Client.Self.Channel.Name {
|
if c.Name == b.Client.Self.Channel.Name {
|
||||||
public = true
|
public = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if public {
|
if public {
|
||||||
b.Notify("msg", e.Sender.Name, e.Message)
|
b.Notify("msg", e.Sender.Name, e.Message)
|
||||||
b.AddOutputMessage(e.Sender, e.Message)
|
b.AddOutputMessage(e.Sender, e.Message)
|
||||||
} else {
|
} else {
|
||||||
var sender string
|
var sender string
|
||||||
if e.Sender == nil {
|
if e.Sender == nil {
|
||||||
sender = "Server"
|
sender = "Server"
|
||||||
} else {
|
} else {
|
||||||
sender = e.Sender.Name
|
sender = e.Sender.Name
|
||||||
}
|
}
|
||||||
b.Notify("pm", sender, e.Message)
|
b.Notify("pm", sender, e.Message)
|
||||||
b.AddOutputPrivateMessage(e.Sender, b.Client.Self, e.Message)
|
b.AddOutputPrivateMessage(e.Sender, b.Client.Self, e.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
|
func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
|
||||||
if e.User != nil {
|
if e.User != nil {
|
||||||
b.UserConfig.UpdateUser(e.User)
|
b.UserConfig.UpdateUser(e.User)
|
||||||
|
|
||||||
// Check if user is joining a muted channel
|
|
||||||
if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) {
|
|
||||||
// If the channel is muted, ensure the user is muted
|
|
||||||
if b.MutedChannels[e.User.Channel.ID] {
|
|
||||||
// Only mute if not already muted
|
|
||||||
if !e.User.LocallyMuted {
|
|
||||||
b.UserConfig.ToggleMute(e.User)
|
|
||||||
}
|
|
||||||
if e.User.AudioSource != nil {
|
|
||||||
e.User.AudioSource.SetGain(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var s = "unknown"
|
// Check if user is joining a muted channel
|
||||||
var t = "unknown"
|
if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) {
|
||||||
if e.Type.Has(gumble.UserChangeConnected) {
|
// If the channel is muted, ensure the user is muted
|
||||||
s = "joined"
|
if b.MutedChannels[e.User.Channel.ID] {
|
||||||
t = "join"
|
// Only mute if not already muted
|
||||||
// Notify about users joining our channel
|
if !e.User.LocallyMuted {
|
||||||
if e.User.Channel.Name == b.Client.Self.Channel.Name {
|
b.UserConfig.ToggleMute(e.User)
|
||||||
b.Notify(t, e.User.Name, e.User.Channel.Name)
|
}
|
||||||
b.AddOutputLine(fmt.Sprintf("%s %s %s", e.User.Name, s, e.User.Channel.Name))
|
if e.User.AudioSource != nil {
|
||||||
}
|
e.User.AudioSource.SetGain(0)
|
||||||
}
|
}
|
||||||
if e.Type.Has(gumble.UserChangeDisconnected) {
|
}
|
||||||
s = "left"
|
}
|
||||||
t = "leave"
|
}
|
||||||
if e.User == b.selectedUser {
|
|
||||||
b.SetSelectedUser(nil)
|
var s = "unknown"
|
||||||
}
|
var t = "unknown"
|
||||||
// Always notify about disconnects if user has channel info and was in our channel
|
if e.Type.Has(gumble.UserChangeConnected) {
|
||||||
if e.User.Channel != nil && e.User.Channel.Name == b.Client.Self.Channel.Name {
|
s = "joined"
|
||||||
b.Notify(t, e.User.Name, e.User.Channel.Name)
|
t = "join"
|
||||||
b.AddOutputLine(fmt.Sprintf("%s %s %s", e.User.Name, s, e.User.Channel.Name))
|
// Notify about users joining our channel
|
||||||
}
|
if e.User.Channel.Name == b.Client.Self.Channel.Name {
|
||||||
}
|
b.Notify(t, e.User.Name, e.User.Channel.Name)
|
||||||
if e.Type.Has(gumble.UserChangeChannel) && e.User == b.Client.Self {
|
b.AddOutputLine(fmt.Sprintf("%s %s %s", e.User.Name, s, e.User.Channel.Name))
|
||||||
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.User.Channel.Name))
|
}
|
||||||
}
|
}
|
||||||
b.UiTree.Rebuild()
|
if e.Type.Has(gumble.UserChangeDisconnected) {
|
||||||
b.Ui.Refresh()
|
s = "left"
|
||||||
|
t = "leave"
|
||||||
|
if e.User == b.selectedUser {
|
||||||
|
b.SetSelectedUser(nil)
|
||||||
|
}
|
||||||
|
// Always notify about disconnects if user has channel info and was in our channel
|
||||||
|
if e.User.Channel != nil && e.User.Channel.Name == b.Client.Self.Channel.Name {
|
||||||
|
b.Notify(t, e.User.Name, e.User.Channel.Name)
|
||||||
|
b.AddOutputLine(fmt.Sprintf("%s %s %s", e.User.Name, s, e.User.Channel.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Type.Has(gumble.UserChangeChannel) && e.User == b.Client.Self {
|
||||||
|
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.User.Channel.Name))
|
||||||
|
}
|
||||||
|
if e.Type.Has(gumble.UserChangeRecording) {
|
||||||
|
b.HandleRecordingChange(e)
|
||||||
|
}
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) {
|
func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) {
|
||||||
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Channel.Name))
|
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Channel.Name))
|
||||||
b.UiTree.Rebuild()
|
b.UiTree.Rebuild()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnPermissionDenied(e *gumble.PermissionDeniedEvent) {
|
func (b *Barnard) OnPermissionDenied(e *gumble.PermissionDeniedEvent) {
|
||||||
var info string
|
var info string
|
||||||
switch e.Type {
|
switch e.Type {
|
||||||
case gumble.PermissionDeniedOther:
|
case gumble.PermissionDeniedOther:
|
||||||
info = e.String
|
info = e.String
|
||||||
case gumble.PermissionDeniedPermission:
|
case gumble.PermissionDeniedPermission:
|
||||||
info = "insufficient permissions"
|
info = "insufficient permissions"
|
||||||
case gumble.PermissionDeniedSuperUser:
|
case gumble.PermissionDeniedSuperUser:
|
||||||
info = "cannot modify SuperUser"
|
info = "cannot modify SuperUser"
|
||||||
case gumble.PermissionDeniedInvalidChannelName:
|
case gumble.PermissionDeniedInvalidChannelName:
|
||||||
info = "invalid channel name"
|
info = "invalid channel name"
|
||||||
case gumble.PermissionDeniedTextTooLong:
|
case gumble.PermissionDeniedTextTooLong:
|
||||||
info = "text too long"
|
info = "text too long"
|
||||||
case gumble.PermissionDeniedTemporaryChannel:
|
case gumble.PermissionDeniedTemporaryChannel:
|
||||||
info = "temporary channel"
|
info = "temporary channel"
|
||||||
case gumble.PermissionDeniedMissingCertificate:
|
case gumble.PermissionDeniedMissingCertificate:
|
||||||
info = "missing certificate"
|
info = "missing certificate"
|
||||||
case gumble.PermissionDeniedInvalidUserName:
|
case gumble.PermissionDeniedInvalidUserName:
|
||||||
info = "invalid user name"
|
info = "invalid user name"
|
||||||
case gumble.PermissionDeniedChannelFull:
|
case gumble.PermissionDeniedChannelFull:
|
||||||
info = "channel full"
|
info = "channel full"
|
||||||
case gumble.PermissionDeniedNestingLimit:
|
case gumble.PermissionDeniedNestingLimit:
|
||||||
info = "nesting limit"
|
info = "nesting limit"
|
||||||
}
|
}
|
||||||
b.AddOutputLine(fmt.Sprintf("Permission denied: %s", info))
|
b.AddOutputLine(fmt.Sprintf("Permission denied: %s", info))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnUserList(e *gumble.UserListEvent) {
|
func (b *Barnard) OnUserList(e *gumble.UserListEvent) {
|
||||||
//for _,u := range e.UserList {
|
//for _,u := range e.UserList {
|
||||||
//b.UserConfig.UpdateUser(u)
|
//b.UserConfig.UpdateUser(u)
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnACL(e *gumble.ACLEvent) {
|
func (b *Barnard) OnACL(e *gumble.ACLEvent) {
|
||||||
@@ -253,4 +268,5 @@ func (b *Barnard) OnContextActionChange(e *gumble.ContextActionChangeEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnServerConfig(e *gumble.ServerConfigEvent) {
|
func (b *Barnard) OnServerConfig(e *gumble.ServerConfigEvent) {
|
||||||
|
b.HandleRecordingAllowed(e.RecordingAllowed)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-16
@@ -1,23 +1,24 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.stormux.org/storm/barnard/uiterm"
|
"git.stormux.org/storm/barnard/uiterm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Hotkeys struct {
|
type Hotkeys struct {
|
||||||
Talk *uiterm.Key
|
Talk *uiterm.Key
|
||||||
VolumeDown *uiterm.Key
|
VolumeDown *uiterm.Key
|
||||||
VolumeUp *uiterm.Key
|
VolumeUp *uiterm.Key
|
||||||
VolumeReset *uiterm.Key
|
VolumeReset *uiterm.Key
|
||||||
MuteToggle *uiterm.Key
|
MuteToggle *uiterm.Key
|
||||||
Exit *uiterm.Key
|
RecordToggle *uiterm.Key
|
||||||
ToggleTimestamps *uiterm.Key
|
Exit *uiterm.Key
|
||||||
SwitchViews *uiterm.Key
|
ToggleTimestamps *uiterm.Key
|
||||||
ClearOutput *uiterm.Key
|
SwitchViews *uiterm.Key
|
||||||
ScrollUp *uiterm.Key
|
ClearOutput *uiterm.Key
|
||||||
ScrollDown *uiterm.Key
|
ScrollUp *uiterm.Key
|
||||||
ScrollToTop *uiterm.Key
|
ScrollDown *uiterm.Key
|
||||||
ScrollToBottom *uiterm.Key
|
ScrollToTop *uiterm.Key
|
||||||
NoiseSuppressionToggle *uiterm.Key
|
ScrollToBottom *uiterm.Key
|
||||||
CycleVoiceEffect *uiterm.Key
|
NoiseSuppressionToggle *uiterm.Key
|
||||||
|
CycleVoiceEffect *uiterm.Key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ type exportableConfig struct {
|
|||||||
NoiseSuppressionThreshold *float32
|
NoiseSuppressionThreshold *float32
|
||||||
VoiceEffect *int
|
VoiceEffect *int
|
||||||
Certificate *string
|
Certificate *string
|
||||||
|
RecordingFormat *string
|
||||||
|
RecordingDirectory *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type server struct {
|
type server struct {
|
||||||
@@ -75,6 +77,7 @@ func (c *Config) LoadConfig() {
|
|||||||
VolumeUp: key(uiterm.KeyF6),
|
VolumeUp: key(uiterm.KeyF6),
|
||||||
VolumeReset: key(uiterm.KeyF8),
|
VolumeReset: key(uiterm.KeyF8),
|
||||||
MuteToggle: key(uiterm.KeyF7), // Added mute toggle hotkey
|
MuteToggle: key(uiterm.KeyF7), // Added mute toggle hotkey
|
||||||
|
RecordToggle: key(uiterm.KeyCtrlR),
|
||||||
Exit: key(uiterm.KeyF10),
|
Exit: key(uiterm.KeyF10),
|
||||||
ToggleTimestamps: key(uiterm.KeyF3),
|
ToggleTimestamps: key(uiterm.KeyF3),
|
||||||
SwitchViews: key(uiterm.KeyTab),
|
SwitchViews: key(uiterm.KeyTab),
|
||||||
@@ -95,6 +98,7 @@ func (c *Config) LoadConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.config = &jc
|
c.config = &jc
|
||||||
|
c.ensureHotkeys()
|
||||||
if c.config.MicVolume == nil {
|
if c.config.MicVolume == nil {
|
||||||
micvol := float32(1.0)
|
micvol := float32(1.0)
|
||||||
jc.MicVolume = &micvol
|
jc.MicVolume = &micvol
|
||||||
@@ -139,6 +143,75 @@ func (c *Config) LoadConfig() {
|
|||||||
cert := string("")
|
cert := string("")
|
||||||
jc.Certificate = &cert
|
jc.Certificate = &cert
|
||||||
}
|
}
|
||||||
|
if c.config.RecordingFormat == nil {
|
||||||
|
format := string("flac")
|
||||||
|
jc.RecordingFormat = &format
|
||||||
|
}
|
||||||
|
if c.config.RecordingDirectory == nil {
|
||||||
|
dir := string("~/Audio")
|
||||||
|
jc.RecordingDirectory = &dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ensureHotkeys() {
|
||||||
|
if c.config.Hotkeys == nil {
|
||||||
|
c.config.Hotkeys = &Hotkeys{}
|
||||||
|
}
|
||||||
|
defaults := Hotkeys{
|
||||||
|
Talk: key(uiterm.KeyF1),
|
||||||
|
VolumeDown: key(uiterm.KeyF5),
|
||||||
|
VolumeUp: key(uiterm.KeyF6),
|
||||||
|
VolumeReset: key(uiterm.KeyF8),
|
||||||
|
MuteToggle: key(uiterm.KeyF7),
|
||||||
|
RecordToggle: key(uiterm.KeyCtrlR),
|
||||||
|
Exit: key(uiterm.KeyF10),
|
||||||
|
ToggleTimestamps: key(uiterm.KeyF3),
|
||||||
|
SwitchViews: key(uiterm.KeyTab),
|
||||||
|
ScrollUp: key(uiterm.KeyPgup),
|
||||||
|
ScrollDown: key(uiterm.KeyPgdn),
|
||||||
|
NoiseSuppressionToggle: key(uiterm.KeyF9),
|
||||||
|
CycleVoiceEffect: key(uiterm.KeyF12),
|
||||||
|
}
|
||||||
|
hotkeys := c.config.Hotkeys
|
||||||
|
if hotkeys.Talk == nil {
|
||||||
|
hotkeys.Talk = defaults.Talk
|
||||||
|
}
|
||||||
|
if hotkeys.VolumeDown == nil {
|
||||||
|
hotkeys.VolumeDown = defaults.VolumeDown
|
||||||
|
}
|
||||||
|
if hotkeys.VolumeUp == nil {
|
||||||
|
hotkeys.VolumeUp = defaults.VolumeUp
|
||||||
|
}
|
||||||
|
if hotkeys.VolumeReset == nil {
|
||||||
|
hotkeys.VolumeReset = defaults.VolumeReset
|
||||||
|
}
|
||||||
|
if hotkeys.MuteToggle == nil {
|
||||||
|
hotkeys.MuteToggle = defaults.MuteToggle
|
||||||
|
}
|
||||||
|
if hotkeys.RecordToggle == nil {
|
||||||
|
hotkeys.RecordToggle = defaults.RecordToggle
|
||||||
|
}
|
||||||
|
if hotkeys.Exit == nil {
|
||||||
|
hotkeys.Exit = defaults.Exit
|
||||||
|
}
|
||||||
|
if hotkeys.ToggleTimestamps == nil {
|
||||||
|
hotkeys.ToggleTimestamps = defaults.ToggleTimestamps
|
||||||
|
}
|
||||||
|
if hotkeys.SwitchViews == nil {
|
||||||
|
hotkeys.SwitchViews = defaults.SwitchViews
|
||||||
|
}
|
||||||
|
if hotkeys.ScrollUp == nil {
|
||||||
|
hotkeys.ScrollUp = defaults.ScrollUp
|
||||||
|
}
|
||||||
|
if hotkeys.ScrollDown == nil {
|
||||||
|
hotkeys.ScrollDown = defaults.ScrollDown
|
||||||
|
}
|
||||||
|
if hotkeys.NoiseSuppressionToggle == nil {
|
||||||
|
hotkeys.NoiseSuppressionToggle = defaults.NoiseSuppressionToggle
|
||||||
|
}
|
||||||
|
if hotkeys.CycleVoiceEffect == nil {
|
||||||
|
hotkeys.CycleVoiceEffect = defaults.CycleVoiceEffect
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) findServer(address string) *server {
|
func (c *Config) findServer(address string) *server {
|
||||||
@@ -284,6 +357,20 @@ func (c *Config) SetVoiceEffect(effect int) {
|
|||||||
c.SaveConfig()
|
c.SaveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetRecordingFormat() string {
|
||||||
|
if c.config.RecordingFormat == nil {
|
||||||
|
return "flac"
|
||||||
|
}
|
||||||
|
return strings.ToLower(strings.TrimSpace(*c.config.RecordingFormat))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetRecordingDirectory() string {
|
||||||
|
if c.config.RecordingDirectory == nil {
|
||||||
|
return resolvePath("~/Audio")
|
||||||
|
}
|
||||||
|
return resolvePath(*c.config.RecordingDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) UpdateUser(u *gumble.User) {
|
func (c *Config) UpdateUser(u *gumble.User) {
|
||||||
var j *eUser
|
var j *eUser
|
||||||
var uc *gumble.Client
|
var uc *gumble.Client
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.stormux.org/storm/barnard/uiterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigBackfillsRecordingDefaults(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "barnard.toml")
|
||||||
|
if err := os.WriteFile(configPath, []byte("[hotkeys]\ntalk = \"f1\"\n"), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := NewConfig(&configPath)
|
||||||
|
|
||||||
|
if got := cfg.GetRecordingFormat(); got != "flac" {
|
||||||
|
t.Fatalf("expected default recording format flac, got %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.GetRecordingDirectory(); got != filepath.Join(os.Getenv("HOME"), "Audio") {
|
||||||
|
t.Fatalf("expected default recording directory ~/Audio, got %q", got)
|
||||||
|
}
|
||||||
|
if cfg.GetHotkeys().RecordToggle == nil {
|
||||||
|
t.Fatal("expected record toggle hotkey to be backfilled")
|
||||||
|
}
|
||||||
|
if got := *cfg.GetHotkeys().RecordToggle; got != uiterm.KeyCtrlR {
|
||||||
|
t.Fatalf("expected record toggle ctrl_r, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,11 @@
|
|||||||
Package MumbleProto is a generated protocol buffer package.
|
Package MumbleProto is a generated protocol buffer package.
|
||||||
|
|
||||||
It is generated from these files:
|
It is generated from these files:
|
||||||
|
|
||||||
Mumble.proto
|
Mumble.proto
|
||||||
|
|
||||||
It has these top-level messages:
|
It has these top-level messages:
|
||||||
|
|
||||||
Version
|
Version
|
||||||
UDPTunnel
|
UDPTunnel
|
||||||
Authenticate
|
Authenticate
|
||||||
@@ -2070,8 +2072,10 @@ type ServerConfig struct {
|
|||||||
// Maximum image message length.
|
// Maximum image message length.
|
||||||
ImageMessageLength *uint32 `protobuf:"varint,5,opt,name=image_message_length,json=imageMessageLength" json:"image_message_length,omitempty"`
|
ImageMessageLength *uint32 `protobuf:"varint,5,opt,name=image_message_length,json=imageMessageLength" json:"image_message_length,omitempty"`
|
||||||
// The maximum number of users allowed on the server.
|
// The maximum number of users allowed on the server.
|
||||||
MaxUsers *uint32 `protobuf:"varint,6,opt,name=max_users,json=maxUsers" json:"max_users,omitempty"`
|
MaxUsers *uint32 `protobuf:"varint,6,opt,name=max_users,json=maxUsers" json:"max_users,omitempty"`
|
||||||
XXX_unrecognized []byte `json:"-"`
|
// Whether using Mumble's recording feature is allowed on the server.
|
||||||
|
RecordingAllowed *bool `protobuf:"varint,7,opt,name=recording_allowed,json=recordingAllowed" json:"recording_allowed,omitempty"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ServerConfig) Reset() { *m = ServerConfig{} }
|
func (m *ServerConfig) Reset() { *m = ServerConfig{} }
|
||||||
@@ -2121,6 +2125,13 @@ func (m *ServerConfig) GetMaxUsers() uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *ServerConfig) GetRecordingAllowed() bool {
|
||||||
|
if m != nil && m.RecordingAllowed != nil {
|
||||||
|
return *m.RecordingAllowed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Sent by the server to inform the clients of suggested client configuration
|
// Sent by the server to inform the clients of suggested client configuration
|
||||||
// specified by the server administrator.
|
// specified by the server administrator.
|
||||||
type SuggestConfig struct {
|
type SuggestConfig struct {
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ type ServerConfigEvent struct {
|
|||||||
MaximumMessageLength *int
|
MaximumMessageLength *int
|
||||||
MaximumImageMessageLength *int
|
MaximumImageMessageLength *int
|
||||||
MaximumUsers *int
|
MaximumUsers *int
|
||||||
|
RecordingAllowed *bool
|
||||||
|
|
||||||
CodecAlpha *int32
|
CodecAlpha *int32
|
||||||
CodecBeta *int32
|
CodecBeta *int32
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ func (c *Client) handleUserRemove(buffer []byte) error {
|
|||||||
event.Type |= UserChangeBanned
|
event.Type |= UserChangeBanned
|
||||||
}
|
}
|
||||||
if event.User == c.Self {
|
if event.User == c.Self {
|
||||||
|
c.disconnectEvent.String = event.String
|
||||||
if packet.Ban != nil && *packet.Ban {
|
if packet.Ban != nil && *packet.Ban {
|
||||||
c.disconnectEvent.Type = DisconnectBanned
|
c.disconnectEvent.Type = DisconnectBanned
|
||||||
} else {
|
} else {
|
||||||
@@ -1236,10 +1237,57 @@ func (c *Client) handleServerConfig(buffer []byte) error {
|
|||||||
val := int(*packet.MaxUsers)
|
val := int(*packet.MaxUsers)
|
||||||
event.MaximumUsers = &val
|
event.MaximumUsers = &val
|
||||||
}
|
}
|
||||||
|
if packet.RecordingAllowed != nil {
|
||||||
|
event.RecordingAllowed = packet.RecordingAllowed
|
||||||
|
} else if val := parseServerConfigRecordingAllowed(buffer); val != nil {
|
||||||
|
event.RecordingAllowed = val
|
||||||
|
}
|
||||||
c.Config.Listeners.onServerConfig(&event)
|
c.Config.Listeners.onServerConfig(&event)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseServerConfigRecordingAllowed(buffer []byte) *bool {
|
||||||
|
for len(buffer) > 0 {
|
||||||
|
key, n := varint.Decode(buffer)
|
||||||
|
if n <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
buffer = buffer[n:]
|
||||||
|
field := key >> 3
|
||||||
|
wireType := key & 0x7
|
||||||
|
if field == 7 && wireType == 0 {
|
||||||
|
val, n := varint.Decode(buffer)
|
||||||
|
if n <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
allowed := val != 0
|
||||||
|
return &allowed
|
||||||
|
}
|
||||||
|
skip := 0
|
||||||
|
switch wireType {
|
||||||
|
case 0:
|
||||||
|
_, skip = varint.Decode(buffer)
|
||||||
|
case 1:
|
||||||
|
skip = 8
|
||||||
|
case 2:
|
||||||
|
length, n := varint.Decode(buffer)
|
||||||
|
if n <= 0 || length < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
skip = n + int(length)
|
||||||
|
case 5:
|
||||||
|
skip = 4
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if skip <= 0 || skip > len(buffer) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
buffer = buffer[skip:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) handleSuggestConfig(buffer []byte) error {
|
func (c *Client) handleSuggestConfig(buffer []byte) error {
|
||||||
var packet MumbleProto.SuggestConfig
|
var packet MumbleProto.SuggestConfig
|
||||||
if err := proto.Unmarshal(buffer, &packet); err != nil {
|
if err := proto.Unmarshal(buffer, &packet); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package gumble
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverConfigListener struct {
|
||||||
|
EventListener
|
||||||
|
event *ServerConfigEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *serverConfigListener) OnServerConfig(e *ServerConfigEvent) {
|
||||||
|
l.event = e
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleServerConfigRecordingAllowed(t *testing.T) {
|
||||||
|
data := []byte{56, 0} // field 7, varint false
|
||||||
|
|
||||||
|
client := &Client{Config: NewConfig()}
|
||||||
|
listener := &serverConfigListener{}
|
||||||
|
client.Config.Attach(listener)
|
||||||
|
|
||||||
|
if err := client.handleServerConfig(data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if listener.event == nil {
|
||||||
|
t.Fatal("expected server config event")
|
||||||
|
}
|
||||||
|
if listener.event.RecordingAllowed == nil {
|
||||||
|
t.Fatal("expected recording allowed value")
|
||||||
|
}
|
||||||
|
if *listener.event.RecordingAllowed {
|
||||||
|
t.Fatal("expected recording to be disallowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/audio"
|
"git.stormux.org/storm/barnard/audio"
|
||||||
@@ -30,6 +31,12 @@ type FilePlayer interface {
|
|||||||
IsPlaying() bool
|
IsPlaying() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Recorder interface {
|
||||||
|
RecordAudioFrame(source uint32, samples []int16)
|
||||||
|
}
|
||||||
|
|
||||||
|
const recorderOutgoingSource uint32 = ^uint32(0)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
|
maxBufferSize = 11520 // Max frame size (2880) * bytes per stereo sample (4)
|
||||||
)
|
)
|
||||||
@@ -72,6 +79,8 @@ type Stream struct {
|
|||||||
effectsProcessor EffectsProcessor
|
effectsProcessor EffectsProcessor
|
||||||
effectsProcessorRight EffectsProcessor
|
effectsProcessorRight EffectsProcessor
|
||||||
filePlayer FilePlayer
|
filePlayer FilePlayer
|
||||||
|
recorderMu sync.RWMutex
|
||||||
|
recorder Recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -161,6 +170,18 @@ func (s *Stream) GetFilePlayer() FilePlayer {
|
|||||||
return s.filePlayer
|
return s.filePlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Stream) SetRecorder(recorder Recorder) {
|
||||||
|
s.recorderMu.Lock()
|
||||||
|
defer s.recorderMu.Unlock()
|
||||||
|
s.recorder = recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) getRecorder() Recorder {
|
||||||
|
s.recorderMu.RLock()
|
||||||
|
defer s.recorderMu.RUnlock()
|
||||||
|
return s.recorder
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Stream) Destroy() {
|
func (s *Stream) Destroy() {
|
||||||
if s.link != nil {
|
if s.link != nil {
|
||||||
s.link.Detach()
|
s.link.Detach()
|
||||||
@@ -265,6 +286,12 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boost = e.User.Boost
|
boost = e.User.Boost
|
||||||
|
recorder := s.getRecorder()
|
||||||
|
var recordBuffer []int16
|
||||||
|
recordPtr := 0
|
||||||
|
if recorder != nil {
|
||||||
|
recordBuffer = make([]int16, len(packet.AudioBuffer)*gumble.AudioChannels)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if sample count suggests stereo data
|
// Check if sample count suggests stereo data
|
||||||
isStereo := samples > gumble.AudioDefaultFrameSize && samples%2 == 0
|
isStereo := samples > gumble.AudioDefaultFrameSize && samples%2 == 0
|
||||||
@@ -290,6 +317,10 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
sample = int16(boosted)
|
sample = int16(boosted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if recorder != nil {
|
||||||
|
recordBuffer[recordPtr] = scaleForRecording(sample, e.User.Volume)
|
||||||
|
recordPtr++
|
||||||
|
}
|
||||||
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
||||||
rawPtr += 2
|
rawPtr += 2
|
||||||
|
|
||||||
@@ -305,6 +336,10 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
sample = int16(boosted)
|
sample = int16(boosted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if recorder != nil {
|
||||||
|
recordBuffer[recordPtr] = scaleForRecording(sample, e.User.Volume)
|
||||||
|
recordPtr++
|
||||||
|
}
|
||||||
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
||||||
rawPtr += 2
|
rawPtr += 2
|
||||||
}
|
}
|
||||||
@@ -322,10 +357,19 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
sample = int16(boosted)
|
sample = int16(boosted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if recorder != nil {
|
||||||
|
recordSample := scaleForRecording(sample, e.User.Volume)
|
||||||
|
recordBuffer[recordPtr] = recordSample
|
||||||
|
recordBuffer[recordPtr+1] = recordSample
|
||||||
|
recordPtr += 2
|
||||||
|
}
|
||||||
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
binary.LittleEndian.PutUint16(raw[rawPtr:], uint16(sample))
|
||||||
rawPtr += 2
|
rawPtr += 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if recorder != nil && recordPtr > 0 {
|
||||||
|
recorder.RecordAudioFrame(e.User.Session, recordBuffer[:recordPtr])
|
||||||
|
}
|
||||||
|
|
||||||
reclaim()
|
reclaim()
|
||||||
if len(emptyBufs) == 0 {
|
if len(emptyBufs) == 0 {
|
||||||
@@ -472,14 +516,31 @@ func (s *Stream) sourceRoutine(inputDevice *string) {
|
|||||||
if hasFileAudio {
|
if hasFileAudio {
|
||||||
// Send stereo buffer when file is playing
|
// Send stereo buffer when file is playing
|
||||||
outgoing <- gumble.AudioBuffer(outputBuffer)
|
outgoing <- gumble.AudioBuffer(outputBuffer)
|
||||||
|
if recorder := s.getRecorder(); recorder != nil {
|
||||||
|
recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer)
|
||||||
|
}
|
||||||
} else if hasMicInput {
|
} else if hasMicInput {
|
||||||
// Send mic when no file is playing
|
// Send mic when no file is playing
|
||||||
outgoing <- gumble.AudioBuffer(int16Buffer)
|
outgoing <- gumble.AudioBuffer(int16Buffer)
|
||||||
|
if recorder := s.getRecorder(); recorder != nil {
|
||||||
|
recorder.RecordAudioFrame(recorderOutgoingSource, int16Buffer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scaleForRecording(sample int16, volume float32) int16 {
|
||||||
|
scaled := int32(float32(sample) * volume)
|
||||||
|
if scaled > 32767 {
|
||||||
|
return 32767
|
||||||
|
}
|
||||||
|
if scaled < -32768 {
|
||||||
|
return -32768
|
||||||
|
}
|
||||||
|
return int16(scaled)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Stream) processMonoSamples(samples []int16) {
|
func (s *Stream) processMonoSamples(samples []int16) {
|
||||||
s.processChannel(samples, s.noiseProcessor, s.micAGC, s.effectsProcessor)
|
s.processChannel(samples, s.noiseProcessor, s.micAGC, s.effectsProcessor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package recording
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatFLAC = "flac"
|
||||||
|
FormatOpus = "opus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Recorder struct {
|
||||||
|
path string
|
||||||
|
format string
|
||||||
|
frameSize int
|
||||||
|
interval time.Duration
|
||||||
|
|
||||||
|
cmd *exec.Cmd
|
||||||
|
stdin io.WriteCloser
|
||||||
|
|
||||||
|
input chan sourceFrame
|
||||||
|
stop chan struct{}
|
||||||
|
done chan struct{}
|
||||||
|
once sync.Once
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type sourceFrame struct {
|
||||||
|
source uint32
|
||||||
|
samples []int16
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(directory string, format string, now time.Time, frameSize int, interval time.Duration) (*Recorder, error) {
|
||||||
|
format = NormalizeFormat(format)
|
||||||
|
if format != FormatFLAC && format != FormatOpus {
|
||||||
|
return nil, fmt.Errorf("unsupported recording format %q", format)
|
||||||
|
}
|
||||||
|
if frameSize <= 0 {
|
||||||
|
return nil, errors.New("invalid recording frame size")
|
||||||
|
}
|
||||||
|
if interval <= 0 {
|
||||||
|
interval = gumble.AudioDefaultInterval
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(directory, 0755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
path := UniquePath(directory, now, format)
|
||||||
|
args := ffmpegArgs(format, path)
|
||||||
|
cmd := exec.Command("ffmpeg", args...)
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
recorder := &Recorder{
|
||||||
|
path: path,
|
||||||
|
format: format,
|
||||||
|
frameSize: frameSize,
|
||||||
|
interval: interval,
|
||||||
|
cmd: cmd,
|
||||||
|
stdin: stdin,
|
||||||
|
input: make(chan sourceFrame, 512),
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go recorder.run()
|
||||||
|
return recorder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeFormat(format string) string {
|
||||||
|
format = strings.ToLower(strings.TrimSpace(format))
|
||||||
|
format = strings.TrimPrefix(format, ".")
|
||||||
|
if format == "" {
|
||||||
|
return FormatFLAC
|
||||||
|
}
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
func UniquePath(directory string, now time.Time, format string) string {
|
||||||
|
base := fmt.Sprintf("barnard-recording-%s", now.Format("20060102-150405"))
|
||||||
|
path := filepath.Join(directory, base+"."+format)
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
for i := 2; ; i++ {
|
||||||
|
path = filepath.Join(directory, fmt.Sprintf("%s-%d.%s", base, i, format))
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) Path() string {
|
||||||
|
return r.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) RecordAudioFrame(source uint32, samples []int16) {
|
||||||
|
if r == nil || len(samples) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(r.input) >= cap(r.input) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
frame := NormalizeStereoFrame(samples, r.frameSize)
|
||||||
|
select {
|
||||||
|
case r.input <- sourceFrame{source: source, samples: frame}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) Stop() error {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.once.Do(func() {
|
||||||
|
close(r.stop)
|
||||||
|
if r.stdin != nil {
|
||||||
|
r.stdin.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
if r.cmd != nil && r.cmd.Process != nil {
|
||||||
|
r.cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
<-r.done
|
||||||
|
}
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) run() {
|
||||||
|
defer close(r.done)
|
||||||
|
ticker := time.NewTicker(r.interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
queues := make(map[uint32][][]int16)
|
||||||
|
frame := make([]int16, r.frameSize*gumble.AudioChannels)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.stop:
|
||||||
|
r.closeEncoder()
|
||||||
|
return
|
||||||
|
case item := <-r.input:
|
||||||
|
queues[item.source] = append(queues[item.source], item.samples)
|
||||||
|
case <-ticker.C:
|
||||||
|
clear(frame)
|
||||||
|
for source, queue := range queues {
|
||||||
|
if len(queue) == 0 {
|
||||||
|
delete(queues, source)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mix(frame, queue[0])
|
||||||
|
queue = queue[1:]
|
||||||
|
if len(queue) == 0 {
|
||||||
|
delete(queues, source)
|
||||||
|
} else {
|
||||||
|
queues[source] = queue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := writePCM(r.stdin, frame); err != nil {
|
||||||
|
r.setError(err)
|
||||||
|
r.closeEncoder()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) closeEncoder() {
|
||||||
|
if r.stdin != nil {
|
||||||
|
if err := r.stdin.Close(); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrClosed) {
|
||||||
|
r.setError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.cmd != nil {
|
||||||
|
if err := r.cmd.Wait(); err != nil {
|
||||||
|
r.setError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) setError(err error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.err == nil {
|
||||||
|
r.err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeStereoFrame(samples []int16, frameSize int) []int16 {
|
||||||
|
out := make([]int16, frameSize*gumble.AudioChannels)
|
||||||
|
if len(samples) >= frameSize*gumble.AudioChannels && len(samples)%gumble.AudioChannels == 0 {
|
||||||
|
copy(out, samples[:frameSize*gumble.AudioChannels])
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
limit := frameSize
|
||||||
|
if len(samples) < limit {
|
||||||
|
limit = len(samples)
|
||||||
|
}
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
out[i*2] = samples[i]
|
||||||
|
out[i*2+1] = samples[i]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func mix(dst []int16, src []int16) {
|
||||||
|
limit := len(dst)
|
||||||
|
if len(src) < limit {
|
||||||
|
limit = len(src)
|
||||||
|
}
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
sum := int32(dst[i]) + int32(src[i])
|
||||||
|
if sum > 32767 {
|
||||||
|
sum = 32767
|
||||||
|
} else if sum < -32768 {
|
||||||
|
sum = -32768
|
||||||
|
}
|
||||||
|
dst[i] = int16(sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePCM(w io.Writer, samples []int16) error {
|
||||||
|
buf := make([]byte, len(samples)*2)
|
||||||
|
for i, sample := range samples {
|
||||||
|
binary.LittleEndian.PutUint16(buf[i*2:], uint16(sample))
|
||||||
|
}
|
||||||
|
_, err := w.Write(buf)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ffmpegArgs(format string, path string) []string {
|
||||||
|
args := []string{
|
||||||
|
"-loglevel", "error",
|
||||||
|
"-f", "s16le",
|
||||||
|
"-ar", fmt.Sprintf("%d", gumble.AudioSampleRate),
|
||||||
|
"-ac", fmt.Sprintf("%d", gumble.AudioChannels),
|
||||||
|
"-i", "pipe:0",
|
||||||
|
}
|
||||||
|
if format == FormatOpus {
|
||||||
|
args = append(args, "-c:a", "libopus")
|
||||||
|
}
|
||||||
|
return append(args, "-y", path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package recording
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeFormat(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"": "flac",
|
||||||
|
" FLAC ": "flac",
|
||||||
|
".opus": "opus",
|
||||||
|
}
|
||||||
|
for input, want := range tests {
|
||||||
|
if got := NormalizeFormat(input); got != want {
|
||||||
|
t.Fatalf("NormalizeFormat(%q) = %q, want %q", input, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniquePathAvoidsCollision(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
now := time.Date(2026, 5, 14, 12, 30, 0, 0, time.Local)
|
||||||
|
first := filepath.Join(dir, "barnard-recording-20260514-123000.flac")
|
||||||
|
if err := os.WriteFile(first, []byte{}, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := UniquePath(dir, now, "flac")
|
||||||
|
want := filepath.Join(dir, "barnard-recording-20260514-123000-2.flac")
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("UniquePath() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStereoFrame(t *testing.T) {
|
||||||
|
mono := NormalizeStereoFrame([]int16{1, -2}, 3)
|
||||||
|
wantMono := []int16{1, 1, -2, -2, 0, 0}
|
||||||
|
for i := range wantMono {
|
||||||
|
if mono[i] != wantMono[i] {
|
||||||
|
t.Fatalf("mono[%d] = %d, want %d", i, mono[i], wantMono[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stereo := NormalizeStereoFrame([]int16{1, 2, 3, 4, 5, 6}, 2)
|
||||||
|
wantStereo := []int16{1, 2, 3, 4}
|
||||||
|
for i := range wantStereo {
|
||||||
|
if stereo[i] != wantStereo[i] {
|
||||||
|
t.Fatalf("stereo[%d] = %d, want %d", i, stereo[i], wantStereo[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMixSaturates(t *testing.T) {
|
||||||
|
dst := []int16{32000, -32000, 10}
|
||||||
|
mix(dst, []int16{2000, -2000, -20})
|
||||||
|
want := []int16{32767, -32768, -10}
|
||||||
|
for i := range want {
|
||||||
|
if dst[i] != want[i] {
|
||||||
|
t.Fatalf("dst[%d] = %d, want %d", i, dst[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
|
"git.stormux.org/storm/barnard/recording"
|
||||||
|
"git.stormux.org/storm/barnard/uiterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *Barnard) OnRecordingToggle(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
b.ToggleRecording()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) CommandRecord(ui *uiterm.Ui, cmd string) {
|
||||||
|
action := strings.ToLower(strings.TrimSpace(cmd))
|
||||||
|
switch action {
|
||||||
|
case "", "toggle":
|
||||||
|
b.ToggleRecording()
|
||||||
|
case "start", "on":
|
||||||
|
b.StartRecording()
|
||||||
|
case "stop", "off":
|
||||||
|
b.StopRecording(true)
|
||||||
|
default:
|
||||||
|
b.AddOutputLine("Usage: /record [start|stop|toggle]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) ToggleRecording() {
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
active := b.Recorder != nil || b.recordingStarting
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
if active {
|
||||||
|
b.StopRecording(true)
|
||||||
|
} else {
|
||||||
|
b.StartRecording()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) StartRecording() {
|
||||||
|
if b.Client == nil || b.Client.Self == nil {
|
||||||
|
b.AddOutputLine("Recording requires an active server connection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
if b.recordingAllowed != nil && !*b.recordingAllowed {
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
b.AddOutputLine("Recording is not allowed by this server.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if b.Recorder != nil || b.recordingStarting {
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
b.AddOutputLine("Recording is already active")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.recordingStarting = true
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
|
||||||
|
b.Client.Self.SetRecording(true)
|
||||||
|
b.AddOutputLine("Recording start requested")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) StopRecording(notifyServer bool) {
|
||||||
|
recorder, path, wasPending := b.detachRecorder()
|
||||||
|
if recorder == nil {
|
||||||
|
if notifyServer && b.Client != nil && b.Client.Self != nil {
|
||||||
|
b.Client.Self.SetRecording(false)
|
||||||
|
}
|
||||||
|
if wasPending {
|
||||||
|
b.AddOutputLine("Recording start cancelled")
|
||||||
|
} else {
|
||||||
|
b.AddOutputLine("Recording is not active")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := recorder.Stop(); err != nil {
|
||||||
|
b.AddOutputLine(fmt.Sprintf("Recording stopped with error: %s", err))
|
||||||
|
} else {
|
||||||
|
b.AddOutputLine(fmt.Sprintf("Recording saved: %s", path))
|
||||||
|
}
|
||||||
|
if notifyServer && b.Client != nil && b.Client.Self != nil {
|
||||||
|
b.Client.Self.SetRecording(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) StopRecordingIfActive(notifyServer bool) {
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
active := b.Recorder != nil || b.recordingStarting
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
if active {
|
||||||
|
b.StopRecording(notifyServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) HandleRecordingChange(e *gumble.UserChangeEvent) {
|
||||||
|
if e.User == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e.User == b.Client.Self {
|
||||||
|
if e.User.Recording {
|
||||||
|
b.finishRecordingStart()
|
||||||
|
} else {
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
hadRecorder := b.Recorder != nil
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
if hadRecorder {
|
||||||
|
b.StopRecording(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e.User.Recording {
|
||||||
|
b.AddOutputLine(fmt.Sprintf("%s started recording", e.User.Name))
|
||||||
|
} else {
|
||||||
|
b.AddOutputLine(fmt.Sprintf("%s stopped recording", e.User.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) HandleRecordingAllowed(allowed *bool) {
|
||||||
|
if allowed == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
value := *allowed
|
||||||
|
b.recordingAllowed = &value
|
||||||
|
active := b.Recorder != nil || b.recordingStarting
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
if !value && active {
|
||||||
|
b.AddOutputLine("Recording is not allowed by this server.")
|
||||||
|
b.StopRecording(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) finishRecordingStart() {
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
if !b.recordingStarting || b.Recorder != nil {
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
|
||||||
|
recorder, err := recording.New(
|
||||||
|
b.UserConfig.GetRecordingDirectory(),
|
||||||
|
b.UserConfig.GetRecordingFormat(),
|
||||||
|
time.Now(),
|
||||||
|
b.Client.Config.AudioFrameSize(),
|
||||||
|
b.Client.Config.AudioInterval,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
b.recordingStarting = false
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
b.AddOutputLine(fmt.Sprintf("Could not start recording: %s", err))
|
||||||
|
b.Client.Self.SetRecording(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
if !b.recordingStarting {
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
recorder.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.Recorder = recorder
|
||||||
|
b.recordingStarting = false
|
||||||
|
b.RecordingMutex.Unlock()
|
||||||
|
if b.Stream != nil {
|
||||||
|
b.Stream.SetRecorder(recorder)
|
||||||
|
}
|
||||||
|
b.AddOutputLine(fmt.Sprintf("Recording started: %s", recorder.Path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) detachRecorder() (*recording.Recorder, string, bool) {
|
||||||
|
b.RecordingMutex.Lock()
|
||||||
|
defer b.RecordingMutex.Unlock()
|
||||||
|
recorder := b.Recorder
|
||||||
|
wasPending := b.recordingStarting
|
||||||
|
path := ""
|
||||||
|
if recorder != nil {
|
||||||
|
path = recorder.Path()
|
||||||
|
}
|
||||||
|
b.Recorder = nil
|
||||||
|
b.recordingStarting = false
|
||||||
|
if b.Stream != nil {
|
||||||
|
b.Stream.SetRecorder(nil)
|
||||||
|
}
|
||||||
|
return recorder, path, wasPending
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) stopRecordingForDisconnect() {
|
||||||
|
recorder, _, _ := b.detachRecorder()
|
||||||
|
if recorder != nil {
|
||||||
|
if err := recorder.Stop(); err != nil {
|
||||||
|
b.AddOutputLine(fmt.Sprintf("Recording stopped with error: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,494 +1,498 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
"git.stormux.org/storm/barnard/uiterm"
|
"git.stormux.org/storm/barnard/uiterm"
|
||||||
"github.com/kennygrant/sanitize"
|
"github.com/kennygrant/sanitize"
|
||||||
"github.com/nsf/termbox-go"
|
"github.com/nsf/termbox-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
uiViewLogo = "logo"
|
uiViewLogo = "logo"
|
||||||
uiViewTop = "top"
|
uiViewTop = "top"
|
||||||
uiViewStatus = "status"
|
uiViewStatus = "status"
|
||||||
uiViewInput = "input"
|
uiViewInput = "input"
|
||||||
uiViewInputStatus = "inputstatus"
|
uiViewInputStatus = "inputstatus"
|
||||||
uiViewOutput = "output"
|
uiViewOutput = "output"
|
||||||
uiViewTree = "tree"
|
uiViewTree = "tree"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func esc(str string) string {
|
func esc(str string) string {
|
||||||
return sanitize.HTML(str)
|
return sanitize.HTML(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) Notify(event string, who string, what string) {
|
func (b *Barnard) Notify(event string, who string, what string) {
|
||||||
b.notifyChannel <- []string{event, who, what}
|
b.notifyChannel <- []string{event, who, what}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) Beep() {
|
func (b *Barnard) Beep() {
|
||||||
Beep()
|
Beep()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) SetSelectedUser(user *gumble.User) {
|
func (b *Barnard) SetSelectedUser(user *gumble.User) {
|
||||||
b.selectedUser = user
|
b.selectedUser = user
|
||||||
if user == nil {
|
if user == nil {
|
||||||
if len(b.UiInput.Text) > 0 {
|
if len(b.UiInput.Text) > 0 {
|
||||||
}
|
}
|
||||||
b.UpdateInputStatus(fmt.Sprintf("[%s]", b.Client.Self.Channel.Name))
|
b.UpdateInputStatus(fmt.Sprintf("[%s]", b.Client.Self.Channel.Name))
|
||||||
} else {
|
} else {
|
||||||
b.UpdateInputStatus(fmt.Sprintf("[@%s]", user.Name))
|
b.UpdateInputStatus(fmt.Sprintf("[@%s]", user.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) GetInputStatus() string {
|
func (b *Barnard) GetInputStatus() string {
|
||||||
return b.UiInputStatus.Text
|
return b.UiInputStatus.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) UpdateInputStatus(status string) {
|
func (b *Barnard) UpdateInputStatus(status string) {
|
||||||
if len(status) > 20 {
|
if len(status) > 20 {
|
||||||
status = status[:17] + "..." + "]"
|
status = status[:17] + "..." + "]"
|
||||||
}
|
}
|
||||||
b.UiInputStatus.Text = status
|
b.UiInputStatus.Text = status
|
||||||
b.UiTree.Rebuild()
|
b.UiTree.Rebuild()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) AddOutputLine(line string) {
|
func (b *Barnard) AddOutputLine(line string) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
b.UiOutput.AddLine(fmt.Sprintf("%s [%02d:%02d:%02d]", line, now.Hour(), now.Minute(), now.Second()))
|
b.UiOutput.AddLine(fmt.Sprintf("%s [%02d:%02d:%02d]", line, now.Hour(), now.Minute(), now.Second()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) AddOutputMessage(sender *gumble.User, message string) {
|
func (b *Barnard) AddOutputMessage(sender *gumble.User, message string) {
|
||||||
if sender == nil {
|
if sender == nil {
|
||||||
b.AddOutputLine(message)
|
b.AddOutputLine(message)
|
||||||
} else {
|
} else {
|
||||||
b.AddOutputLine(fmt.Sprintf("%s: %s", sender.Name, strings.TrimSpace(esc(message))))
|
b.AddOutputLine(fmt.Sprintf("%s: %s", sender.Name, strings.TrimSpace(esc(message))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) AddOutputPrivateMessage(source *gumble.User, dest *gumble.User, message string) {
|
func (b *Barnard) AddOutputPrivateMessage(source *gumble.User, dest *gumble.User, message string) {
|
||||||
var sender string
|
var sender string
|
||||||
if source == nil {
|
if source == nil {
|
||||||
sender = "Server"
|
sender = "Server"
|
||||||
} else {
|
} else {
|
||||||
sender = source.Name
|
sender = source.Name
|
||||||
}
|
}
|
||||||
b.AddOutputLine(fmt.Sprintf("pm/%s/%s: %s", sender, dest.Name, strings.TrimSpace(esc(message))))
|
b.AddOutputLine(fmt.Sprintf("pm/%s/%s: %s", sender, dest.Name, strings.TrimSpace(esc(message))))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnTimestampToggle(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnTimestampToggle(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.UiOutput.ToggleTimestamps()
|
b.UiOutput.ToggleTimestamps()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnNoiseSuppressionToggle(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnNoiseSuppressionToggle(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
enabled := !b.UserConfig.GetNoiseSuppressionEnabled()
|
enabled := !b.UserConfig.GetNoiseSuppressionEnabled()
|
||||||
b.UserConfig.SetNoiseSuppressionEnabled(enabled)
|
b.UserConfig.SetNoiseSuppressionEnabled(enabled)
|
||||||
b.NoiseSuppressor.SetEnabled(enabled)
|
b.NoiseSuppressor.SetEnabled(enabled)
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
b.UpdateGeneralStatus("Noise suppression: ON", false)
|
b.UpdateGeneralStatus("Noise suppression: ON", false)
|
||||||
} else {
|
} else {
|
||||||
b.UpdateGeneralStatus("Noise suppression: OFF", false)
|
b.UpdateGeneralStatus("Noise suppression: OFF", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnVoiceEffectCycle(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnVoiceEffectCycle(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
effect := b.VoiceEffects.CycleEffect()
|
effect := b.VoiceEffects.CycleEffect()
|
||||||
b.UserConfig.SetVoiceEffect(int(effect))
|
b.UserConfig.SetVoiceEffect(int(effect))
|
||||||
b.UpdateGeneralStatus(fmt.Sprintf("Voice effect: %s", effect.String()), false)
|
b.UpdateGeneralStatus(fmt.Sprintf("Voice effect: %s", effect.String()), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (b *Barnard) UpdateGeneralStatus(text string, notice bool) {
|
func (b *Barnard) UpdateGeneralStatus(text string, notice bool) {
|
||||||
if notice {
|
if notice {
|
||||||
b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold
|
b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold
|
||||||
b.UiStatus.Bg = uiterm.ColorRed
|
b.UiStatus.Bg = uiterm.ColorRed
|
||||||
} else {
|
} else {
|
||||||
b.UiStatus.Fg = uiterm.ColorBlack
|
b.UiStatus.Fg = uiterm.ColorBlack
|
||||||
b.UiStatus.Bg = uiterm.ColorWhite
|
b.UiStatus.Bg = uiterm.ColorWhite
|
||||||
}
|
}
|
||||||
b.UiStatus.Text = text
|
b.UiStatus.Text = text
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnVoiceToggle(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnVoiceToggle(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.setTransmit(ui, 2)
|
b.setTransmit(ui, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandLog(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandLog(ui *uiterm.Ui, cmd string) {
|
||||||
b.AddOutputLine("command " + cmd)
|
b.AddOutputLine("command " + cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandTalk(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandTalk(ui *uiterm.Ui, cmd string) {
|
||||||
b.setTransmit(ui, 2)
|
b.setTransmit(ui, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandMicUp(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandMicUp(ui *uiterm.Ui, cmd string) {
|
||||||
b.setTransmit(ui, 1)
|
b.setTransmit(ui, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandMicDown(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandMicDown(ui *uiterm.Ui, cmd string) {
|
||||||
b.setTransmit(ui, 0)
|
b.setTransmit(ui, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandNoiseSuppressionToggle(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandNoiseSuppressionToggle(ui *uiterm.Ui, cmd string) {
|
||||||
enabled := !b.UserConfig.GetNoiseSuppressionEnabled()
|
enabled := !b.UserConfig.GetNoiseSuppressionEnabled()
|
||||||
b.UserConfig.SetNoiseSuppressionEnabled(enabled)
|
b.UserConfig.SetNoiseSuppressionEnabled(enabled)
|
||||||
b.NoiseSuppressor.SetEnabled(enabled)
|
b.NoiseSuppressor.SetEnabled(enabled)
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
b.AddOutputLine("Noise suppression enabled")
|
b.AddOutputLine("Noise suppression enabled")
|
||||||
} else {
|
} else {
|
||||||
b.AddOutputLine("Noise suppression disabled")
|
b.AddOutputLine("Noise suppression disabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandPlayFile(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandPlayFile(ui *uiterm.Ui, cmd string) {
|
||||||
// cmd contains just the filename part (everything after "/file ")
|
// cmd contains just the filename part (everything after "/file ")
|
||||||
filename := strings.TrimSpace(cmd)
|
filename := strings.TrimSpace(cmd)
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
b.AddOutputLine("Usage: /file <filename or URL>")
|
b.AddOutputLine("Usage: /file <filename or URL>")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a URL
|
// Check if it's a URL
|
||||||
isURL := strings.HasPrefix(filename, "http://") ||
|
isURL := strings.HasPrefix(filename, "http://") ||
|
||||||
strings.HasPrefix(filename, "https://") ||
|
strings.HasPrefix(filename, "https://") ||
|
||||||
strings.HasPrefix(filename, "ftp://") ||
|
strings.HasPrefix(filename, "ftp://") ||
|
||||||
strings.HasPrefix(filename, "rtmp://")
|
strings.HasPrefix(filename, "rtmp://")
|
||||||
|
|
||||||
if !isURL {
|
if !isURL {
|
||||||
// Expand ~ to home directory for local files
|
// Expand ~ to home directory for local files
|
||||||
if strings.HasPrefix(filename, "~") {
|
if strings.HasPrefix(filename, "~") {
|
||||||
homeDir := os.Getenv("HOME")
|
homeDir := os.Getenv("HOME")
|
||||||
filename = strings.Replace(filename, "~", homeDir, 1)
|
filename = strings.Replace(filename, "~", homeDir, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if local file exists
|
// Check if local file exists
|
||||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||||
b.AddOutputLine(fmt.Sprintf("File not found: %s", filename))
|
b.AddOutputLine(fmt.Sprintf("File not found: %s", filename))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !b.Connected {
|
if !b.Connected {
|
||||||
b.AddOutputLine("Not connected to server")
|
b.AddOutputLine("Not connected to server")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.FileStreamMutex.Lock()
|
b.FileStreamMutex.Lock()
|
||||||
defer b.FileStreamMutex.Unlock()
|
defer b.FileStreamMutex.Unlock()
|
||||||
|
|
||||||
if b.FileStream != nil && b.FileStream.IsPlaying() {
|
if b.FileStream != nil && b.FileStream.IsPlaying() {
|
||||||
b.AddOutputLine("Already playing a file. Use /stop first.")
|
b.AddOutputLine("Already playing a file. Use /stop first.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.FileStream.PlayFile(filename)
|
err := b.FileStream.PlayFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.AddOutputLine(fmt.Sprintf("Error playing file: %s", err.Error()))
|
b.AddOutputLine(fmt.Sprintf("Error playing file: %s", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable stereo encoder for file playback
|
// Enable stereo encoder for file playback
|
||||||
b.Client.EnableStereoEncoder()
|
b.Client.EnableStereoEncoder()
|
||||||
|
|
||||||
// Auto-start transmission if not already transmitting
|
// Auto-start transmission if not already transmitting
|
||||||
if !b.Tx {
|
if !b.Tx {
|
||||||
err := b.Stream.StartSource(b.UserConfig.GetInputDevice())
|
err := b.Stream.StartSource(b.UserConfig.GetInputDevice())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.AddOutputLine(fmt.Sprintf("Error starting transmission: %s", err.Error()))
|
b.AddOutputLine(fmt.Sprintf("Error starting transmission: %s", err.Error()))
|
||||||
b.FileStream.Stop()
|
b.FileStream.Stop()
|
||||||
b.Client.DisableStereoEncoder()
|
b.Client.DisableStereoEncoder()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.Tx = true
|
b.Tx = true
|
||||||
b.UpdateGeneralStatus(" File ", true)
|
b.UpdateGeneralStatus(" File ", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isURL {
|
if isURL {
|
||||||
b.AddOutputLine(fmt.Sprintf("Streaming: %s (stereo)", filename))
|
b.AddOutputLine(fmt.Sprintf("Streaming: %s (stereo)", filename))
|
||||||
} else {
|
} else {
|
||||||
b.AddOutputLine(fmt.Sprintf("Playing: %s (stereo)", filename))
|
b.AddOutputLine(fmt.Sprintf("Playing: %s (stereo)", filename))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandStopFile(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandStopFile(ui *uiterm.Ui, cmd string) {
|
||||||
b.FileStreamMutex.Lock()
|
b.FileStreamMutex.Lock()
|
||||||
defer b.FileStreamMutex.Unlock()
|
defer b.FileStreamMutex.Unlock()
|
||||||
|
|
||||||
if b.FileStream == nil || !b.FileStream.IsPlaying() {
|
if b.FileStream == nil || !b.FileStream.IsPlaying() {
|
||||||
b.AddOutputLine("No file playing")
|
b.AddOutputLine("No file playing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.FileStream.Stop()
|
err := b.FileStream.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.AddOutputLine(fmt.Sprintf("Error stopping file: %s", err.Error()))
|
b.AddOutputLine(fmt.Sprintf("Error stopping file: %s", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable stereo encoder when file stops
|
// Disable stereo encoder when file stops
|
||||||
b.Client.DisableStereoEncoder()
|
b.Client.DisableStereoEncoder()
|
||||||
|
|
||||||
b.AddOutputLine("File playback stopped")
|
b.AddOutputLine("File playback stopped")
|
||||||
|
|
||||||
// Note: We keep transmission active even after file stops
|
// 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
|
// User can manually stop with talk key or it will stop when they're done talking
|
||||||
b.UpdateGeneralStatus(" Idle ", false)
|
b.UpdateGeneralStatus(" Idle ", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) {
|
func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) {
|
||||||
if b.Tx && val == 1 {
|
if b.Tx && val == 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if b.Tx == false && val == 0 {
|
if b.Tx == false && val == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if b.Tx {
|
if b.Tx {
|
||||||
b.Notify("micdown", "me", "")
|
b.Notify("micdown", "me", "")
|
||||||
b.Tx = false
|
b.Tx = false
|
||||||
b.UpdateGeneralStatus(" Idle ", false)
|
b.UpdateGeneralStatus(" Idle ", false)
|
||||||
b.Stream.StopSource()
|
b.Stream.StopSource()
|
||||||
} else if b.Connected == false {
|
} else if b.Connected == false {
|
||||||
b.Notify("error", "me", "no tx while disconnected")
|
b.Notify("error", "me", "no tx while disconnected")
|
||||||
b.Tx = false
|
b.Tx = false
|
||||||
b.UpdateGeneralStatus("no tx while disconnected", true)
|
b.UpdateGeneralStatus("no tx while disconnected", true)
|
||||||
} else if b.MutedChannels[b.Client.Self.Channel.ID] {
|
} else if b.MutedChannels[b.Client.Self.Channel.ID] {
|
||||||
// Check if current channel is muted
|
// Check if current channel is muted
|
||||||
b.Notify("error", "me", "cannot transmit in muted channel")
|
b.Notify("error", "me", "cannot transmit in muted channel")
|
||||||
b.Tx = false
|
b.Tx = false
|
||||||
b.UpdateGeneralStatus("cannot transmit in muted channel", true)
|
b.UpdateGeneralStatus("cannot transmit in muted channel", true)
|
||||||
} else {
|
} else {
|
||||||
b.Tx = true
|
b.Tx = true
|
||||||
err := b.Stream.StartSource(b.UserConfig.GetInputDevice())
|
err := b.Stream.StartSource(b.UserConfig.GetInputDevice())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Notify("error", "me", err.Error())
|
b.Notify("error", "me", err.Error())
|
||||||
b.UpdateGeneralStatus(err.Error(), true)
|
b.UpdateGeneralStatus(err.Error(), true)
|
||||||
} else {
|
} else {
|
||||||
b.Notify("micup", "me", "")
|
b.Notify("micup", "me", "")
|
||||||
b.UpdateGeneralStatus(" Tx ", true)
|
b.UpdateGeneralStatus(" Tx ", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnMicVolumeDown(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnMicVolumeDown(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.Stream.SetMicVolume(-0.1, true)
|
b.Stream.SetMicVolume(-0.1, true)
|
||||||
b.UserConfig.SetMicVolume(b.Stream.GetMicVolume())
|
b.UserConfig.SetMicVolume(b.Stream.GetMicVolume())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnMicVolumeUp(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnMicVolumeUp(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.Stream.SetMicVolume(0.1, true)
|
b.Stream.SetMicVolume(0.1, true)
|
||||||
b.UserConfig.SetMicVolume(b.Stream.GetMicVolume())
|
b.UserConfig.SetMicVolume(b.Stream.GetMicVolume())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnQuitPress(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnQuitPress(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.Client.Disconnect()
|
b.StopRecordingIfActive(true)
|
||||||
b.Ui.Close()
|
b.Client.Disconnect()
|
||||||
|
b.Ui.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandExit(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandExit(ui *uiterm.Ui, cmd string) {
|
||||||
b.Client.Disconnect()
|
b.StopRecordingIfActive(true)
|
||||||
b.Ui.Close()
|
b.Client.Disconnect()
|
||||||
|
b.Ui.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) CommandStatus(ui *uiterm.Ui, cmd string) {
|
func (b *Barnard) CommandStatus(ui *uiterm.Ui, cmd string) {
|
||||||
if b.Tx {
|
if b.Tx {
|
||||||
b.Notify("status", "me", "transmitting")
|
b.Notify("status", "me", "transmitting")
|
||||||
} else {
|
} else {
|
||||||
b.Notify("status", "me", "not transmitting")
|
b.Notify("status", "me", "not transmitting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnClearPress(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnClearPress(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.UiOutput.Clear()
|
b.UiOutput.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnScrollOutputUp(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnScrollOutputUp(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.UiOutput.ScrollUp()
|
b.UiOutput.ScrollUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnScrollOutputDown(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnScrollOutputDown(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.UiOutput.ScrollDown()
|
b.UiOutput.ScrollDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnScrollOutputTop(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnScrollOutputTop(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.UiOutput.ScrollTop()
|
b.UiOutput.ScrollTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnScrollOutputBottom(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnScrollOutputBottom(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
b.UiOutput.ScrollBottom()
|
b.UiOutput.ScrollBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnFocusPress(ui *uiterm.Ui, key uiterm.Key) {
|
func (b *Barnard) OnFocusPress(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
active := b.Ui.Active()
|
active := b.Ui.Active()
|
||||||
if active == uiViewInput {
|
if active == uiViewInput {
|
||||||
b.Ui.SetActive(uiViewTree)
|
b.Ui.SetActive(uiViewTree)
|
||||||
} else if active == uiViewTree {
|
} else if active == uiViewTree {
|
||||||
b.Ui.SetActive(uiViewInput)
|
b.Ui.SetActive(uiViewInput)
|
||||||
}
|
}
|
||||||
width, height := termbox.Size()
|
width, height := termbox.Size()
|
||||||
b.OnUiResize(ui, width, height)
|
b.OnUiResize(ui, width, height)
|
||||||
ui.Refresh()
|
ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnTextInput(ui *uiterm.Ui, textbox *uiterm.Textbox, text string) {
|
func (b *Barnard) OnTextInput(ui *uiterm.Ui, textbox *uiterm.Textbox, text string) {
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a command (starts with /)
|
// Check if this is a command (starts with /)
|
||||||
if strings.HasPrefix(text, "/") {
|
if strings.HasPrefix(text, "/") {
|
||||||
// Remove the leading slash and process as command
|
// Remove the leading slash and process as command
|
||||||
cmdText := strings.TrimPrefix(text, "/")
|
cmdText := strings.TrimPrefix(text, "/")
|
||||||
parts := strings.SplitN(cmdText, " ", 2)
|
parts := strings.SplitN(cmdText, " ", 2)
|
||||||
cmdName := parts[0]
|
cmdName := parts[0]
|
||||||
cmdArgs := ""
|
cmdArgs := ""
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
cmdArgs = parts[1]
|
cmdArgs = parts[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle built-in commands
|
// Handle built-in commands
|
||||||
switch cmdName {
|
switch cmdName {
|
||||||
case "file":
|
case "file":
|
||||||
b.CommandPlayFile(ui, cmdArgs)
|
b.CommandPlayFile(ui, cmdArgs)
|
||||||
case "stop":
|
case "stop":
|
||||||
b.CommandStopFile(ui, cmdArgs)
|
b.CommandStopFile(ui, cmdArgs)
|
||||||
case "exit":
|
case "exit":
|
||||||
b.CommandExit(ui, cmdArgs)
|
b.CommandExit(ui, cmdArgs)
|
||||||
case "status":
|
case "status":
|
||||||
b.CommandStatus(ui, cmdArgs)
|
b.CommandStatus(ui, cmdArgs)
|
||||||
case "noise":
|
case "noise":
|
||||||
b.CommandNoiseSuppressionToggle(ui, cmdArgs)
|
b.CommandNoiseSuppressionToggle(ui, cmdArgs)
|
||||||
case "micup":
|
case "record":
|
||||||
b.CommandMicUp(ui, cmdArgs)
|
b.CommandRecord(ui, cmdArgs)
|
||||||
case "micdown":
|
case "micup":
|
||||||
b.CommandMicDown(ui, cmdArgs)
|
b.CommandMicUp(ui, cmdArgs)
|
||||||
case "toggle", "talk":
|
case "micdown":
|
||||||
b.CommandTalk(ui, cmdArgs)
|
b.CommandMicDown(ui, cmdArgs)
|
||||||
default:
|
case "toggle", "talk":
|
||||||
b.AddOutputLine(fmt.Sprintf("Unknown command: /%s", cmdName))
|
b.CommandTalk(ui, cmdArgs)
|
||||||
}
|
default:
|
||||||
return
|
b.AddOutputLine(fmt.Sprintf("Unknown command: /%s", cmdName))
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Not a command, send as chat message
|
// Not a command, send as chat message
|
||||||
if b.Client != nil && b.Client.Self != nil {
|
if b.Client != nil && b.Client.Self != nil {
|
||||||
if b.selectedUser != nil {
|
if b.selectedUser != nil {
|
||||||
b.selectedUser.Send(text)
|
b.selectedUser.Send(text)
|
||||||
b.AddOutputPrivateMessage(b.Client.Self, b.selectedUser, text)
|
b.AddOutputPrivateMessage(b.Client.Self, b.selectedUser, text)
|
||||||
} else {
|
} else {
|
||||||
b.Client.Self.Channel.Send(text, false)
|
b.Client.Self.Channel.Send(text, false)
|
||||||
b.AddOutputMessage(b.Client.Self, text)
|
b.AddOutputMessage(b.Client.Self, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) GotoChat() {
|
func (b *Barnard) GotoChat() {
|
||||||
b.OnFocusPress(b.Ui, uiterm.KeyTab)
|
b.OnFocusPress(b.Ui, uiterm.KeyTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnUiDoneInitialize(ui *uiterm.Ui) {
|
func (b *Barnard) OnUiDoneInitialize(ui *uiterm.Ui) {
|
||||||
b.start()
|
b.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
|
func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
|
||||||
ui.Add(uiViewLogo, &uiterm.Label{
|
ui.Add(uiViewLogo, &uiterm.Label{
|
||||||
Text: "Barnard ",
|
Text: "Barnard ",
|
||||||
Fg: uiterm.ColorWhite | uiterm.AttrBold,
|
Fg: uiterm.ColorWhite | uiterm.AttrBold,
|
||||||
Bg: uiterm.ColorMagenta,
|
Bg: uiterm.ColorMagenta,
|
||||||
})
|
})
|
||||||
|
|
||||||
b.UiStatus = uiterm.Label{
|
b.UiStatus = uiterm.Label{
|
||||||
Text: " Idle ",
|
Text: " Idle ",
|
||||||
Fg: uiterm.ColorBlack,
|
Fg: uiterm.ColorBlack,
|
||||||
Bg: uiterm.ColorWhite,
|
Bg: uiterm.ColorWhite,
|
||||||
}
|
}
|
||||||
ui.Add(uiViewStatus, &b.UiStatus)
|
ui.Add(uiViewStatus, &b.UiStatus)
|
||||||
|
|
||||||
b.UiInput = uiterm.Textbox{
|
b.UiInput = uiterm.Textbox{
|
||||||
Fg: uiterm.ColorWhite,
|
Fg: uiterm.ColorWhite,
|
||||||
Bg: uiterm.ColorBlack,
|
Bg: uiterm.ColorBlack,
|
||||||
Input: b.OnTextInput,
|
Input: b.OnTextInput,
|
||||||
}
|
}
|
||||||
ui.Add(uiViewInput, &b.UiInput)
|
ui.Add(uiViewInput, &b.UiInput)
|
||||||
|
|
||||||
b.UiInputStatus = uiterm.Label{
|
b.UiInputStatus = uiterm.Label{
|
||||||
Fg: uiterm.ColorBlack,
|
Fg: uiterm.ColorBlack,
|
||||||
Bg: uiterm.ColorWhite,
|
Bg: uiterm.ColorWhite,
|
||||||
}
|
}
|
||||||
ui.Add(uiViewInputStatus, &b.UiInputStatus)
|
ui.Add(uiViewInputStatus, &b.UiInputStatus)
|
||||||
|
|
||||||
b.UiOutput = uiterm.Textview{
|
b.UiOutput = uiterm.Textview{
|
||||||
Fg: uiterm.ColorWhite,
|
Fg: uiterm.ColorWhite,
|
||||||
Bg: uiterm.ColorBlack,
|
Bg: uiterm.ColorBlack,
|
||||||
}
|
}
|
||||||
ui.Add(uiViewOutput, &b.UiOutput)
|
ui.Add(uiViewOutput, &b.UiOutput)
|
||||||
|
|
||||||
b.UiTree = uiterm.Tree{
|
b.UiTree = uiterm.Tree{
|
||||||
Generator: b.TreeItemBuild,
|
Generator: b.TreeItemBuild,
|
||||||
KeyListener: b.TreeItemKeyPress,
|
KeyListener: b.TreeItemKeyPress,
|
||||||
CharacterListener: b.TreeItemCharacter,
|
CharacterListener: b.TreeItemCharacter,
|
||||||
Fg: uiterm.ColorWhite,
|
Fg: uiterm.ColorWhite,
|
||||||
Bg: uiterm.ColorBlack,
|
Bg: uiterm.ColorBlack,
|
||||||
}
|
}
|
||||||
ui.Add(uiViewTree, &b.UiTree)
|
ui.Add(uiViewTree, &b.UiTree)
|
||||||
|
|
||||||
b.Ui.AddCommandListener(b.CommandMicUp, "micup")
|
b.Ui.AddCommandListener(b.CommandMicUp, "micup")
|
||||||
b.Ui.AddCommandListener(b.CommandMicDown, "micdown")
|
b.Ui.AddCommandListener(b.CommandMicDown, "micdown")
|
||||||
b.Ui.AddCommandListener(b.CommandTalk, "toggle")
|
b.Ui.AddCommandListener(b.CommandTalk, "toggle")
|
||||||
b.Ui.AddCommandListener(b.CommandTalk, "talk")
|
b.Ui.AddCommandListener(b.CommandTalk, "talk")
|
||||||
b.Ui.AddCommandListener(b.CommandExit, "exit")
|
b.Ui.AddCommandListener(b.CommandExit, "exit")
|
||||||
b.Ui.AddCommandListener(b.CommandStatus, "status")
|
b.Ui.AddCommandListener(b.CommandStatus, "status")
|
||||||
b.Ui.AddCommandListener(b.CommandNoiseSuppressionToggle, "noise")
|
b.Ui.AddCommandListener(b.CommandNoiseSuppressionToggle, "noise")
|
||||||
b.Ui.AddCommandListener(b.CommandPlayFile, "file")
|
b.Ui.AddCommandListener(b.CommandPlayFile, "file")
|
||||||
b.Ui.AddCommandListener(b.CommandStopFile, "stop")
|
b.Ui.AddCommandListener(b.CommandStopFile, "stop")
|
||||||
b.Ui.AddKeyListener(b.OnFocusPress, b.Hotkeys.SwitchViews)
|
b.Ui.AddCommandListener(b.CommandRecord, "record")
|
||||||
b.Ui.AddKeyListener(b.OnVoiceToggle, b.Hotkeys.Talk)
|
b.Ui.AddKeyListener(b.OnFocusPress, b.Hotkeys.SwitchViews)
|
||||||
b.Ui.AddKeyListener(b.OnTimestampToggle, b.Hotkeys.ToggleTimestamps)
|
b.Ui.AddKeyListener(b.OnVoiceToggle, b.Hotkeys.Talk)
|
||||||
b.Ui.AddKeyListener(b.OnNoiseSuppressionToggle, b.Hotkeys.NoiseSuppressionToggle)
|
b.Ui.AddKeyListener(b.OnTimestampToggle, b.Hotkeys.ToggleTimestamps)
|
||||||
b.Ui.AddKeyListener(b.OnVoiceEffectCycle, b.Hotkeys.CycleVoiceEffect)
|
b.Ui.AddKeyListener(b.OnNoiseSuppressionToggle, b.Hotkeys.NoiseSuppressionToggle)
|
||||||
b.Ui.AddKeyListener(b.OnQuitPress, b.Hotkeys.Exit)
|
b.Ui.AddKeyListener(b.OnVoiceEffectCycle, b.Hotkeys.CycleVoiceEffect)
|
||||||
b.Ui.AddKeyListener(b.OnScrollOutputUp, b.Hotkeys.ScrollUp)
|
b.Ui.AddKeyListener(b.OnRecordingToggle, b.Hotkeys.RecordToggle)
|
||||||
b.Ui.AddKeyListener(b.OnScrollOutputDown, b.Hotkeys.ScrollDown)
|
b.Ui.AddKeyListener(b.OnQuitPress, b.Hotkeys.Exit)
|
||||||
b.Ui.AddKeyListener(b.OnScrollOutputTop, b.Hotkeys.ScrollToTop)
|
b.Ui.AddKeyListener(b.OnScrollOutputUp, b.Hotkeys.ScrollUp)
|
||||||
b.Ui.AddKeyListener(b.OnScrollOutputBottom, b.Hotkeys.ScrollToBottom)
|
b.Ui.AddKeyListener(b.OnScrollOutputDown, b.Hotkeys.ScrollDown)
|
||||||
b.Ui.SetActive(uiViewInput)
|
b.Ui.AddKeyListener(b.OnScrollOutputTop, b.Hotkeys.ScrollToTop)
|
||||||
b.UiTree.Rebuild()
|
b.Ui.AddKeyListener(b.OnScrollOutputBottom, b.Hotkeys.ScrollToBottom)
|
||||||
b.Ui.Refresh()
|
b.Ui.SetActive(uiViewInput)
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) OnUiResize(ui *uiterm.Ui, width, height int) {
|
func (b *Barnard) OnUiResize(ui *uiterm.Ui, width, height int) {
|
||||||
treeHeight := 0
|
treeHeight := 0
|
||||||
outputHeight := 0
|
outputHeight := 0
|
||||||
active := b.Ui.Active()
|
active := b.Ui.Active()
|
||||||
if active == uiViewTree {
|
if active == uiViewTree {
|
||||||
treeHeight = height - 4
|
treeHeight = height - 4
|
||||||
outputHeight = 0
|
outputHeight = 0
|
||||||
} else {
|
} else {
|
||||||
treeHeight = 0
|
treeHeight = 0
|
||||||
outputHeight = height - 4
|
outputHeight = height - 4
|
||||||
}
|
}
|
||||||
ui.SetBounds(uiViewOutput, 0, 1, width, outputHeight+1)
|
ui.SetBounds(uiViewOutput, 0, 1, width, outputHeight+1)
|
||||||
ui.SetBounds(uiViewTree, 0, 1, width, treeHeight+1)
|
ui.SetBounds(uiViewTree, 0, 1, width, treeHeight+1)
|
||||||
ui.SetBounds(uiViewStatus, 0, height-2, width, height-1)
|
ui.SetBounds(uiViewStatus, 0, height-2, width, height-1)
|
||||||
ui.SetBounds(uiViewInputStatus, 0, height-1, len(b.GetInputStatus()), height)
|
ui.SetBounds(uiViewInputStatus, 0, height-1, len(b.GetInputStatus()), height)
|
||||||
ui.SetBounds(uiViewInput, len(b.GetInputStatus())+1, height-1, width, height)
|
ui.SetBounds(uiViewInput, len(b.GetInputStatus())+1, height-1, width, height)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user