diff --git a/codec/pcm/filters.go b/codec/pcm/filters.go new file mode 100644 index 00000000..7c23a5aa --- /dev/null +++ b/codec/pcm/filters.go @@ -0,0 +1,328 @@ +/* +NAME + filters.go + +DESCRIPTION + filter.go contains functions for filtering PCM audio. + +AUTHOR + David Sutton + +LICENSE + filters_test.go is Copyright (C) 2023 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It 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 in gpl.txt. + If not, see [GNU licenses](http://www.gnu.org/licenses). +*/ + +package pcm + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math" + + "github.com/mjibson/go-dsp/fft" + "github.com/mjibson/go-dsp/window" +) + +// AudioFilter is an interface which contains an Apply function. +// Apply is used to apply the filter to the given buffer of PCM data (b.Data). +type AudioFilter interface { + Apply(b Buffer) +} + +// SelectiveFrequencyFilter is a struct which contains all the filter specifications required for a +// lowpass, highpass, bandpass, or bandstop filter. +type SelectiveFrequencyFilter struct { + coeffs []float64 + cutoff [2]float64 + sampleRate uint + taps int + buffInfo BufferFormat +} + +// NewLowPass populates a LowPass struct with the specified data. The function also +// generates a lowpass filter based off the given specifications, and returns a pointer. +func NewLowPass(fc float64, info BufferFormat, length int) (*SelectiveFrequencyFilter, error) { + return newLoHiFilter(fc, info, length, [2]float64{0, fc}) +} + +// NewHighPass populates a HighPass struct with the specified data. The function also +// generates a highpass filter based off the given specifications, and returns a pointer. +func NewHighPass(fc float64, info BufferFormat, length int) (*SelectiveFrequencyFilter, error) { + return newLoHiFilter(fc, info, length, [2]float64{fc, 0}) +} + +// NewBandPass populates a BandPass struct with the specified data. The function also +// generates a bandpass filter based off the given specifications, and returns a pointer. +func NewBandPass(fc_lower, fc_upper float64, info BufferFormat, length int) (*SelectiveFrequencyFilter, error) { + newFilter, lp, hp, err := newBandFilter([2]float64{fc_lower, fc_upper}, info, length) + if err != nil { + return nil, fmt.Errorf("could not create new band filter: %w", err) + } + + // Convolve the filters to create a bandpass filter. + newFilter.coeffs, err = fastConvolve(hp.coeffs, lp.coeffs) + if err != nil { + return nil, fmt.Errorf("could not compute fast convolution: %w", err) + } + + // Return a pointer to the filter. + return newFilter, nil +} + +// NewBandStop populates a BandStop struct with the specified data. The function also +// generates a bandstop filter based off the given specifications, and returns a pointer. +func NewBandStop(fc_lower, fc_upper float64, info BufferFormat, length int) (*SelectiveFrequencyFilter, error) { + newFilter, lp, hp, err := newBandFilter([2]float64{fc_upper, fc_lower}, info, length) + if err != nil { + return nil, fmt.Errorf("could not create new band filter: %w", err) + } + size := newFilter.taps + 1 + newFilter.coeffs = make([]float64, size) + for i := range lp.coeffs { + newFilter.coeffs[i] = lp.coeffs[i] + hp.coeffs[i] + } + + // Return a pointer to the filter. + return newFilter, nil +} + +// Apply is the SelectiveFrequencyFilter implementation of the AudioFilter interface. This implementation +// takes the buffer data (b.Data), applies the highpass filter and returns a byte slice of filtered audio. +func (filter *SelectiveFrequencyFilter) Apply(b Buffer) ([]byte, error) { + // Apply the lowpass filter to the given audio buffer. + return convolveFromBytes(b.Data, filter.coeffs) +} + +// Amplifier is a struct which contains the factor of amplification to be used in the application +// of the filter. +type Amplifier struct { + factor float64 +} + +// NewAmplifier defines the factor of amplification for an amplifying filter. +func NewAmplifier(factor float64) Amplifier { + // Return populated Amplifier filter. + // Uses the absolute value of the factor to ensure compatibility. + return Amplifier{factor: math.Abs(factor)} +} + +// Apply implemented for an amplifier takes the buffer data (b.Data), applies +// the amplification and returns a byte slice of filtered audio. +func (amp *Amplifier) Apply(b Buffer) ([]byte, error) { + inputAsFloat, err := bytesToFloats(b.Data) + if err != nil { + return nil, fmt.Errorf("failed to convert to floats: %w", err) + } + + // Multiply every sample by the factor of amplification. + floatOutput := make([]float64, len(inputAsFloat)) + for i := range inputAsFloat { + floatOutput[i] = inputAsFloat[i] * amp.factor + // Stop audio artifacting by clipping outputs. + if floatOutput[i] > 1 { + floatOutput[i] = 1 + } else if floatOutput[i] < -1 { + floatOutput[i] = -1 + } + } + outBytes, err := floatsToBytes(floatOutput) + if err != nil { + return nil, fmt.Errorf("failed to convert to bytes: %w", err) + } + return outBytes, nil +} + +// newLoHiFilter is a function which checks for the validity of the input parameters, and calls the newLoHiFilter function +// to return a pointer to either a lowpass or a highpass filter. +func newLoHiFilter(fc float64, info BufferFormat, length int, cutoff [2]float64) (*SelectiveFrequencyFilter, error) { + // Ensure that all input values are valid. + if fc <= 0 || fc >= float64(info.Rate)/2 { + return nil, errors.New("cutoff frequency out of bounds") + } else if length <= 0 { + return nil, errors.New("cannot create filter with length <= 0") + } + + // Determine the type of filter to be generated. + var fd float64 + var factor1 float64 + var factor2 float64 + if cutoff[0] == 0 { // For a lowpass filter, cutoff[0] = 0, cutoff[1] = fc. + // The filter must be a lowpass filter. + fd = cutoff[1] / float64(info.Rate) + factor1 = 1 + factor2 = 2 * fd + } else if cutoff[1] == 0 { // For a highpass filter, cutoff[0] = fc, cutoff[1] = 0. + // The filter must be a highpass filter. + fd = cutoff[0] / float64(info.Rate) + factor1 = -1 + factor2 = 1 - 2*fd + } else { + // Otherwise the filter must be a different type of filter. + return nil, errors.New("tried to use newLoHiFilter to generate bandpass or bandstop filter") + } + + // Create a new filter struct to return, populated with all correct data. + var newFilter = SelectiveFrequencyFilter{cutoff: cutoff, sampleRate: info.Rate, taps: length, buffInfo: info} + + // Create a filter with characteristics from struct. + size := newFilter.taps + 1 + newFilter.coeffs = make([]float64, size) + b := 2 * math.Pi * fd + winData := window.FlatTop(size) + for n := 0; n < (newFilter.taps / 2); n++ { + c := float64(n) - float64(newFilter.taps)/2 + y := math.Sin(c*b) / (math.Pi * c) + newFilter.coeffs[n] = factor1 * y * winData[n] + newFilter.coeffs[size-1-n] = newFilter.coeffs[n] + } + newFilter.coeffs[newFilter.taps/2] = factor2 * winData[newFilter.taps/2] + + // Return a pointer to the filter. + return &newFilter, nil +} + +// newBandFilter creates a ensures the validity of the input parameters, and generates appropriate lowpass and highpass filters +// required for the creation of the specific band filter. +func newBandFilter(cutoff [2]float64, info BufferFormat, length int) (new, lp, hp *SelectiveFrequencyFilter, err error) { + // Ensure that all input values are valid. + if cutoff[0] <= 0 || cutoff[0] >= float64(info.Rate)/2 { + return nil, nil, nil, errors.New("cutoff frequencies out of bounds") + } else if cutoff[1] <= 0 || cutoff[1] >= float64(info.Rate)/2 { + return nil, nil, nil, errors.New("cutoff frequencies out of bounds") + } else if length <= 0 { + return nil, nil, nil, errors.New("cannot create filter with length <= 0") + } + // Create a new filter struct to return, populated with all correct data. + // For a bandpass filter, cutoff[0] = fc_l, cutoff[1] = fc_u. + // For a bandstop filter, cutoff[0] = fc_u, cutoff[1] = fc_l. + var newFilter = SelectiveFrequencyFilter{cutoff: cutoff, sampleRate: info.Rate, taps: length, buffInfo: info} + + // Generate lowpass and highpass filters to create bandpass filter with. + hp, err = NewHighPass(newFilter.cutoff[0], newFilter.buffInfo, newFilter.taps) + if err != nil { + return nil, nil, nil, fmt.Errorf("could not create new highpass filter: %w", err) + } + lp, err = NewLowPass(newFilter.cutoff[1], newFilter.buffInfo, newFilter.taps) + if err != nil { + return nil, nil, nil, fmt.Errorf("could not create new lowpass filter: %w", err) + } + + // Return pointer to new filter. + return &newFilter, hp, lp, nil +} + +// convolveFromBytes takes in a byte slice and a float64 slice for a filter, converts to floats, +// convolves the two signals, and converts back to bytes and returns the convolution. +func convolveFromBytes(b []byte, filter []float64) ([]byte, error) { + bufAsFloats, err := bytesToFloats(b) + if err != nil { + return nil, fmt.Errorf("could not convert to floats: %w", err) + } + + // Convolve the floats with the filter. + convolution, err := fastConvolve(bufAsFloats, filter) + if err != nil { + return nil, fmt.Errorf("could not compute fast convolution: %w", err) + } + outBytes, err := floatsToBytes(convolution) + if err != nil { + return nil, fmt.Errorf("could not convert convolution to bytes: %w", err) + } + return outBytes, nil +} + +func bytesToFloats(b []byte) ([]float64, error) { + // Ensure the validity of the input. + if len(b) == 0 { + return nil, errors.New("no audio to convert to floats") + } else if len(b)%2 != 0 { + return nil, errors.New("uneven number of bytes (not whole number of samples)") + } + + // Convert bytes to floats. + inputAsFloat := make([]float64, len(b)/2) + inputAsInt := make([]int16, len(b)/2) + bReader := bytes.NewReader(b) + for i := range inputAsFloat { + binary.Read(bReader, binary.LittleEndian, &inputAsInt[i]) + inputAsFloat[i] = float64(inputAsInt[i]) / (math.MaxInt16 + 1) + } + return inputAsFloat, nil +} + +// floatsToBytes converts a slice of float64 PCM data into a slice of signed 16bit PCM data. +// The input float slice should contains values between -1 and 1. The function converts these values +// to a proportionate unsigned value between 0 and 65536. This 16bit integer is split into two bytes, +// then returned in Little Endian notation in a byte slice double the length of the input. +func floatsToBytes(f []float64) ([]byte, error) { + buf := new(bytes.Buffer) + bytes := make([]byte, len(f)*2) + for i := range f { + err := binary.Write(buf, binary.LittleEndian, int16(f[i]*math.MaxInt16)) + if err != nil { + return nil, fmt.Errorf("failed to write ints as bytes: %w", err) + } + } + n, err := buf.Read(bytes) + if err != nil { + return nil, fmt.Errorf("failed to read bytes from buffer: %w", err) + } else if n != len(bytes) { + return nil, fmt.Errorf("buffer and output length mismatch read %d bytes, expected %d: %w", n, len(bytes), err) + } + + return bytes, nil +} + +// fastConvolve takes in a signal and an FIR filter and computes the convolution (runs in O(nlog(n)) time). +func fastConvolve(x, h []float64) ([]float64, error) { + // Ensure valid data to convolve. + if len(x) == 0 || len(h) == 0 { + return nil, errors.New("convolution requires slice of length > 0") + } + + // Calculate the length of the linear convolution. + convLen := len(x) + len(h) - 1 + + // Pad signals to the next largest power of 2 larger than convLen. + padLen := int(math.Pow(2, math.Ceil(math.Log2(float64(convLen))))) + zeros := make([]float64, padLen-len(x), padLen-len(h)) + x = append(x, zeros...) + zeros = make([]float64, padLen-len(h)) + h = append(h, zeros...) + + // Compute DFFTs. + x_fft, h_fft := fft.FFTReal(x), fft.FFTReal(h) + + // Compute the multiplication of the two signals in the freq domain. + y_fft := make([]complex128, padLen) + for i := range x_fft { + y_fft[i] = x_fft[i] * h_fft[i] + } + + // Compute the IDFFT. + iy := fft.IFFT(y_fft) + + // Convert to []float64. + y := make([]float64, padLen) + for i := range iy { + y[i] = real(iy[i]) + } + + // Trim to length of linear convolution and return. + return y[0:convLen], nil +} diff --git a/codec/pcm/filters_test.go b/codec/pcm/filters_test.go new file mode 100644 index 00000000..83cdac29 --- /dev/null +++ b/codec/pcm/filters_test.go @@ -0,0 +1,368 @@ +/* +NAME + filters_test.go + +DESCRIPTION + filter_test.go contains functions for testing functions in filters.go. + +AUTHOR + David Sutton + +LICENSE + filters_test.go is Copyright (C) 2023 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It 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 in gpl.txt. + If not, see [GNU licenses](http://www.gnu.org/licenses). +*/ + +package pcm + +import ( + "math" + "math/cmplx" + "os" + "testing" + + "github.com/mjibson/go-dsp/fft" +) + +// Set constant values for testing. +const ( + sampleRate = 44100 + filterLength = 500 + freqTest = 1000 +) + +// TestLowPass is used to test the lowpass constructor and application. Testing is done by ensuring frequency response as well as +// comparing against an expected audio file. +func TestLowPass(t *testing.T) { + // Generate an audio buffer to run test on. + genAudio, err := generate() + if err != nil { + t.Fatal(err) + } + var buf = Buffer{Data: genAudio, Format: BufferFormat{SFormat: S16_LE, Rate: sampleRate, Channels: 1}} + + // Create a lowpass filter to test. + const fc = 4500.0 + lp, err := NewLowPass(fc, buf.Format, filterLength) + if err != nil { + t.Fatal(err) + } + + // Filter the audio. + filteredAudio, err := lp.Apply(buf) + if err != nil { + t.Fatal(err) + } + + // Take the FFT of the signal. + filteredFloats, err := bytesToFloats(filteredAudio) + if err != nil { + t.Fatal(err) + } + filteredFFT := fft.FFTReal(filteredFloats) + + // Check if the lowpass filter worked (any high values in filteredFFT above cutoff freq result in fail). + for i := int(fc); i < sampleRate/2; i++ { + mag := math.Pow(cmplx.Abs(filteredFFT[i]), 2) + if mag > freqTest { + t.Error("Lowpass filter failed to meet spec.") + break + } + } + + // Read audio from the test location. + const fileName = "../../../test/test-data/av/input/lp_4500.pcm" + expectedAudio, err := os.ReadFile(fileName) + if err != nil { + t.Fatalf("File for comparison not read.\n\t%s", err) + } + + // Compare the filtered audio against the expected signal. + compare(filteredAudio, expectedAudio, t) +} + +// TestHighPass is used to test the highpass constructor and application. Testing is done by ensuring frequency response as well as +// comparing against an expected audio file. +func TestHighPass(t *testing.T) { + // Generate an audio buffer to run test on. + genAudio, err := generate() + if err != nil { + t.Fatal(err) + } + var buf = Buffer{Data: genAudio, Format: BufferFormat{SFormat: S16_LE, Rate: sampleRate, Channels: 1}} + + // Create a highpass filter to test. + const fc = 4500.0 + hp, err := NewHighPass(fc, buf.Format, filterLength) + if err != nil { + t.Fatal(err) + } + + // Filter the audio. + filteredAudio, err := hp.Apply(buf) + if err != nil { + t.Fatal(err) + } + + // Take the FFT of signal. + filteredFloats, err := bytesToFloats(filteredAudio) + if err != nil { + t.Fatal(err) + } + filteredFFT := fft.FFTReal(filteredFloats) + + // Check if the highpass filter worked (any high values in filteredFFT below cutoff freq result in fail). + for i := 0; i < int(fc); i++ { + mag := math.Pow(cmplx.Abs(filteredFFT[i]), 2) + if mag > freqTest { + t.Error("Highpass Filter doesn't meet Spec", i) + } + } + + // Read audio from the test location. + const fileName = "../../../test/test-data/av/input/hp_4500.pcm" + expectedAudio, err := os.ReadFile(fileName) + if err != nil { + t.Fatalf("File for comparison not read.\n\t%s", err) + } + + // Compare against expected results. + compare(expectedAudio, filteredAudio, t) +} + +// TestBandPass is used to test the bandpass constructor and application. Testing is done by ensuring frequency response as well as +// comparing against an expected audio file. +func TestBandPass(t *testing.T) { + // Generate an audio buffer to run test on. + genAudio, err := generate() + if err != nil { + t.Fatal(err) + } + var buf = Buffer{Data: genAudio, Format: BufferFormat{SFormat: S16_LE, Rate: sampleRate, Channels: 1}} + + // Create a bandpass filter to test. + const ( + fc_l = 4500.0 + fc_u = 9500.0 + ) + hp, err := NewBandPass(fc_l, fc_u, buf.Format, filterLength) + if err != nil { + t.Fatal(err) + } + + // Filter audio with filter. + filteredAudio, err := hp.Apply(buf) + if err != nil { + t.Fatal(err) + } + + // Take FFT of signal. + filteredFloats, err := bytesToFloats(filteredAudio) + if err != nil { + t.Fatal(err) + } + filteredFFT := fft.FFTReal(filteredFloats) + + // Check if the bandpass filter worked (any high values in filteredFFT above cutoff or below cutoff freq result in fail). + for i := 0; i < int(fc_l); i++ { + mag := math.Pow(cmplx.Abs(filteredFFT[i]), 2) + if mag > freqTest { + t.Error("Bandpass Filter doesn't meet Spec", i) + } + } + + for i := int(fc_u); i < sampleRate/2; i++ { + mag := math.Pow(cmplx.Abs(filteredFFT[i]), 2) + if mag > freqTest { + t.Error("Bandpass Filter doesn't meet Spec", i) + } + } + + // Read audio from test location. + const fileName = "../../../test/test-data/av/input/bp_4500-9500.pcm" + expectedAudio, err := os.ReadFile(fileName) + if err != nil { + t.Fatalf("File for comparison not read.\n\t%s", err) + } + + // Compare against the expected audio. + compare(expectedAudio, filteredAudio, t) +} + +// TestBandPass is used to test the bandpass constructor and application. Testing is done by ensuring frequency response as well as +// comparing against an expected audio file. +func TestBandStop(t *testing.T) { + // Generate an audio buffer to run test on. + genAudio, err := generate() + if err != nil { + t.Fatal(err) + } + var buf = Buffer{Data: genAudio, Format: BufferFormat{SFormat: S16_LE, Rate: sampleRate, Channels: 1}} + + // Create a bandpass filter to test. + const ( + fc_l = 4500.0 + fc_u = 9500.0 + ) + bs, err := NewBandStop(fc_l, fc_u, buf.Format, filterLength) + if err != nil { + t.Fatal(err) + } + + // Filter audio with filter. + filteredAudio, err := bs.Apply(buf) + if err != nil { + t.Fatal(err) + } + + // Take FFT of signal. + filteredFloats, err := bytesToFloats(filteredAudio) + if err != nil { + t.Fatal(err) + } + filteredFFT := fft.FFTReal(filteredFloats) + + // Check if the bandpass filter worked (any high values in filteredFFT above cutoff or below cutoff freq result in fail). + for i := int(fc_l); i < int(fc_u); i++ { + mag := math.Pow(cmplx.Abs(filteredFFT[i]), 2) + if mag > freqTest { + t.Error("BandStop Filter doesn't meet Spec", i) + } + } + + // Read audio from test location. + const fileName = "../../../test/test-data/av/input/bs_4500-9500.pcm" + expectedAudio, err := os.ReadFile(fileName) + if err != nil { + t.Fatalf("File for comparison not read.\n\t%s", err) + } + + // Compare against the expected audio. + compare(expectedAudio, filteredAudio, t) +} + +// TestAmplifier is used to test the amplifier constructor and application. Testing is done by checking the maximum value before and +// after application, as well as comparing against an expected audio file. +func TestAmplifier(t *testing.T) { + // Load a simple sine wave with amplitude of 0.1 and load into buffer. + const audioFileName = "../../../test/test-data/av/input/sine.pcm" + lowSine, err := os.ReadFile(audioFileName) + if err != nil { + t.Errorf("File for filtering not read.\n\t%s", err) + t.FailNow() + } + var buf = Buffer{Data: lowSine, Format: BufferFormat{SFormat: S16_LE, Rate: sampleRate, Channels: 1}} + + // Create an amplifier filter. + const factor = 5.0 + amp := NewAmplifier(factor) + + // Apply the amplifier to the audio. + filteredAudio, err := amp.Apply(buf) + if err != nil { + t.Fatal(err) + } + + // Find the maximum sample before and after amplification. + dataFloats, err := bytesToFloats(buf.Data) + if err != nil { + t.Fatal(err) + } + preMax := max(dataFloats) + filteredFloats, err := bytesToFloats(filteredAudio) + if err != nil { + t.Fatal(err) + } + postMax := max(filteredFloats) + + // Compare the values. + if preMax*factor > 1 && postMax > 0.99 { + } else if postMax/preMax > 1.01*factor || postMax/preMax < 0.99*factor { + t.Error("Amplifier failed to meet spec, expected:", factor, " got:", postMax/preMax) + } + + // Load expected audio file. + const compFileName = "../../../test/test-data/av/input/amp_5.pcm" + expectedAudio, err := os.ReadFile(compFileName) + if err != nil { + t.Fatalf("File for comparison not read.\n\t%s", err) + } + + // Compare against the expected audio file. + compare(filteredAudio, expectedAudio, t) +} + +// generate returns a byte slice in the same format that would be read from a PCM file. +// The function generates a sound with a range of frequencies for testing against, +// with a length of 1 second. +func generate() ([]byte, error) { + // Create an slice to generate values across. + t := make([]float64, sampleRate) + s := make([]float64, sampleRate) + // Define spacing of generated frequencies. + const ( + deltaFreq = 1000 + maxFreq = 21000 + amplitude = float64(deltaFreq) / float64((maxFreq - deltaFreq)) + ) + for n := 0; n < sampleRate; n++ { + t[n] = float64(n) / float64(sampleRate) + // Generate sinewaves of different frequencies. + s[n] = 0 + for f := deltaFreq; f < maxFreq; f += deltaFreq { + s[n] += amplitude * math.Sin(float64(f)*2*math.Pi*t[n]) + } + } + // Return the spectrum as bytes (PCM). + bytesOut, err := floatsToBytes(s) + if err != nil { + return nil, err + } + return bytesOut, nil +} + +// compare takes in two audio files (S16_LE), compares them, and returns an error if the +// signals are more than 10% different at any individual sample. +func compare(a, b []byte, t *testing.T) { + // Convert to floats to compare. + aFloats, err := bytesToFloats(a) + if err != nil { + t.Fatal(err) + } + bFloats, err := bytesToFloats(b) + if err != nil { + t.Fatal(err) + } + + // Compare against filtered audio. + for i := range aFloats { + diff := (bFloats[i] - aFloats[i]) + if math.Abs(diff) > 0.1 { + t.Error("Filtered audio is too different to database") + return + } + } +} + +// max takes a float slice and returns the absolute largest value in the slice. +func max(a []float64) float64 { + var runMax float64 = -1 + for i := range a { + if math.Abs(a[i]) > runMax { + runMax = math.Abs(a[i]) + } + } + return runMax +} diff --git a/go.mod b/go.mod index 8ba7ec85..e41bb19c 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,12 @@ require ( github.com/google/go-cmp v0.4.1 github.com/kidoman/embd v0.0.0-20170508013040-d3d8c0c5c68d github.com/mewkiz/flac v1.0.5 + github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 github.com/pkg/errors v0.9.1 github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e gocv.io/x/gocv v0.29.0 - gonum.org/v1/gonum v0.9.3 - gonum.org/v1/plot v0.10.0 + golang.org/x/tools v0.5.0 // indirect + gonum.org/v1/gonum v0.8.2 + gonum.org/v1/plot v0.9.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) diff --git a/go.sum b/go.sum index d340b280..e1454d15 100644 --- a/go.sum +++ b/go.sum @@ -16,13 +16,11 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWso github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/adrianmo/go-nmea v1.1.1-0.20190109062325-c448653979f7/go.mod h1:HHPxPAm2kmev+61qmkZh7xgZF/7qHtSpsWppip2Ipv8= github.com/aerth/playwav v0.0.0-20170324024803-17dfe21a406f/go.mod h1:aTANbm/GXj1ilCRMsIiSpX0i7LUkqjAPj6R0fpWbLNA= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20210923152817-c3b6e2f0c527 h1:NImof/JkF93OVWZY+PINgl6fPtQyF6f+hNUtZ0QZA1c= -github.com/ajstarks/svgo v0.0.0-20210923152817-c3b6e2f0c527/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= github.com/cocoonlife/goalsa v0.0.0-20160812085113-b711ae6f3eff/go.mod h1:5aLO409bJnd+jCw0t/SB/DhHkVBhPAE31lnHJnYQxy0= github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64/go.mod h1:LoCAz53rbPcqs8Da2BjB/yDy4gxMtiSQmqnYI/DGH+U= @@ -46,18 +44,13 @@ github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884 h1:2TaXIaVA4ff/MHHezO github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884/go.mod h1:UiqzUyfX0zs3pJ/DPyvS5v8sN6s5bXPUDDIVA5v8dks= github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1 h1:wBrPaMkrXFBW3qXpXAjiKljdVUMxn9bX2ia3XjPHoik= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07 h1:OTlfMvwR1rLyf9goVmXfuS5AJn80+Vmj4rTf4n46SOs= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= -github.com/go-pdf/fpdf v0.5.0 h1:GHpcYsiDV2hdo77VTOuTF9k1sN8F8IY7NjnCo9x+NPY= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -79,17 +72,18 @@ github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21 h1:Hc1iKlyxNHp3CV59 github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s= github.com/mewkiz/flac v1.0.5 h1:dHGW/2kf+/KZ2GGqSVayNEhL9pluKn/rr/h/QqD9Ogc= github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs= +github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 h1:dd7vnTDfjtwCETZDrRe+GPYNLA1jBtbZeyfyE8eZCyk= +github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12/go.mod h1:i/KKcxEWEO8Yyl11DYafRPKOPVYTrhxiTRigjtEEXZU= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -102,6 +96,7 @@ github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cb github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e h1:3NIzz7weXhh3NToPgbtlQtKiVgerEaG4/nY2skGoGG0= github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e/go.mod h1:CaowXBWOiSGWEpBBV8LoVnQTVPV4ycyviC9IBLj8dRw= github.com/yryz/ds18b20 v0.0.0-20180211073435-3cf383a40624/go.mod h1:MqFju5qeLDFh+S9PqxYT7TEla8xeW7bgGr/69q3oki0= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= @@ -115,6 +110,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -129,17 +125,22 @@ golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030 h1:lP9pYkih3DUSC641giIXa2XqfTIbbbRr0w2EOTA7wHA= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -149,34 +150,43 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= +golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0 h1:3sEo36Uopv1/SA/dMFFaxXoL5XyikJ9Sf2Vll/k6+2E= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.0 h1:ymLukg4XJlQnYUJCp+coQq5M7BsUJFk6XQE4HPflwdw= -gonum.org/v1/plot v0.10.0/go.mod h1:JWIHJ7U20drSQb/aDpTetJzfC1KlAPldJLpkSy88dvQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=