// // rhimportd // // The Radio Helsinki Rivendell Import Daemon // // // Copyright (C) 2015-2016 Christian Pointner // // This file is part of rhimportd. // // rhimportd is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // rhimportd is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with rhimportd. If not, see . // package rhimport import ( "bytes" "errors" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" ) type fetchConverter interface { io.WriteCloser GetResult(ctx *Context, res *Result) (result string, err error) } type fetchConverterResult struct { output string err error loudnessCorr float64 } func newFetchConverter(ctx *Context, filename string, metadata map[string]string) (fetchConverter, string, error) { switch ctx.FetchConverter { case "null": // no support for loudness evaluation - leave normalization to Rivendell return newNullFetchConverter(ctx, filename, metadata) case "ffmpeg": // no support for loudness evaluation - leave normalization to Rivendell return newFFMpegFetchConverter(ctx, filename, metadata) case "bs1770": ctx.NormalizationLevel = 0 // disable Rivendell normalization return newBS1770FetchConverter(ctx, filename, metadata) case "ffmpeg-bs1770": ctx.NormalizationLevel = 0 // disable Rivendell normalization return newFFMpegBS1770FetchConverter(ctx, filename, metadata) } return nil, "", errors.New("unknown fetch converter type: " + ctx.FetchConverter) } // // NUll Converter aka File Writer // type nullFetchConverter struct { file *os.File } func newNullFetchConverter(ctx *Context, filename string, metadata map[string]string) (n *nullFetchConverter, newFilename string, err error) { n = &nullFetchConverter{} newFilename = filepath.Dir(filename) + "/conv-null" + filepath.Ext(filename) ctx.stdlog.Printf("null-converter: opening file '%s' -> '%s'", filename, newFilename) n.file, err = os.OpenFile(newFilename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) return } func (c *nullFetchConverter) Write(p []byte) (n int, err error) { return c.file.Write(p) } func (c *nullFetchConverter) Close() (err error) { return c.file.Close() } func (c *nullFetchConverter) GetResult(ctx *Context, res *Result) (result string, err error) { return "", nil } // // FFMpeg Converter: converts all files into flac // type ffmpegFetchConverter struct { cmd *exec.Cmd pipe io.WriteCloser result chan fetchConverterResult } func newFFMpegFetchConverter(ctx *Context, filename string, metadata map[string]string) (ff *ffmpegFetchConverter, filenameFlac string, err error) { ff = &ffmpegFetchConverter{} filenameFlac = filepath.Dir(filename) + "/conv-ffmpeg.flac" ctx.stdlog.Printf("ffmpeg-converter: starting ffmpeg for file '%s' -> '%s'", filename, filenameFlac) ff.cmd = exec.Command("ffmpeg", "-loglevel", "warning", "-i", "-", "-map_metadata", "0") if metadata != nil { for key, value := range metadata { ff.cmd.Args = append(ff.cmd.Args, "-metadata", fmt.Sprintf("%s=%s", key, value)) } } ff.cmd.Args = append(ff.cmd.Args, "-ar", strconv.FormatUint(uint64(ctx.conf.SampleRate), 10), "-ac", strconv.FormatUint(uint64(ctx.Channels), 10), "-f", "flac", filenameFlac) if ff.pipe, err = ff.cmd.StdinPipe(); err != nil { return nil, "", err } ff.result = make(chan fetchConverterResult, 1) go func() { output, err := ff.cmd.CombinedOutput() ff.result <- fetchConverterResult{strings.TrimSpace(string(output)), err, 0.0} }() return } func (ff *ffmpegFetchConverter) Write(p []byte) (n int, err error) { return ff.pipe.Write(p) } func (ff *ffmpegFetchConverter) Close() (err error) { return ff.pipe.Close() } func (ff *ffmpegFetchConverter) GetResult(ctx *Context, res *Result) (result string, err error) { if ff.result != nil { select { case r := <-ff.result: ctx.LoudnessCorr = r.loudnessCorr return r.output, r.err case <-ctx.Cancel: ff.cmd.Process.Kill() res.ResponseCode = http.StatusNoContent res.ErrorString = "canceled" return "", errors.New("canceled") } } return "", nil } // // BS1770 Converter: calculates loudness correction value using ITU BS1770 (EBU R128) // type bs1770FetchConverter struct { cmd *exec.Cmd file *os.File pipe io.WriteCloser multi io.Writer result chan fetchConverterResult } func newBS1770FetchConverter(ctx *Context, filename string, metadata map[string]string) (bs *bs1770FetchConverter, newFilename string, err error) { bs = &bs1770FetchConverter{} newFilename = filepath.Dir(filename) + "/conv-bs1770" + filepath.Ext(filename) ctx.stdlog.Printf("bs1770-converter: starting bs1770gain for file '%s' -> '%s'", filename, newFilename) bs.file, err = os.OpenFile(newFilename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) bs.cmd = exec.Command("bs1770gain", "--ebu", "-i", "--xml", "-") if bs.pipe, err = bs.cmd.StdinPipe(); err != nil { return nil, "", err } bs.multi = io.MultiWriter(bs.file, bs.pipe) var bsStdout, bsStderr bytes.Buffer bs.cmd.Stdout = &bsStdout bs.cmd.Stderr = &bsStderr bs.result = make(chan fetchConverterResult, 1) go func() { if err := bs.cmd.Run(); err != nil { bs.result <- fetchConverterResult{strings.TrimSpace(string(bsStderr.String())), err, 0.0} } res, err := newBS1770ResultFromXML(&bsStdout) if err != nil { bs.result <- fetchConverterResult{bsStdout.String(), err, 0.0} return } if len(res.Album.Tracks) == 0 { bs.result <- fetchConverterResult{bsStdout.String(), fmt.Errorf("bs1770gain returned no/invalid result"), 0.0} return } bs.result <- fetchConverterResult{"", nil, res.Album.Tracks[0].Integrated.LU} }() return } func (bs *bs1770FetchConverter) Write(p []byte) (n int, err error) { return bs.multi.Write(p) } func (bs *bs1770FetchConverter) Close() (err error) { errPipe := bs.pipe.Close() errFile := bs.file.Close() if errFile != nil { return errFile } return errPipe } func (bs *bs1770FetchConverter) GetResult(ctx *Context, res *Result) (result string, err error) { if bs.result != nil { select { case r := <-bs.result: ctx.LoudnessCorr = r.loudnessCorr return r.output, r.err case <-ctx.Cancel: bs.cmd.Process.Kill() res.ResponseCode = http.StatusNoContent res.ErrorString = "canceled" return "", errors.New("canceled") } } return "", nil } // // FFMpeg/BS1770 Converter: converts all files into flac and calculates loudness correction value // using ITU BS1770 (EBU R128) // type ffmpegBS1770FetchConverter struct { ffmpeg *exec.Cmd bs1770 *exec.Cmd pipe io.WriteCloser resultFF chan fetchConverterResult resultBS chan fetchConverterResult } func newFFMpegBS1770FetchConverter(ctx *Context, filename string, metadata map[string]string) (ff *ffmpegBS1770FetchConverter, filenameFlac string, err error) { ff = &ffmpegBS1770FetchConverter{} filenameFlac = filepath.Dir(filename) + "/conv-ffmpeg-bs1770.flac" ctx.stdlog.Printf("ffmpeg-bs1770-converter: starting ffmpeg and bs1770gain for file '%s' -> '%s'", filename, filenameFlac) ff.ffmpeg = exec.Command("ffmpeg", "-loglevel", "warning", "-i", "pipe:0", "-map_metadata", "0") if metadata != nil { for key, value := range metadata { ff.ffmpeg.Args = append(ff.ffmpeg.Args, "-metadata", fmt.Sprintf("%s=%s", key, value)) } } ff.ffmpeg.Args = append(ff.ffmpeg.Args, "-ar", strconv.FormatUint(uint64(ctx.conf.SampleRate), 10), "-ac", strconv.FormatUint(uint64(ctx.Channels), 10), "-f", "flac", filenameFlac) ff.ffmpeg.Args = append(ff.ffmpeg.Args, "-ar", strconv.FormatUint(uint64(ctx.conf.SampleRate), 10), "-ac", strconv.FormatUint(uint64(ctx.Channels), 10), "-f", "flac", "pipe:1") if ff.pipe, err = ff.ffmpeg.StdinPipe(); err != nil { return nil, "", err } var ffStderr bytes.Buffer ff.ffmpeg.Stderr = &ffStderr ff.bs1770 = exec.Command("bs1770gain", "--ebu", "-i", "--xml", "-") var ffstdout io.WriteCloser if ffstdout, err = ff.bs1770.StdinPipe(); err != nil { return nil, "", err } ff.ffmpeg.Stdout = ffstdout var bsStdout, bsStderr bytes.Buffer ff.bs1770.Stdout = &bsStdout ff.bs1770.Stderr = &bsStderr ff.resultFF = make(chan fetchConverterResult, 1) ff.resultBS = make(chan fetchConverterResult, 1) go func() { err := ff.ffmpeg.Run() ffstdout.Close() ff.resultFF <- fetchConverterResult{strings.TrimSpace(string(ffStderr.String())), err, 0.0} }() go func() { if err := ff.bs1770.Run(); err != nil { ff.resultBS <- fetchConverterResult{strings.TrimSpace(string(bsStderr.String())), err, 0.0} } res, err := newBS1770ResultFromXML(&bsStdout) if err != nil { ff.resultBS <- fetchConverterResult{bsStdout.String(), err, 0.0} return } if len(res.Album.Tracks) == 0 { ff.resultBS <- fetchConverterResult{bsStdout.String(), fmt.Errorf("bs1770gain returned no/invalid result"), 0.0} return } ff.resultBS <- fetchConverterResult{"", nil, res.Album.Tracks[0].Integrated.LU} }() return } func (ff *ffmpegBS1770FetchConverter) Write(p []byte) (n int, err error) { return ff.pipe.Write(p) } func (ff *ffmpegBS1770FetchConverter) Close() (err error) { return ff.pipe.Close() } func (ff *ffmpegBS1770FetchConverter) GetResult(ctx *Context, res *Result) (result string, err error) { if ff.resultFF == nil || ff.resultBS == nil { return "", nil } var rff, rbs *fetchConverterResult for { select { case r := <-ff.resultFF: rff = &r case r := <-ff.resultBS: rbs = &r case <-ctx.Cancel: ff.ffmpeg.Process.Kill() ff.bs1770.Process.Kill() res.ResponseCode = http.StatusNoContent res.ErrorString = "canceled" return "", errors.New("canceled") } if rff != nil && rbs != nil { break } } if rff.err != nil { return rff.output, fmt.Errorf("ffmpeg: %v", rff.err) } if rbs.err != nil { return rbs.output, fmt.Errorf("bs1770gain: %v", rbs.err) } ctx.LoudnessCorr = rbs.loudnessCorr return "", nil }