diff --git a/container/mts/encoder_test.go b/container/mts/encoder_test.go index fb32ac24..3c9c5dd5 100644 --- a/container/mts/encoder_test.go +++ b/container/mts/encoder_test.go @@ -191,7 +191,7 @@ func TestEncodePcm(t *testing.T) { sampleSize := 2 blockSize := 16000 writeFreq := float64(sampleRate*sampleSize) / float64(blockSize) - e, err := NewEncoder(nopCloser{&buf}, (*testLogger)(t), PacketBasedPSI(10), Rate(int(writeFreq)), MediaType(EncodeAudio)) + e, err := NewEncoder(nopCloser{&buf}, (*testLogger)(t), PacketBasedPSI(10), Rate(writeFreq), MediaType(EncodeAudio)) if err != nil { t.Fatalf("could not create MTS encoder, failed with error: %v", err) } diff --git a/container/mts/options.go b/container/mts/options.go index eee206cf..0d46692f 100644 --- a/container/mts/options.go +++ b/container/mts/options.go @@ -97,12 +97,12 @@ func MediaType(mt int) func(*Encoder) error { // Rate is an option that can be passed to NewEncoder. It is used to specifiy // the rate at which the access units should be played in playback. This will // be used to create timestamps and counts such as PTS and PCR. -func Rate(r int) func(*Encoder) error { +func Rate(r float64) func(*Encoder) error { return func(e *Encoder) error { if r < 1 || r > 60 { return ErrInvalidRate } - e.writePeriod = time.Duration(float64(time.Second) / float64(r)) + e.writePeriod = time.Duration(float64(time.Second) / r) return nil } } diff --git a/device/alsa/alsa.go b/device/alsa/alsa.go index 3a6e7b14..0ebfa931 100644 --- a/device/alsa/alsa.go +++ b/device/alsa/alsa.go @@ -45,11 +45,10 @@ import ( ) const ( - pkg = "alsa: " - rbTimeout = 100 * time.Millisecond - rbNextTimeout = 2000 * time.Millisecond - rbLen = 200 - defaultSampleRate = 48000 + pkg = "alsa: " + rbTimeout = 100 * time.Millisecond + rbNextTimeout = 2000 * time.Millisecond + rbLen = 200 ) // "running" means the input goroutine is reading from the ALSA device and writing to the ringbuffer. @@ -61,6 +60,23 @@ const ( stopped ) +const ( + defaultSampleRate = 48000 + defaultBitDepth = 16 + defaultChannels = 1 + defaultRecPeriod = 1.0 + defaultCodec = codecutil.PCM +) + +// Configuration field errors. +var ( + errInvalidSampleRate = errors.New("invalid sample rate, defaulting") + errInvalidChannels = errors.New("invalid number of channels, defaulting") + errInvalidBitDepth = errors.New("invalid bitdepth, defaulting") + errInvalidRecPeriod = errors.New("invalid record period, defaulting") + errInvalidCodec = errors.New("invalid audio codec, defaulting") +) + // An ALSA device holds everything we need to know about the audio input stream and implements io.Reader and device.AVDevice. type ALSA struct { l Logger // Logger for device's routines to log to. @@ -100,26 +116,31 @@ func (d *ALSA) Name() string { return "ALSA" } -// Set will take a Config struct, check the validity of the relevant fields -// and then performs any configuration necessary. If fields are not valid, +// Setup will take a Config struct, check the validity of the relevant fields +// and then perform any configuration necessary. If fields are not valid, // an error is added to the multiError and a default value is used. // It then initialises the ALSA device which can then be started, read from, and stopped. -func (d *ALSA) Set(c config.Config) error { +func (d *ALSA) Setup(c config.Config) error { var errs device.MultiError if c.SampleRate <= 0 { - errs = append(errs, fmt.Errorf("invalid sample rate: %v", c.SampleRate)) + errs = append(errs, errInvalidSampleRate) + c.SampleRate = defaultSampleRate } if c.Channels <= 0 { - errs = append(errs, fmt.Errorf("invalid number of channels: %v", c.Channels)) + errs = append(errs, errInvalidChannels) + c.Channels = defaultChannels } if c.BitDepth <= 0 { - errs = append(errs, fmt.Errorf("invalid bitdepth: %v", c.BitDepth)) + errs = append(errs, errInvalidBitDepth) + c.BitDepth = defaultBitDepth } if c.RecPeriod <= 0 { - errs = append(errs, fmt.Errorf("invalid recording period: %v", c.RecPeriod)) + errs = append(errs, errInvalidRecPeriod) + c.RecPeriod = defaultRecPeriod } - if !codecutil.IsValid(c.InputCodec) { - errs = append(errs, errors.New("invalid codec")) + if c.InputCodec != codecutil.ADPCM && c.InputCodec != codecutil.PCM { + errs = append(errs, errInvalidCodec) + c.InputCodec = defaultCodec } d.Config = Config{ SampleRate: c.SampleRate, @@ -164,6 +185,14 @@ func (d *ALSA) Set(c config.Config) error { return nil } +// Set exists to satisfy the implementation of the Device interface that revid uses. +// Everything that would usually be in Set is in the Setup function. +// This is because an ALSA device is different to other devices in that it +// outputs binary non-packetised data and it requires a different configuration procedure. +func (d *ALSA) Set(c config.Config) error { + return nil +} + // Start will start recording audio and writing to the ringbuffer. // Once an ALSA device has been stopped it cannot be started again. This is likely to change in future. func (d *ALSA) Start() error { @@ -437,6 +466,12 @@ func (d *ALSA) formatBuffer() pcm.Buffer { return formatted } +// DataSize returns the size in bytes of the data ALSA device d will +// output in the duration of a single recording period. +func (d *ALSA) DataSize() int { + return pcm.DataSize(d.SampleRate, d.Channels, d.BitDepth, d.RecPeriod, d.Codec) +} + // nearestPowerOfTwo finds and returns the nearest power of two to the given integer. // If the lower and higher power of two are the same distance, it returns the higher power. // For negative values, 1 is returned. diff --git a/device/alsa/alsa_test.go b/device/alsa/alsa_test.go index aeeb6ac9..918a022a 100644 --- a/device/alsa/alsa_test.go +++ b/device/alsa/alsa_test.go @@ -53,7 +53,7 @@ func TestDevice(t *testing.T) { // Create a new ALSA device, start, read/lex, and then stop it. l := logger.New(logger.Debug, os.Stderr, true) ai := New(l) - err := ai.Set(c) + err := ai.Setup(c) // If there was an error opening the device, skip this test. if _, ok := err.(OpenError); ok { t.Skip(err) @@ -119,7 +119,7 @@ func TestIsRunning(t *testing.T) { l := logger.New(logger.Debug, &bytes.Buffer{}, true) // Discard logs. d := New(l) - err := d.Set(config.Config{ + err := d.Setup(config.Config{ SampleRate: sampleRate, Channels: channels, BitDepth: bitDepth, diff --git a/revid/audio_linux.go b/revid/audio_linux.go index d7c78b28..d3d08a58 100644 --- a/revid/audio_linux.go +++ b/revid/audio_linux.go @@ -29,13 +29,32 @@ import ( "strconv" "bitbucket.org/ausocean/av/codec/codecutil" - "bitbucket.org/ausocean/av/codec/pcm" "bitbucket.org/ausocean/av/container/mts" "bitbucket.org/ausocean/av/device/alsa" "bitbucket.org/ausocean/utils/logger" ) func (r *Revid) setupAudio() error { + // Create new ALSA device. + d := alsa.New(r.cfg.Logger) + r.input = d + + // Configure ALSA device. + r.cfg.Logger.Log(logger.Debug, "configuring input device") + err := d.Setup(r.cfg) + if err != nil { + r.cfg.Logger.Log(logger.Warning, "errors from configuring input device", "errors", err) + } + r.cfg.Logger.Log(logger.Info, "input device configured") + + // Set revid's lexer. + l, err := codecutil.NewByteLexer(d.DataSize()) + if err != nil { + return err + } + r.lexTo = l.Lex + + // Add metadata. mts.Meta.Add("sampleRate", strconv.Itoa(int(r.cfg.SampleRate))) mts.Meta.Add("channels", strconv.Itoa(int(r.cfg.Channels))) mts.Meta.Add("period", fmt.Sprintf("%.6f", r.cfg.RecPeriod)) @@ -50,13 +69,5 @@ func (r *Revid) setupAudio() error { r.cfg.Logger.Log(logger.Fatal, "no audio codec set in config") } - r.input = alsa.New(r.cfg.Logger) - - l, err := codecutil.NewByteLexer(pcm.DataSize(r.cfg.SampleRate, r.cfg.Channels, r.cfg.BitDepth, r.cfg.RecPeriod, r.cfg.InputCodec)) - if err != nil { - return err - } - r.lexTo = l.Lex - return nil } diff --git a/revid/config/config.go b/revid/config/config.go index 52a1264c..cefe81d4 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -50,7 +50,6 @@ const ( InputAudio // Outputs. - OutputAudio OutputRTMP OutputRTP OutputHTTP @@ -231,9 +230,8 @@ type Config struct { // qualityStandard, qualityFair, qualityGood, qualityGreat and qualityExcellent. VBRQuality Quality - VerticalFlip bool // VerticalFlip flips video vertically for Raspivid input. - Width uint // Width defines the input video width Raspivid input. - WriteRate float64 // WriteRate is how many times a second revid encoders will be written to. + VerticalFlip bool // VerticalFlip flips video vertically for Raspivid input. + Width uint // Width defines the input video width Raspivid input. } // Validate checks for any errors in the config fields and defaults settings diff --git a/revid/config/config_test.go b/revid/config/config_test.go index 36831eda..b59410e3 100644 --- a/revid/config/config_test.go +++ b/revid/config/config_test.go @@ -51,7 +51,6 @@ func TestValidate(t *testing.T) { BurstPeriod: defaultBurstPeriod, MinFrames: defaultMinFrames, FrameRate: defaultFrameRate, - WriteRate: defaultWriteRate, ClipDuration: defaultClipDuration, PSITime: defaultPSITime, FileFPS: defaultFileFPS, diff --git a/revid/config/parameter/generate_parameters.go b/revid/config/parameter/generate_parameters.go index dacdc1bf..ad2cff36 100644 --- a/revid/config/parameter/generate_parameters.go +++ b/revid/config/parameter/generate_parameters.go @@ -130,7 +130,6 @@ var params = []Param{ {N: "VBRQuality", BT: "uint8", E: []string{"Standard", "Fair", "Good", "Great", "Excellent"}}, {N: "VerticalFlip", BT: "bool"}, {N: "Width", BT: "uint", Min: 640, Max: 1920}, - {N: "WriteRate", BT: "float64"}, } const fileHeader = ` diff --git a/revid/config/parameter/parameters.go b/revid/config/parameter/parameters.go index 6d0fa773..bd127ed7 100644 --- a/revid/config/parameter/parameters.go +++ b/revid/config/parameter/parameters.go @@ -915,16 +915,3 @@ func (w *Width) Set(val string) error { *w = Width(_v) return nil } - -type WriteRate float64 - -func (w *WriteRate) Type() string { return "float64" } -func (w *WriteRate) Set(val string) error { - _v, err := strconv.ParseFloat(val, 64) - if err != nil { - return fmt.Errorf("could not convert set string to float: %w", err) - } - - *w = WriteRate(_v) - return nil -} diff --git a/revid/config/variables.go b/revid/config/variables.go index 0f0b6af5..f84fa02b 100644 --- a/revid/config/variables.go +++ b/revid/config/variables.go @@ -40,20 +40,18 @@ import ( // Default variable values. const ( // General revid defaults. - defaultInput = InputRaspivid - defaultOutput = OutputHTTP - defaultInputCodec = codecutil.H264 - defaultVerbosity = logger.Error - defaultRTPAddr = "localhost:6970" - defaultCameraIP = "192.168.1.50" - defaultBurstPeriod = 10 // Seconds - defaultMinFrames = 100 - defaultFrameRate = 25 - defaultWriteRate = 25 - defaultClipDuration = 0 - defaultAudioInputCodec = codecutil.ADPCM - defaultPSITime = 2 - defaultFileFPS = 0 + defaultInput = InputRaspivid + defaultOutput = OutputHTTP + defaultInputCodec = codecutil.H264 + defaultVerbosity = logger.Error + defaultRTPAddr = "localhost:6970" + defaultCameraIP = "192.168.1.50" + defaultBurstPeriod = 10 // Seconds + defaultMinFrames = 100 + defaultFrameRate = 25 + defaultClipDuration = 0 + defaultPSITime = 2 + defaultFileFPS = 0 // Ring buffer defaults. defaultRBCapacity = 50000000 // => 50MB @@ -138,6 +136,11 @@ var Variables = []struct { } }, }, + { + Name: "Channels", + Type_: "uint", + Update: func(c *Config, v string) { c.Channels = parseUint("Channels", v, c) }, + }, { Name: "Exposure", Type_: "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks", @@ -243,14 +246,8 @@ var Variables = []struct { switch c.InputCodec { case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM: default: - switch c.Input { - case OutputAudio: - c.LogInvalidField("InputCodec", defaultAudioInputCodec) - c.InputCodec = defaultAudioInputCodec - default: - c.LogInvalidField("InputCodec", defaultInputCodec) - c.InputCodec = defaultInputCodec - } + c.LogInvalidField("InputCodec", defaultInputCodec) + c.InputCodec = defaultInputCodec } }, }, @@ -449,6 +446,17 @@ var Variables = []struct { c.RBWriteTimeout = lessThanOrEqual("RBWriteTimeout", c.RBWriteTimeout, 0, c, defaultRBWriteTimeout) }, }, + { + Name: "RecPeriod", + Type_: "float", + Update: func(c *Config, v string) { + _v, err := strconv.ParseFloat(v, 64) + if err != nil { + c.Logger.Log(logger.Warning, fmt.Sprintf("invalid %s param", "RecPeriod"), "value", v) + } + c.RecPeriod = _v + }, + }, { Name: "Rotation", Type_: "uint", @@ -470,6 +478,11 @@ var Variables = []struct { } }, }, + { + Name: "SampleRate", + Type_: "uint", + Update: func(c *Config, v string) { c.SampleRate = parseUint("SampleRate", v, c) }, + }, { Name: "Saturation", Type_: "int", @@ -522,14 +535,6 @@ var Variables = []struct { Type_: "uint", Update: func(c *Config, v string) { c.Width = parseUint("Width", v, c) }, }, - { - Name: "WriteRate", - Type_: "uint", - Update: func(c *Config, v string) { c.WriteRate = float64(parseUint("WriteRate", v, c)) }, - Validate: func(c *Config) { - c.WriteRate = float64(lessThanOrEqual("WriteRate", uint(c.WriteRate), 0, c, defaultWriteRate)) - }, - }, } func parseUint(n, v string, c *Config) uint { diff --git a/revid/revid.go b/revid/revid.go index d0d981b4..5b9b2fb0 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -169,7 +169,7 @@ func (r *Revid) reset(c config.Config) error { r.cfg.Logger.Log(logger.Debug, "setting up revid pipeline") err = r.setupPipeline( - func(dst io.WriteCloser, fps float64) (io.WriteCloser, error) { + func(dst io.WriteCloser, rate float64) (io.WriteCloser, error) { var st int var encOptions []func(*mts.Encoder) error @@ -211,10 +211,12 @@ func (r *Revid) reset(c config.Config) error { } case config.InputAudio: st = mts.EncodeAudio + encOptions = append(encOptions, mts.TimeBasedPSI(time.Duration(r.cfg.PSITime)*time.Second)) + rate = 1 / r.cfg.RecPeriod default: panic("unknown input type") } - encOptions = append(encOptions, mts.MediaType(st), mts.Rate(int(fps))) + encOptions = append(encOptions, mts.MediaType(st), mts.Rate(rate)) return mts.NewEncoder(dst, &encLog{r.cfg.Logger}, encOptions...) }, func(dst io.WriteCloser, fps int) (io.WriteCloser, error) { @@ -310,7 +312,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. // as a destination. if len(mtsSenders) != 0 { mw := multiWriter(mtsSenders...) - e, _ := mtsEnc(mw, r.cfg.WriteRate) + e, _ := mtsEnc(mw, float64(r.cfg.FrameRate)) encoders = append(encoders, e) } @@ -454,9 +456,12 @@ func (r *Revid) Start() error { } r.cfg.Logger.Log(logger.Info, "revid reset") - // Calculate delay between frames based on FileFPS. + // Calculate delay between frames based on FileFPS for video or + // between recording periods for audio. d := time.Duration(0) - if r.cfg.FileFPS != 0 { + if r.cfg.Input == config.InputAudio { + d = time.Duration(r.cfg.RecPeriod * float64(time.Second)) + } else if r.cfg.FileFPS != 0 { d = time.Duration(1000/r.cfg.FileFPS) * time.Millisecond }