├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── Makefile ├── assets ├── songs.txt └── test │ ├── 440.wav │ ├── 440_880.wav │ └── piano.wav ├── cmd └── musig │ ├── cmd │ ├── listen.go │ ├── load.go │ ├── read.go │ ├── record.go │ ├── root.go │ └── spec.go │ └── main.go ├── docs ├── musig.gif └── notes.md ├── go.mod ├── go.sum ├── internal └── pkg │ ├── db │ ├── bolt.go │ ├── bolt_test.go │ ├── db.go │ └── db_test.go │ ├── fingerprint │ ├── fingerprint.go │ ├── fingerprint_test.go │ └── simple.go │ ├── model │ ├── model.go │ ├── model_test.go │ ├── table.go │ └── table_test.go │ ├── pipeline │ └── pipeline.go │ └── sound │ ├── sound.go │ └── wav.go ├── pkg ├── dsp │ ├── dsp.go │ ├── dsp_test.go │ ├── fft.go │ ├── fft_test.go │ ├── filter.go │ ├── filter_test.go │ ├── img.go │ ├── score.go │ ├── spectrogram.go │ ├── spectrogram_test.go │ ├── window.go │ └── window_test.go └── stats │ ├── stats.go │ └── stats_test.go ├── readme.md └── scripts └── dl_dataset.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.12 6 | working_directory: /go/src/github.com/sfluor/musig 7 | steps: 8 | - checkout 9 | - run: 10 | name: Setup Environment Variables 11 | command: echo 'export GO111MODULE=on' >> $BASH_ENV 12 | - run: 13 | name: "--- Install portaudio ---" 14 | command: sudo apt install portaudio19-dev 15 | - run: 16 | name: "--- Testing ---" 17 | command: make test 18 | - run: 19 | name: "--- Building ---" 20 | command: make build 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | bin/* 15 | 16 | # Ignore dataset by default 17 | assets/dataset/* 18 | 19 | .idea/ 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sami Tabet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY = musig 2 | GOARCH = amd64 3 | 4 | COMMIT=$(shell git rev-parse HEAD) 5 | BRANCH=$(shell git rev-parse --abbrev-ref HEAD) 6 | 7 | # Setup the -ldflags option for go build here, interpolate the variable values 8 | LDFLAGS = -ldflags "-X main.VERSION=${BRANCH}:${COMMIT}" 9 | 10 | # Enable go modules 11 | GOCMD = GO111MODULE=on go 12 | 13 | # Build the project 14 | all: build 15 | 16 | .PHONY: build 17 | build: 18 | ${GOCMD} build ${LDFLAGS} -o ./bin/${BINARY} ./cmd/musig/main.go 19 | 20 | .PHONY: linux 21 | linux: 22 | GOOS=linux GOARCH=${GOARCH} ${GOCMD} build ${LDFLAGS} -o ${BINARY}-linux-${GOARCH} . 23 | 24 | .PHONY: macos 25 | macos: 26 | GOOS=darwin GOARCH=${GOARCH} ${GOCMD} build ${LDFLAGS} -o ${BINARY}-macos-${GOARCH} . 27 | 28 | .PHONY: windows 29 | windows: 30 | GOOS=windows GOARCH=${GOARCH} ${GOCMD} build ${LDFLAGS} -o ${BINARY}-windows-${GOARCH}.exe . 31 | 32 | cross: linux macos windows 33 | 34 | .PHONY: test 35 | test: 36 | ${GOCMD} get -v ./...; \ 37 | ${GOCMD} vet $$(go list ./... | grep -v /vendor/); \ 38 | ${GOCMD} test -v -race ./...; \ 39 | 40 | .PHONY: fmt 41 | fmt: 42 | ${GOCMD} fmt $$(go list ./... | grep -v /vendor/) 43 | 44 | .PHONY: tidy 45 | tidy: 46 | ${GOCMD} mod tidy 47 | 48 | .PHONY: download 49 | download: 50 | ./scripts/dl_dataset.sh 51 | -------------------------------------------------------------------------------- /assets/songs.txt: -------------------------------------------------------------------------------- 1 | https://www.dropbox.com/s/whmvyauu31wey2v/Summer%20Was%20Fun%20%26%20Laura%20Brehm%20-%20Prism%20%5BNCS%20Release%5D.mp3 2 | https://www.dropbox.com/s/htvwk8myjtreira/BEAUZ%20%26%20Momo%20-%20Won%27t%20Look%20Back%20%5BNCS%20Release%5D.mp3 3 | https://www.dropbox.com/s/t310jf1v9k1qhwo/Maduk%20-%20Go%20Home%20%28Original%20Mix%29%20%5BNCS%20Release%5D.mp3 4 | https://www.dropbox.com/s/8c6cyjr7tbzqwcm/EMDI%20x%20R%C3%98GUENETHVN%20-%20Let%20Your%20Heartbreak%20%28feat.%20Leo%20the%20Kind%29%20%5BNCS%20Release%5D.mp3 5 | https://www.dropbox.com/s/d2hl8mbaca5kjmo/Unknown%20Brain%20-%20Unknown%20Brain%20-%20Perfect%2010%20%28Unknown%20Brain%20%26%20Rudelies%20VIP%29%20%5BRadio%20Edit.%5D%20%5BNCS%20Release%5D.mp3 6 | -------------------------------------------------------------------------------- /assets/test/440.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfluor/musig/f445fa128b826c1cee527afdcc97e3e1e8433c32/assets/test/440.wav -------------------------------------------------------------------------------- /assets/test/440_880.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfluor/musig/f445fa128b826c1cee527afdcc97e3e1e8433c32/assets/test/440_880.wav -------------------------------------------------------------------------------- /assets/test/piano.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfluor/musig/f445fa128b826c1cee527afdcc97e3e1e8433c32/assets/test/piano.wav -------------------------------------------------------------------------------- /cmd/musig/cmd/listen.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "os" 6 | "path" 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | rootCmd.AddCommand(listenCmd) 14 | listenCmd.Flags().DurationP("duration", "d", 10*time.Second, "duration of the listening") 15 | } 16 | 17 | // listenCmd represents the listen command 18 | var listenCmd = &cobra.Command{ 19 | Use: "listen", 20 | Short: "listen will record the microphone input and try to find a matching song from the database (Ctrl-C will stop the recording)", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | name := path.Join(os.TempDir(), "musig_record.wav") 23 | dur, err := cmd.Flags().GetDuration("duration") 24 | failIff(err, "could not get duration, got: %v", dur) 25 | 26 | defer func() { 27 | if err := os.Remove(name); err != nil { 28 | log.Errorf("Failed to remove temporary file stored at %s used to record the sample: %s", name , err) 29 | } 30 | }() 31 | 32 | recordAudioToFile(name, dur) 33 | cmdRead(name) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /cmd/musig/cmd/load.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/sfluor/musig/internal/pkg/pipeline" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | rootCmd.AddCommand(loadCmd) 14 | loadCmd.Flags().BoolP("dry-run", "d", false, "disable saving to the database") 15 | loadCmd.Flags().BoolP("reset", "r", false, "reset the database if it already exists") 16 | loadCmd.Flags().BoolP("verbose", "v", false, "enable verbose output") 17 | } 18 | 19 | // loadCmd represents the load command 20 | var loadCmd = &cobra.Command{ 21 | Use: "load [glob]", 22 | Short: "Load loads all the audio files matching the provided glob into the database (TODO: only .wav are supported for now)", 23 | Args: cobra.ExactArgs(1), 24 | Run: func(cmd *cobra.Command, args []string) { 25 | files, err := filepath.Glob(args[0]) 26 | failIff(err, "error finding files") 27 | 28 | if files == nil { 29 | log.Infof("no files matched pattern: %s", args[0]) 30 | os.Exit(0) 31 | } 32 | 33 | resetDB, err := cmd.Flags().GetBool("reset") 34 | if resetDB && err == nil { 35 | log.Info("removing the existing database...") 36 | if err := os.Remove(dbFile); err != nil { 37 | log.Errorf("Error removing the database at %s: %s", dbFile, err) 38 | } 39 | } 40 | 41 | p, err := pipeline.NewDefaultPipeline(dbFile) 42 | failIff(err, "error creating pipeline") 43 | defer p.Close() 44 | 45 | process := p.ProcessAndStore 46 | 47 | dryRun, err := cmd.Flags().GetBool("dry-run") 48 | if dryRun && err == nil { 49 | log.Info("enabling dry-run (results won't be saved to the database)") 50 | process = p.Process 51 | } 52 | 53 | verbose, err := cmd.Flags().GetBool("verbose") 54 | verbose = verbose && err == nil 55 | 56 | for _, file := range files { 57 | res, err := process(file) 58 | failIff(err, "error processing file %s", file) 59 | if verbose { 60 | log.Infof("Processed file at %s and got: %+v", file, res.Fingerprint) 61 | } 62 | } 63 | }, 64 | } 65 | -------------------------------------------------------------------------------- /cmd/musig/cmd/read.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sfluor/musig/internal/pkg/model" 6 | "github.com/sfluor/musig/internal/pkg/pipeline" 7 | "github.com/sfluor/musig/pkg/dsp" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func init() { 12 | rootCmd.AddCommand(readCmd) 13 | } 14 | 15 | // readCmd represents the read command 16 | var readCmd = &cobra.Command{ 17 | Use: "read [file]", 18 | Short: "Read reads the given audio file trying to find it's song name", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | cmdRead(args[0]) 21 | }, 22 | } 23 | 24 | func cmdRead(file string) { 25 | p, err := pipeline.NewDefaultPipeline(dbFile) 26 | failIff(err, "error creating pipeline") 27 | defer p.Close() 28 | 29 | res, err := p.Process(file) 30 | failIff(err, "error processing file %s", file) 31 | 32 | // Will hold a count of songID => occurrences 33 | keys := make([]model.EncodedKey, 0, len(res.Fingerprint)) 34 | sample := map[model.EncodedKey]model.TableValue{} 35 | // songID => points that matched 36 | matches := map[uint32]map[model.EncodedKey]model.TableValue{} 37 | 38 | for k, v := range res.Fingerprint { 39 | keys = append(keys, k) 40 | sample[k] = v 41 | } 42 | 43 | m, err := p.DB.Get(keys) 44 | for key, values := range m { 45 | for _, val := range values { 46 | 47 | if _, ok := matches[val.SongID]; !ok { 48 | matches[val.SongID] = map[model.EncodedKey]model.TableValue{} 49 | } 50 | 51 | matches[val.SongID][key] = val 52 | } 53 | } 54 | 55 | // songID => correlation 56 | scores := map[uint32]float64{} 57 | for songID, points := range matches { 58 | scores[songID] = dsp.MatchScore(sample, points) 59 | } 60 | 61 | var song string 62 | var max float64 63 | fmt.Println("Matches:") 64 | for id, score := range scores { 65 | name, err := p.DB.GetSong(id) 66 | failIff(err, "error getting song id: %d", id) 67 | fmt.Printf("\t- %s, score: %f\n", name, score) 68 | if score > max { 69 | song, max = name, score 70 | } 71 | } 72 | 73 | fmt.Println("---") 74 | fmt.Printf("Song is: %s (score: %f)\n", song, max) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/musig/cmd/record.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "time" 7 | 8 | "github.com/sfluor/musig/internal/pkg/sound" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | rootCmd.AddCommand(recordCmd) 14 | recordCmd.Flags().DurationP("duration", "d", 10*time.Second, "duration of the listening") 15 | } 16 | 17 | // recordCmd represents the listen command 18 | var recordCmd = &cobra.Command{ 19 | Use: "record", 20 | Short: "record will record the microphone input and save the signal to the given file", 21 | Args: cobra.MinimumNArgs(1), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | dur, err := cmd.Flags().GetDuration("duration") 24 | failIff(err, "could not get duration, got: %v", dur) 25 | 26 | recordAudioToFile(args[0], dur) 27 | }, 28 | } 29 | 30 | func recordAudioToFile(name string, duration time.Duration) { 31 | file, err := os.Create(name) 32 | failIff(err, "error creating file for recording in %s", name) 33 | 34 | stopCh := make(chan struct{}, 1) 35 | sig := make(chan os.Signal, 1) 36 | signal.Notify(sig, os.Interrupt, os.Kill) 37 | 38 | go func() { 39 | defer func() { 40 | stopCh <- struct{}{} 41 | }() 42 | for { 43 | select { 44 | case <-time.After(duration): 45 | return 46 | case <-sig: 47 | return 48 | } 49 | } 50 | }() 51 | 52 | err = sound.RecordWAV(file, stopCh) 53 | failIff(err, "an error occurred recording WAV file") 54 | failIff(file.Sync(), "error syncing temp file") 55 | } 56 | -------------------------------------------------------------------------------- /cmd/musig/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var dbFile string 11 | 12 | // rootCmd represents the base command when called without any subcommands 13 | var rootCmd = &cobra.Command{ 14 | Use: "musig", 15 | Short: "A shazam like CLI tool", 16 | } 17 | 18 | // Execute adds all child commands to the root command and sets flags appropriately. 19 | func Execute(version string) { 20 | rootCmd.Version = version 21 | if err := rootCmd.Execute(); err != nil { 22 | fmt.Fprint(os.Stderr, err) 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | func init() { 28 | rootCmd.PersistentFlags().StringVar(&dbFile, "database", "/tmp/musig.bolt", "database file to use") 29 | } 30 | 31 | func failIff(err error, msg string, args ...interface{}) { 32 | if err != nil { 33 | fmt.Fprintf(os.Stderr, msg, args...) 34 | os.Exit(1) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cmd/musig/cmd/spec.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "image/png" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/sfluor/musig/internal/pkg/model" 11 | "github.com/sfluor/musig/pkg/dsp" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func init() { 16 | rootCmd.AddCommand(specCmd) 17 | } 18 | 19 | // specCmd represents the spectrogram command 20 | var specCmd = &cobra.Command{ 21 | Use: "spectrogram [audio_file] [output_img]", 22 | Short: "spectrogram generate a spectrogram image for the given audio file in png (TODO: only .wav are supported for now)", 23 | Args: cobra.ExactArgs(2), 24 | Run: func(cmd *cobra.Command, args []string) { 25 | err := genSpectrogram(args[0], args[1]) 26 | failIff(err, "error generating spectrogram") 27 | }, 28 | } 29 | 30 | // genSpectrogram generates a spectrogram for the given file 31 | func genSpectrogram(path string, imgPath string) error { 32 | log.Infof("reading %s to save it's spectrogram to: %s", path, imgPath) 33 | file, err := os.Open(path) 34 | if err != nil { 35 | return errors.Wrapf(err, "error opening file at %s", path) 36 | } 37 | defer file.Close() 38 | 39 | s := dsp.NewSpectrogrammer(model.DownsampleRatio, model.MaxFreq, model.SampleSize, true) 40 | 41 | spec, _, err := s.Spectrogram(file) 42 | if err != nil { 43 | return errors.Wrap(err, "error generating spectrogram") 44 | } 45 | 46 | img := dsp.SpecToImg(spec) 47 | 48 | f, err := os.Create(imgPath) 49 | if err != nil { 50 | return errors.Wrapf(err, "error creating image file at %s", imgPath) 51 | } 52 | defer f.Close() 53 | 54 | if err := png.Encode(f, img); err != nil { 55 | return errors.Wrap(err, "error encoding image file") 56 | } 57 | log.Infof("successfully saved spectrogram to file: %s", imgPath) 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /cmd/musig/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/sfluor/musig/cmd/musig/cmd" 4 | 5 | var VERSION string 6 | 7 | func main() { 8 | cmd.Execute(VERSION) 9 | } 10 | -------------------------------------------------------------------------------- /docs/musig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfluor/musig/f445fa128b826c1cee527afdcc97e3e1e8433c32/docs/musig.gif -------------------------------------------------------------------------------- /docs/notes.md: -------------------------------------------------------------------------------- 1 | # Analog -> Digital 2 | 3 | - Remove frequencies over 20khz before sampling to avoid aliasing (otherwise those frequencies will end up between 0Hz and 20khz) 4 | 5 | # Mp3 -> headphones 6 | 7 | - Uses PCM (Pulse coded modulation), a PCM stream is a stream of bits that can be composed of multiple channels, for instance stereo has 2 channels. 8 | 9 | - In a stream the amplitude of the signal is divided into samples (a 44,1khz sampled music will have 44100 samples per second), each sample gives the (quantized) amplitude of the sound for the corresponding time interval. 10 | 11 | # How to get frequencies ? 12 | 13 | - Apply an FFT on small subsets of the original signal (0.1 s intervals for instance). This will give us the amplitude and frequency over a time period 14 | 15 | # Resampling 16 | 17 | - Resampling a song allows to process the song faster, if the song was sampled at a 44,1khz rate (and we were doing a 4096-sample window) and we resample it with a 11.khz rate, we will have frequencies from 0 to 5khz only but this would allow us to have the same frequency resolution when doing a 1024-sample window FFT. 18 | 19 | - Having frequencies only from 0 to 5kz is not really an issue with songs since the most important frequencies for musics are within this range. 20 | 21 | - An easy way of doing resampling is by doing an average of the signal, (for example 4 samples averaged) 22 | 23 | - However we also have to be careful about aliasing, we have to apply a low pass filter to avoid it since we now have frequencies from 0kz to 5khz. 24 | 25 | # Stereo to mono 26 | 27 | We also have to transform stereo to mono 28 | 29 | # Some sump up 30 | 31 | We can have a spectrogram: 32 | 33 | - From 0Hz to 5kHz 34 | - With a bin size of 10.7 Hz (11 025 / 1024) 35 | 36 | [comment]: # 'TODO figure out this' 37 | 38 | - 512 possible frequencies (here a "frequency" is in fact a bin, this comes from the DFT, try to understand this) 39 | - And a unit of time of 0.1 second 40 | 41 | # Filtering 42 | 43 | - Since we want to be noise tolerant we have to keep only the loudest notes, but we can't just do that on every 0.1 second bin: 44 | - - Since human ears have more difficulties to hear some low freqs, their amplitude is sometimes increased , if you only take the most powerful frequencies, you will end up with only the low ones 45 | - - Spectral leakage over a powerful frequency will create other powerful frequencies 46 | 47 | The solution: 48 | 49 | - For each FFT result we put the 512 frequency bins inside 6 logarithmic bands (we could use another number of bands here): 50 | - - [0, 10] 51 | - - [10, 20] 52 | - - [20, 40] 53 | - - [40, 80] 54 | - - [80, 160] 55 | - - [160, 511] 56 | 57 | - For each band we keep the strongest bin of frequencies 58 | - Compute the average value of these 6 maximums 59 | - Keep only the bins above this mean (multiplied by a coefficient) 60 | - This can have some caveats though (search `this algorithm has a limitation` [here](http://coding-geek.com/how-shazam-works/)), also search `a simple algorithm to filter the spectrogram` for another way of doing 61 | 62 | - We then end up with a spectrogram but without the amplitude, only points (time, frequency) 63 | 64 | --- 65 | 66 | # Hashing 67 | 68 | ## Using anchor points and target zones 69 | 70 | ### Fingerprinting 71 | 72 | Given a spectrogram like [this one](https://cdn-images-1.medium.com/max/1600/0*Y-24_LZlSLWzMSaI.) we can use anchor points and target zones to identify parts of the song when trying to match a song extract. 73 | 74 | We store each point in a target zone by hashing: 75 | 76 | - The frequency of the anchor for this target zone 77 | - The frequency of the point 78 | - Time between the anchor and the point 79 | 80 | Each hash is then represented by a _uint32_ and an additional time offset from the beginning of the song to the anchor point. 81 | 82 | To increase the accuracy of the matching process we can increase the number of points in the target zones, however this will also increase the storage and complexity of the fingerprinting part. 83 | If we have `N` points and a target zones of size `T` we will have roughly `N*T` hashes to compute and to store 84 | 85 | ### Searching 86 | 87 | --- 88 | 89 | # Docs 90 | 91 | - http://coding-geek.com/how-shazam-works/ 92 | - https://medium.com/@treycoopermusic/how-shazam-works-d97135fb4582 93 | - http://hpac.rwth-aachen.de/teaching/sem-mus-17/Reports/Froitzheim.pdf 94 | - https://royvanrijn.com/blog/2010/06/creating-shazam-in-java/ 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sfluor/musig 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/gordonklaus/portaudio v0.0.0-20180817120803-00e7307ccd93 7 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 8 | github.com/pkg/errors v0.9.1 9 | github.com/sirupsen/logrus v1.4.2 10 | github.com/spf13/cobra v0.0.6 11 | github.com/spf13/pflag v1.0.5 // indirect 12 | github.com/stretchr/testify v1.3.0 13 | github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb 14 | github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320 15 | go.etcd.io/bbolt v1.3.3 16 | golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c // indirect 17 | gonum.org/v1/gonum v0.0.0-20200222091724-332e2c454720 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 13 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 14 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 15 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 16 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 22 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 23 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 26 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 27 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 28 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 29 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 30 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 31 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 32 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 33 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 34 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 35 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 39 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 40 | github.com/gordonklaus/portaudio v0.0.0-20180817120803-00e7307ccd93 h1:TSG+DyZBnazM22ZHyHLeUkzM34ClkJRjIWHTq4btvek= 41 | github.com/gordonklaus/portaudio v0.0.0-20180817120803-00e7307ccd93/go.mod h1:HfYnZi/ARQKG0dwH5HNDmPCHdLiFiBf+SI7DbhW7et4= 42 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 43 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 44 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 45 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 46 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 47 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 48 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 49 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 50 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 51 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 52 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 53 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 54 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 55 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 56 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 57 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 58 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 59 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 60 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 61 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 62 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 63 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 64 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 65 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 66 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 67 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 68 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 69 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 70 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 71 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 73 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 74 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 75 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 76 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 77 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 78 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 79 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 80 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 81 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 82 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 83 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 84 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 85 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 86 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 87 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 88 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 89 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 90 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 91 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 92 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 93 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 94 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 95 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 96 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 97 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 98 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 99 | github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= 100 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 101 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 102 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 103 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 104 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 105 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 106 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 107 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 108 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 109 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 110 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 111 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 112 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 113 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 114 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 115 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 116 | github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb h1:RDh7U5Di6o7fblIBe7rVi9KnrcOXUbLwvvLLdP2InSI= 117 | github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ= 118 | github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320 h1:YuM+QcBOD3fVTF1p5upTKvfAn+CaI1Tsh2s0dxdEzhI= 119 | github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320/go.mod h1:Zf+Ju+8Ofy5zx/YWWArfcGnl5FAsWumLq/uHeRGgL60= 120 | go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= 121 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 122 | go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= 123 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 124 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 125 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 126 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 127 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 128 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 129 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 130 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 131 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU= 132 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 133 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 134 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 135 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 136 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 137 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 138 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 139 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 140 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 141 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 142 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 143 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 144 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 145 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= 147 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 148 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 149 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 150 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= 151 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 152 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 153 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 154 | golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ= 155 | golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 156 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 157 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 158 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 159 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 160 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 161 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 162 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 163 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 164 | gonum.org/v1/gonum v0.0.0-20190430210020-9827ae2933ff h1:PSmLTFCI0KBBLcaxSbM8ejKR6f7XuDyQS3R8t72ailE= 165 | gonum.org/v1/gonum v0.0.0-20190430210020-9827ae2933ff/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= 166 | gonum.org/v1/gonum v0.0.0-20200222091724-332e2c454720 h1:2+QXe2PEYd9J/CSAOhNofFhUoyK2XKWsfgmuQxxi6pw= 167 | gonum.org/v1/gonum v0.0.0-20200222091724-332e2c454720/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= 168 | gonum.org/v1/gonum v0.6.2 h1:4r+yNT0+8SWcOkXP+63H2zQbN+USnC73cjGUxnDF94Q= 169 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= 170 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 171 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 172 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 173 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 174 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 175 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 176 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 177 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 178 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 179 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 180 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 181 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 182 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 183 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 184 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 185 | -------------------------------------------------------------------------------- /internal/pkg/db/bolt.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/sfluor/musig/internal/pkg/model" 9 | "go.etcd.io/bbolt" 10 | ) 11 | 12 | var _ Database = &BoltDB{} 13 | 14 | // BoltDB implements the Database interface using a bolt database 15 | type BoltDB struct { 16 | *bbolt.DB 17 | fingerprintBucket []byte 18 | songBucket []byte 19 | } 20 | 21 | // NewBoltDB returns a new bolt database 22 | func NewBoltDB(path string) (*BoltDB, error) { 23 | db, err := bbolt.Open(path, 0666, nil) 24 | if err != nil { 25 | return nil, errors.Wrapf(err, "error creating bolt db with path:%s", path) 26 | } 27 | 28 | boltDB := &BoltDB{ 29 | DB: db, 30 | fingerprintBucket: []byte("fingerprint"), 31 | songBucket: []byte("song"), 32 | } 33 | 34 | // Create buckets 35 | err = db.Update(func(tx *bbolt.Tx) error { 36 | if _, err := tx.CreateBucketIfNotExists(boltDB.fingerprintBucket); err != nil { 37 | return err 38 | } 39 | if _, err := tx.CreateBucketIfNotExists(boltDB.songBucket); err != nil { 40 | return err 41 | } 42 | return nil 43 | }) 44 | 45 | return boltDB, errors.Wrap(err, "error creating buckets") 46 | } 47 | 48 | // Get retrieves the provided keys' values from the bolt file 49 | func (db *BoltDB) Get(keys []model.EncodedKey) (map[model.EncodedKey][]model.TableValue, error) { 50 | res := make(map[model.EncodedKey][]model.TableValue, len(keys)) 51 | 52 | err := db.View(func(tx *bbolt.Tx) error { 53 | b := tx.Bucket(db.fingerprintBucket) 54 | 55 | for _, k := range keys { 56 | raw := b.Get(k.Bytes()) 57 | if len(raw) == 0 { 58 | continue 59 | } 60 | val, err := model.ValuesFromBytes(b.Get(k.Bytes())) 61 | if err != nil { 62 | return errors.Wrapf(err, "wrong record stored: %v", raw) 63 | } 64 | 65 | res[k] = val 66 | } 67 | 68 | return nil 69 | }) 70 | 71 | return res, errors.Wrap(err, "an error occurred when reading from bolt") 72 | } 73 | 74 | // Set stores the list of (key, value) into the bolt file 75 | func (db *BoltDB) Set(batch map[model.EncodedKey]model.TableValue) error { 76 | err := db.Update(func(tx *bbolt.Tx) error { 77 | b := tx.Bucket(db.fingerprintBucket) 78 | 79 | for k, v := range batch { 80 | // We append the value to the existing array 81 | // this is because multiple songs can have the same keys 82 | // TODO: check if the value is not already in the database array to avoid having a same value multiple times 83 | rawKey := k.Bytes() 84 | existing := b.Get(rawKey) 85 | 86 | if err := b.Put(rawKey, append(existing, v.Bytes()...)); err != nil { 87 | return errors.Wrapf(err, "error setting (key: %v, val: %v)", k, v) 88 | } 89 | } 90 | return nil 91 | }) 92 | 93 | return errors.Wrap(err, "an error occurred when writing to bolt") 94 | } 95 | 96 | // GetSongID does a song name => songID lookup in the database 97 | func (db *BoltDB) GetSongID(name string) (uint32, error) { 98 | var id uint32 99 | 100 | err := db.View(func(tx *bbolt.Tx) error { 101 | b := tx.Bucket(db.songBucket) 102 | c := b.Cursor() 103 | 104 | // TODO: fixme this is not really efficient 105 | // Iterate over all the song records 106 | for k, v := c.First(); k != nil; k, v = c.Next() { 107 | if string(v) == name { 108 | id = binary.LittleEndian.Uint32(k) 109 | return nil 110 | } 111 | } 112 | 113 | return fmt.Errorf("could not find id for song name: %s", name) 114 | }) 115 | 116 | return id, errors.Wrap(err, "an error occurred when reading from bolt") 117 | } 118 | 119 | // GetSong does a songID => song name lookup in the database 120 | func (db *BoltDB) GetSong(songID uint32) (string, error) { 121 | var name string 122 | 123 | err := db.View(func(tx *bbolt.Tx) error { 124 | b := tx.Bucket(db.songBucket) 125 | 126 | raw := b.Get(itob(songID)) 127 | if len(raw) == 0 { 128 | return fmt.Errorf("got empty song name") 129 | } 130 | 131 | name = string(raw) 132 | 133 | return nil 134 | }) 135 | 136 | return name, errors.Wrap(err, "an error occurred when reading from bolt") 137 | } 138 | 139 | // SetSong stores a song name in the database and returns it's song ID 140 | // It first checks if the song is not already stored in the database 141 | func (db *BoltDB) SetSong(song string) (uint32, error) { 142 | // First check if we haven't the song in the db already 143 | id, err := db.GetSongID(song) 144 | if err == nil { 145 | return id, nil 146 | } 147 | 148 | var songID uint32 149 | err = db.Update(func(tx *bbolt.Tx) error { 150 | b, err := tx.CreateBucketIfNotExists(db.songBucket) 151 | if err != nil { 152 | return errors.Wrapf(err, "error creating bucket") 153 | } 154 | 155 | id, err := b.NextSequence() 156 | if err != nil { 157 | return errors.Wrap(err, "error getting next sequence") 158 | } 159 | 160 | songID = uint32(id) 161 | rawKey := itob(songID) 162 | 163 | return errors.Wrap(b.Put(rawKey, []byte(song)), "error setting song") 164 | }) 165 | 166 | return songID, errors.Wrap(err, "an error occurred when writing to bolt") 167 | } 168 | 169 | func itob(s uint32) []byte { 170 | raw := make([]byte, 4) 171 | binary.LittleEndian.PutUint32(raw, s) 172 | return raw 173 | } 174 | -------------------------------------------------------------------------------- /internal/pkg/db/bolt_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestBoltDB(t *testing.T) { 11 | testFile := "/tmp/test.db" 12 | db, err := NewBoltDB(testFile) 13 | require.NoError(t, err) 14 | 15 | testDatabase(t, db) 16 | 17 | err = os.Remove(testFile) 18 | require.NoError(t, err) 19 | } 20 | -------------------------------------------------------------------------------- /internal/pkg/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "github.com/sfluor/musig/internal/pkg/model" 4 | 5 | // Database is an interface for storing fingerprint parts in a database and retrieving them 6 | type Database interface { 7 | Get([]model.EncodedKey) (map[model.EncodedKey][]model.TableValue, error) 8 | Set(map[model.EncodedKey]model.TableValue) error 9 | 10 | GetSongID(name string) (songID uint32, err error) 11 | GetSong(songID uint32) (name string, err error) 12 | SetSong(name string) (songID uint32, err error) 13 | 14 | Close() error 15 | } 16 | -------------------------------------------------------------------------------- /internal/pkg/db/db_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sfluor/musig/internal/pkg/model" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type testTuple struct { 12 | key model.EncodedKey 13 | val model.TableValue 14 | } 15 | 16 | var testValues = []testTuple{ 17 | {key: model.EncodedKey(1), val: model.TableValue{AnchorTimeMs: 1500, SongID: 100}}, 18 | {key: model.EncodedKey(2), val: model.TableValue{AnchorTimeMs: 15, SongID: 42}}, 19 | {key: model.EncodedKey(100), val: model.TableValue{AnchorTimeMs: 66, SongID: 999}}, 20 | {key: model.EncodedKey(200), val: model.TableValue{AnchorTimeMs: 72, SongID: 100}}, 21 | {key: model.EncodedKey(1000), val: model.TableValue{AnchorTimeMs: 1500, SongID: 999}}, 22 | {key: model.EncodedKey(2000), val: model.TableValue{AnchorTimeMs: 65, SongID: 77}}, 23 | {key: model.EncodedKey(30000), val: model.TableValue{AnchorTimeMs: 190, SongID: 100}}, 24 | {key: model.EncodedKey(50000), val: model.TableValue{AnchorTimeMs: 38, SongID: 100}}, 25 | {key: model.EncodedKey(428298445), val: model.TableValue{AnchorTimeMs: 3, SongID: 10}}, 26 | } 27 | 28 | func testDatabase(t *testing.T, db Database) { 29 | // Close the database 30 | defer func() { require.NoError(t, db.Close()) }() 31 | 32 | t.Run("fingerprints", func(t *testing.T) { 33 | // Should return nothing without error 34 | res, err := db.Get(nil) 35 | require.NoError(t, err) 36 | assert.Len(t, res, 0) 37 | 38 | m1 := genTestMap(testValues[:4]) 39 | err = db.Set(m1) 40 | require.NoError(t, err) 41 | 42 | keys := []model.EncodedKey{} 43 | for k := range m1 { 44 | keys = append(keys, k) 45 | } 46 | 47 | resMap, err := db.Get(keys) 48 | require.NoError(t, err) 49 | assert.Len(t, resMap, len(keys)) 50 | for k, v := range m1 { 51 | assert.ElementsMatch(t, []model.TableValue{v}, resMap[k]) 52 | } 53 | 54 | m2 := genTestMap(testValues[4:]) 55 | err = db.Set(m2) 56 | require.NoError(t, err) 57 | 58 | keys = []model.EncodedKey{} 59 | for k := range m2 { 60 | keys = append(keys, k) 61 | } 62 | 63 | resMap, err = db.Get(keys) 64 | require.NoError(t, err) 65 | assert.Len(t, resMap, len(keys)) 66 | for k, v := range m2 { 67 | assert.ElementsMatch(t, []model.TableValue{v}, resMap[k]) 68 | } 69 | }) 70 | 71 | t.Run("song_id_retrieval", func(t *testing.T) { 72 | song1 := "my song !" 73 | song2 := "my second song !" 74 | 75 | id1, err := db.SetSong(song1) 76 | require.NoError(t, err) 77 | 78 | // Try setting the id again 79 | id1Dup, err := db.SetSong(song1) 80 | require.NoError(t, err) 81 | 82 | // They should be equal 83 | assert.Equal(t, id1, id1Dup) 84 | 85 | id2, err := db.SetSong(song2) 86 | require.NoError(t, err) 87 | 88 | assert.NotEqual(t, id1, id2) 89 | 90 | id2Dup, err := db.SetSong(song2) 91 | require.NoError(t, err) 92 | 93 | assert.Equal(t, id2, id2Dup) 94 | 95 | // Double check the names 96 | name1, err := db.GetSong(id1) 97 | require.NoError(t, err) 98 | assert.Equal(t, song1, name1) 99 | 100 | name2, err := db.GetSong(id2) 101 | require.NoError(t, err) 102 | assert.Equal(t, song2, name2) 103 | 104 | // And the IDs 105 | songID1, err := db.GetSongID(song1) 106 | require.NoError(t, err) 107 | assert.Equal(t, id1, songID1) 108 | 109 | songID2, err := db.GetSongID(song2) 110 | require.NoError(t, err) 111 | assert.Equal(t, id2, songID2) 112 | }) 113 | 114 | t.Run("song_names", func(t *testing.T) { 115 | song1 := "my song !" 116 | song2 := "my second song !" 117 | 118 | name, err := db.GetSong(10) 119 | require.Error(t, err) 120 | assert.Empty(t, name) 121 | 122 | id, err := db.SetSong(song1) 123 | require.NoError(t, err) 124 | assert.True(t, id != 0) 125 | 126 | id2, err := db.SetSong(song2) 127 | require.NoError(t, err) 128 | assert.True(t, id2 != 0) 129 | 130 | name, err = db.GetSong(id2) 131 | require.NoError(t, err) 132 | assert.Equal(t, song2, name) 133 | }) 134 | } 135 | 136 | func genTestMap(tuples []testTuple) map[model.EncodedKey]model.TableValue { 137 | m := make(map[model.EncodedKey]model.TableValue, len(tuples)) 138 | for _, t := range tuples { 139 | m[t.key] = t.val 140 | } 141 | return m 142 | } 143 | -------------------------------------------------------------------------------- /internal/pkg/fingerprint/fingerprint.go: -------------------------------------------------------------------------------- 1 | package fingerprint 2 | 3 | import "github.com/sfluor/musig/internal/pkg/model" 4 | 5 | type Fingerprinter interface { 6 | Fingerprint(uint32, []model.ConstellationPoint) map[model.EncodedKey]model.TableValue 7 | } 8 | -------------------------------------------------------------------------------- /internal/pkg/fingerprint/fingerprint_test.go: -------------------------------------------------------------------------------- 1 | package fingerprint 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/sfluor/musig/internal/pkg/model" 9 | "github.com/sfluor/musig/pkg/dsp" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const AssetsDir = "../../../assets/test" 15 | 16 | func TestFingerprinting440(t *testing.T) { 17 | testFingerprintingOnFile(t, path.Join(AssetsDir, "440.wav")) 18 | } 19 | 20 | func TestFingerprinting440And880(t *testing.T) { 21 | testFingerprintingOnFile(t, path.Join(AssetsDir, "440_880.wav")) 22 | } 23 | 24 | func testFingerprintingOnFile(t *testing.T, path string) { 25 | sampleSize := model.SampleSize 26 | 27 | s := dsp.NewSpectrogrammer( 28 | model.DownsampleRatio, 29 | model.MaxFreq, 30 | sampleSize, 31 | true, 32 | ) 33 | 34 | file, err := os.Open(path) 35 | require.NoError(t, err) 36 | defer file.Close() 37 | 38 | spec, spr, err := s.Spectrogram(file) 39 | require.NoError(t, err) 40 | 41 | f := NewDefaultFingerprinter() 42 | 43 | cMap := s.ConstellationMap(spec, spr) 44 | 45 | // Apply a second constellation map only on a sub part of the file 46 | subSpec := spec[40:60] 47 | subCMap := s.ConstellationMap(subSpec, spr) 48 | 49 | table := f.Fingerprint(0, cMap) 50 | subTable := f.Fingerprint(0, subCMap) 51 | 52 | for key := range subTable { 53 | assert.Contains(t, table, key) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/pkg/fingerprint/simple.go: -------------------------------------------------------------------------------- 1 | package fingerprint 2 | 3 | import "github.com/sfluor/musig/internal/pkg/model" 4 | 5 | var _ Fingerprinter = &SimpleFingerprinter{} 6 | 7 | // SimpleFingerprinter is a fingerprinter that uses a target zone and an anchor offset 8 | type SimpleFingerprinter struct { 9 | anchorOffset int 10 | targetZoneSize int 11 | lastOffset int 12 | } 13 | 14 | // NewDefaultFingerprinter returns a new fingerprinter using the simple strategy 15 | // using an anchor offset of 2 and target zone size of 5 16 | func NewDefaultFingerprinter() *SimpleFingerprinter { 17 | return NewSimpleFingerprinter(2, 5) 18 | } 19 | 20 | // NewSimpleFingerprinter returns a new simple fingerprinter 21 | func NewSimpleFingerprinter(anchorOffset, targetZoneSize int) *SimpleFingerprinter { 22 | return &SimpleFingerprinter{ 23 | anchorOffset: anchorOffset, 24 | targetZoneSize: targetZoneSize, 25 | lastOffset: anchorOffset + targetZoneSize, 26 | } 27 | } 28 | 29 | // Fingerprint builds a fingerprint from the given constellation map 30 | func (sf *SimpleFingerprinter) Fingerprint(songID uint32, cMap []model.ConstellationPoint) map[model.EncodedKey]model.TableValue { 31 | length := len(cMap) 32 | res := map[model.EncodedKey]model.TableValue{} 33 | 34 | for i := 0; i+sf.lastOffset < length; i++ { 35 | anchor := cMap[i] 36 | for _, p := range cMap[i+sf.anchorOffset : i+sf.lastOffset] { 37 | res[model.NewAnchorKey(anchor, p).Encode()] = *model.NewTableValue(songID, anchor) 38 | } 39 | } 40 | 41 | return res 42 | } 43 | -------------------------------------------------------------------------------- /internal/pkg/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // DownsampleRatio is the default down sample ratio (4) 10 | const DownsampleRatio = 4 11 | 12 | // SampleSize is the default sample size (1024) 13 | const SampleSize = 1024.0 14 | 15 | // MaxFreq is 5kHz 16 | const MaxFreq = 5000.0 17 | 18 | // ConstellationPoint represents a point in the constellation map (time + frequency) 19 | type ConstellationPoint struct { 20 | Time float64 21 | Freq float64 22 | } 23 | 24 | func (cp ConstellationPoint) String() string { 25 | return fmt.Sprintf("(t: %f, f: %f)", cp.Time, cp.Freq) 26 | } 27 | 28 | // SongNameFromPath returns the song file name from the given path (removing the path and the extension) 29 | func SongNameFromPath(path string) string { 30 | ext := filepath.Ext(path) 31 | return strings.TrimRight(filepath.Base(path), ext) 32 | } 33 | -------------------------------------------------------------------------------- /internal/pkg/model/model_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSongNameFromPath(t *testing.T) { 10 | for _, tc := range []struct { 11 | path string 12 | expected string 13 | }{ 14 | { 15 | path: "assets/dataset/wav/Summer Was Fun & Laura Brehm - Prism [NCS Release].wav", 16 | expected: "Summer Was Fun & Laura Brehm - Prism [NCS Release]", 17 | }, 18 | { 19 | path: " assets/dataset/wav/BEAUZ & Momo - Won't Look Back [NCS Release].wav ", 20 | expected: "BEAUZ & Momo - Won't Look Back [NCS Release]", 21 | }, 22 | { 23 | path: "assets/dataset/wav/Maduk - Go Home (Original Mix) [NCS Release].wav", 24 | expected: "Maduk - Go Home (Original Mix) [NCS Release]", 25 | }, 26 | { 27 | path: "./test.mp3", 28 | expected: "test", 29 | }, 30 | } { 31 | assert.Equal(t, tc.expected, SongNameFromPath(tc.path)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/pkg/model/table.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "math" 7 | ) 8 | 9 | // Used to down size the frequences to a 9 bit ints 10 | // XXX: we could also use 10.7Hz directly 11 | const freqStep = MaxFreq / float64(1<<9) 12 | 13 | // Used to down size the delta times to 14 bit ints (we use 16s as the max duration) 14 | const deltaTimeStep = 16 / float64(1<<14) 15 | 16 | // TableValueSize represents the TableValueSize when encoded in bytes 17 | const TableValueSize = 8 18 | 19 | // AnchorKey represents a anchor key 20 | type AnchorKey struct { 21 | // Frequency of the anchor point for the given point's target zone 22 | AnchorFreq float64 23 | // Frequency of the given point 24 | PointFreq float64 25 | // Delta time between the anchor point and the given point 26 | DeltaT float64 27 | } 28 | 29 | // EncodedKey represents an encoded key 30 | type EncodedKey uint32 31 | 32 | // NewAnchorKey creates a new anchor key from the given anchor and the given point 33 | func NewAnchorKey(anchor, point ConstellationPoint) *AnchorKey { 34 | return &AnchorKey{ 35 | AnchorFreq: anchor.Freq, 36 | PointFreq: point.Freq, 37 | // Use absolute just in case anchor is after the target zone 38 | DeltaT: math.Abs(point.Time - anchor.Time), 39 | } 40 | } 41 | 42 | // Bytes encodes the key in bytes 43 | func (ek EncodedKey) Bytes() []byte { 44 | // uint32 is 4 bytes 45 | bk := make([]byte, 4) 46 | binary.LittleEndian.PutUint32(bk, uint32(ek)) 47 | return bk 48 | } 49 | 50 | // Encode encodes the anchor key using: 51 | // 9 bits for the “frequency of the anchor”: fa 52 | // 9 bits for the ” frequency of the point”: fp 53 | // 14 bits for the ”delta time between the anchor and the point”: dt 54 | // The result is then dt | fa | fp 55 | // XXX: this only works if frequencies are coded in 9 bits or less (if we used a 1024 samples FFT, it will be the case) 56 | func (tk *AnchorKey) Encode() EncodedKey { 57 | // down size params 58 | fp := uint32(tk.PointFreq / freqStep) 59 | fa := uint32(tk.AnchorFreq / freqStep) 60 | dt := uint32(tk.DeltaT / deltaTimeStep) 61 | 62 | res := fp 63 | res |= fa << 9 64 | res |= dt << 23 65 | return EncodedKey(res) 66 | } 67 | 68 | // TableValue represents a table value 69 | type TableValue struct { 70 | // AnchorTimeMs is the time of the anchor in the related song in milliseconds 71 | AnchorTimeMs uint32 72 | // SongID is an ID representing the related song 73 | SongID uint32 74 | } 75 | 76 | // NewTableValue creates a new table value from the given song ID and anchor point 77 | func NewTableValue(song uint32, anchor ConstellationPoint) *TableValue { 78 | return &TableValue{ 79 | AnchorTimeMs: uint32(anchor.Time * 1000), 80 | SongID: song, 81 | } 82 | } 83 | 84 | // Bytes encodes the given table value in bytes 85 | func (tv *TableValue) Bytes() []byte { 86 | // Use a uint64 (8 bytes) 87 | b := make([]byte, TableValueSize) 88 | binary.LittleEndian.PutUint32(b[:4], tv.AnchorTimeMs) 89 | binary.LittleEndian.PutUint32(b[4:], tv.SongID) 90 | return b 91 | } 92 | 93 | // ValuesFromBytes decodes a list of table values from the given byte array 94 | func ValuesFromBytes(b []byte) ([]TableValue, error) { 95 | if len(b)%TableValueSize != 0 { 96 | return nil, fmt.Errorf("error wrong size for value: %d (got: %v) expected a multiple of %d", len(b), b, TableValueSize) 97 | } 98 | 99 | N := len(b) / TableValueSize 100 | res := make([]TableValue, 0, N) 101 | 102 | for i := 0; i < N; i++ { 103 | tv := TableValue{} 104 | tv.AnchorTimeMs = binary.LittleEndian.Uint32(b[i*8 : i*8+4]) 105 | tv.SongID = binary.LittleEndian.Uint32(b[i*8+4 : (i+1)*8]) 106 | res = append(res, tv) 107 | } 108 | 109 | return res, nil 110 | } 111 | 112 | // String returns a string representation of a TableValue 113 | func (tv TableValue) String() string { 114 | return fmt.Sprintf("(anchor_time_ms: %d, song_id: %d)", tv.AnchorTimeMs, tv.SongID) 115 | } 116 | -------------------------------------------------------------------------------- /internal/pkg/model/table_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestEncodingDecodingValue(t *testing.T) { 11 | for _, tv := range []TableValue{ 12 | {123, 123}, 13 | {1, 5}, 14 | {9, 1000000}, 15 | {3255, 42}, 16 | } { 17 | encoded := tv.Bytes() 18 | arr, err := ValuesFromBytes(encoded) 19 | decoded := arr[0] 20 | require.NoError(t, err) 21 | encoded2 := decoded.Bytes() 22 | 23 | assert.Equal(t, tv, decoded) 24 | assert.Equal(t, encoded, encoded2) 25 | } 26 | } 27 | 28 | func TestDecodingMultipleValues(t *testing.T) { 29 | arr := []TableValue{ 30 | {123, 123}, 31 | {1, 5}, 32 | {9, 1000000}, 33 | {3255, 42}, 34 | } 35 | 36 | var b []byte 37 | 38 | for _, v := range arr { 39 | b = append(b, v.Bytes()...) 40 | } 41 | 42 | res, err := ValuesFromBytes(b) 43 | require.NoError(t, err) 44 | assert.Equal(t, arr, res) 45 | } 46 | -------------------------------------------------------------------------------- /internal/pkg/pipeline/pipeline.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/sfluor/musig/internal/pkg/db" 8 | "github.com/sfluor/musig/internal/pkg/fingerprint" 9 | "github.com/sfluor/musig/internal/pkg/model" 10 | "github.com/sfluor/musig/pkg/dsp" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Pipeline is a struct that allows to operate on audio files 15 | type Pipeline struct { 16 | s *dsp.Spectrogrammer 17 | DB db.Database 18 | fpr fingerprint.Fingerprinter 19 | } 20 | 21 | // NewDefaultPipeline creates a new default pipeline using a Bolt DB, the default fingerprinter and the default spectrogrammer 22 | func NewDefaultPipeline(dbFile string) (*Pipeline, error) { 23 | db, err := db.NewBoltDB(dbFile) 24 | if err != nil { 25 | return nil, errors.Wrapf(err, "error connection to database at: %s", dbFile) 26 | } 27 | s := dsp.NewSpectrogrammer(model.DownsampleRatio, model.MaxFreq, model.SampleSize, true) 28 | fpr := fingerprint.NewDefaultFingerprinter() 29 | 30 | return &Pipeline{ 31 | s: s, 32 | DB: db, 33 | fpr: fpr, 34 | }, nil 35 | } 36 | 37 | // NewPipeline creates a new pipeline 38 | func NewPipeline(s *dsp.Spectrogrammer, db db.Database, fpr fingerprint.Fingerprinter) *Pipeline { 39 | return &Pipeline{ 40 | s: s, 41 | DB: db, 42 | fpr: fpr, 43 | } 44 | } 45 | 46 | // Close closes the underlying database 47 | func (p *Pipeline) Close() { 48 | p.DB.Close() 49 | } 50 | 51 | // Result represents the output of a pipeline 52 | type Result struct { 53 | Path string 54 | CMap []model.ConstellationPoint 55 | SongID uint32 56 | SamplingRate float64 57 | Spectrogram [][]float64 58 | Fingerprint map[model.EncodedKey]model.TableValue 59 | } 60 | 61 | // ProcessAndStore process the given audio file and store it in the database 62 | // the computed results are returned 63 | func (p *Pipeline) ProcessAndStore(path string) (*Result, error) { 64 | partial, err := p.read(path) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | id, err := p.DB.SetSong(model.SongNameFromPath(path)) 70 | if err != nil { 71 | return nil, errors.Wrap(err, "error storing song name in database") 72 | } 73 | 74 | songFpr := p.fpr.Fingerprint(id, partial.cMap) 75 | if err := p.DB.Set(songFpr); err != nil { 76 | return nil, errors.Wrap(err, "error storing song fingerprint in database") 77 | } 78 | 79 | log.Infof("sucessfully loaded %s into the database", path) 80 | return &Result{ 81 | Path: path, 82 | CMap: partial.cMap, 83 | SongID: id, 84 | SamplingRate: partial.samplingRate, 85 | Spectrogram: partial.spectrogram, 86 | Fingerprint: songFpr, 87 | }, nil 88 | } 89 | 90 | // Process process the given audio file and returns a Result 91 | func (p *Pipeline) Process(path string) (*Result, error) { 92 | partial, err := p.read(path) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | // Use 0 as the ID 98 | var id uint32 99 | songFpr := p.fpr.Fingerprint(id, partial.cMap) 100 | 101 | log.Infof("sucessfully loaded %s into the database", path) 102 | return &Result{ 103 | Path: path, 104 | CMap: partial.cMap, 105 | SongID: id, 106 | SamplingRate: partial.samplingRate, 107 | Spectrogram: partial.spectrogram, 108 | Fingerprint: songFpr, 109 | }, nil 110 | } 111 | 112 | type partialResult struct { 113 | cMap []model.ConstellationPoint 114 | samplingRate float64 115 | spectrogram [][]float64 116 | } 117 | 118 | func (p *Pipeline) read(path string) (*partialResult, error) { 119 | file, err := os.Open(path) 120 | if err != nil { 121 | return nil, errors.Wrapf(err, "error opening file at %s", path) 122 | } 123 | defer file.Close() 124 | 125 | spec, spr, err := p.s.Spectrogram(file) 126 | if err != nil { 127 | return nil, errors.Wrap(err, "error generating spectrogram") 128 | } 129 | 130 | cMap := p.s.ConstellationMap(spec, spr) 131 | 132 | return &partialResult{ 133 | cMap: cMap, 134 | samplingRate: spr, 135 | spectrogram: spec, 136 | }, nil 137 | } 138 | -------------------------------------------------------------------------------- /internal/pkg/sound/sound.go: -------------------------------------------------------------------------------- 1 | package sound 2 | 3 | // Reader interface allows you to read audio files 4 | type Reader interface { 5 | Read([]float64) (int, error) 6 | SampleRate() float64 7 | } 8 | -------------------------------------------------------------------------------- /internal/pkg/sound/wav.go: -------------------------------------------------------------------------------- 1 | package sound 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/gordonklaus/portaudio" 7 | "github.com/pkg/errors" 8 | "github.com/youpy/go-riff" 9 | "github.com/youpy/go-wav" 10 | ) 11 | 12 | var _ Reader = &WAVReader{} 13 | 14 | // WAVReader implements the sound.Reader interface 15 | type WAVReader struct { 16 | wr *wav.Reader 17 | isStereo bool 18 | sampleRate float64 19 | } 20 | 21 | // NewWAVReader creates a new wav reader 22 | func NewWAVReader(r riff.RIFFReader) (*WAVReader, error) { 23 | reader := wav.NewReader(r) 24 | f, err := reader.Format() 25 | if err != nil { 26 | return nil, errors.Wrap(err, "could not get wav format") 27 | } 28 | 29 | return &WAVReader{ 30 | wr: reader, 31 | isStereo: f.NumChannels == 2, 32 | sampleRate: float64(f.SampleRate), 33 | }, nil 34 | } 35 | 36 | // Read reads from the given wav file and return raw audio data or an error if an error occured 37 | func (r *WAVReader) Read(dst []float64) (int, error) { 38 | N := uint32(len(dst)) 39 | 40 | // go-wav uses 4 * the number of samples we want to read as parameter 41 | samples, err := r.wr.ReadSamples(N) 42 | if err == io.EOF { 43 | return 0, err 44 | } 45 | if err != nil { 46 | return 0, errors.Wrap(err, "could not read samples") 47 | } 48 | 49 | // Take care of mono / stereo 50 | // If sound is in stereo we want to get it into mono 51 | size := len(samples) 52 | if r.isStereo { 53 | size *= 2 54 | } 55 | 56 | if r.isStereo { 57 | for i, sample := range samples { 58 | // We average the two entries in case of stereo 59 | dst[i] = float64(sample.Values[0]+sample.Values[1]) / 2 60 | } 61 | return size / 2, nil 62 | } 63 | 64 | for i, sample := range samples { 65 | dst[i] = float64(sample.Values[0]) 66 | } 67 | 68 | return size, nil 69 | } 70 | 71 | // SampleRate returns the sample rate for the given reader 72 | func (r *WAVReader) SampleRate() float64 { 73 | return r.sampleRate 74 | } 75 | 76 | // RecordWAV listens on the microphone and saves the signal to the given io.Writer 77 | // it takes a stop channel to interrupt the recording 78 | func RecordWAV(writer io.Writer, stopCh <-chan struct{}) error { 79 | samples := []wav.Sample{} 80 | channels := 1 81 | bitsPerSample := 32 82 | sampleRate := 44100 83 | 84 | portaudio.Initialize() 85 | defer portaudio.Terminate() 86 | 87 | in := make([]int32, 64) 88 | stream, err := portaudio.OpenDefaultStream(channels, 0, float64(sampleRate), len(in), in) 89 | if err != nil { 90 | return errors.Wrap(err, "error opening portaudio stream") 91 | } 92 | defer stream.Close() 93 | 94 | err = stream.Start() 95 | if err != nil { 96 | return errors.Wrap(err, "error starting stream") 97 | } 98 | 99 | listen: 100 | for { 101 | err = stream.Read() 102 | if err != nil { 103 | return errors.Wrap(err, "error reading from stream") 104 | } 105 | 106 | for _, v := range in { 107 | samples = append(samples, wav.Sample{Values: [2]int{ 108 | // Append the same value twice, worst case it will be used as stereo and averaged 109 | int(v), int(v), 110 | }}) 111 | } 112 | 113 | select { 114 | case <-stopCh: 115 | break listen 116 | default: 117 | } 118 | } 119 | 120 | if err = stream.Stop(); err != nil { 121 | return errors.Wrap(err, "error stoping stream") 122 | } 123 | 124 | wr := wav.NewWriter(writer, uint32(len(samples)), uint16(channels), uint32(sampleRate), uint16(bitsPerSample)) 125 | if err = wr.WriteSamples(samples); err != nil { 126 | return errors.Wrap(err, "error writing samples") 127 | } 128 | 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /pkg/dsp/dsp.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | // Downsample downsamples the given array using the given ratio by averaging 4 | func Downsample(arr []float64, ratio int) []float64 { 5 | res := make([]float64, 0, len(arr)/ratio) 6 | 7 | // Used to divide the samples (might be different than ratio if len(arr) % ratio != 0 8 | div := float64(0) 9 | 10 | for i := 0; i < len(arr); i += ratio { 11 | res = append(res, 0) 12 | for j := 0; j < ratio && i+j < len(arr); j++ { 13 | div++ 14 | res[i/ratio] += arr[i+j] 15 | } 16 | res[i/ratio] /= div 17 | div = 0 18 | } 19 | 20 | return res 21 | } 22 | 23 | // Reshape takes an array and a bin size and will split the given array 24 | // into multiple bins of the given bin size 25 | // if len(arr) % size != 0, the data at the end will be dropped 26 | // XXX: maybe don't drop it ? 27 | func Reshape(arr []float64, size int) [][]float64 { 28 | res := make([][]float64, len(arr)/size) 29 | for i := range res { 30 | res[i] = arr[i*size : (i+1)*size] 31 | } 32 | return res 33 | } 34 | -------------------------------------------------------------------------------- /pkg/dsp/dsp_test.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDownsample(t *testing.T) { 11 | for _, tc := range []struct { 12 | input []float64 13 | expected []float64 14 | ratio int 15 | desc string 16 | }{ 17 | { 18 | []float64{1, 2, 3, 4, 5}, 19 | []float64{1, 2, 3, 4, 5}, 20 | 1, 21 | "1-ratio (no downsampling)", 22 | }, 23 | { 24 | []float64{1, 2, 3, 4, 5}, 25 | []float64{1.5, 3.5, 5}, 26 | 2, 27 | "2-ratio", 28 | }, 29 | { 30 | []float64{1, 2, 3, 4, 5}, 31 | []float64{2, 4.5}, 32 | 3, 33 | "3-ratio", 34 | }, 35 | { 36 | []float64{1, 2, 3, 4, 5}, 37 | []float64{2.5, 5}, 38 | 4, 39 | "4-ratio", 40 | }, 41 | { 42 | []float64{1, 2, 3, 4, 5}, 43 | []float64{3}, 44 | 5, 45 | "5-ratio", 46 | }, 47 | } { 48 | output := Downsample(tc.input, tc.ratio) 49 | require.EqualValues(t, tc.expected, output, tc.desc) 50 | } 51 | } 52 | 53 | func TestReshape(t *testing.T) { 54 | for i, tc := range []struct { 55 | input []float64 56 | size int 57 | expected [][]float64 58 | }{ 59 | {[]float64{}, 1, [][]float64{}}, 60 | {[]float64{1, 2, 3}, 1, [][]float64{ 61 | {1}, 62 | {2}, 63 | {3}, 64 | }}, 65 | {[]float64{1, 2, 3}, 2, [][]float64{ 66 | {1, 2}, 67 | }}, 68 | {[]float64{1, 2, 3, 4, 5, 6, 7}, 3, [][]float64{ 69 | {1, 2, 3}, 70 | {4, 5, 6}, 71 | }}, 72 | {[]float64{1, 2, 3, 4, 5, 6, 7, 8}, 4, [][]float64{ 73 | {1, 2, 3, 4}, 74 | {5, 6, 7, 8}, 75 | }}, 76 | {[]float64{1, 2, 3, 4, 5, 6, 7, 8, 9}, 2, [][]float64{ 77 | {1, 2}, 78 | {3, 4}, 79 | {5, 6}, 80 | {7, 8}, 81 | }}, 82 | } { 83 | require.EqualValues(t, tc.expected, Reshape(tc.input, tc.size), fmt.Sprintf("test %d", i+1)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/dsp/fft.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "math" 5 | "math/cmplx" 6 | 7 | "gonum.org/v1/gonum/dsp/fourier" 8 | ) 9 | 10 | // DFT is a discrete fourier transform implementation (it is slow O(N^2)) 11 | // TODO parallelize ? 12 | func DFT(arr []float64) []float64 { 13 | N := len(arr) 14 | res := make([]float64, N) 15 | theta := -2i * math.Pi / itoc(N) 16 | for n := range res { 17 | for k := 0; k < N; k++ { 18 | res[n] += arr[k] * real(cmplx.Exp(theta*itoc(k)*itoc(n))) 19 | } 20 | } 21 | return res 22 | } 23 | 24 | // FFT is a fast fourier transform using gonum/fourier 25 | // TODO remove adding duplicate frequencies (it's ~33% slower with them) 26 | func FFT(in []float64) []float64 { 27 | fft := fourier.NewFFT(len(in)) 28 | coefs := fft.Coefficients(nil, in) 29 | C := len(coefs) 30 | 31 | res := make([]float64, len(in)) 32 | 33 | for i, c := range coefs { 34 | res[i] = real(c) 35 | } 36 | 37 | // Add duplicate frequencies 38 | for i := 0; i < len(res)-C; i++ { 39 | res[i+C] = res[i+1] 40 | } 41 | 42 | return res 43 | } 44 | 45 | // Int to complex 46 | func itoc(i int) complex128 { 47 | return complex(float64(i), 0) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/dsp/fft_test.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFFT(t *testing.T) { 12 | for _, fft := range []func([]float64) []float64{ 13 | FFT, DFT, 14 | } { 15 | for j, tc := range []struct { 16 | input []float64 17 | expected []float64 18 | }{ 19 | { 20 | []float64{1, 0, 0, 0, 0, 0, 0, 0}, 21 | []float64{1, 1, 1, 1, 1, 1, 1, 1}, 22 | }, 23 | { 24 | []float64{1, 2}, 25 | []float64{3, -1}, 26 | }, 27 | { 28 | []float64{1, 2, 3}, 29 | []float64{6, -1.5, -1.5}, 30 | }, 31 | { 32 | []float64{-1, 0, 1, 0}, 33 | []float64{0, -2, 0, -2}, 34 | }, 35 | { 36 | []float64{1, 2, 3, 4, 5, 6}, 37 | []float64{21, -3, -3, -3, -3, -3}, 38 | }, 39 | } { 40 | output := fft(tc.input) 41 | require.Equal(t, len(tc.expected), len(output), fmt.Sprintf("test %d", j+1)) 42 | require.InDeltaSlice(t, tc.expected, output, 0.01, fmt.Sprintf("test %d", j+1)) 43 | } 44 | } 45 | } 46 | 47 | func BenchmarkFT(b *testing.B) { 48 | times := []int{10, 50, 100, 500, 1000, 5000} 49 | inputs := map[int][]float64{} 50 | f := func(f float64) float64 { return math.Sin(2*f) + math.Cos(3*f) } 51 | 52 | // Compute inputs 53 | for _, N := range times { 54 | inputs[N] = make([]float64, 0, N) 55 | for i := 0; i < N; i++ { 56 | inputs[N] = append(inputs[N], f(float64(i))) 57 | } 58 | } 59 | 60 | b.ResetTimer() 61 | for N, input := range inputs { 62 | b.Run(fmt.Sprintf("DFT_%d", N), benchOneFT(b, input, DFT)) 63 | b.Run(fmt.Sprintf("FFT_%d", N), benchOneFT(b, input, FFT)) 64 | } 65 | } 66 | 67 | func benchOneFT(b *testing.B, input []float64, fft func([]float64) []float64) func(b *testing.B) { 68 | return func(b *testing.B) { 69 | for i := 0; i < b.N; i++ { 70 | fft(input) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/dsp/filter.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Filterer is the interface used for filters 8 | type Filterer interface { 9 | Filter(arr []float64) []float64 10 | } 11 | 12 | // LPFilter is a first order low pass filter usig H(p) = 1 / (1 + pRC) 13 | type LPFilter struct { 14 | alpha float64 15 | rc float64 16 | } 17 | 18 | // NewLPFilter creates a new low pass Filter 19 | func NewLPFilter(cutoff, sampleRate float64) *LPFilter { 20 | rc := 1 / (cutoff * 2 * math.Pi) 21 | dt := 1 / sampleRate 22 | alpha := dt / (rc + dt) 23 | return &LPFilter{alpha: alpha, rc: rc} 24 | } 25 | 26 | // XXX: This filter could be improved 27 | // Filter filters the given array of values, panics if array is empty 28 | func (lp *LPFilter) Filter(arr []float64) []float64 { 29 | res := make([]float64, 0, len(arr)) 30 | 31 | // First value should be arr[0] / RC using the initial value theorem 32 | res = append(res, arr[0]*lp.alpha) 33 | 34 | for i := range arr[:len(arr)-1] { 35 | res = append( 36 | res, 37 | // Formula used is: 38 | // y(n+1) = y(n) + alpha * (x(n+1) -y(n)) 39 | res[i]+lp.alpha*(arr[i+1]-res[i]), 40 | ) 41 | } 42 | return res 43 | } 44 | -------------------------------------------------------------------------------- /pkg/dsp/filter_test.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestLowPassFilter(t *testing.T) { 11 | // Number of samples 12 | N := float64(20000) 13 | // Sampling time 14 | te := float64(0.001) 15 | 16 | // Cutoff frequency is 10 / 2 * PI 17 | cutoff := 10 / (2 * math.Pi) 18 | lp := NewLPFilter(cutoff, 1/te) 19 | 20 | for _, tc := range []struct { 21 | f func(float64) float64 22 | minAmpl float64 23 | maxAmpl float64 24 | desc string 25 | }{ 26 | // Those should not be affected 27 | {math.Sin, 1.98, 2, "sin(t)"}, 28 | {math.Cos, 1.98, 2, "cos(t)"}, 29 | { 30 | func(t float64) float64 { return math.Sin(t) + math.Cos(t) }, 31 | 2.8, 3, "sin(t) + cos(t)", 32 | }, 33 | // Those should be filtered out 34 | { 35 | func(t float64) float64 { return math.Sin(t * 1000) }, 36 | 0, 0.05, "sin(1000 * t)", 37 | }, 38 | { 39 | func(t float64) float64 { return math.Cos(t * 1000) }, 40 | 0, 0.05, "cos(1000 * t)", 41 | }, 42 | // Those should be splitted in two 43 | { 44 | func(t float64) float64 { return math.Sin(t) + 10*math.Sin(t*1000) }, 45 | 1.98, 2.2, "sin(t) + 50 * sin(t * 10e5)", 46 | }, 47 | { 48 | func(t float64) float64 { return math.Cos(t) + 10*math.Cos(t*1000) }, 49 | 1.98, 2.2, "cos(t) + 50 * cos(t * 10e5)", 50 | }, 51 | // This should not be affected 52 | { 53 | func(t float64) float64 { return 1 }, 54 | 0.99, 1, "constant", 55 | }, 56 | } { 57 | values := make([]float64, 0, int(N)) 58 | 59 | for n := float64(0); n < N; n++ { 60 | values = append(values, tc.f(n*te)) 61 | } 62 | ampl := sigAmpl(lp.Filter(values)) 63 | require.Truef(t, ampl <= tc.maxAmpl, "max amplitude: expected %f (filtered) < %f (max) for '%s'", ampl, tc.maxAmpl, tc.desc) 64 | require.Truef(t, tc.minAmpl <= ampl, "min amplitude: expected %f (min) < %f (filtered) for '%s'", tc.minAmpl, ampl, tc.desc) 65 | } 66 | } 67 | 68 | func sigAmpl(arr []float64) float64 { 69 | max, min := arr[0], arr[0] 70 | for _, x := range arr { 71 | max = math.Max(max, x) 72 | min = math.Min(min, x) 73 | } 74 | return max - min 75 | } 76 | -------------------------------------------------------------------------------- /pkg/dsp/img.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "math" 7 | 8 | "github.com/sfluor/musig/pkg/stats" 9 | ) 10 | 11 | // SpecToImg takes a spectrogram (matrix of floats representing m[time][frequency] = amplitude) 12 | // and return an image 13 | func SpecToImg(matrix [][]float64) image.Image { 14 | // TODO stop hardcoding this 15 | // 10 Pixel for 1 frequency step 16 | // 150 Pixels for 1 timeStep 17 | nTime, nFreq := 150, 20 18 | 19 | img := image.NewRGBA(image.Rect(0, 0, nTime*len(matrix), nFreq*len(matrix[0]))) 20 | 21 | // XXX the way values are normalized could be changed 22 | invMax := 1 / absMax(matrix) 23 | for time, row := range matrix { 24 | for freq, a := range row { 25 | color := colorbar(math.Abs(a) * invMax) 26 | for t := 0; t < nTime; t++ { 27 | for f := 0; f < nFreq; f++ { 28 | img.Set( 29 | // x 30 | nTime*time+t, 31 | // y 32 | nFreq*len(matrix[0])-nFreq*freq+f, 33 | // color 34 | color, 35 | ) 36 | } 37 | } 38 | } 39 | } 40 | 41 | return img 42 | } 43 | 44 | // absMax returns the maximum value of abs(mat) where mat is the given matrix 45 | func absMax(mat [][]float64) (max float64) { 46 | for _, row := range mat { 47 | max = math.Max(max, stats.AbsMax(row)) 48 | } 49 | return max 50 | } 51 | 52 | // colorbar is function to map a value in [0, 1] to a color 53 | func colorbar(val float64) color.RGBA { 54 | r := 255 * math.Min(math.Max(0, 1.5-math.Abs(1-4*(val-0.5))), 1) 55 | g := 255 * math.Min(math.Max(0, 1.5-math.Abs(1-4*(val-0.25))), 1) 56 | b := 255 * math.Min(math.Max(0, 1.5-math.Abs(1-4*val)), 1) 57 | return color.RGBA{uint8(r), uint8(g), uint8(b), 255} 58 | } 59 | -------------------------------------------------------------------------------- /pkg/dsp/score.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "github.com/sfluor/musig/internal/pkg/model" 5 | "github.com/sfluor/musig/pkg/stats" 6 | ) 7 | 8 | // MatchScore computes a match score between the two transformed audio samples (into a list of Key + TableValue) 9 | func MatchScore(sample, match map[model.EncodedKey]model.TableValue) float64 { 10 | // Will hold a list of points (time in the sample sound file, time in the matched database sound file) 11 | points := [2][]float64{} 12 | matches := 0.0 13 | for k, sampleValue := range sample { 14 | if matchValue, ok := match[k]; ok { 15 | points[0] = append(points[0], float64(sampleValue.AnchorTimeMs)) 16 | points[1] = append(points[1], float64(matchValue.AnchorTimeMs)) 17 | matches++ 18 | } 19 | } 20 | corr := stats.Correlation(points[0], points[1]) 21 | return corr * corr * matches 22 | } 23 | -------------------------------------------------------------------------------- /pkg/dsp/spectrogram.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/sfluor/musig/internal/pkg/model" 9 | "github.com/sfluor/musig/internal/pkg/sound" 10 | "github.com/sfluor/musig/pkg/stats" 11 | ) 12 | 13 | // Spectrogrammer is a struct that allows to create spectrograms 14 | type Spectrogrammer struct { 15 | dsRatio float64 16 | maxFreq float64 17 | binSize float64 18 | // thresholdCoefficient is used to filter out the important frequencies 19 | // increasing it decreases the size of the constellation maps returned 20 | thresholdCoefficient float64 21 | // windowing is used to activate / deactivate the windowing function 22 | windowing bool 23 | } 24 | 25 | // NewSpectrogrammer creates a new spectrogrammer 26 | func NewSpectrogrammer(dsRatio, maxFreq, binSize float64, windowing bool) *Spectrogrammer { 27 | return &Spectrogrammer{ 28 | dsRatio: dsRatio, 29 | maxFreq: maxFreq, 30 | binSize: binSize, 31 | // TODO stop hardcoding this 32 | thresholdCoefficient: 1, 33 | windowing: windowing, 34 | } 35 | } 36 | 37 | // Spectrogram reads the provided audio file and returns a spectrogram for it 38 | // Matrix is in the following format: 39 | // TIME : FREQUENCY : Value 40 | // time is t * binSize * dsp.DownsampleRatio / reader.SampleRate() 41 | // frequency is f * freqBinSize 42 | func (s *Spectrogrammer) Spectrogram(file *os.File) ([][]float64, float64, error) { 43 | reader, err := sound.NewWAVReader(file) 44 | if err != nil { 45 | return nil, 0, errors.Wrap(err, "error reading wav") 46 | } 47 | 48 | spr := reader.SampleRate() 49 | lp := NewLPFilter(s.maxFreq, spr) 50 | var matrix [][]float64 51 | 52 | bin := make([]float64, int(s.binSize*s.dsRatio)) 53 | for { 54 | n, err := reader.Read(bin) 55 | if err != nil { 56 | if err == io.EOF { 57 | break 58 | } 59 | return nil, 0, errors.Wrap(err, "error reading from sound file") 60 | } 61 | 62 | // TODO handle this edge case 63 | if n != int(s.binSize*s.dsRatio) { 64 | break 65 | } 66 | 67 | sampled := Downsample( 68 | lp.Filter(bin[:n]), 69 | int(s.dsRatio), 70 | ) 71 | if s.windowing { 72 | ApplyWindow(sampled, HammingWindow) 73 | } 74 | fft := FFT(sampled) 75 | 76 | // TODO remove slicing here when removing duplicate values returned by the FFT 77 | matrix = append(matrix, fft[:len(fft)-(len(fft)-1)/2-1]) 78 | } 79 | 80 | return matrix, spr, nil 81 | } 82 | 83 | // ConstellationMap takes a spectrogram, its sample rate and returns the highest frequencies and their time in the audio file 84 | // The returned slice is ordered by time and is ordered by frequency for a constant time: 85 | // If two time-frequency points have the same time, the time-frequency point with the lowest frequency is before the other one. 86 | // If a time time-frequency point has a lower time than another point one then it is before. 87 | func (s *Spectrogrammer) ConstellationMap(spec [][]float64, sampleRate float64) []model.ConstellationPoint { 88 | // For each 512-sized bins create logarithmic bands 89 | // [0, 10], [10, 20], [20, 40], [40, 80], [80, 160], [160, 511] 90 | bands := [][]int{{0, 10}, {10, 20}, {20, 40}, {40, 80}, {80, 160}, {160, 512}} 91 | 92 | var res []model.ConstellationPoint 93 | 94 | // Frequency bin size 95 | fbs := s.freqBinSize(sampleRate) 96 | 97 | // Time step 98 | timeStep := s.dsRatio * s.binSize / sampleRate 99 | 100 | // Maximum of amplitude and their corresponding frequencies 101 | var maxs, freqs []float64 102 | var idx int 103 | for t, row := range spec { 104 | maxs, freqs = make([]float64, len(bands)), make([]float64, len(bands)) 105 | 106 | // We retrieve the maximum amplitudes and their frequency 107 | for i, band := range bands { 108 | maxs[i], idx = stats.ArgAbsMax(row[band[0]:band[1]]) 109 | freqs[i] = float64(band[0]+idx) * fbs 110 | } 111 | 112 | // Keep only the bins above the average times the threshold coeff of the max bins 113 | avg := stats.Avg(maxs) 114 | indices := stats.ArgAbove(avg*s.thresholdCoefficient, maxs) 115 | 116 | // Register the frequencies we kept and their time of apparition 117 | time := timeStep * float64(t) 118 | 119 | for _, idx := range indices { 120 | res = append(res, model.ConstellationPoint{Time: time, Freq: freqs[idx]}) 121 | } 122 | } 123 | 124 | return res 125 | } 126 | 127 | // Returns the bin size for a frequency bin given a sample rate 128 | func (s *Spectrogrammer) freqBinSize(spr float64) float64 { 129 | return spr / s.dsRatio / s.binSize 130 | } 131 | -------------------------------------------------------------------------------- /pkg/dsp/spectrogram_test.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "math" 5 | "os" 6 | "path" 7 | "sort" 8 | "testing" 9 | 10 | "github.com/sfluor/musig/internal/pkg/model" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | const AssetsDir = "../../assets/test" 16 | 17 | func TestSpectrogram440(t *testing.T) { 18 | sampleSize := model.SampleSize 19 | 20 | s := NewSpectrogrammer( 21 | model.DownsampleRatio, 22 | model.MaxFreq, 23 | sampleSize, 24 | // No windowing for tests since we want to verify that frequency are in the correct range 25 | false, 26 | ) 27 | 28 | file, err := os.Open(path.Join(AssetsDir, "440.wav")) 29 | require.NoError(t, err) 30 | defer file.Close() 31 | 32 | spec, spr, err := s.Spectrogram(file) 33 | require.NoError(t, err) 34 | 35 | invMax := 1 / absMax(spec) 36 | 37 | // Threshold to remove frequencies we don't want 38 | threshold := 0.5 39 | freqBinSize := spr / model.DownsampleRatio / sampleSize 40 | 41 | checkFreq := func(f, time, ampl float64) { 42 | require.InDeltaf( 43 | t, 44 | 440, 45 | f, 46 | 2*freqBinSize, 47 | "time: %f, frequency: %f, amplitude: %f", 48 | time, 49 | f, 50 | ampl, 51 | ) 52 | } 53 | 54 | for time, row := range spec { 55 | for f := range row { 56 | spec[time][f] = math.Abs(spec[time][f]) * invMax 57 | if spec[time][f] > threshold { 58 | // Check that the frequency of the current point is within [440 - binSize, 440 + bin Size] 59 | checkFreq(float64(f)*freqBinSize, float64(time)*model.DownsampleRatio/spr, spec[time][f]) 60 | } 61 | } 62 | } 63 | 64 | cMap := s.ConstellationMap(spec, spr) 65 | for _, point := range cMap { 66 | // Use 0 since we don't care here 67 | checkFreq(point.Freq, point.Time, 0) 68 | } 69 | 70 | assertIsSorted(t, cMap) 71 | } 72 | 73 | func TestSpectrogram440And880(t *testing.T) { 74 | sampleSize := model.SampleSize 75 | 76 | s := NewSpectrogrammer( 77 | model.DownsampleRatio, 78 | model.MaxFreq, 79 | sampleSize, 80 | // No windowing for tests since we want to verify that frequency are in the correct range 81 | false, 82 | ) 83 | 84 | file, err := os.Open(path.Join(AssetsDir, "440_880.wav")) 85 | require.NoError(t, err) 86 | defer file.Close() 87 | 88 | spec, spr, err := s.Spectrogram(file) 89 | require.NoError(t, err) 90 | 91 | invMax := 1 / absMax(spec) 92 | 93 | // Threshold to remove frequencies we don't want 94 | threshold := 0.5 95 | freqBinSize := spr / model.DownsampleRatio / sampleSize 96 | 97 | checkFreq := func(freq, time, ampl float64) { 98 | // Check that the frequency of the current point is within [440 - binSize, 440 + bin Size] 99 | // or within [880 - binSize, 880 + bin Size] 100 | flag := ((freq-440) >= -freqBinSize && (freq-440) <= freqBinSize) || 101 | ((freq-880) >= -freqBinSize && (freq-880) <= freqBinSize) 102 | require.Truef( 103 | t, 104 | flag, 105 | "time: %f, frequency: %f, amplitude: %f", 106 | time, 107 | freq, 108 | ampl, 109 | ) 110 | } 111 | 112 | for time, row := range spec { 113 | for f := range row { 114 | spec[time][f] = math.Abs(spec[time][f]) * invMax 115 | if spec[time][f] > threshold { 116 | freq := float64(f) * freqBinSize 117 | checkFreq(freq, float64(time)*model.DownsampleRatio/spr, spec[time][f]) 118 | } 119 | } 120 | } 121 | 122 | cMap := s.ConstellationMap(spec, spr) 123 | for _, point := range cMap { 124 | // Use 0 since we don't care here 125 | checkFreq(point.Freq, point.Time, 0) 126 | } 127 | 128 | assertIsSorted(t, cMap) 129 | } 130 | 131 | func assertIsSorted(t *testing.T, cMap []model.ConstellationPoint) { 132 | times := make([]float64, len(cMap)) 133 | lastTime := 0.0 134 | lastFreq := 0.0 135 | for i, p := range cMap { 136 | times[i] = p.Time 137 | if lastTime == p.Time { 138 | assert.True(t, p.Freq > lastFreq) 139 | } 140 | lastTime = p.Time 141 | lastFreq = p.Freq 142 | } 143 | 144 | assert.True(t, sort.IsSorted(sort.Float64Slice(times))) 145 | } 146 | -------------------------------------------------------------------------------- /pkg/dsp/window.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import "math" 4 | 5 | // WindowFunc represents a window function 6 | // It takes the number of points we want in the output window 7 | type WindowFunc func(M int) []float64 8 | 9 | // ApplyWindow applies the provided window on the given array 10 | func ApplyWindow(arr []float64, wf WindowFunc) { 11 | for i, v := range wf(len(arr)) { 12 | arr[i] *= v 13 | } 14 | } 15 | 16 | // HammingWindow is a hamming window 17 | // the formula used is w(n) = 0.54 - 0.46 * cos(2 * pi * n / (M - 1)) for M in [0, M-1] 18 | func HammingWindow(M int) []float64 { 19 | switch M { 20 | case 0: 21 | return []float64{} 22 | case 1: 23 | return []float64{1} 24 | default: 25 | f := 2 * math.Pi / float64(M-1) 26 | res := make([]float64, M) 27 | for n := range res { 28 | res[n] = 0.54 - 0.46*math.Cos(f*float64(n)) 29 | } 30 | return res 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/dsp/window_test.go: -------------------------------------------------------------------------------- 1 | package dsp 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestHammingWindow(t *testing.T) { 11 | for _, tc := range []struct { 12 | m int 13 | expected []float64 14 | }{ 15 | {0, []float64{}}, 16 | {1, []float64{1}}, 17 | {2, []float64{0.08, 0.08}}, 18 | {5, []float64{0.08, 0.54, 1., 0.54, 0.08}}, 19 | {11, []float64{0.08, 0.16785218, 0.39785218, 0.68214782, 0.91214782, 1., 0.91214782, 0.68214782, 0.39785218, 0.16785218, 0.08}}, 20 | } { 21 | require.InDeltaSlice(t, tc.expected, HammingWindow(tc.m), 0.0001) 22 | } 23 | } 24 | 25 | func TestApplyHammingWindow(t *testing.T) { 26 | for i, tc := range []struct { 27 | input []float64 28 | expected []float64 29 | }{ 30 | {[]float64{}, []float64{}}, 31 | {[]float64{1}, []float64{1}}, 32 | {[]float64{1, 1}, []float64{0.08, 0.08}}, 33 | {[]float64{10, 2, 0, 5, 4}, []float64{0.8, 1.08, 0, 2.7, 0.32}}, 34 | {[]float64{0, 0, 0}, []float64{0, 0, 0}}, 35 | } { 36 | ApplyWindow(tc.input, HammingWindow) 37 | require.InDeltaSlice(t, tc.expected, tc.input, 0.0001, fmt.Sprintf("test %d", i+1)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Correlation computes the correlation between 2 series of points 8 | // the length used is x's 9 | func Correlation(x []float64, y []float64) float64 { 10 | n := len(x) 11 | meanX, meanY := Avg(x[:n]), Avg(y[:n]) 12 | 13 | sXY := 0.0 14 | sX := 0.0 15 | sY := 0.0 16 | 17 | for i, xp := range x { 18 | dx := xp - meanX 19 | dy := y[i] - meanY 20 | 21 | sX += dx * dx 22 | sY += dy * dy 23 | 24 | sXY += dx * dy 25 | } 26 | 27 | return sXY / (math.Sqrt(sX) * math.Sqrt(sY)) 28 | } 29 | 30 | // AbsMax returns max(|x| for x in arr) 31 | func AbsMax(arr []float64) float64 { 32 | m, _ := ArgAbsMax(arr) 33 | return m 34 | } 35 | 36 | // ArgAbsMax returns max(|x| for x in arr) and the index 37 | func ArgAbsMax(arr []float64) (max float64, idx int) { 38 | for i, v := range arr { 39 | if math.Abs(v) > max { 40 | idx = i 41 | max = math.Abs(v) 42 | } 43 | } 44 | return max, idx 45 | } 46 | 47 | // Avg computes the average of the given array 48 | func Avg(arr []float64) float64 { 49 | if len(arr) == 0 { 50 | return 0 51 | } 52 | 53 | sum := 0.0 54 | for _, v := range arr { 55 | sum += v 56 | } 57 | 58 | return sum / float64(len(arr)) 59 | } 60 | 61 | // ArgAbove returns the indices for the values from the array that are above the given threshold 62 | func ArgAbove(threshold float64, arr []float64) []int { 63 | var res []int 64 | for idx, v := range arr { 65 | if v > threshold { 66 | res = append(res, idx) 67 | } 68 | } 69 | return res 70 | } 71 | -------------------------------------------------------------------------------- /pkg/stats/stats_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestAbsMax(t *testing.T) { 10 | for _, tc := range []struct { 11 | input []float64 12 | max float64 13 | idx int 14 | }{ 15 | { 16 | []float64{-1.5, 1, -0.2, 1.45}, 17 | 1.5, 18 | 0, 19 | }, 20 | { 21 | []float64{-100, 101}, 22 | 101, 23 | 1, 24 | }, 25 | { 26 | []float64{-100, 100, 50}, 27 | 100, 28 | 0, 29 | }, 30 | } { 31 | m, i := ArgAbsMax(tc.input) 32 | require.Equal(t, tc.max, m) 33 | require.Equal(t, tc.idx, i) 34 | } 35 | } 36 | 37 | func TestAvg(t *testing.T) { 38 | for _, tc := range []struct { 39 | input []float64 40 | expected float64 41 | }{ 42 | { 43 | []float64{-1.5, 1, -0.2, 1.45}, 44 | 0.1875, 45 | }, 46 | { 47 | []float64{-100, 101}, 48 | 0.5, 49 | }, 50 | { 51 | []float64{-100, 100, 50}, 52 | 16.6666, 53 | }, 54 | } { 55 | require.InDelta(t, tc.expected, Avg(tc.input), 0.01) 56 | } 57 | } 58 | 59 | func TestArgAbove(t *testing.T) { 60 | for _, tc := range []struct { 61 | input []float64 62 | threshold float64 63 | expected []int 64 | }{ 65 | { 66 | []float64{-1.5, 1, -0.2, 1.45}, 67 | -2, 68 | []int{0, 1, 2, 3}, 69 | }, 70 | { 71 | []float64{-1.5, 1, -0.2, 1.45}, 72 | 0, 73 | []int{1, 3}, 74 | }, 75 | { 76 | []float64{-100, 101}, 77 | 101.1, 78 | nil, 79 | }, 80 | { 81 | []float64{-100, 100, 50}, 82 | 25, 83 | []int{1, 2}, 84 | }, 85 | } { 86 | require.EqualValues(t, tc.expected, ArgAbove(tc.threshold, tc.input)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # musig :speaker: 2 | 3 | [![GoDoc](https://godoc.org/github.com/sfluor/musig?status.svg)](https://godoc.org/github.com/sfluor/musig) 4 | [![CircleCI](https://circleci.com/gh/sfluor/musig/tree/master.svg?style=svg)](https://circleci.com/gh/sfluor/musig/tree/master) 5 | 6 | A shazam-like tool that allows you to compute song's fingerprints and reverse lookup song names. 7 | 8 | It's more or less an implementation of the shazam paper as described in [this awesome article](http://coding-geek.com/how-shazam-works/) 9 | 10 | ## Installation 11 | 12 | You will need to have [go](https://golang.org/doc/install) on your computer (version > 1.11 to be able to use go modules). 13 | 14 | You will also need to have [portaudio](http://www.portaudio.com/) installed (`brew install portaudio` on macOS, `apt install portaudio19-dev` on Ubuntu / Debian, for other distributions you can search for the `portaudio` package), it is required for the `listen` command that listens on your microphone to match the recording against the database. 15 | 16 | To build the binary: 17 | 18 | ```bash 19 | git clone git@github.com:sfluor/musig.git 20 | cd musig 21 | make 22 | ``` 23 | 24 | You will then be able to run the binary with: 25 | 26 | `./bin/musig help` 27 | 28 | ## Usage 29 | 30 | ![musig usage](./docs/musig.gif) 31 | 32 | To do some testing you can download `wav` songs by doing `make download`. 33 | 34 | Load them with `./bin/musig load "./assets/dataset/wav/*.wav"` 35 | 36 | And try to find one of your song name with: 37 | 38 | `./bin/musig read "$(ls ./assets/dataset/wav/*.wav | head -n 1)"` 39 | 40 | You can also try to use it with your microphone using the `listen` command: 41 | 42 | `./bin/musig listen` 43 | 44 | If you want to record a sample and reuse it multiple times after you can also use the `record` command: 45 | 46 | `./bin/musig record` 47 | 48 | For more details on the usage see the help command: 49 | 50 | ``` 51 | A shazam like CLI tool 52 | 53 | Usage: 54 | musig [command] 55 | 56 | Available Commands: 57 | help Help about any command 58 | listen listen will record the microphone input and try to find a matching song from the database (Ctrl-C will stop the recording) 59 | load Load loads all the audio files matching the provided glob into the database (TODO: only .wav are supported for now) 60 | read Read reads the given audio file trying to find it's song name 61 | record record will record the microphone input and save the signal to the given file 62 | spectrogram spectrogram generate a spectrogram image for the given audio file in png (TODO: only .wav are supported for now) 63 | 64 | Flags: 65 | --database string database file to use (default "/tmp/musig.bolt") 66 | -h, --help help for musig 67 | 68 | Use "musig [command] --help" for more information about a command. 69 | ``` 70 | 71 | ## Testing 72 | 73 | To run the tests you can use `make test` in the root directory. 74 | 75 | ## TODOs 76 | 77 | - [ ] improve the documentation 78 | - [ ] support for `mp3` files 79 | -------------------------------------------------------------------------------- /scripts/dl_dataset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")" 3 | 4 | # This script downloads the songs listed in the songs.txt file and convert them to wav 5 | # it will always clean the dl directory 6 | 7 | DL_DIR="../assets/dataset/downloads" 8 | WAV_DIR="../assets/dataset/wav" 9 | 10 | # Start by clean the download directory 11 | rm -r $DL_DIR 12 | 13 | # Create the wav directory 14 | mkdir -p $WAV_DIR 15 | 16 | # Download the songs in the dl directory 17 | cat ../assets/songs.txt | xargs wget -q -P $DL_DIR 18 | 19 | echo "Done downloading the songs !" 20 | 21 | # Convert the mp3 files into wav files 22 | for i in $DL_DIR/*.mp3; do 23 | # Remove path from name 24 | name="$(echo $i | sed "s#.*/##")" 25 | ffmpeg -i "$i" -acodec pcm_s16le -ar 44100 "$WAV_DIR/${name%.*}.wav" 26 | done 27 | --------------------------------------------------------------------------------