refactor(encode): use a replaygain enum
This commit is contained in:
@@ -20,13 +20,20 @@ const (
|
|||||||
ffmpeg = "ffmpeg"
|
ffmpeg = "ffmpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type replayGain int
|
||||||
|
|
||||||
|
const (
|
||||||
|
rgForce replayGain = iota
|
||||||
|
rgHigh
|
||||||
|
)
|
||||||
|
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
Format string
|
Format string
|
||||||
Bitrate int
|
Bitrate int
|
||||||
ffmpegOptions []string
|
|
||||||
forceRG bool
|
options []string
|
||||||
hiGainRG bool
|
replayGain replayGain
|
||||||
upsample bool
|
upsample bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileExists(filename string) bool {
|
func fileExists(filename string) bool {
|
||||||
@@ -39,11 +46,35 @@ func fileExists(filename string) bool {
|
|||||||
|
|
||||||
func Profiles() map[string]Profile {
|
func Profiles() map[string]Profile {
|
||||||
return map[string]Profile{
|
return map[string]Profile{
|
||||||
"mp3": {"mp3", 128, []string{"-c:a", "libmp3lame"}, false, false, false},
|
"mp3": {
|
||||||
"mp3_rg": {"mp3", 128, []string{"-c:a", "libmp3lame"}, true, false, false},
|
Format: "mp3",
|
||||||
"opus": {"opus", 96, []string{"-c:a", "libopus", "-vbr", "on"}, false, false, false},
|
Bitrate: 128,
|
||||||
"opus_rg": {"opus", 96, []string{"-c:a", "libopus", "-vbr", "on"}, true, false, false},
|
options: []string{"-c:a", "libmp3lame"},
|
||||||
"opus_car": {"opus", 96, []string{"-c:a", "libopus", "-vbr", "on"}, true, true, true},
|
},
|
||||||
|
"mp3_rg": {
|
||||||
|
Format: "mp3",
|
||||||
|
Bitrate: 128,
|
||||||
|
options: []string{"-c:a", "libmp3lame"},
|
||||||
|
replayGain: rgForce,
|
||||||
|
},
|
||||||
|
"opus": {
|
||||||
|
Format: "opus",
|
||||||
|
Bitrate: 96,
|
||||||
|
options: []string{"-c:a", "libopus", "-vbr", "on"},
|
||||||
|
},
|
||||||
|
"opus_rg": {
|
||||||
|
Format: "opus",
|
||||||
|
Bitrate: 96,
|
||||||
|
options: []string{"-c:a", "libopus", "-vbr", "on"},
|
||||||
|
replayGain: rgForce,
|
||||||
|
},
|
||||||
|
"opus_car": {
|
||||||
|
Format: "opus",
|
||||||
|
Bitrate: 96,
|
||||||
|
options: []string{"-c:a", "libopus", "-vbr", "on"},
|
||||||
|
replayGain: rgHigh,
|
||||||
|
upsample: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,45 +127,40 @@ func ffmpegCommand(filePath string, profile Profile) (*exec.Cmd, error) {
|
|||||||
"-vn",
|
"-vn",
|
||||||
"-b:a", fmt.Sprintf("%dk", profile.Bitrate),
|
"-b:a", fmt.Sprintf("%dk", profile.Bitrate),
|
||||||
}
|
}
|
||||||
args = append(args, profile.ffmpegOptions...)
|
args = append(args, profile.options...)
|
||||||
if profile.forceRG {
|
|
||||||
aBaselineGain := 6
|
|
||||||
if profile.hiGainRG {
|
|
||||||
// This baseline gain results in final track being +3~5dB louder
|
|
||||||
// than Foobar2000's default ReplayGain target volume.
|
|
||||||
// This makes it easier to listen to music in a car, where all other
|
|
||||||
// sources are usually ten thousand times louder than RG-adjusted music.
|
|
||||||
// -- @spijet
|
|
||||||
aBaselineGain = 15
|
|
||||||
}
|
|
||||||
aFilters := []string{
|
|
||||||
fmt.Sprintf("volume=replaygain=track:replaygain_preamp=%ddB:replaygain_noclip=0", aBaselineGain),
|
|
||||||
"alimiter=level=disabled",
|
|
||||||
"asidedata=mode=delete:type=REPLAYGAIN",
|
|
||||||
}
|
|
||||||
|
|
||||||
// opus always forces output to 48kHz sampling rate, but we can still use upsampling
|
var aFilters []string
|
||||||
// to increase RG and alimiter's peak limiting precision, which is desirable in some
|
var aMetadata []string
|
||||||
// cases. ffmpeg's `soxr` resampler is quite fast on x86-64: it takes around 5 seconds
|
|
||||||
// on my Ryzen 3600 to transcode an 8-minute FLAC with 2x upsample and RG applied.
|
// opus always forces output to 48kHz sampling rate, but we can still use upsampling
|
||||||
// -- @spijet
|
// to increase RG and alimiter's peak limiting precision, which is desirable in some
|
||||||
if profile.upsample {
|
// cases. ffmpeg's `soxr` resampler is quite fast on x86-64: it takes around 5 seconds
|
||||||
aFilters = append([]string{"aresample=96000:resampler=soxr"}, aFilters...)
|
// on my Ryzen 3600 to transcode an 8-minute FLAC with 2x upsample and RG applied.
|
||||||
}
|
// -- @spijet
|
||||||
aFilterString := strings.Join(aFilters, ", ")
|
if profile.upsample {
|
||||||
args = append(args,
|
aFilters = append(aFilters, "aresample=96000:resampler=soxr")
|
||||||
// set up replaygain processing
|
|
||||||
"-af", aFilterString,
|
|
||||||
// drop redundant replaygain tags
|
|
||||||
"-metadata", "replaygain_album_gain=",
|
|
||||||
"-metadata", "replaygain_album_peak=",
|
|
||||||
"-metadata", "replaygain_track_gain=",
|
|
||||||
"-metadata", "replaygain_track_peak=",
|
|
||||||
"-metadata", "r128_album_gain=",
|
|
||||||
"-metadata", "r128_track_gain=",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch profile.replayGain {
|
||||||
|
case rgForce:
|
||||||
|
aFilters = append(aFilters, ffmpegPreamp(6)...)
|
||||||
|
aMetadata = append(aMetadata, ffmpegStripRG()...)
|
||||||
|
case rgHigh:
|
||||||
|
// this baseline gain results in final track being +3~5dB louder
|
||||||
|
// than Foobar2000's default ReplayGain target volume.
|
||||||
|
// this makes it easier to listen to music in a car, where all other
|
||||||
|
// sources are usually ten thousand times louder than RG-adjusted music.
|
||||||
|
// -- @spijet
|
||||||
|
aFilters = append(aFilters, ffmpegPreamp(15)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aFilters) > 0 {
|
||||||
|
args = append(args, "-af", strings.Join(aFilters, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, aMetadata...)
|
||||||
args = append(args, "-f", profile.Format, "-")
|
args = append(args, "-f", profile.Format, "-")
|
||||||
|
|
||||||
ffmpegPath, err := exec.LookPath(ffmpeg)
|
ffmpegPath, err := exec.LookPath(ffmpeg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("find ffmpeg binary path: %w", err)
|
return nil, fmt.Errorf("find ffmpeg binary path: %w", err)
|
||||||
@@ -144,6 +170,25 @@ func ffmpegCommand(filePath string, profile Profile) (*exec.Cmd, error) {
|
|||||||
// but please do let me know if you see otherwise
|
// but please do let me know if you see otherwise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ffmpegPreamp(dB int) []string {
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("volume=replaygain=track:replaygain_preamp=%ddB:replaygain_noclip=0", dB),
|
||||||
|
"alimiter=level=disabled",
|
||||||
|
"asidedata=mode=delete:type=REPLAYGAIN",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ffmpegStripRG() []string {
|
||||||
|
return []string{
|
||||||
|
"-metadata", "replaygain_album_gain=",
|
||||||
|
"-metadata", "replaygain_album_peak=",
|
||||||
|
"-metadata", "replaygain_track_gain=",
|
||||||
|
"-metadata", "replaygain_track_peak=",
|
||||||
|
"-metadata", "r128_album_gain=",
|
||||||
|
"-metadata", "r128_track_gain=",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func encode(out io.Writer, trackPath, cachePath string, profile Profile) error {
|
func encode(out io.Writer, trackPath, cachePath string, profile Profile) error {
|
||||||
// prepare cache part file path
|
// prepare cache part file path
|
||||||
cachePartPath := fmt.Sprintf("%s.part", cachePath)
|
cachePartPath := fmt.Sprintf("%s.part", cachePath)
|
||||||
|
|||||||
Reference in New Issue
Block a user