Audiofiltering:

Interface the function with pre-existing data structures. The filters can now be generated into a filter type using a generic Generate function.
This generates a filter off of the specifications within the filter struct. There is a generic Apply function which takes in a buffer of PCM data (defined in pcm.go),
and outputs to a []byte.
This commit is contained in:
ausocean-david 2022-12-19 20:46:20 +10:30
parent 75124b4494
commit d029038db9
7 changed files with 249 additions and 272 deletions

View File

@ -1,5 +0,0 @@
module bitbucket.org/ausocean/av/cmd/audiofiltering
go 1.19
require github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12

View File

@ -1,2 +0,0 @@
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=

View File

@ -1,259 +1,56 @@
package main package main
import( import (
"math"
"fmt" "fmt"
"math"
"os" "os"
"encoding/binary"
"github.com/mjibson/go-dsp/fft"
"github.com/mjibson/go-dsp/window"
"math/cmplx"
"time" "time"
"sync"
"bitbucket.org/ausocean/av/codec/pcm"
) )
// Define the constants to be used in the generation of the sound files. // main is a driver function for testing the filters defined in codec/pcm/filters.go
const(
SampleRate float64 = 44100
Duration = 2
tau = math.Pi * 2
length int = 88200
)
// main is a driver function which generates a sound file which contains sine waves from across the spectrum.
// Filters can then be generated and applied to the audio, in order to test their frequency response.
func main() { func main() {
// Create waitgroups to ensure program runs correctly. // Define start time for execution timing.
var wg1, wg2 sync.WaitGroup
// Take a measurement of the starting point of the program execution to use for execution timing.
start := time.Now() start := time.Now()
// Generate sine waves with different frequencies to test frequency response.
n := 100
wg1.Add(n)
audio := make([][]float64, n)
for i:=0; i<n; i++ {
go func(i int, audio [][]float64, wg *sync.WaitGroup) {
audio[i] = generate(float64((i)*(22000/n)))
wg.Done()
} (i, audio, &wg1)
}
// Create the filter.
filterLen := 500
wg2.Add(1)
ch := make(chan []float64, 1)
go BandPass(filterLen, 2000, 5000, &wg2, ch)
// Read the audio data from the file.
// Combine all the generated sine waves into a single audio slice. input, _ := os.ReadFile("spike.pcm")
wg1.Wait()
combinedAudio := make([]float64, length)
for i := range audio[0] {
combinedAudio[i] = 0
for j:=0; j<n; j++ {
combinedAudio[i] += audio[j][i]
}
}
// Get the filter from the filter goroutine called earlier. // Create Buffer in struct format defined in pcm.go
wg2.Wait() format := pcm.BufferFormat{Rate: 44100, Channels: 1, SFormat: pcm.S16_LE}
filter := <- ch buf := pcm.Buffer{Format: format, Data: input}
wg1.Add(2) // Create a filter.
// Save the unfiltered audio to compare the filtered audio against. lp := pcm.Filter{BuffInfo: buf.Format, Type: pcm.LP, Upper: 10000, Taps: 50}
go SaveAudioData(combinedAudio, "unfiltered", &wg1) lp.Generate()
// Apply the filter to the combined audio file by computing the convolution of the 2 slices. // Apply the lowpass filter to the buffer.
wg2.Add(1) output := lp.Apply(buf)
filteredAudio := Convolve(combinedAudio, filter, &wg2)
// Save the transformed audio.
// Save the filtered audio. f, _ := os.Create("output.pcm")
wg2.Wait() f.Write(output[50 : len(output)-50])
go SaveAudioData(filteredAudio, "bandpass-2-5KHz", &wg1)
// Display execution time.
// Print the total time of execution, to compare different algorithms. fmt.Println("Finished execution. Total time:", time.Since(start))
wg1.Wait()
fmt.Println(time.Since(start))
} }
// generate is used to generate a sine wave of the given frequency, calls on the global variables of Duration and SampleRate // LEGACY FUNCTION FOR TESTING
func generate(Frequency float64) []float64 { // generate is used to generate a sine wave of the given frequency, calls on the global variables of Duration and SampleRate (bytes)
func generate(freq float64, SampleRate, duration int) []byte {
// Deteremine the number of samples required based off Duration and SampleRate.
nsamps := Duration * SampleRate
// Generate the x-values to plot against.
var angle float64 = tau / float64(nsamps)
// Create an array for the generated samples. // Create slices to store values.
samp := make([]float64, int(nsamps)) signal := make([]byte, 2*duration*SampleRate)
t := make([]float64, 2*duration*SampleRate)
// Generate the samples. // Generate the values to plot.
for i := 0; i < int(nsamps); i++ { for n := range t {
samp[i] = math.Sin(angle * Frequency * float64(4*i)) t[n] = math.Pi * float64(n) / float64(SampleRate)
signal[n] = byte(math.Round(127 * math.Sin(freq*t[n])))
} }
return samp return signal
} }
// max returns the absolute highest value in a given array.
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
}
// LowPass returns an array of coeffiecients over the given channel that can be used in a convolution with a signal to perform
// a lowpass filtering with the given cut-off frequency fc.
func LowPass (taps int, fc float64, wg *sync.WaitGroup, ch chan []float64) {
size := taps + 1
fd := fc/SampleRate
b := (2*math.Pi) * fd
filter := make([]float64, size)
winData := window.FlatTop(size)
for n:=0; n<(taps/2); n++ {
c := float64(n) - float64(taps)/2
y := math.Sin(c*b) / (math.Pi * c)
filter[n] = y * winData[n]
filter[size-1-n] = filter[n]
}
filter[taps/2] = 2*fd*winData[taps/2]
ch <- filter
wg.Done()
}
// HighPass returns an array of coeffiecients over the given channel that can be used in a convolution with a signal to perform
// a highpass filtering with the given cut-off frequency fc.
func HighPass (taps int, fc float64, wg *sync.WaitGroup, ch chan []float64) {
size := taps + 1
fd := fc/SampleRate
b := (2*math.Pi) * fd
filter := make([]float64, size)
winData := window.FlatTop(size)
for n:=0; n<(taps/2); n++ {
c := float64(n) - float64(taps)/2
y := math.Sin(c*b) / (math.Pi * c)
filter[n] = -y * winData[n]
filter[size-1-n] = filter[n]
}
filter[taps/2] = (1 - 2*fd) * winData[taps/2]
ch <- filter
wg.Done()
}
// BandPass returns an array of coeffiecients over the given channel that can be used in a convolution with a signal to perform
// a bandpass filtering with the given lower and upper cutoff frequencies (using the low and highpass filter functions).
func BandPass (taps int, lower, upper float64, wg *sync.WaitGroup, ch chan []float64) {
// Create a waitgroup and channel to be used internally within the function.
var bpwg sync.WaitGroup
bpch := make(chan []float64, 2)
// Call low and highpass filter functions.
filters := make([][]float64, 2)
bpwg.Add(2)
go LowPass(taps, upper, &bpwg, bpch)
go HighPass(taps, lower, &bpwg, bpch)
bpwg.Wait()
for i:=0; i<2; i++ {
filters[i] = <- bpch
}
// Convolve lowpass filter with highpass filter to get bandpass filter.
bpwg.Add(1)
bpFilter := Convolve(filters[0], filters[1], &bpwg)
ch <- bpFilter
wg.Done()
}
// Convolve returns the real convolution of the 2 given arrays.
func Convolve (x, h []float64, wg *sync.WaitGroup) []float64 {
// Create a waitgroup to be used in goroutines called by Convolution
var convwg sync.WaitGroup
// Compute the convolution
convLen := len(x)+len(h)-1
y := make([]float64, convLen)
for n:=0; n<convLen; n++ {
convwg.Add(1)
go func(n int, y []float64, wg *sync.WaitGroup) {
var sum float64 = 0
for k:=0; k<len(x); k++ {
if n-k >= 0 && n-k < len(h) {
sum += x[k]*h[n-k]
}
}
y[n] = sum
wg.Done()
} (n, y, &convwg)
}
wg.Done()
return y
}
// SaveAudioData takes an input signal and filename and saves the frequency spectrum (to .txt) and the audio signal (to .bin).
func SaveAudioData (signal []float64, fileName string, wg1 *sync.WaitGroup) {
// Compute the FFT of the signal
FFTaudio := fft.FFTReal(signal)
// Normalise the frequency response (-1:1).
spectrum := make([]float64, len(signal))
for i := range FFTaudio {
spectrum[i] = cmplx.Abs(FFTaudio[i])/float64(length)
}
maximum := max(spectrum)
for i := range spectrum {
spectrum[i] = spectrum[i]/maximum
}
// Save the frequency response/spectrum.
wg1.Add(2)
go func (fileName string, length int, spectrum []float64, wg1 *sync.WaitGroup) {
file := fileName + ".txt"
f, _ := os.Create(file)
for i:=0; i<len(spectrum)/2; i++ {
fmt.Fprintf(f, "%v\n", 20*math.Log10(spectrum[i]))
}
fmt.Printf("Saved spectrum values to: %s\n", fileName)
wg1.Done()
}(fileName, 44100, spectrum, wg1)
// Save the audio file.
go func (fileName string, signal []float64, wg1 *sync.WaitGroup) {
file := fileName + ".bin"
f, _ := os.Create(file)
var buf [8]byte
for i:=0; i<len(signal); i++ {
binary.LittleEndian.PutUint64(buf[:], math.Float64bits(signal[i]))
_, _ = f.Write(buf[:])
}
fmt.Printf("Saved binary values to: %s\n", fileName)
wg1.Done()
}(fileName, signal, wg1)
wg1.Done()
}

