Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/build/
/lib/
/testing_files/
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ There isn't really any requirement for track based REPLAYGAIN. The script will f
As for album based REPLAYGAIN: the script considers audio files, that are in the same directory to belong into the same album.
So, rsgain will calculate REPLAYGAIN_ALBUM_GAIN for those, as if they are in one album.

This isn't an issue at all, if you have your albums grouped into fodlers,
This isn't an issue at all, if you have your albums grouped into folders,
you don't make rsgain calculate album REPLAYGAIN or just don't use that.

## Dependencies
Expand Down
120 changes: 71 additions & 49 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,91 +19,103 @@ var command = []string{"custom"}
var rsgainSemaphore chan int

func main() {
albumMode := flag.Bool("a", false, "Calculate album gain and peak.")
skipExisting := flag.Bool("S", false, "Don't scan files with existing ReplayGain information.")
tagMode := flag.String("s", "s", "Tagmode:\ns: scan only\ni: write tags\nd: delete tags")
targetLoudness := flag.Int("l", -18, "Use n LUFS as target loudness (-30 ≤ n ≤ -5), default: -18")
clipMode := flag.String("c", "n", "n: no clipping protection (default),\np: clipping protection enabled for positive gain values only,\na: Use max peak level n dB for clipping protection")
quiet := flag.Bool("q", false, "(rsgain) Don't print scanning status messages.")
rsgainLimit := flag.Int("r", 100, "Limit, how many rsgain instances can run at a time.")
albumMode := *flag.Bool("a", false, "Calculate album gain and peak.")
skipExisting := *flag.Bool("S", false, "Don't scan files with existing ReplayGain information.")
tagMode := *flag.String("s", "s", "Tagmode:\ns: scan only\ni: write tags\nd: delete tags")
targetLoudness := *flag.Int("l", -18, "Use n LUFS as target loudness (-30 ≤ n ≤ -5), default: -18")
clipMode := *flag.String("c", "n", "n: no clipping protection (default),\np: clipping protection enabled for positive gain values only,\na: Use max peak level n dB for clipping protection")
quiet := *flag.Bool("q", false, "(rsgain) Don't print scanning status messages.")
rsgainLimit := *flag.Int("r", 100, "Limit, how many rsgain instances can run at a time.")
flag.Parse()

libraryRoot := flag.Arg(0)

// build the rsgain custom command and check values

if *albumMode {
if albumMode {
command = append(command, "-a")
}

if *skipExisting {
if skipExisting {
command = append(command, "-S")
}

if !slices.Contains([]string{"s", "d", "i"}, *tagMode) {
fmt.Printf("Invalid clip mode: %s", *tagMode)
if !slices.Contains([]string{"s", "d", "i"}, tagMode) {
fmt.Printf("Invalid tagmode: %s", tagMode)
os.Exit(2)
}
command = append(command, "-s", *tagMode)
command = append(command, "-s", tagMode)

if !(-30 <= *targetLoudness && *targetLoudness <= -5) {
if !(-30 <= targetLoudness && targetLoudness <= -5) {
fmt.Println("Target loudness n needs to be -30 ≤ n ≤ -5")
os.Exit(2)
}
command = append(command, "-l", strconv.Itoa(*targetLoudness))
command = append(command, "-l", strconv.Itoa(targetLoudness))

if !slices.Contains([]string{"n", "p", "a"}, *clipMode) {
fmt.Printf("Invalid clip mode: %s", *clipMode)
if !slices.Contains([]string{"n", "p", "a"}, clipMode) {
fmt.Printf("Invalid clip mode: %s", clipMode)
os.Exit(2)
}
command = append(command, "-c", *clipMode)
command = append(command, "-c", clipMode)

if libraryRoot == "" {
fmt.Println("No library path specified.")
os.Exit(2)
}

if *quiet {
if quiet {
command = append(command, "-q")
}

rsgainSemaphore = make(chan int, *rsgainLimit)
rsgainSemaphore = make(chan int, rsgainLimit)

/* Used for debugging
ctx, cancel := context.WithCancel(context.Background())
go monitorRsgainProcesses(ctx, 500*time.Millisecond)
*/

// scan for album folders
wg.Add(1)
err := walker(libraryRoot)
// if root is just a file
if isSupportedMusicFile(libraryRoot) {
err := runRSGain([]string{libraryRoot}, quiet)
if err != nil {
errLog.Printf("Something went wrong scanning %s: '%s\n'", libraryRoot, err)
}
} else { // if we have a folder, scan them with WalkDir
wg.Add(1)
err := walker(libraryRoot, quiet)

if err != nil {
errLog.Printf("Error walking library folder: %s\n", err)
}
if err != nil {
errLog.Printf("Error walking library folder: %s\n", err)
}

wg.Wait()
wg.Wait()
}

/*cancel()*/
}

func walker(root string) error {
func walker(root string, isQuiet bool) error {
defer wg.Done()
return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

// skip creating walkers on the initial directory (it would create infinite threads lol)
if d.IsDir() && path != root {
// when walked into a directory, launch a new walker on that
wg.Add(1)
go func() {
err := walker(path)
if err != nil {
errLog.Printf("Error walking %s: %s\n", path, err)
}
}()
if d.IsDir() {
// skip creating walkers on the initial directory (it would create infinite threads lol)
if path != root {
// when walked into a directory, launch a new walker on that
wg.Add(1)
go func() {
err := walker(path, isQuiet)
if err != nil {
errLog.Printf("Error walking %s: %s\n", path, err)
}
}()

// skip the current directory (the newly summoned walker is dealing with it)
return fs.SkipDir
}

// process supported files (separate thread)
wg.Add(1)
Expand All @@ -128,25 +140,35 @@ func walker(root string) error {
rsgainSemaphore <- 0 //add a slot to the semaphore
defer func() { <-rsgainSemaphore }()

cmd := exec.Command("rsgain", append(command, audioFiles...)...)
err := cmd.Run()

if err != nil {
errLog.Printf("Error calling rsgain on these files: '%v'\n", audioFiles)
errLog.Printf("Command failed: %s\nError: %v\n", cmd.String(), err)

}
err = runRSGain(audioFiles, isQuiet)
}
}()

// skip the current directory (the newly summoned walker is dealing with it)
return fs.SkipDir
}

return nil
return err
})
}

func runRSGain(audioFiles []string, isQuiet bool) error {
cmd := exec.Command("rsgain", append(command, audioFiles...)...)

// Stream output to console if set
if !isQuiet {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}

err := cmd.Run()

if err != nil {
errLog.Printf("Error calling rsgain on these files: '%v'\n", audioFiles)
errLog.Printf("Command failed: %s\nError: %v\n", cmd.String(), err)

}

return err
}

func isSupportedMusicFile(path string) bool {
supportedFiles := []string{
".aiff", ".flac", ".flac",
Expand Down
Loading