Set default volume to MAX
Set MAX volume for SDL2 Mixer, as well as the volume for all audio sinks via wpctl to 1.0. This forces the computer to always be at absolute max volume, without creating scripts on the machine to run wpctl commands. Leaving the display or connected speakers to be in full control over the volume of music.
This commit is contained in:
@@ -120,7 +120,6 @@ func Run() {
|
||||
InitialFontSize: 128,
|
||||
StandardFontSize: 32,
|
||||
LinePattern: "fibonacci",
|
||||
LastVolume: 128,
|
||||
AudioEnabled: true,
|
||||
}
|
||||
if err := DB.Create(&initialConfig).Error; err != nil {
|
||||
@@ -443,14 +442,6 @@ func (app *Application) initKeybinds() {
|
||||
keybinds.Register(sdl.K_q, sdl.KMOD_RCTRL, func() {
|
||||
app.Stop()
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_UP, sdl.KMOD_SHIFT, func() {
|
||||
mixer.SetVolume128(volumeStepSize, app.Config.Load())
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_DOWN, sdl.KMOD_SHIFT, func() {
|
||||
mixer.SetVolume128(-volumeStepSize, app.Config.Load())
|
||||
})
|
||||
}
|
||||
|
||||
func (app *Application) Stop() {
|
||||
|
||||
@@ -15,7 +15,6 @@ type AppConfig struct {
|
||||
InitialFontSize int // How large to create the font atlas
|
||||
StandardFontSize int // The font size used for most text rendering
|
||||
LinePattern string // "fibonacci"
|
||||
LastVolume int // 0-128
|
||||
AudioEnabled bool
|
||||
}
|
||||
|
||||
|
||||
+176
-37
@@ -2,8 +2,13 @@
|
||||
package mixer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/veandco/go-sdl2/mix"
|
||||
@@ -33,6 +38,13 @@ var (
|
||||
refreshCacheCallback func()
|
||||
)
|
||||
|
||||
type Sink struct {
|
||||
ID int
|
||||
Name string
|
||||
Volume float64
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
// Init initializes SDL audio + SDL_mixer. Safe to call multiple times.
|
||||
func Init(cfg *db.AppConfig, onUpdate func()) error {
|
||||
mu.Lock()
|
||||
@@ -58,12 +70,7 @@ func Init(cfg *db.AppConfig, onUpdate func()) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIX 2: Actually set a default volume!
|
||||
currentVolume = 128 // MAX_VOLUME
|
||||
if cfg.LastVolume > 0 {
|
||||
currentVolume = cfg.LastVolume
|
||||
}
|
||||
mix.VolumeMusic(currentVolume)
|
||||
SetAllSinkVolumes(1.0)
|
||||
|
||||
inited = true
|
||||
return nil
|
||||
@@ -131,10 +138,9 @@ func Play(path string, loopForever bool, cfg *db.AppConfig) error {
|
||||
currentLoops = 0
|
||||
}
|
||||
|
||||
// DELETE the openDeviceLocked block that was here!
|
||||
|
||||
// Apply remembered volume
|
||||
mix.VolumeMusic(currentVolume)
|
||||
mix.VolumeMusic(mix.MAX_VOLUME)
|
||||
volume := mix.VolumeMusic(-1)
|
||||
logger.Info("Current volume is %d", volume)
|
||||
|
||||
if err := currentMusic.Play(currentLoops); err != nil {
|
||||
currentMusic.Free()
|
||||
@@ -171,33 +177,6 @@ func Stop(fadeMs int, cfg *db.AppConfig) {
|
||||
currentLoops = 0
|
||||
}
|
||||
|
||||
// SetVolume128 sets music volume using SDL_mixer’s 0..128 scale.
|
||||
func SetVolume128(increment int, cfg *db.AppConfig) {
|
||||
if !cfg.AudioEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
currentVolume += increment
|
||||
|
||||
if currentVolume < 0 {
|
||||
currentVolume = 0
|
||||
} else if currentVolume > mix.MAX_VOLUME {
|
||||
currentVolume = int(mix.MAX_VOLUME)
|
||||
}
|
||||
|
||||
// Store in config
|
||||
cfg.LastVolume = currentVolume
|
||||
refreshCacheCallback()
|
||||
|
||||
// Apply to SDL_mixer
|
||||
if inited && deviceOpen {
|
||||
mix.VolumeMusic(currentVolume)
|
||||
}
|
||||
}
|
||||
|
||||
// IsPlaying reports whether music is currently playing (or fading).
|
||||
func IsPlaying() bool {
|
||||
mu.Lock()
|
||||
@@ -208,6 +187,166 @@ func IsPlaying() bool {
|
||||
return mix.PlayingMusic()
|
||||
}
|
||||
|
||||
// SetDefaultSinkVolume sets volume 0.0–1.0 on the default sink
|
||||
func SetDefaultSinkVolume(level float64) error {
|
||||
if level < 0 || level > 1 {
|
||||
return fmt.Errorf("volume must be between 0.0 and 1.0, got %f", level)
|
||||
}
|
||||
pct := fmt.Sprintf("%.2f", level)
|
||||
cmd := exec.Command("wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", pct)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("wpctl failed: %w\noutput: %s", err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listSinkIDs returns just the numeric IDs of all audio sinks.
|
||||
func listSinkIDs() ([]int, error) {
|
||||
sinks, err := listSinks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]int, len(sinks))
|
||||
for i, s := range sinks {
|
||||
ids[i] = s.ID
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// listSinks returns full Sink info for all audio sinks.
|
||||
func listSinks() ([]Sink, error) {
|
||||
out, err := exec.Command("wpctl", "status").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wpctl status: %w", err)
|
||||
}
|
||||
return parseSinks(out)
|
||||
}
|
||||
|
||||
// SetAllSinkVolumes sets volume on every audio sink found via wpctl status
|
||||
func SetAllSinkVolumes(level float64) error {
|
||||
ids, err := listSinkIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pct := fmt.Sprintf("%.2f", level)
|
||||
for _, id := range ids {
|
||||
cmd := exec.Command("wpctl", "set-volume", strconv.Itoa(id), pct)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("wpctl set-volume %d failed: %w\noutput: %s", id, err, out)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseSinks parses the output of `wpctl status` and extracts audio sinks.
|
||||
func parseSinks(data []byte) ([]Sink, error) {
|
||||
var sinks []Sink
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
|
||||
inAudioSection := false
|
||||
inSinksSection := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
stripped := strings.TrimLeft(line, " │├└─")
|
||||
|
||||
// Detect top-level sections: "Audio", "Video", "Settings", etc.
|
||||
// These are lines with no leading tree characters.
|
||||
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "│") {
|
||||
inAudioSection = strings.TrimSpace(line) == "Audio"
|
||||
inSinksSection = false
|
||||
continue
|
||||
}
|
||||
|
||||
if !inAudioSection {
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect subsection headers within Audio
|
||||
if strings.Contains(stripped, "Sinks:") {
|
||||
inSinksSection = true
|
||||
continue
|
||||
}
|
||||
// Any other subsection header resets sink parsing
|
||||
if strings.HasSuffix(strings.TrimSpace(stripped), ":") {
|
||||
inSinksSection = false
|
||||
continue
|
||||
}
|
||||
|
||||
if !inSinksSection {
|
||||
continue
|
||||
}
|
||||
|
||||
sink, ok := parseSinkLine(stripped)
|
||||
if ok {
|
||||
sinks = append(sinks, sink)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("scanning wpctl output: %w", err)
|
||||
}
|
||||
|
||||
return sinks, nil
|
||||
}
|
||||
|
||||
// parseSinkLine parses a single sink entry line like:
|
||||
//
|
||||
// "*51. USB Audio [vol: 1.00]"
|
||||
// "33. Built-in Audio Analog Stereo [vol: 0.65]"
|
||||
func parseSinkLine(s string) (Sink, bool) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return Sink{}, false
|
||||
}
|
||||
|
||||
var sink Sink
|
||||
|
||||
// Leading '*' marks the default sink
|
||||
if strings.HasPrefix(s, "*") {
|
||||
sink.IsDefault = true
|
||||
s = strings.TrimPrefix(s, "*")
|
||||
s = strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// Expect "<id>. <name> ..."
|
||||
dotIdx := strings.Index(s, ".")
|
||||
if dotIdx < 0 {
|
||||
return Sink{}, false
|
||||
}
|
||||
|
||||
idStr := strings.TrimSpace(s[:dotIdx])
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return Sink{}, false
|
||||
}
|
||||
sink.ID = id
|
||||
|
||||
rest := strings.TrimSpace(s[dotIdx+1:])
|
||||
|
||||
// Extract [vol: X.XX] if present
|
||||
volStart := strings.Index(rest, "[vol:")
|
||||
if volStart >= 0 {
|
||||
sink.Name = strings.TrimSpace(rest[:volStart])
|
||||
volPart := rest[volStart:]
|
||||
// "[vol: 0.65]" or "[vol: 0.65 MUTED]"
|
||||
volPart = strings.TrimPrefix(volPart, "[vol:")
|
||||
volPart = strings.TrimSpace(volPart)
|
||||
fields := strings.Fields(volPart)
|
||||
if len(fields) > 0 {
|
||||
v, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err == nil {
|
||||
sink.Volume = v
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sink.Name = strings.TrimSpace(rest)
|
||||
}
|
||||
|
||||
return sink, true
|
||||
}
|
||||
|
||||
// ListDevices returns the list of available output device names (from core SDL).
|
||||
func ListDevices() []string {
|
||||
mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user