198
codec/pcm/filters.go Normal file
View File

@ -0,0 +1,198 @@
package pcm
import (
"encoding/binary"
"math"
"sync"
"github.com/mjibson/go-dsp/window"
)
// FilterType is the type of filter which can be generated.
type FilterType int
// Currently implemented filter types.
const (
LP FilterType = iota
HP
BP
BS
)
// Filter contains the specifications of the filter, as well as the coefficients to the filter function itself.
type Filter struct {
Coeffs []float64
Type FilterType
SampleRate uint
Lower, Upper float64
Taps int
BuffInfo BufferFormat
}
// AudioFilter interface contains Generate and Apply. Generate is used to generate the coefficients of the filter based off
// the specifications within the Filter struct.
// Apply is used to apply the filter to the given buffer of PCM data (b.Data).
type AudioFilter interface {
Generate()
Apply(b Buffer)
}
// Generate is used to generate the coefficients of the filter function used for convolution with a signal to
// perform specified filter.
func (filter *Filter) Generate() {
// Update the sample rate from the buffer info (if supplied).
if filter.BuffInfo.Rate != 0 {
filter.SampleRate = filter.BuffInfo.Rate
}
// Determine the type of filter to generate (based off filter.Type).
switch filter.Type {
case LP:
// Create a lowpass filter with characteristics from struct.
size := filter.Taps + 1
filter.Coeffs = make([]float64, size, size)
fd := (filter.Upper * 0.7) / float64(filter.SampleRate)
b := (2 * math.Pi) * fd
winData := window.FlatTop(size)
for n := 0; n < (filter.Taps / 2); n++ {
c := float64(n) - float64(filter.Taps)/2
y := math.Sin(c*b) / (math.Pi * c)
filter.Coeffs[n] = (y * winData[n])
filter.Coeffs[size-1-n] = filter.Coeffs[n]
}
filter.Coeffs[filter.Taps/2] = 2 * fd * winData[filter.Taps/2]
case HP:
// Create a Highpass filter with characteristics from struct.
size := filter.Taps + 1
filter.Coeffs = make([]float64, size, size)
fd := (filter.Lower + 3000) / float64(filter.SampleRate)
b := (2 * math.Pi) * fd
winData := window.FlatTop(size)
for n := 0; n < (filter.Taps / 2); n++ {
c := float64(n) - float64(filter.Taps)/2
y := math.Sin(c*b) / (math.Pi * c)
filter.Coeffs[n] = -y * winData[n]
filter.Coeffs[size-1-n] = filter.Coeffs[n]
}
filter.Coeffs[filter.Taps/2] = (1 - 2*fd) * winData[filter.Taps/2]
case BP:
// Make Low and Highpass filters.
lp := Filter{Type: LP, SampleRate: filter.SampleRate, Upper: filter.Upper, Taps: filter.Taps}
hp := Filter{Type: HP, SampleRate: filter.SampleRate, Lower: filter.Lower, Taps: filter.Taps}
lp.Generate()
hp.Generate()
// Convolve lowpass filter with highpass filter to get bandpass filter.
var wg sync.WaitGroup
ch := make(chan []float64)
wg.Add(1)
go Convolve(lp.Coeffs, hp.Coeffs, &wg, ch)
wg.Wait()
filter.Coeffs = <-ch
case BS:
// Make Low and Highpass filters.
lp := Filter{Type: LP, SampleRate: filter.SampleRate, Upper: filter.Lower, Taps: filter.Taps}
hp := Filter{Type: HP, SampleRate: filter.SampleRate, Lower: filter.Upper, Taps: filter.Taps}
lp.Generate()
hp.Generate()
// Add lowpass filter to highpass filter to get bandstop filter.
size := filter.Taps + 1
filter.Coeffs = make([]float64, size, size)
for i := range lp.Coeffs {
filter.Coeffs[i] = lp.Coeffs[i] + hp.Coeffs[i]
}
}
}
// Apply takes in a buffer of PCM audio, applies the filter and returns the filtered audio (in byte slice)
func (filter *Filter) Apply(b Buffer) []byte {
// Convert input to floats.
inputAsFloat := make([]float64, len(b.Data)/2)
temp := make([]byte, 2)
for i := range inputAsFloat {
temp[0] = b.Data[2*i]
temp[1] = b.Data[2*i+1]
inputAsFloat[i] = float64(binary.LittleEndian.Uint16(temp))
if inputAsFloat[i] > 32767 {
inputAsFloat[i] -= 32768 * 2
}
inputAsFloat[i] /= (32767)
}
// Convolve input with filter.
var wg sync.WaitGroup
ch := make(chan []float64, 1)
wg.Add(1)
go Convolve(inputAsFloat, filter.Coeffs, &wg, ch)
wg.Wait()
convolution := <-ch
// Convert convolution output back to bytes.
var output []byte
buf := make([]byte, 2)
for i := range convolution {
convolution[i] = convolution[i] * 32767
if convolution[i] < 0 {
convolution[i] = convolution[i] + 32768*2
}
binary.LittleEndian.PutUint16(buf[:], uint16(convolution[i]))
output = append(output, buf[0], buf[1])
}
return output
}
func Convolve(x, h []float64, wg *sync.WaitGroup, ch chan []float64) {
// Create a waitgroup to be used in goroutines called by Convolution
var convwg sync.WaitGroup
// Compute the convolution
convLen := len(x) + len(h) - 1
y := make([]float64, convLen)
for n := 0; n < convLen; n++ {
convwg.Add(1)
go func(n int, y []float64, convwg *sync.WaitGroup) {
var sum float64 = 0
for k := 0; k < len(x); k++ {
if n-k >= 0 && n-k < len(h) {
sum += x[k] * h[n-k]
} else if n-k < 0 {
break
}
}
y[n] = sum
convwg.Done()
}(n, y, &convwg)
}
ch <- y
close(ch)
wg.Done()
}
// LEGACY FUNCTION FOR TESTING
// Max returns the absolute highest value in a given array.
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
}

