Implement per-user audio control (vol/mute) and User View UI
This commit is contained in:
224
cmd/tui/model.go
224
cmd/tui/model.go
@@ -21,8 +21,16 @@ const (
|
|||||||
FocusChannels Focus = iota
|
FocusChannels Focus = iota
|
||||||
FocusChat
|
FocusChat
|
||||||
FocusInput
|
FocusInput
|
||||||
|
FocusUserView
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ListItem represents an item in the navigation tree (Channel or User)
|
||||||
|
type ListItem struct {
|
||||||
|
IsUser bool
|
||||||
|
Channel *ChannelNode
|
||||||
|
User *UserNode
|
||||||
|
}
|
||||||
|
|
||||||
// ChatMessage represents a message in the chat
|
// ChatMessage represents a message in the chat
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
Time time.Time
|
Time time.Time
|
||||||
@@ -65,6 +73,7 @@ type Model struct {
|
|||||||
focus Focus
|
focus Focus
|
||||||
width, height int
|
width, height int
|
||||||
channels []ChannelNode
|
channels []ChannelNode
|
||||||
|
items []ListItem // Flattened list for navigation
|
||||||
selectedIdx int
|
selectedIdx int
|
||||||
chatMessages []ChatMessage
|
chatMessages []ChatMessage
|
||||||
logMessages []string // Debug logs shown in chat panel
|
logMessages []string // Debug logs shown in chat panel
|
||||||
@@ -88,6 +97,10 @@ type Model struct {
|
|||||||
// Program reference for sending messages from event handlers
|
// Program reference for sending messages from event handlers
|
||||||
program *tea.Program
|
program *tea.Program
|
||||||
showLog bool
|
showLog bool
|
||||||
|
|
||||||
|
// User View
|
||||||
|
showUserView bool
|
||||||
|
viewUser *UserNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// addLog adds a message to the log panel
|
// addLog adds a message to the log panel
|
||||||
@@ -428,6 +441,20 @@ func (m *Model) updateChannelList(channels []*ts3client.Channel) {
|
|||||||
// We'll stick to valid tree.
|
// We'll stick to valid tree.
|
||||||
|
|
||||||
m.channels = sortedNodes
|
m.channels = sortedNodes
|
||||||
|
|
||||||
|
// Build flattened list for navigation
|
||||||
|
m.items = make([]ListItem, 0)
|
||||||
|
for i := range m.channels {
|
||||||
|
ch := &m.channels[i]
|
||||||
|
m.items = append(m.items, ListItem{IsUser: false, Channel: ch})
|
||||||
|
|
||||||
|
if ch.Expanded {
|
||||||
|
for j := range ch.Users {
|
||||||
|
u := &ch.Users[j]
|
||||||
|
m.items = append(m.items, ListItem{IsUser: true, User: u, Channel: ch})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
@@ -459,6 +486,7 @@ func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Global Shortcuts (Only when NOT in Input)
|
// 3. Global Shortcuts (Only when NOT in Input)
|
||||||
|
if m.focus != FocusInput && m.focus != FocusUserView {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "q":
|
case "q":
|
||||||
// Quit (same as ctrl+c)
|
// Quit (same as ctrl+c)
|
||||||
@@ -524,6 +552,7 @@ func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.showLog = !m.showLog
|
m.showLog = !m.showLog
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Focus-specific keys
|
// Focus-specific keys
|
||||||
switch m.focus {
|
switch m.focus {
|
||||||
@@ -533,6 +562,8 @@ func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m.handleInputKeys(msg)
|
return m.handleInputKeys(msg)
|
||||||
case FocusChat:
|
case FocusChat:
|
||||||
return m.handleChatKeys(msg)
|
return m.handleChatKeys(msg)
|
||||||
|
case FocusUserView:
|
||||||
|
return m.handleUserViewKeys(msg)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -541,6 +572,49 @@ func (m *Model) handleChatKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleUserViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
if m.viewUser == nil || m.audioPlayer == nil {
|
||||||
|
if msg.String() == "esc" || msg.String() == "q" {
|
||||||
|
m.focus = FocusChannels
|
||||||
|
m.showUserView = false
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u := m.viewUser
|
||||||
|
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc", "q":
|
||||||
|
m.focus = FocusChannels
|
||||||
|
m.showUserView = false
|
||||||
|
|
||||||
|
case "2":
|
||||||
|
// Toggle mute for this user
|
||||||
|
_, muted := m.audioPlayer.GetUserSettings(u.ID)
|
||||||
|
m.audioPlayer.SetUserMuted(u.ID, !muted)
|
||||||
|
m.addLog("Toggled mute for %s. Now muted: %v", u.Nickname, !muted)
|
||||||
|
|
||||||
|
case "+", "=":
|
||||||
|
// Increase volume
|
||||||
|
currentVol, _ := m.audioPlayer.GetUserSettings(u.ID)
|
||||||
|
newVol := currentVol + 0.1
|
||||||
|
if newVol > 2.0 {
|
||||||
|
newVol = 2.0
|
||||||
|
} // Allow up to 200% boost
|
||||||
|
m.audioPlayer.SetUserVolume(u.ID, newVol)
|
||||||
|
|
||||||
|
case "-", "_":
|
||||||
|
// Decrease volume
|
||||||
|
currentVol, _ := m.audioPlayer.GetUserSettings(u.ID)
|
||||||
|
newVol := currentVol - 0.1
|
||||||
|
if newVol < 0.0 {
|
||||||
|
newVol = 0.0
|
||||||
|
}
|
||||||
|
m.audioPlayer.SetUserVolume(u.ID, newVol)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) handleChannelKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleChannelKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
@@ -548,20 +622,33 @@ func (m *Model) handleChannelKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.selectedIdx--
|
m.selectedIdx--
|
||||||
}
|
}
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
if m.selectedIdx < len(m.channels)-1 {
|
if m.selectedIdx < len(m.items)-1 {
|
||||||
m.selectedIdx++
|
m.selectedIdx++
|
||||||
}
|
}
|
||||||
case "enter":
|
case "enter":
|
||||||
// Join selected channel
|
// Join selected channel OR open user view
|
||||||
if m.selectedIdx < len(m.channels) && m.client != nil {
|
if m.selectedIdx < len(m.items) && m.client != nil {
|
||||||
ch := m.channels[m.selectedIdx]
|
item := m.items[m.selectedIdx]
|
||||||
|
if !item.IsUser {
|
||||||
|
// Channel
|
||||||
|
ch := item.Channel
|
||||||
m.addLog("Attempting to join channel: %s (ID=%d)", ch.Name, ch.ID)
|
m.addLog("Attempting to join channel: %s (ID=%d)", ch.Name, ch.ID)
|
||||||
err := m.client.JoinChannel(ch.ID)
|
err := m.client.JoinChannel(ch.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.addLog("Error joining channel: %v", err)
|
m.addLog("Error joining channel: %v", err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// User
|
||||||
|
m.viewUser = item.User
|
||||||
|
m.showUserView = true
|
||||||
|
m.focus = FocusUserView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Bound check
|
||||||
|
if m.selectedIdx >= len(m.items) && len(m.items) > 0 {
|
||||||
|
m.selectedIdx = len(m.items) - 1
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,10 +733,15 @@ func (m *Model) View() string {
|
|||||||
}
|
}
|
||||||
channelPanel := channelPanelStyle.Render(channelContent)
|
channelPanel := channelPanelStyle.Render(channelContent)
|
||||||
|
|
||||||
// Right panel (Chat or Log)
|
// Right panel (Chat, Log, or User View)
|
||||||
var rightPanel string
|
var rightPanel string
|
||||||
|
|
||||||
if m.showLog {
|
if m.showUserView && m.viewUser != nil {
|
||||||
|
// User View
|
||||||
|
userViewContent := m.renderUserView()
|
||||||
|
userViewStyle := chatPanelStyle.Copy().BorderForeground(lipgloss.Color("208")) // Orange focus
|
||||||
|
rightPanel = userViewStyle.Render(userViewContent)
|
||||||
|
} else if m.showLog {
|
||||||
// Log View
|
// Log View
|
||||||
// Use chatPanelStyle default but add focus logic
|
// Use chatPanelStyle default but add focus logic
|
||||||
logStyle := chatPanelStyle.Copy()
|
logStyle := chatPanelStyle.Copy()
|
||||||
@@ -807,7 +899,7 @@ func (m *Model) renderStatusBar() string {
|
|||||||
var spacerRegex = regexp.MustCompile(`^\[([*cZr]?spacer[\w\d]*)\](.*)`)
|
var spacerRegex = regexp.MustCompile(`^\[([*cZr]?spacer[\w\d]*)\](.*)`)
|
||||||
|
|
||||||
func (m *Model) renderChannels() string {
|
func (m *Model) renderChannels() string {
|
||||||
if len(m.channels) == 0 {
|
if len(m.items) == 0 {
|
||||||
return "No channels..."
|
return "No channels..."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,34 +907,37 @@ func (m *Model) renderChannels() string {
|
|||||||
lines = append(lines, lipgloss.NewStyle().Bold(true).Render("CHANNELS"))
|
lines = append(lines, lipgloss.NewStyle().Bold(true).Render("CHANNELS"))
|
||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
|
|
||||||
for i, ch := range m.channels {
|
for i, item := range m.items {
|
||||||
// Indentation based on depth
|
// If selected
|
||||||
|
isSelected := (i == m.selectedIdx)
|
||||||
|
|
||||||
|
if !item.IsUser {
|
||||||
|
// CHANNEL
|
||||||
|
ch := item.Channel
|
||||||
indent := strings.Repeat(" ", ch.Depth)
|
indent := strings.Repeat(" ", ch.Depth)
|
||||||
prefix := indent + " "
|
prefix := indent + " "
|
||||||
if i == m.selectedIdx {
|
if isSelected {
|
||||||
prefix = indent + "► "
|
prefix = indent + "► "
|
||||||
}
|
}
|
||||||
|
|
||||||
style := lipgloss.NewStyle()
|
style := lipgloss.NewStyle()
|
||||||
if i == m.selectedIdx {
|
if isSelected {
|
||||||
style = style.Bold(true).Foreground(lipgloss.Color("212"))
|
style = style.Bold(true).Foreground(lipgloss.Color("212"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for spacer
|
|
||||||
displayName := ch.Name
|
displayName := ch.Name
|
||||||
|
|
||||||
|
// Spacer rendering logic
|
||||||
matches := spacerRegex.FindStringSubmatch(ch.Name)
|
matches := spacerRegex.FindStringSubmatch(ch.Name)
|
||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
tag := matches[1] // e.g. "spacer0", "cspacer", "*spacer"
|
tag := matches[1]
|
||||||
content := matches[2] // e.g. "---", "Section"
|
content := matches[2]
|
||||||
|
|
||||||
// Calculate effective width: width/4 - 6 (border+padding)
|
|
||||||
width := (m.width / 4) - 6
|
width := (m.width / 4) - 6
|
||||||
if width < 0 {
|
if width < 0 {
|
||||||
width = 0
|
width = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(tag, "*") {
|
if strings.HasPrefix(tag, "*") {
|
||||||
// Repeat content
|
|
||||||
if len(content) > 0 {
|
if len(content) > 0 {
|
||||||
count := width / len(content)
|
count := width / len(content)
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
@@ -852,9 +947,6 @@ func (m *Model) renderChannels() string {
|
|||||||
displayName = strings.Repeat("-", width)
|
displayName = strings.Repeat("-", width)
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(tag, "c") {
|
} else if strings.HasPrefix(tag, "c") {
|
||||||
// Center content
|
|
||||||
if len(content) > len(displayName) { // If parsed content is safer?
|
|
||||||
// No, just align content
|
|
||||||
gap := (width - len(content)) / 2
|
gap := (width - len(content)) / 2
|
||||||
if gap > 0 {
|
if gap > 0 {
|
||||||
displayName = strings.Repeat(" ", gap) + content
|
displayName = strings.Repeat(" ", gap) + content
|
||||||
@@ -862,38 +954,36 @@ func (m *Model) renderChannels() string {
|
|||||||
displayName = content
|
displayName = content
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback if measurement is tricky, just show content
|
|
||||||
// Actually, let's try to center strictly
|
|
||||||
gap := (width - len(content)) / 2
|
|
||||||
if gap > 0 {
|
|
||||||
displayName = strings.Repeat(" ", gap) + content
|
|
||||||
} else {
|
|
||||||
displayName = content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Standard spacer (left align)
|
|
||||||
displayName = content
|
displayName = content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines = append(lines, style.Render(prefix+displayName))
|
lines = append(lines, style.Render(prefix+displayName))
|
||||||
|
} else {
|
||||||
|
// USER
|
||||||
|
user := item.User
|
||||||
|
ch := item.Channel // Parent
|
||||||
|
indent := strings.Repeat(" ", ch.Depth)
|
||||||
|
|
||||||
// Show users in channel
|
prefix := indent + " └─ "
|
||||||
for _, user := range ch.Users {
|
if isSelected {
|
||||||
userPrefix := " └─ "
|
prefix = indent + " => "
|
||||||
userStyle := lipgloss.NewStyle().Faint(true)
|
|
||||||
|
|
||||||
// Talking users get bright green color and speaker icon
|
|
||||||
if user.Talking {
|
|
||||||
userStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("46")) // Bright green
|
|
||||||
userPrefix = " └─ 🔊 "
|
|
||||||
} else if user.IsMe {
|
|
||||||
userStyle = userStyle.Foreground(lipgloss.Color("82"))
|
|
||||||
userPrefix = " └─ ► "
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines = append(lines, userStyle.Render(userPrefix+user.Nickname))
|
userStyle := lipgloss.NewStyle().Faint(true)
|
||||||
|
|
||||||
|
if user.Talking {
|
||||||
|
userStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("46"))
|
||||||
|
prefix = indent + " 🔊 "
|
||||||
|
} else if user.IsMe {
|
||||||
|
userStyle = userStyle.Foreground(lipgloss.Color("82"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSelected {
|
||||||
|
userStyle = userStyle.Bold(true).Foreground(lipgloss.Color("212")) // Pink selection
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, userStyle.Render(prefix+user.Nickname))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,3 +1025,49 @@ func (m *Model) renderChat() string {
|
|||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) renderUserView() string {
|
||||||
|
if m.viewUser == nil {
|
||||||
|
return "No user selected"
|
||||||
|
}
|
||||||
|
|
||||||
|
u := m.viewUser
|
||||||
|
|
||||||
|
// Get audio settings
|
||||||
|
vol := float32(1.0)
|
||||||
|
muted := false
|
||||||
|
if m.audioPlayer != nil {
|
||||||
|
vol, muted = m.audioPlayer.GetUserSettings(u.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("208")).Underline(true)
|
||||||
|
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||||
|
valStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||||
|
|
||||||
|
muteStr := "No"
|
||||||
|
if muted {
|
||||||
|
muteStr = "YES"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info section
|
||||||
|
info := []string{
|
||||||
|
titleStyle.Render(fmt.Sprintf("User: %s", u.Nickname)),
|
||||||
|
"",
|
||||||
|
fmt.Sprintf("%s %s", labelStyle.Render("ID:"), valStyle.Render(fmt.Sprintf("%d", u.ID))),
|
||||||
|
fmt.Sprintf("%s %v", labelStyle.Render("Talking:"), valStyle.Render(fmt.Sprintf("%v", m.talkingClients[u.ID]))),
|
||||||
|
fmt.Sprintf("%s %v", labelStyle.Render("Muted (Server):"), valStyle.Render(fmt.Sprintf("%v", u.Muted))),
|
||||||
|
"",
|
||||||
|
"--- Audio Settings ---",
|
||||||
|
fmt.Sprintf("%s %d%%", labelStyle.Render("Volume:"), int(vol*100)),
|
||||||
|
fmt.Sprintf("%s %s", labelStyle.Render("Local Mute:"), muteStr),
|
||||||
|
"",
|
||||||
|
"--- Menu ---",
|
||||||
|
"1. Poke (Not Impl)",
|
||||||
|
"2. Toggle Local Mute",
|
||||||
|
"+/-: Adjust Volume",
|
||||||
|
"",
|
||||||
|
"(Press ESC to close)",
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, info...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,9 +26,17 @@ type Player struct {
|
|||||||
// User buffers for mixing
|
// User buffers for mixing
|
||||||
// map[SenderID] -> AudioQueue
|
// map[SenderID] -> AudioQueue
|
||||||
userBuffers map[uint16][]int16
|
userBuffers map[uint16][]int16
|
||||||
|
|
||||||
|
// User settings
|
||||||
|
userSettings map[uint16]*UserSettings
|
||||||
bufferMu sync.Mutex
|
bufferMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserSettings struct {
|
||||||
|
Volume float32 // 0.0 - 1.0 (or higher for boost)
|
||||||
|
Muted bool
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
frameSamples = 960 // 20ms at 48kHz
|
frameSamples = 960 // 20ms at 48kHz
|
||||||
)
|
)
|
||||||
@@ -105,6 +113,7 @@ func NewPlayer() (*Player, error) {
|
|||||||
muted: false,
|
muted: false,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
userBuffers: make(map[uint16][]int16),
|
userBuffers: make(map[uint16][]int16),
|
||||||
|
userSettings: make(map[uint16]*UserSettings),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +171,11 @@ func (p *Player) PlayPCM(senderID uint16, samples []int16) {
|
|||||||
p.bufferMu.Lock()
|
p.bufferMu.Lock()
|
||||||
defer p.bufferMu.Unlock()
|
defer p.bufferMu.Unlock()
|
||||||
|
|
||||||
|
// Check per-user mute
|
||||||
|
if settings, ok := p.userSettings[senderID]; ok && settings.Muted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Append to user's specific buffer
|
// Append to user's specific buffer
|
||||||
// This ensures sequential playback for the same user
|
// This ensures sequential playback for the same user
|
||||||
p.userBuffers[senderID] = append(p.userBuffers[senderID], samples...)
|
p.userBuffers[senderID] = append(p.userBuffers[senderID], samples...)
|
||||||
@@ -201,13 +215,45 @@ func (p *Player) SetMuted(muted bool) {
|
|||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMuted returns mute state
|
|
||||||
func (p *Player) IsMuted() bool {
|
func (p *Player) IsMuted() bool {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
return p.muted
|
return p.muted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserVolume sets volume for a specific user (1.0 is default)
|
||||||
|
func (p *Player) SetUserVolume(clientID uint16, vol float32) {
|
||||||
|
p.bufferMu.Lock()
|
||||||
|
defer p.bufferMu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := p.userSettings[clientID]; !ok {
|
||||||
|
p.userSettings[clientID] = &UserSettings{Volume: 1.0, Muted: false}
|
||||||
|
}
|
||||||
|
p.userSettings[clientID].Volume = vol
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserMuted sets mute state for a specific user
|
||||||
|
func (p *Player) SetUserMuted(clientID uint16, muted bool) {
|
||||||
|
p.bufferMu.Lock()
|
||||||
|
defer p.bufferMu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := p.userSettings[clientID]; !ok {
|
||||||
|
p.userSettings[clientID] = &UserSettings{Volume: 1.0, Muted: false}
|
||||||
|
}
|
||||||
|
p.userSettings[clientID].Muted = muted
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserSettings gets current volume and mute state for user
|
||||||
|
func (p *Player) GetUserSettings(clientID uint16) (float32, bool) {
|
||||||
|
p.bufferMu.Lock()
|
||||||
|
defer p.bufferMu.Unlock()
|
||||||
|
|
||||||
|
if settings, ok := p.userSettings[clientID]; ok {
|
||||||
|
return settings.Volume, settings.Muted
|
||||||
|
}
|
||||||
|
return 1.0, false
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Player) playbackLoop() {
|
func (p *Player) playbackLoop() {
|
||||||
ticker := time.NewTicker(20 * time.Millisecond)
|
ticker := time.NewTicker(20 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -249,7 +295,14 @@ func (p *Player) writeFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < toTake; i++ {
|
for i := 0; i < toTake; i++ {
|
||||||
mixed[i] += int32(buf[i])
|
sample := int32(buf[i])
|
||||||
|
|
||||||
|
// Apply user volume if set
|
||||||
|
if settings, ok := p.userSettings[id]; ok {
|
||||||
|
sample = int32(float32(sample) * settings.Volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
mixed[i] += sample
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance buffer
|
// Advance buffer
|
||||||
|
|||||||
Reference in New Issue
Block a user