diff --git a/api.go b/api.go index 50a130b4c7de512e1baf4edc1816e425d27556ca..63f5c777f902dc90d0ada262d9af980814922ca9 100644 --- a/api.go +++ b/api.go @@ -50,22 +50,51 @@ type EncodingParams struct { BitRate int SampleRate int Channels int + StereoMode string Quality float64 } +// NewEncodingParams sets some default values. +func NewEncodingParams() *EncodingParams { + return &EncodingParams{ + SampleRate: 44100, + Channels: 2, + Quality: -1, + } +} + func (p *EncodingParams) Valid() error { - if p.Format == "" { + switch p.Format { + case "mp3", "mp3.cbr", "mp3.abr", "vorbis.cbr", "vorbis.abr": + if p.BitRate == 0 { + return errors.New("bitrate not specified") + } + case "mp3.vbr": + if p.Quality < 0 || p.Quality > 9 { + return errors.New("quality must be in range [0, 9]") + } + case "vorbis": + if p.Quality < -0.2 || p.Quality > 1 { + return errors.New("quality must be in range [-0.2, 1]") + } + case "": return errors.New("format not specified") + default: + return fmt.Errorf("unknown format \"%s\"", p.Format) } if p.SampleRate == 0 { return errors.New("sample rate not specified") } - if p.BitRate == 0 && p.Quality == 0 { - return errors.New("either bitrate or quality must be specified") - } - if p.Channels < 1 { + if p.Channels < 1 || p.Channels > 2 { return errors.New("bad number of channels") } + if p.Channels > 1 { + switch p.StereoMode { + case "", "stereo", "joint_stereo", "default": + default: + return fmt.Errorf("unknown stereo mode \"%s\"", p.StereoMode) + } + } return nil } diff --git a/cmd/radioctl/radioctl.go b/cmd/radioctl/radioctl.go index 2325cdb48c89deba18c4b86fa3ee711bb28e5ca0..f0209ec28ba38e73a232b329dfd585bac194dc43 100644 --- a/cmd/radioctl/radioctl.go +++ b/cmd/radioctl/radioctl.go @@ -8,6 +8,7 @@ import ( "log" "net/url" "os" + "strconv" "strings" "git.autistici.org/ale/autoradio" @@ -18,6 +19,97 @@ import ( // Format for output of structured data. var outputFormat = flag.String("format", "txt", "Output format for structured data (json, txt)") +type optionalValue struct { + isSet bool +} + +func (v *optionalValue) IsSet() bool { + return v.isSet +} + +type stringOptionalValue struct { + *optionalValue + value string +} + +func newStringOptionalValue() *stringOptionalValue { + return &stringOptionalValue{} +} + +func (v *stringOptionalValue) Set(val string) error { + v.isSet = true + v.value = val + return nil +} + +func (v *stringOptionalValue) Value() string { + return v.value +} + +func (v *stringOptionalValue) String() string { + return v.value +} + +func (v *stringOptionalValue) Get() interface{} { + return v.value +} + +type intOptionalValue struct { + *optionalValue + value int +} + +func newIntOptionalValue() *intOptionalValue { + return &intOptionalValue{} +} + +func (v *intOptionalValue) Set(val string) error { + ival, err := strconv.Atoi(val) + v.value = ival + v.isSet = true + return err +} + +func (v *intOptionalValue) Value() int { + return v.value +} + +func (v *intOptionalValue) String() string { + return fmt.Sprintf("%v", v.value) +} + +func (v *intOptionalValue) Get() interface{} { + return v.value +} + +type floatOptionalValue struct { + *optionalValue + value float64 +} + +func newFloatOptionalValue() *floatOptionalValue { + return &floatOptionalValue{} +} + +func (v *floatOptionalValue) Set(val string) error { + fval, err := strconv.ParseFloat(val, 64) + v.value = fval + v.isSet = true + return err +} + +func (v *floatOptionalValue) Value() float64 { + return v.value +} + +func (v *floatOptionalValue) String() string { + return fmt.Sprintf("%v", v.value) +} + +func (v *floatOptionalValue) Get() interface{} { + return v.value +} + type HasAddFlags interface { AddFlags(*gonutsflag.FlagSet) } @@ -99,15 +191,6 @@ func printMount(m *autoradio.Mount) { } } -func addEncodingFlags(f *gonutsflag.FlagSet, p *autoradio.EncodingParams) { - f.StringVar(&p.SourceName, "source", "", "Source mountpoint") - f.StringVar(&p.Format, "codec", "", "Encoding format") - f.IntVar(&p.BitRate, "bitrate", 0, "Bitrate (kbps)") - f.IntVar(&p.SampleRate, "samplerate", 0, "Sample rate (Hz)") - f.IntVar(&p.Channels, "channels", 2, "Number of channels") - f.Float64Var(&p.Quality, "quality", 0, "Quality (alternatively to bitrate for some encoders)") -} - func mountExists(name string, client *autoradio.Client) bool { m, _ := client.GetMount(name) return m != nil @@ -184,16 +267,26 @@ func newCreateTranscodingMountCommand() *createTranscodingMountCommand { return &createTranscodingMountCommand{ BaseCommand: BaseCommand{ UsageLine: "create-transcoding-mount <path>", - Short: "Create a transcoded mount", + Short: "Create a transcoding mount", Long: ` Create a new stream that will transcode the parent stream with different encoding parameters. `, }, - params: &autoradio.EncodingParams{}, + params: autoradio.NewEncodingParams(), } } +func addEncodingFlags(f *gonutsflag.FlagSet, p *autoradio.EncodingParams) { + f.StringVar(&p.SourceName, "source", "", "Source mountpoint") + f.StringVar(&p.Format, "codec", p.Format, "Encoding format") + f.Float64Var(&p.Quality, "quality", p.Quality, "Quality (for VBR encoders)") + f.IntVar(&p.BitRate, "bitrate", p.BitRate, "Bitrate (kbps)") + f.IntVar(&p.SampleRate, "samplerate", p.SampleRate, "Sample rate (Hz)") + f.IntVar(&p.Channels, "channels", p.Channels, "Number of channels") + f.StringVar(&p.StereoMode, "stereo-mode", p.StereoMode, "Stereo mode for mp3 codec (stereo, joint_stereo)") +} + func (cmd *createTranscodingMountCommand) AddFlags(f *gonutsflag.FlagSet) { addEncodingFlags(f, cmd.params) f.StringVar(&cmd.fallback, "fallback", "", "Fallback stream URL") @@ -242,9 +335,15 @@ func (cmd *createTranscodingMountCommand) Run(args []string) { // Edit a mountpoint. type editMountCommand struct { BaseCommand - params *autoradio.EncodingParams - relay string - fallback string + relay *stringOptionalValue + fallback *stringOptionalValue + transFormat *stringOptionalValue + transSource *stringOptionalValue + transBitRate *intOptionalValue + transSampleRate *intOptionalValue + transChannels *intOptionalValue + transStereoMode *stringOptionalValue + transQuality *floatOptionalValue } var UNSET = "UNSET" @@ -262,18 +361,45 @@ default, non-relay behavior, set the relay to the empty string (with --relay=""). `, }, - params: &autoradio.EncodingParams{}, + relay: newStringOptionalValue(), + fallback: newStringOptionalValue(), + transFormat: newStringOptionalValue(), + transSource: newStringOptionalValue(), + transBitRate: newIntOptionalValue(), + transSampleRate: newIntOptionalValue(), + transChannels: newIntOptionalValue(), + transStereoMode: newStringOptionalValue(), + transQuality: newFloatOptionalValue(), } } func (cmd *editMountCommand) AddFlags(f *gonutsflag.FlagSet) { - // Note that we use a magic value to figure out whether a flag - // has been specified or not, to make it possible to clear a - // field (by setting it to the empty string). There might be - // better way to do this. - f.StringVar(&cmd.relay, "relay", UNSET, "Upstream URL to relay") - f.StringVar(&cmd.fallback, "fallback", UNSET, "Fallback stream URL") - addEncodingFlags(f, cmd.params) + f.Var(cmd.relay, "relay", "Upstream URL to relay") + f.Var(cmd.fallback, "fallback", "Fallback stream URL") + + f.Var(cmd.transSource, "source", "[transcoding] Source mountpoint") + f.Var(cmd.transFormat, "codec", "[transcoding] Encoding format") + f.Var(cmd.transQuality, "quality", "[transcoding] Quality (for VBR encoders)") + f.Var(cmd.transBitRate, "bitrate", "[transcoding] Bitrate (kbps)") + f.Var(cmd.transSampleRate, "samplerate", "[transcoding] Sample rate (Hz)") + f.Var(cmd.transChannels, "channels", "[transcoding] Number of channels") + f.Var(cmd.transStereoMode, "stereo-mode", "[transcoding] Stereo mode for mp3 encoding (stereo, joint_stereo)") +} + +func (cmd *editMountCommand) transcodingOptionsSet() bool { + opts := []interface { + IsSet() bool + }{ + cmd.transSource, cmd.transFormat, cmd.transQuality, + cmd.transBitRate, cmd.transSampleRate, cmd.transChannels, + cmd.transStereoMode, + } + for _, o := range opts { + if o.IsSet() { + return true + } + } + return false } func (cmd *editMountCommand) Run(args []string) { @@ -290,11 +416,38 @@ func (cmd *editMountCommand) Run(args []string) { log.Fatal("ERROR: mount not found") } - if cmd.fallback != UNSET { - m.Fallback = cmd.fallback + // Only set those fields that were passed on the command line. + if cmd.fallback.IsSet() { + m.Fallback = cmd.fallback.Value() + } + if cmd.relay.IsSet() { + setRelay(m, cmd.relay.Value()) + } + + if cmd.transcodingOptionsSet() && m.Transcoding == nil { + log.Fatal("ERROR: can't set transcoding options on a non-transcoding mount (delete and re-create)") + } + + if cmd.transFormat.IsSet() { + m.Transcoding.Format = cmd.transFormat.Value() + } + if cmd.transSource.IsSet() { + m.Transcoding.SourceName = cmd.transSource.Value() + } + if cmd.transBitRate.IsSet() { + m.Transcoding.BitRate = cmd.transBitRate.Value() + } + if cmd.transSampleRate.IsSet() { + m.Transcoding.SampleRate = cmd.transSampleRate.Value() + } + if cmd.transQuality.IsSet() { + m.Transcoding.Quality = cmd.transQuality.Value() + } + if cmd.transChannels.IsSet() { + m.Transcoding.Channels = cmd.transChannels.Value() } - if cmd.relay != UNSET { - setRelay(m, cmd.relay) + if cmd.transStereoMode.IsSet() { + m.Transcoding.StereoMode = cmd.transStereoMode.Value() } if err := m.Valid(); err != nil { diff --git a/node/liquidsoap.go b/node/liquidsoap.go index 6298c06b6aafa4881d25e3f55b3408d1295b4557..bb6a03f044018e501f8fee61c14067ddca6f4d8f 100644 --- a/node/liquidsoap.go +++ b/node/liquidsoap.go @@ -24,7 +24,7 @@ set("log.file", false) set("log.stdout", true) upstream = mksafe(input.http("{{.SourceURL}}", buffer=5.0)) -output.icecast(%{{.Format}}(samplerate={{.SampleRate}}, {{if gt .BitRate 0}}bitrate={{.BitRate}}, {{end}}{{if gt .Quality 0.0}}quality={{.Quality}}, {{end}}{{if eq .Channels 2}}stereo{{else}}mono{{end}}), +output.icecast(%{{.Format}}(samplerate={{.SampleRate}}, {{if gt .BitRate 0}}bitrate={{.BitRate}}, {{end}}{{if gt .Quality -1.0}}quality={{.Quality}}, {{end}}{{if .StereoMode}}stereo_mode={{.StereoMode}}, {{end}}{{if eq .Channels 2}}stereo{{else}}mono{{end}}), mount="{{.TargetMount}}", host="{{.TargetIP}}", port={{.TargetPort}}, user="{{.TargetUsername}}", password="{{.TargetPassword}}", upstream) ` @@ -51,6 +51,7 @@ type liquidsoapParams struct { BitRate int SampleRate int Channels int + StereoMode string Quality float32 } @@ -69,6 +70,7 @@ func newLiquidsoapParams(mount *autoradio.Mount) *liquidsoapParams { BitRate: mount.Transcoding.BitRate, SampleRate: mount.Transcoding.SampleRate, Channels: mount.Transcoding.Channels, + StereoMode: mount.Transcoding.StereoMode, Quality: float32(mount.Transcoding.Quality), } }