View File

@ -73,9 +73,9 @@ func DataSize(rate, channels, bitDepth uint, period float64) int {
// Resample takes Buffer c and resamples the pcm audio data to 'rate' Hz and returns a Buffer with the resampled data. // Resample takes Buffer c and resamples the pcm audio data to 'rate' Hz and returns a Buffer with the resampled data.
// Notes: // Notes:
// - Currently only downsampling is implemented and c's rate must be divisible by 'rate' or an error will occur. // - Currently only downsampling is implemented and c's rate must be divisible by 'rate' or an error will occur.
// - If the number of bytes in c.Data is not divisible by the decimation factor (ratioFrom), the remaining bytes will // - If the number of bytes in c.Data is not divisible by the decimation factor (ratioFrom), the remaining bytes will
// not be included in the result. Eg. input of length 480002 downsampling 6:1 will result in output length 80000. // not be included in the result. Eg. input of length 480002 downsampling 6:1 will result in output length 80000.
func Resample(c Buffer, rate uint) (Buffer, error) { func Resample(c Buffer, rate uint) (Buffer, error) {
if c.Format.Rate == rate { if c.Format.Rate == rate {
return c, nil return c, nil

5
go.mod
View File

@ -12,10 +12,11 @@ require (
github.com/google/go-cmp v0.4.1 github.com/google/go-cmp v0.4.1
github.com/kidoman/embd v0.0.0-20170508013040-d3d8c0c5c68d github.com/kidoman/embd v0.0.0-20170508013040-d3d8c0c5c68d
github.com/mewkiz/flac v1.0.5 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/pkg/errors v0.9.1
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e
gocv.io/x/gocv v0.29.0 gocv.io/x/gocv v0.29.0
gonum.org/v1/gonum v0.9.3 gonum.org/v1/gonum v0.8.2
gonum.org/v1/plot v0.10.0 gonum.org/v1/plot v0.9.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
) )

32
go.sum
View File

@ -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/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/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/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-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 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 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.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/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/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= 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-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 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= 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/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.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-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-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-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/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 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 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/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 h1:dHGW/2kf+/KZ2GGqSVayNEhL9pluKn/rr/h/QqD9Ogc=
github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs= 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/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/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.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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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-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 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 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= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -129,10 +123,8 @@ 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-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-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-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-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/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.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -152,9 +144,8 @@ golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.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.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/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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-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-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -163,20 +154,17 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 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= 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.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.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 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 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.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.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 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/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 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=