├── .codeclimate.yml ├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── aiff ├── aiff.go ├── chunk.go ├── decoder.go ├── decoder_test.go ├── doc.go ├── encoder.go ├── encoder_test.go ├── examples_test.go └── fixtures │ ├── bloop.aif │ ├── delivery.aiff │ ├── kick.aif │ ├── kick32b.aiff │ ├── kick8b.aiff │ ├── subsynth.aif │ ├── zipper.aiff │ └── zipper24b.aiff ├── audio.go ├── caf ├── caf.go ├── chunks.go ├── cmd │ └── debug │ │ └── main.go ├── decoder.go ├── decoder_test.go ├── doc.go ├── fixtures │ ├── bass.caf │ └── ring.caf └── metadata.go ├── decoder └── decoder.go ├── demos └── decimator │ └── main.go ├── doc.go ├── dsp ├── analysis │ ├── cmd │ │ └── main.go │ ├── dft.go │ ├── energy.go │ ├── fftshift.go │ └── min_max.go ├── filters │ ├── fir.go │ ├── fir_test.go │ ├── sinc.go │ └── sinc_test.go └── windows │ ├── blackman.go │ ├── blackman_test.go │ ├── function.go │ ├── hamming.go │ └── nuttall.go ├── formats.go ├── generator ├── cmd │ └── main.go ├── generator.go ├── generator_test.go ├── osc.go └── osc_test.go ├── midi ├── README.md ├── cmd │ ├── decoder │ │ └── main.go │ └── gen │ │ └── main.go ├── control_change.go ├── decoder.go ├── decoder_event.go ├── decoder_test.go ├── doc.go ├── encoder.go ├── encoder_test.go ├── event.go ├── event_mapping.go ├── fixtures │ ├── bossa.mid │ ├── closedHat.mid │ ├── elise.mid │ └── elise1track.mid ├── grid.go ├── midi.go ├── note.go ├── note_test.go ├── smpte_offset.go ├── time_signature.go ├── track.go ├── varint.go └── varint_test.go ├── mp3 ├── cmd │ └── validator │ │ └── main.go ├── decoder.go ├── decoder_test.go ├── fixtures │ ├── HousyStab.mp3 │ ├── frame.mp3 │ ├── idv3-24.mp3 │ ├── nullbytes.mp3 │ ├── slayer.mp3 │ └── weird_duration.mp3 ├── frame.go ├── frame_header.go ├── id3v1 │ ├── id3v1.go │ ├── tag.go │ └── tag_plus.go ├── id3v2 │ ├── id3v2.go │ └── tag_header.go └── mp3.go ├── pcm_buffer.go ├── riff ├── README.md ├── chunk.go ├── chunk_test.go ├── doc.go ├── fixtures │ ├── junkKick.wav │ ├── sample.avi │ ├── sample.rmi │ └── sample.wav ├── parser.go ├── parser_test.go ├── riff.go ├── riff_test.go └── riffinfo │ └── main.go ├── transforms ├── bit_crush.go ├── decimator.go ├── doc │ ├── fullwaverectifier.png │ └── source.png ├── filters │ └── filters.go ├── mono.go ├── nominal_scale.go ├── normalize.go ├── pcm_scale.go ├── presenters │ ├── csv.go │ └── gnuplot.go ├── quantize.go ├── resample.go └── transforms.go ├── vendor └── go-dsp │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── dsputils │ ├── compare.go │ ├── dsputils.go │ ├── dsputils_test.go │ ├── matrix.go │ └── matrix_test.go │ ├── fft │ ├── bluestein.go │ ├── fft.go │ ├── fft_test.go │ └── radix2.go │ ├── spectral │ ├── pwelch.go │ ├── pwelch_test.go │ ├── spectral.go │ └── spectral_test.go │ ├── wav │ ├── float.wav │ ├── small.wav │ ├── wav.go │ └── wav_test.go │ └── window │ ├── window.go │ └── window_test.go └── wav ├── decoder.go ├── decoder_test.go ├── encoder.go ├── encoder_test.go ├── examples_test.go ├── fixtures ├── bass.wav ├── dirty-kick-24b441k.wav ├── kick-16b441k.wav ├── kick.wav └── logicBounce.wav └── wav.go /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | fixme: 4 | enabled: true 5 | gofmt: 6 | enabled: true 7 | golint: 8 | enabled: true 9 | govet: 10 | enabled: true 11 | ratings: 12 | paths: [] 13 | exclude_paths: 14 | - vendor/ 15 | - "**_test.go" 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | decoder 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.5.4 4 | - 1.6.3 5 | - 1.7.3 6 | - tip 7 | sudo: false 8 | env: 9 | - GO15VENDOREXPERIMENT=1 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "aiff tests", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "test", 9 | "remotePath": "", 10 | "port": 2345, 11 | "host": "127.0.0.1", 12 | "program": "${workspaceRoot}/aiff", 13 | "env": {}, 14 | "args": [] 15 | }, 16 | { 17 | "name": "wav tests", 18 | "type": "go", 19 | "request": "launch", 20 | "mode": "test", 21 | "remotePath": "", 22 | "port": 2345, 23 | "host": "127.0.0.1", 24 | "program": "${workspaceRoot}/wav", 25 | "env": {}, 26 | "args": [] 27 | }, 28 | { 29 | "name": "mp3 tests", 30 | "type": "go", 31 | "request": "launch", 32 | "mode": "test", 33 | "remotePath": "", 34 | "program": "${workspaceRoot}/mp3", 35 | "env": {}, 36 | "args": [] 37 | }, 38 | { 39 | "name": "generator", 40 | "type": "go", 41 | "request": "launch", 42 | "mode": "debug", 43 | "remotePath": "", 44 | "port": 2345, 45 | "host": "127.0.0.1", 46 | "program": "${workspaceRoot}/generator/cmd", 47 | "env": {}, 48 | "args": [] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Matt Aimonetti 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Production ready/stable version available under a dedicated org 2 | 3 | [go-audio](https://github.com/go-audio) for the non-playground version. 4 | 5 | ## See sub packages 6 | 7 | [![GoDoc](http://godoc.org/github.com/mattetti/audio?status.svg)](http://godoc.org/github.com/mattetti/audio) 8 | 9 | [![Build 10 | Status](https://travis-ci.org/mattetti/audio.png)](https://travis-ci.org/mattetti/audio) 11 | -------------------------------------------------------------------------------- /aiff/aiff.go: -------------------------------------------------------------------------------- 1 | package aiff 2 | 3 | import "errors" 4 | 5 | var ( 6 | formID = [4]byte{'F', 'O', 'R', 'M'} 7 | aiffID = [4]byte{'A', 'I', 'F', 'F'} 8 | aifcID = [4]byte{'A', 'I', 'F', 'C'} 9 | COMMID = [4]byte{'C', 'O', 'M', 'M'} 10 | SSNDID = [4]byte{'S', 'S', 'N', 'D'} 11 | 12 | // AIFC encodings 13 | encNone = [4]byte{'N', 'O', 'N', 'E'} 14 | // inverted byte order LE instead of BE (not really compression) 15 | encSowt = [4]byte{'s', 'o', 'w', 't'} 16 | // inverted byte order LE instead of BE (not really compression) 17 | encTwos = [4]byte{'t', 'w', 'o', 's'} 18 | encRaw = [4]byte{'r', 'a', 'w', ' '} 19 | encIn24 = [4]byte{'i', 'n', '2', '4'} 20 | enc42n1 = [4]byte{'4', '2', 'n', '1'} 21 | encIn32 = [4]byte{'i', 'n', '3', '2'} 22 | enc23ni = [4]byte{'2', '3', 'n', 'i'} 23 | 24 | encFl32 = [4]byte{'f', 'l', '3', '2'} 25 | encFL32 = [4]byte{'F', 'L', '3', '2'} 26 | encFl64 = [4]byte{'f', 'l', '6', '4'} 27 | encFL64 = [4]byte{'F', 'L', '6', '4'} 28 | 29 | envUlaw = [4]byte{'u', 'l', 'a', 'w'} 30 | encULAW = [4]byte{'U', 'L', 'A', 'W'} 31 | encAlaw = [4]byte{'a', 'l', 'a', 'w'} 32 | encALAW = [4]byte{'A', 'L', 'A', 'W'} 33 | 34 | encDwvw = [4]byte{'D', 'W', 'V', 'W'} 35 | encGsm = [4]byte{'G', 'S', 'M', ' '} 36 | encIma4 = [4]byte{'i', 'm', 'a', '4'} 37 | 38 | // ErrFmtNotSupported is a generic error reporting an unknown format. 39 | ErrFmtNotSupported = errors.New("format not supported") 40 | // ErrUnexpectedData is a generic error reporting that the parser encountered unexpected data. 41 | ErrUnexpectedData = errors.New("unexpected data content") 42 | ) 43 | -------------------------------------------------------------------------------- /aiff/chunk.go: -------------------------------------------------------------------------------- 1 | package aiff 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | ) 9 | 10 | // Chunk is a struct representing a data chunk 11 | // the reader is shared with the container but convenience methods 12 | // are provided. 13 | // The reader always starts at the beggining of the data. 14 | // SSND chunk is the sound chunk 15 | // Chunk specs: 16 | // http://www.onicos.com/staff/iz/formats/aiff.html 17 | // AFAn seems to be an OS X specific chunk, meaning & format TBD 18 | type Chunk struct { 19 | ID [4]byte 20 | Size int 21 | R io.Reader 22 | Pos int 23 | } 24 | 25 | // Done makes sure the entire chunk was read. 26 | func (ch *Chunk) Done() { 27 | if !ch.IsFullyRead() { 28 | ch.drain() 29 | } 30 | } 31 | 32 | func (ch *Chunk) drain() error { 33 | bytesAhead := ch.Size - ch.Pos 34 | if bytesAhead > 0 { 35 | _, err := io.CopyN(ioutil.Discard, ch.R, int64(bytesAhead)) 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | // Read implements the reader interface 42 | func (ch *Chunk) Read(p []byte) (n int, err error) { 43 | if ch == nil || ch.R == nil { 44 | return 0, errors.New("nil chunk/reader pointer") 45 | } 46 | n, err = ch.R.Read(p) 47 | ch.Pos += n 48 | return n, err 49 | } 50 | 51 | // ReadLE reads the Little Endian chunk data into the passed struct 52 | func (ch *Chunk) ReadLE(dst interface{}) error { 53 | if ch == nil || ch.R == nil { 54 | return errors.New("nil chunk/reader pointer") 55 | } 56 | if ch.IsFullyRead() { 57 | return io.EOF 58 | } 59 | ch.Pos += binary.Size(dst) 60 | return binary.Read(ch.R, binary.LittleEndian, dst) 61 | } 62 | 63 | // ReadBE reads the Big Endian chunk data into the passed struct 64 | func (ch *Chunk) ReadBE(dst interface{}) error { 65 | if ch.IsFullyRead() { 66 | return io.EOF 67 | } 68 | ch.Pos += binary.Size(dst) 69 | return binary.Read(ch.R, binary.BigEndian, dst) 70 | } 71 | 72 | // ReadByte reads and returns a single byte 73 | func (ch *Chunk) ReadByte() (byte, error) { 74 | if ch.IsFullyRead() { 75 | return 0, io.EOF 76 | } 77 | var r byte 78 | err := ch.ReadLE(&r) 79 | return r, err 80 | } 81 | 82 | // IsFullyRead checks if we're finished reading the chunk 83 | func (ch *Chunk) IsFullyRead() bool { 84 | if ch == nil || ch.R == nil { 85 | return true 86 | } 87 | return ch.Size <= ch.Pos 88 | } 89 | 90 | // Jump jumps ahead in the chunk 91 | func (ch *Chunk) Jump(bytesAhead int) error { 92 | var err error 93 | var n int64 94 | if bytesAhead > 0 { 95 | n, err = io.CopyN(ioutil.Discard, ch.R, int64(bytesAhead)) 96 | ch.Pos += int(n) 97 | } 98 | return err 99 | } 100 | -------------------------------------------------------------------------------- /aiff/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package aiff is a AIFF/AIFC decoder and encoder. 3 | It extracts the basic information of the file and provide decoded frames for AIFF files. 4 | 5 | 6 | This package also allows for quick access to the AIFF LPCM raw audio data: 7 | 8 | in, err := os.Open("audiofile.aiff") 9 | if err != nil { 10 | log.Fatal("couldn't open audiofile.aiff %v", err) 11 | } 12 | d := NewDecoder(in) 13 | frames, err := d.Frames() 14 | in.Close() 15 | 16 | A frame is a slice where each entry is a channel and each value is the sample value. 17 | For instance, a frame in a stereo file will have 2 entries (left and right) and each entry will 18 | have the value of the sample. 19 | 20 | Note that this approach isn't memory efficient at all. In most cases 21 | you want to access the decoder's Clip. 22 | You can read the clip's frames in a buffer that you decode in small chunks. 23 | 24 | Finally, the encoder allows the encoding of LPCM audio data into a valid AIFF file. 25 | Look at the encoder_test.go file for a more complete example. 26 | 27 | Currently only AIFF is properly supported, AIFC files will more than likely not be properly processed. 28 | 29 | */ 30 | package aiff 31 | -------------------------------------------------------------------------------- /aiff/encoder_test.go: -------------------------------------------------------------------------------- 1 | package aiff_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "os" 7 | "testing" 8 | 9 | "github.com/mattetti/audio/aiff" 10 | ) 11 | 12 | // TODO(mattetti): switch to using github.com/mattetti/filebuffer 13 | 14 | func TestEncoderRoundTrip(t *testing.T) { 15 | os.Mkdir("testOutput", 0777) 16 | testCases := []struct { 17 | in string 18 | out string 19 | // a round trip decoding/encoding doesn't mean we get a perfect match 20 | // in this test, we do drop extra chunks 21 | perfectMatch bool 22 | }{ 23 | // 22050, 8bit, mono 24 | {"fixtures/kick8b.aiff", "testOutput/kick8b.aiff", true}, 25 | // 22050, 16bit, mono (extra chunk) 26 | {"fixtures/kick.aif", "testOutput/kick.aif", false}, 27 | // 22050, 16bit, mono 28 | {"fixtures/kick32b.aiff", "testOutput/kick32b.aiff", true}, 29 | // 44100, 16bit, mono 30 | {"fixtures/subsynth.aif", "testOutput/subsynth.aif", true}, 31 | // 44100, 16bit, stereo 32 | {"fixtures/bloop.aif", "testOutput/bloop.aif", true}, 33 | // 48000, 16bit, stereo 34 | {"fixtures/zipper.aiff", "testOutput/zipper.aiff", true}, 35 | // 48000, 24bit, stereo 36 | {"fixtures/zipper24b.aiff", "testOutput/zipper24b.aiff", true}, 37 | } 38 | 39 | for i, tc := range testCases { 40 | t.Logf("%d - in: %s, out: %s", i, tc.in, tc.out) 41 | in, err := os.Open(tc.in) 42 | if err != nil { 43 | t.Fatalf("couldn't open %s %v", tc.in, err) 44 | } 45 | d := aiff.NewDecoder(in) 46 | buf, err := d.FullPCMBuffer() 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | defer in.Close() 51 | 52 | // encode the decoded file 53 | out, err := os.Create(tc.out) 54 | if err != nil { 55 | t.Fatalf("couldn't create %s %v", tc.out, err) 56 | } 57 | 58 | e := aiff.NewEncoder(out, int(d.SampleRate), int(d.BitDepth), int(d.NumChans)) 59 | if err := e.Write(buf); err != nil { 60 | t.Fatal(err) 61 | } 62 | if err := e.Close(); err != nil { 63 | t.Fatal(err) 64 | } 65 | if err := out.Close(); err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | // open the re-encoded file 70 | nf, err := os.Open(tc.out) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | d2 := aiff.NewDecoder(nf) 76 | d2.ReadInfo() 77 | // TODO(mattetti): using d2.Duration() messes the later Frames() call 78 | info, err := nf.Stat() 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | expectedHeaderSize := info.Size() - 8 83 | if d.Size != d2.Size { 84 | t.Logf("the encoded size didn't match the original, expected: %d, got %d", d.Size, d2.Size) 85 | } 86 | if expectedHeaderSize != int64(d2.Size) { 87 | t.Fatalf("wrong header size data, expected %d, got %d", expectedHeaderSize, d2.Size) 88 | } 89 | d2buf, err := d2.FullPCMBuffer() 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | if d2.SampleRate != d.SampleRate { 94 | t.Fatalf("sample rate didn't support roundtripping exp: %d, got: %d", d.SampleRate, d2.SampleRate) 95 | } 96 | if d2.BitDepth != d.BitDepth { 97 | t.Fatalf("sample size didn't support roundtripping exp: %d, got: %d", d.BitDepth, d2.BitDepth) 98 | } 99 | if d2.NumChans != d.NumChans { 100 | t.Fatalf("the number of channels didn't support roundtripping exp: %d, got: %d", d.NumChans, d2.NumChans) 101 | } 102 | 103 | if buf.Size() != d2buf.Size() { 104 | t.Fatalf("the number of frames didn't support roundtripping, exp: %d, got: %d", buf.Size(), d2buf.Size()) 105 | } 106 | originalSamples := buf.AsInts() 107 | newSamples := d2buf.AsInts() 108 | for i := 0; i < len(originalSamples); i++ { 109 | if originalSamples[i] != newSamples[i] { 110 | t.Fatalf("%d didn't match, expected %d, got %d", i, originalSamples[i], newSamples[i]) 111 | } 112 | } 113 | 114 | if tc.perfectMatch { 115 | // binary comparison 116 | in.Seek(0, 0) 117 | nf.Seek(0, 0) 118 | buf1 := make([]byte, 32) 119 | buf2 := make([]byte, 32) 120 | 121 | var err1, err2 error 122 | var n int 123 | readBytes := 0 124 | for err1 == nil && err2 == nil { 125 | n, err1 = in.Read(buf1) 126 | _, err2 = nf.Read(buf2) 127 | readBytes += n 128 | if bytes.Compare(buf1, buf2) != 0 { 129 | t.Fatalf("round trip failed, data differed after %d bytes\n%s\n%s", readBytes, hex.Dump(buf1), hex.Dump(buf2)) 130 | } 131 | } 132 | } 133 | 134 | nf.Close() 135 | os.Remove(nf.Name()) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /aiff/examples_test.go: -------------------------------------------------------------------------------- 1 | package aiff_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/mattetti/audio/aiff" 10 | ) 11 | 12 | func ExampleDecoder_Duration() { 13 | path, _ := filepath.Abs("fixtures/kick.aif") 14 | f, err := os.Open(path) 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer f.Close() 19 | 20 | c := aiff.NewDecoder(f) 21 | d, _ := c.Duration() 22 | fmt.Printf("kick.aif has a duration of %f seconds\n", d.Seconds()) 23 | // Output: 24 | // kick.aif has a duration of 0.203356 seconds 25 | } 26 | 27 | func ExampleDecoder_IsValidFile() { 28 | f, err := os.Open("fixtures/kick.aif") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer f.Close() 33 | fmt.Printf("is this file valid: %t", aiff.NewDecoder(f).IsValidFile()) 34 | // Output: is this file valid: true 35 | } 36 | -------------------------------------------------------------------------------- /aiff/fixtures/bloop.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/aiff/fixtures/bloop.aif -------------------------------------------------------------------------------- /aiff/fixtures/delivery.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/aiff/fixtures/delivery.aiff -------------------------------------------------------------------------------- /aiff/fixtures/kick.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/aiff/fixtures/kick.aif -------------------------------------------------------------------------------- /aiff/fixtures/kick32b.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/aiff/fixtures/kick32b.aiff -------------------------------------------------------------------------------- /aiff/fixtures/kick8b.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/aiff/fixtures/kick8b.aiff -------------------------------------------------------------------------------- /aiff/fixtures/subsynth.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/aiff/fixtures/subsynth.aif -------------------------------------------------------------------------------- /aiff/fixtures/zipper.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/aiff/fixtures/zipper.aiff -------------------------------------------------------------------------------- /aiff/fixtures/zipper24b.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/aiff/fixtures/zipper24b.aiff -------------------------------------------------------------------------------- /audio.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "math" 4 | 5 | var ( 6 | // RootA or concert A is the reference frequency for A4. 7 | // Modify this package variable if you need to change it to 435 (classical) or 8 | // 415 (baroque). Methods refering to this root A note will use this variable. 9 | RootA = 440.0 10 | ) 11 | 12 | // AvgInt averages the int values passed 13 | func AvgInt(xs ...int) int { 14 | var output int 15 | for i := 0; i < len(xs); i++ { 16 | output += xs[i] 17 | } 18 | return output / len(xs) 19 | } 20 | 21 | // IntMaxSignedValue returns the max value of an integer 22 | // based on its memory size 23 | func IntMaxSignedValue(b int) int { 24 | switch b { 25 | case 8: 26 | return 255 / 2 27 | case 16: 28 | return 65535 / 2 29 | case 24: 30 | return 16777215 / 2 31 | case 32: 32 | return 4294967295 / 2 33 | default: 34 | return 0 35 | } 36 | } 37 | 38 | // IeeeFloatToInt converts a 10 byte IEEE float into an int. 39 | func IeeeFloatToInt(b [10]byte) int { 40 | var i uint32 41 | // Negative number 42 | if (b[0] & 0x80) == 1 { 43 | return 0 44 | } 45 | 46 | // Less than 1 47 | if b[0] <= 0x3F { 48 | return 1 49 | } 50 | 51 | // Too big 52 | if b[0] > 0x40 { 53 | return 67108864 54 | } 55 | 56 | // Still too big 57 | if b[0] == 0x40 && b[1] > 0x1C { 58 | return 800000000 59 | } 60 | 61 | i = (uint32(b[2]) << 23) | (uint32(b[3]) << 15) | (uint32(b[4]) << 7) | (uint32(b[5]) >> 1) 62 | i >>= (29 - uint32(b[1])) 63 | 64 | return int(i) 65 | } 66 | 67 | // IntToIeeeFloat converts an int into a 10 byte IEEE float. 68 | func IntToIeeeFloat(i int) [10]byte { 69 | b := [10]byte{} 70 | num := float64(i) 71 | 72 | var sign int 73 | var expon int 74 | var fMant, fsMant float64 75 | var hiMant, loMant uint 76 | 77 | if num < 0 { 78 | sign = 0x8000 79 | } else { 80 | sign = 0 81 | } 82 | 83 | if num == 0 { 84 | expon = 0 85 | hiMant = 0 86 | loMant = 0 87 | } else { 88 | fMant, expon = math.Frexp(num) 89 | if (expon > 16384) || !(fMant < 1) { /* Infinity or NaN */ 90 | expon = sign | 0x7FFF 91 | hiMant = 0 92 | loMant = 0 /* infinity */ 93 | } else { /* Finite */ 94 | expon += 16382 95 | if expon < 0 { /* denormalized */ 96 | fMant = math.Ldexp(fMant, expon) 97 | expon = 0 98 | } 99 | expon |= sign 100 | fMant = math.Ldexp(fMant, 32) 101 | fsMant = math.Floor(fMant) 102 | hiMant = uint(fsMant) 103 | fMant = math.Ldexp(fMant-fsMant, 32) 104 | fsMant = math.Floor(fMant) 105 | loMant = uint(fsMant) 106 | } 107 | } 108 | 109 | b[0] = byte(expon >> 8) 110 | b[1] = byte(expon) 111 | b[2] = byte(hiMant >> 24) 112 | b[3] = byte(hiMant >> 16) 113 | b[4] = byte(hiMant >> 8) 114 | b[5] = byte(hiMant) 115 | b[6] = byte(loMant >> 24) 116 | b[7] = byte(loMant >> 16) 117 | b[8] = byte(loMant >> 8) 118 | b[9] = byte(loMant) 119 | 120 | return b 121 | } 122 | 123 | // Uint24to32 converts a 3 byte uint23 into a uint32 124 | // BigEndian! 125 | func Uint24to32(bytes []byte) uint32 { 126 | var output uint32 127 | output |= uint32(bytes[2]) << 0 128 | output |= uint32(bytes[1]) << 8 129 | output |= uint32(bytes[0]) << 16 130 | 131 | return output 132 | } 133 | 134 | // Uint32toUint24Bytes converts a uint32 into a 3 byte uint24 representation 135 | func Uint32toUint24Bytes(n uint32) []byte { 136 | bytes := make([]byte, 3) 137 | bytes[0] = byte(n >> 16) 138 | bytes[1] = byte(n >> 8) 139 | bytes[2] = byte(n >> 0) 140 | 141 | return bytes 142 | } 143 | 144 | // Int32toInt24LEBytes converts a int32 into a 3 byte int24 representation 145 | func Int32toInt24LEBytes(n int32) []byte { 146 | bytes := make([]byte, 3) 147 | bytes[2] = byte(n >> 24) 148 | bytes[1] = byte(n >> 16) 149 | bytes[0] = byte(n >> 8) 150 | return bytes 151 | } 152 | -------------------------------------------------------------------------------- /caf/caf.go: -------------------------------------------------------------------------------- 1 | package caf 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | var ( 9 | fileHeaderID = [4]byte{'c', 'a', 'f', 'f'} 10 | 11 | // Chunk IDs 12 | StreamDescriptionChunkID = [4]byte{'d', 'e', 's', 'c'} 13 | AudioDataChunkID = [4]byte{'d', 'a', 't', 'a'} 14 | ChannelLayoutChunkID = [4]byte{'c', 'h', 'a', 'n'} 15 | FillerChunkID = [4]byte{'f', 'r', 'e', 'e'} 16 | MarkerChunkID = [4]byte{'m', 'a', 'r', 'k'} 17 | RegionChunkID = [4]byte{'r', 'e', 'g', 'n'} 18 | InstrumentChunkID = [4]byte{'i', 'n', 's', 't'} 19 | MagicCookieID = [4]byte{'k', 'u', 'k', 'i'} 20 | InfoStringsChunkID = [4]byte{'i', 'n', 'f', 'o'} 21 | EditCommentsChunkID = [4]byte{'e', 'd', 'c', 't'} 22 | PacketTableChunkID = [4]byte{'p', 'a', 'k', 't'} 23 | StringsChunkID = [4]byte{'s', 't', 'r', 'g'} 24 | UUIDChunkID = [4]byte{'u', 'u', 'i', 'd'} 25 | PeakChunkID = [4]byte{'p', 'e', 'a', 'k'} 26 | OverviewChunkID = [4]byte{'o', 'v', 'v', 'w'} 27 | MIDIChunkID = [4]byte{'m', 'i', 'd', 'i'} 28 | UMIDChunkID = [4]byte{'u', 'm', 'i', 'd'} 29 | FormatListID = [4]byte{'l', 'd', 's', 'c'} 30 | IXMLChunkID = [4]byte{'i', 'X', 'M', 'L'} 31 | 32 | // Format IDs 33 | // Linear PCM 34 | AudioFormatLinearPCM = [4]byte{'l', 'p', 'c', 'm'} 35 | // Apple’s implementation of IMA 4:1 ADPCM. Has no format flags. 36 | AudioFormatAppleIMA4 = [4]byte{'i', 'm', 'a', '4'} 37 | // MPEG-4 AAC. The mFormatFlags field must contain the MPEG-4 audio object type constant indicating the specific kind of data. 38 | AudioFormatMPEG4AAC = [4]byte{'a', 'a', 'c', ' '} 39 | // MACE 3:1; has no format flags. 40 | AudioFormatMACE3 = [4]byte{'M', 'A', 'C', '3'} 41 | // MACE 6:1; has no format flags. 42 | AudioFormatMACE6 = [4]byte{'M', 'A', 'C', '6'} 43 | // μLaw 2:1; has no format flags. 44 | AudioFormatULaw = [4]byte{'u', 'l', 'a', 'w'} 45 | // aLaw 2:1; has no format flags. 46 | AudioFormatALaw = [4]byte{'a', 'l', 'a', 'w'} 47 | // MPEG-1 or 2, Layer 1 audio. Has no format flags. 48 | AudioFormatMPEGLayer1 = [4]byte{'.', 'm', 'p', '1'} 49 | // MPEG-1 or 2, Layer 2 audio. Has no format flags. 50 | AudioFormatMPEGLayer2 = [4]byte{'.', 'm', 'p', '2'} 51 | // MPEG-1 or 2, Layer 3 audio (that is, MP3). Has no format flags. 52 | AudioFormatMPEGLayer3 = [4]byte{'.', 'm', 'p', '3'} 53 | // Apple Lossless; has no format flags. 54 | AudioFormatAppleLossless = [4]byte{'a', 'l', 'a', 'c'} 55 | 56 | // ErrFmtNotSupported is a generic error reporting an unknown format. 57 | ErrFmtNotSupported = errors.New("format not supported") 58 | // ErrUnexpectedData is a generic error reporting that the parser encountered unexpected data. 59 | ErrUnexpectedData = errors.New("unexpected data content") 60 | ) 61 | 62 | // NewDecoder creates a new reader reading the given reader. It is the caller's 63 | // responsibility to call Close on the reader when done. 64 | func NewDecoder(r io.ReadSeeker) *Decoder { 65 | return &Decoder{r: r} 66 | } 67 | -------------------------------------------------------------------------------- /caf/chunks.go: -------------------------------------------------------------------------------- 1 | package caf 2 | 3 | type stringsChunk struct { 4 | // The number of information strings in the chunk. Must always be valid. 5 | numEntries uint32 6 | // Apple reserves keys that are all lowercase (see Information Entry Keys). 7 | // Application-defined keys should include at least one uppercase character. 8 | // For any key that ends with ' date' (that is, the space character followed 9 | // by the word 'date'—for example, 'recorded date'), the value must be a 10 | // time-of-day string. See Time Of Day Data Format. Using a '.' (period) 11 | // character as the first character of a key means that the key-value pair 12 | // is not to be displayed. This allows you to store private information that 13 | // should be preserved by other applications but not displayed to a user. 14 | stringID map[string]string 15 | } 16 | -------------------------------------------------------------------------------- /caf/cmd/debug/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/mattetti/audio/caf" 9 | ) 10 | 11 | func main() { 12 | if len(os.Args) < 2 { 13 | log.Fatalf("You need to pass the path of the file to analyze.") 14 | } 15 | 16 | path := os.Args[1] 17 | fmt.Println(path) 18 | f, err := os.Open(path) 19 | if err != nil { 20 | log.Fatalf("Failed to open the passed path - %v", err) 21 | } 22 | defer f.Close() 23 | 24 | d := caf.NewDecoder(f) 25 | if err = d.ReadInfo(); err != nil { 26 | log.Fatalf("Failed to read information - %v", err) 27 | } 28 | /* 29 | var chk *chunk.Reader 30 | for err == nil { 31 | chk, err = d.NextChunk() 32 | if err == nil { 33 | fmt.Println(string(chk.ID[:])) 34 | chk.Done() 35 | } 36 | } 37 | */ 38 | fmt.Println(d) 39 | } 40 | -------------------------------------------------------------------------------- /caf/decoder_test.go: -------------------------------------------------------------------------------- 1 | package caf 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/mattetti/filebuffer" 9 | ) 10 | 11 | func TestBadFileHeaderData(t *testing.T) { 12 | r := filebuffer.New([]byte{'m', 'a', 't', 't', 0, 0, 0}) 13 | d := NewDecoder(r) 14 | if err := d.ReadInfo(); err == nil { 15 | t.Fatalf("Expected bad data to return %s", ErrFmtNotSupported) 16 | } 17 | 18 | r = filebuffer.New([]byte{'c', 'a', 'f', 'f', 2, 0, 0}) 19 | d = NewDecoder(r) 20 | if err := d.ReadInfo(); err == nil { 21 | t.Fatalf("Expected bad data to return %s", ErrFmtNotSupported) 22 | } 23 | } 24 | 25 | func TestParsingFile(t *testing.T) { 26 | expectations := []struct { 27 | path string 28 | format [4]byte 29 | version uint16 30 | flags uint16 31 | }{ 32 | {"fixtures/ring.caf", fileHeaderID, 1, 0}, 33 | {"fixtures/bass.caf", fileHeaderID, 1, 0}, 34 | } 35 | 36 | for _, exp := range expectations { 37 | t.Run(exp.path, func(t *testing.T) { 38 | path, _ := filepath.Abs(exp.path) 39 | f, err := os.Open(path) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | defer f.Close() 44 | d := NewDecoder(f) 45 | if err := d.ReadInfo(); err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | if d.Format != exp.format { 50 | t.Fatalf("%s of %s didn't match %v, got %v", "format", exp.path, exp.format, d.Format) 51 | } 52 | if d.Version != exp.version { 53 | t.Fatalf("%s of %s didn't match %d, got %v", "version", exp.path, exp.version, d.Version) 54 | } 55 | if d.Flags != exp.flags { 56 | t.Fatalf("%s of %s didn't match %d, got %v", "flags", exp.path, exp.flags, d.Flags) 57 | } 58 | 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /caf/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | caf package implements an Apple Core Audio Format (CAF) parser. 3 | 4 | Apple’s Core Audio Format (CAF) is a file format (container) for storing and transporting digital audio data. 5 | It simplifies the management and manipulation of many types of audio data without the file-size limitations 6 | of other audio file formats. 7 | CAF provides high performance and flexibility and is scalable to future ultra-high resolution audio recording, editing, and playback. 8 | CAF files can contain audio or even define patches, or musical voice configurations. 9 | 10 | The primary goal of this package is to allow the transcoding/conversion of CAF files to other formats. 11 | Note that because CAF fiels contain other metadata than just audio data, the conversion will be lossy, not in the sound 12 | quality meaning of the sense but in the data senses. 13 | 14 | 15 | That said here is some information about CAF provided by Apple. 16 | 17 | CAF files have several advantages over other standard audio file formats: 18 | 19 | * Unrestricted file size 20 | Whereas AIFF, AIFF-C, and WAV files are limited in size to 4 gigabytes, which might represent as little as 15 minutes of audio, 21 | CAF files use 64-bit file offsets, eliminating practical limits. 22 | A standard CAF file can hold audio data with a playback duration of hundreds of years. 23 | 24 | * Safe and efficient recording 25 | Applications writing AIFF and WAV files must either update the data header’s size field at the end of recording—which can result in an unusable file 26 | if recording is interrupted before the header is finalized—or they must update the size field after recording each packet of data, which is inefficient. With CAF files, 27 | in contrast, an application can append new audio data to the end of the file in a manner that allows it 28 | to determine the amount of data even if the size field in the header has not been finalized. 29 | 30 | * Support for many data formats 31 | CAF files serve as wrappers for a wide variety of audio data formats. 32 | The flexibility of the CAF file structure and the many types of metadata that can be recorded enable CAF files to be used with practically any type of audio data. 33 | Furthermore, CAF files can store any number of audio channels. 34 | 35 | * Support for many types of auxiliary data 36 | In addition to audio data, CAF files can store text annotations, markers, channel layouts, and many other types of information that can help in the interpretation, 37 | analysis, or editing of the audio. 38 | 39 | * Support for data dependencies 40 | Certain metadata in CAF files is linked to the audio data by an edit count value. 41 | You can use this value to determine when metadata has a dependency on the audio data and, furthermore, 42 | when the audio data has changed since the metadata was written. 43 | 44 | 45 | Vocab: 46 | 47 | * Sample 48 | One number for one channel of digitized audio data. 49 | 50 | * Frame 51 | A set of samples representing one sample for each channel. The samples in a frame are intended to be played together (that is, simultaneously). 52 | Note that this definition might be different from the use of the term “frame” by codecs, video files, and audio or video processing applications. 53 | 54 | * Packet 55 | The smallest, indivisible block of data. For linear PCM (pulse-code modulated) data, each packet contains exactly one frame. 56 | For compressed audio data formats, the number of frames in a packet depends on the encoding. 57 | For example, a packet of AAC represents 1024 frames of PCM. In some formats, the number of frames per packet varies. 58 | 59 | * Sample rate 60 | The number of complete frames of samples per second of noncompressed or decompressed data. 61 | 62 | 63 | 64 | The format is documented by Apple at 65 | https://developer.apple.com/library/mac/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html#//apple_ref/doc/uid/TP40001862-CH210-TPXREF101 66 | 67 | */ 68 | package caf 69 | -------------------------------------------------------------------------------- /caf/fixtures/bass.caf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/caf/fixtures/bass.caf -------------------------------------------------------------------------------- /caf/fixtures/ring.caf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/caf/fixtures/ring.caf -------------------------------------------------------------------------------- /caf/metadata.go: -------------------------------------------------------------------------------- 1 | package caf 2 | 3 | // Metadata represent the amount of metadata one can store/retrieve from a caf file. 4 | // See https://developer.apple.com/library/content/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html 5 | type Metadata struct { 6 | // instrument chunk 7 | 8 | // BaseNote The MIDI note number, and fractional pitch, for 9 | // the base note of the MIDI instrument. The integer portion of this field 10 | // indicates the base note, in the integer range 0 to 127, where a value of 11 | // 60 represents middle C and each integer is a step on a standard piano 12 | // keyboard (for example, 61 is C# above middle C). The fractional part of 13 | // the field specifies the fractional pitch; for example, 60.5 is a pitch 14 | // halfway between notes 60 and 61. 15 | BaseNote float32 16 | // MIDILowNote The lowest note for the region, in the integer range 0 to 17 | // 127, where a value of 60 represents middle C (following the MIDI 18 | // convention). This value represents the suggested lowest note on a 19 | // keyboard for playback of this instrument definition. The sound data 20 | // should be played if the instrument is requested to play a note between 21 | // MIDILowNote and MIDIHighNote, inclusive. The BaseNote value must be 22 | // within this range. 23 | MIDILowNote uint8 24 | // MIDIHighNote The highest note for the region when used as a MIDI 25 | // instrument, in the integer range 0 to 127, where a value of 60 represents 26 | // middle C. See the discussions of the BaseNote and MIDILowNote fields 27 | // for more information. 28 | MIDIHighNote uint8 29 | // MIDILowVelocity The lowest MIDI velocity for playing the region , in the integer range 0 to 127. 30 | MIDILowVelocity uint8 31 | MIDIHighVelocity uint8 32 | DBGain float32 33 | StartRegionID uint32 34 | SustainRegionID uint32 35 | ReleaseRegionID uint32 36 | InstrumentID uint32 37 | // 38 | } 39 | -------------------------------------------------------------------------------- /decoder/decoder.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/go-audio/aiff" 10 | "github.com/go-audio/audio" 11 | "github.com/go-audio/wav" 12 | ) 13 | 14 | var ( 15 | ErrInvalidPath = errors.New("invalid path") 16 | ) 17 | 18 | type Format string 19 | 20 | var ( 21 | Unknown Format = "unknown" 22 | Wav Format = "wav" 23 | Aif Format = "aiff" 24 | ) 25 | 26 | type Decoder interface { 27 | FullPCMBuffer() (*audio.IntBuffer, error) 28 | PCMLen() int64 29 | FwdToPCM() error 30 | PCMBuffer(*audio.IntBuffer) (int, error) 31 | WasPCMAccessed() bool 32 | SampleBitDepth() int32 33 | Format() *audio.Format 34 | Err() error 35 | } 36 | 37 | // FileFormat returns the known format of the passed path. 38 | func FileFormat(path string) (Format, error) { 39 | if !fileExists(path) { 40 | return "", ErrInvalidPath 41 | } 42 | f, err := os.Open(path) 43 | if err != nil { 44 | return "", err 45 | } 46 | defer f.Close() 47 | var triedWav bool 48 | var triedAif bool 49 | 50 | ext := strings.ToLower(filepath.Ext(path)) 51 | switch ext { 52 | case ".wav", ".wave": 53 | triedWav = true 54 | d := wav.NewDecoder(f) 55 | if d.IsValidFile() { 56 | return Wav, nil 57 | } 58 | case ".aif", ".aiff": 59 | triedAif = true 60 | d := aiff.NewDecoder(f) 61 | if d.IsValidFile() { 62 | return Aif, nil 63 | } 64 | } 65 | // extension doesn't match, let's try again 66 | f.Seek(0, 0) 67 | if !triedWav { 68 | wd := wav.NewDecoder(f) 69 | if wd.IsValidFile() { 70 | return Wav, nil 71 | } 72 | f.Seek(0, 0) 73 | } 74 | if !triedAif { 75 | ad := aiff.NewDecoder(f) 76 | if ad.IsValidFile() { 77 | return Aif, nil 78 | } 79 | } 80 | return Unknown, nil 81 | } 82 | 83 | // helper checking if a file exists 84 | func fileExists(path string) bool { 85 | if _, err := os.Stat(path); os.IsNotExist(err) { 86 | return false 87 | } 88 | return true 89 | } 90 | -------------------------------------------------------------------------------- /demos/decimator/main.go: -------------------------------------------------------------------------------- 1 | // given a PCM audio file, convert it to mono and decimates it. 2 | // Because of nyquist law, we can't simply average or drop samples otherwise we will have alisasing. 3 | // A proper decimation is needed http://dspguru.com/dsp/faqs/multirate/decimation 4 | // Decimation is useful to reduce the amount of data to process. 5 | // Max hearing frequency would be around 20kHz, so we need to low pass to remove anything above 20kHz so 6 | // we don't get any aliasing. 7 | package main 8 | 9 | import ( 10 | "flag" 11 | "fmt" 12 | "os" 13 | 14 | "github.com/mattetti/audio" 15 | "github.com/mattetti/audio/generator" 16 | "github.com/mattetti/audio/transforms" 17 | "github.com/mattetti/audio/wav" 18 | ) 19 | 20 | var ( 21 | fileFlag = flag.String("file", "", "file to downsample (copy will be done)") 22 | factorFlag = flag.Int("factor", 2, "The decimator factor divides the sampling rate") 23 | outputFlag = flag.String("format", "aiff", "output format, aiff or wav") 24 | ) 25 | 26 | func main() { 27 | flag.Parse() 28 | 29 | if *fileFlag == "" { 30 | freq := audio.RootA 31 | fs := 44100 32 | fmt.Printf("Resampling from %dHz to %dHz back to %dHz\n", fs, fs / *factorFlag, fs) 33 | 34 | // generate a wave sine 35 | osc := generator.NewOsc(generator.WaveSine, freq, fs) 36 | data := osc.Signal(fs * 4) 37 | buf := audio.NewPCMFloatBuffer(data, audio.FormatMono4410016bBE) 38 | // drop the sample rate 39 | if err := transforms.Decimate(buf, *factorFlag); err != nil { 40 | panic(err) 41 | } 42 | 43 | fmt.Println("bit crushing to 8 bit sound") 44 | // the bitcrusher switches the data range to PCM scale 45 | transforms.BitCrush(buf, 8) 46 | transforms.Resample(buf, float64(fs)) 47 | 48 | // sideBuf := buf.Clone() 49 | // sideBuf.SwitchPrimaryType(audio.Float) 50 | // truncate 51 | // sideBuf.Floats = sideBuf.Floats[:1024] 52 | // transforms.NormalizeMax(sideBuf) 53 | // export as a gnuplot binary file 54 | // if err := presenters.GnuplotText(sideBuf, "decimator.dat"); err != nil { 55 | // panic(err) 56 | // } 57 | // if err := presenters.CSV(sideBuf, "data.csv"); err != nil { 58 | // panic(err) 59 | // } 60 | 61 | // encode the sound file 62 | o, err := os.Create("resampled.wav") 63 | if err != nil { 64 | panic(err) 65 | } 66 | defer o.Close() 67 | e := wav.NewEncoder(o, buf.Format.SampleRate, buf.Format.BitDepth, 1, 1) 68 | if err := e.Write(buf); err != nil { 69 | panic(err) 70 | } 71 | e.Close() 72 | fmt.Println("checkout resampled.wav") 73 | return 74 | } 75 | 76 | /* 77 | ext := filepath.Ext(*fileFlag) 78 | var codec string 79 | switch strings.ToLower(ext) { 80 | case ".aif", ".aiff": 81 | codec = "aiff" 82 | case ".wav", ".wave": 83 | codec = "wav" 84 | default: 85 | fmt.Printf("files with extension %s not supported\n", ext) 86 | } 87 | 88 | f, err := os.Open(*fileFlag) 89 | if err != nil { 90 | panic(err) 91 | } 92 | defer f.Close() 93 | 94 | var monoFrames audio.Frames 95 | var sampleRate int 96 | var sampleSize int 97 | switch codec { 98 | case "aiff": 99 | d := aiff.NewDecoder(f) 100 | frames, err := d.Frames() 101 | if err != nil { 102 | panic(err) 103 | } 104 | sampleRate = d.SampleRate 105 | sampleSize = int(d.BitDepth) 106 | monoFrames = audio.ToMonoFrames(frames) 107 | 108 | case "wav": 109 | info, frames, err := wav.NewDecoder(f, nil).ReadFrames() 110 | if err != nil { 111 | panic(err) 112 | } 113 | sampleRate = int(info.SampleRate) 114 | sampleSize = int(info.BitsPerSample) 115 | monoFrames = audio.ToMonoFrames(frames) 116 | } 117 | 118 | fmt.Printf("undersampling -> %s file at %dHz to %d samples (%d)\n", codec, sampleRate, sampleRate / *factorFlag, sampleSize) 119 | 120 | switch sampleRate { 121 | case 44100: 122 | case 48000: 123 | default: 124 | log.Fatalf("input sample rate of %dHz not supported", sampleRate) 125 | } 126 | 127 | amplitudesF := make([]float64, len(monoFrames)) 128 | for i, f := range monoFrames { 129 | amplitudesF[i] = float64(f[0]) 130 | } 131 | 132 | // low pass filter before we drop some samples to avoid aliasing 133 | s := &filters.Sinc{Taps: 62, SamplingFreq: sampleRate, CutOffFreq: float64(sampleRate / 2), Window: windows.Blackman} 134 | fir := &filters.FIR{Sinc: s} 135 | filtered, err := fir.LowPass(amplitudesF) 136 | if err != nil { 137 | panic(err) 138 | } 139 | frames := make([][]int, len(amplitudesF) / *factorFlag) 140 | for i := 0; i < len(frames); i++ { 141 | frames[i] = []int{int(filtered[i**factorFlag])} 142 | } 143 | 144 | of, err := os.Create("resampled.aiff") 145 | if err != nil { 146 | panic(err) 147 | } 148 | defer of.Close() 149 | aiffe := aiff.NewEncoder(of, sampleRate / *factorFlag, sampleSize, 1) 150 | aiffe.Frames = frames 151 | if err := aiffe.Write(); err != nil { 152 | panic(err) 153 | } 154 | */ 155 | } 156 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Package audio is a work in progress package 4 | containing audio related sub packages. 5 | 6 | The APIs are not stable, the code is unfinalized and should 7 | not be used in production. 8 | 9 | See sub packages. 10 | 11 | */ 12 | package audio 13 | -------------------------------------------------------------------------------- /dsp/analysis/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | /* 5 | path, _ := filepath.Abs("../../../decimator/beat.aiff") 6 | ext := filepath.Ext(path) 7 | var codec string 8 | switch strings.ToLower(ext) { 9 | case ".aif", ".aiff": 10 | codec = "aiff" 11 | case ".wav", ".wave": 12 | codec = "wav" 13 | default: 14 | fmt.Printf("files with extension %s not supported\n", ext) 15 | } 16 | 17 | f, err := os.Open(path) 18 | if err != nil { 19 | panic(err) 20 | } 21 | defer f.Close() 22 | 23 | var monoFrames audio.Frames 24 | var sampleRate int 25 | var sampleSize int 26 | switch codec { 27 | case "aiff": 28 | d := aiff.NewDecoder(f) 29 | frames, err := d.Frames() 30 | if err != nil { 31 | panic(err) 32 | } 33 | sampleRate = d.SampleRate 34 | sampleSize = int(d.BitDepth) 35 | monoFrames = audio.ToMonoFrames(frames) 36 | 37 | case "wav": 38 | info, frames, err := wav.NewDecoder(f, nil).ReadFrames() 39 | if err != nil { 40 | panic(err) 41 | } 42 | sampleRate = int(info.SampleRate) 43 | sampleSize = int(info.BitsPerSample) 44 | monoFrames = audio.ToMonoFrames(frames) 45 | } 46 | 47 | data := make([]float64, len(monoFrames)) 48 | for i, f := range monoFrames { 49 | data[i] = float64(f[0]) 50 | } 51 | dft := analysis.NewDFT(sampleRate, data) 52 | sndData := dft.IFFT() 53 | frames := make([][]int, len(sndData)) 54 | for i := 0; i < len(frames); i++ { 55 | frames[i] = []int{int(sndData[i])} 56 | } 57 | of, err := os.Create("roundtripped.aiff") 58 | if err != nil { 59 | panic(err) 60 | } 61 | defer of.Close() 62 | aiffe := aiff.NewEncoder(of, sampleRate, sampleSize, 1) 63 | aiffe.Frames = frames 64 | if err := aiffe.Write(); err != nil { 65 | panic(err) 66 | } 67 | */ 68 | } 69 | -------------------------------------------------------------------------------- /dsp/analysis/dft.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import ( 4 | "math" 5 | "math/cmplx" 6 | 7 | "github.com/mjibson/go-dsp/fft" 8 | ) 9 | 10 | // DFT is the Discrete Fourier Transform representation of a signal 11 | // https://en.wikipedia.org/wiki/Discrete_Fourier_transform 12 | type DFT struct { 13 | // in audio, we only get real numbers 14 | // Coefs are the amount of signal energy at those frequency, 15 | // the amplitude is relative but can be compared as absolute values 16 | // between buffers. 17 | Coefs []complex128 18 | SampleRate int 19 | 20 | _binWidth int 21 | } 22 | 23 | // NewDFT returns the FFT result wrapped in a DFT struct 24 | func NewDFT(sr int, x []float64) *DFT { 25 | return &DFT{ 26 | SampleRate: sr, 27 | Coefs: fft.FFTReal(x), 28 | } 29 | } 30 | 31 | // IFFT runs an inverse fast fourrier transform and returns the float values 32 | func (d *DFT) IFFT() []float64 { 33 | sndDataCmplx := fft.IFFT(d.Coefs) 34 | sndData := make([]float64, len(sndDataCmplx)) 35 | for i, cpx := range sndDataCmplx { 36 | sndData[i] = cmplx.Abs(cpx) 37 | } 38 | return sndData 39 | } 40 | 41 | // BinWidth is the width of a bin (in frequency). 42 | // It is calculate by using the Nyquist frequency (sample rate/2) divided by 43 | // the DFT size. 44 | func (d *DFT) BinWidth() int { 45 | if d == nil { 46 | return 0 47 | } 48 | if d._binWidth > 0 { 49 | return d._binWidth 50 | } 51 | d._binWidth = (d.SampleRate / 2) / len(d.Coefs) 52 | return d._binWidth 53 | } 54 | 55 | // ToFreqRange returns a map with the frequency and their values (normalized) 56 | func (d *DFT) ToFreqRange() map[int]float64 { 57 | if d == nil { 58 | return nil 59 | } 60 | output := make(map[int]float64, len(d.Coefs)/2) 61 | for i := 0; i < len(d.Coefs)/2; i++ { 62 | f := (i * d.SampleRate) / (len(d.Coefs)) 63 | // calculate the magnitude 64 | output[f] = math.Log10(math.Sqrt(math.Pow(real(d.Coefs[i]), 2) + math.Pow(imag(d.Coefs[i]), 2))) 65 | } 66 | return output 67 | } 68 | -------------------------------------------------------------------------------- /dsp/analysis/energy.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import "github.com/mattetti/audio" 4 | 5 | // TotalEnergy is the the sum of squared moduli 6 | // See https://www.dsprelated.com/freebooks/mdft/Signal_Metrics.html 7 | func TotalEnergy(buf *audio.PCMBuffer) float64 { 8 | var e float64 9 | for _, v := range buf.AsFloat64s() { 10 | e += v * v 11 | } 12 | return e 13 | } 14 | -------------------------------------------------------------------------------- /dsp/analysis/fftshift.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | // FFTShiftF shifts a buffer of floats. The passed buffer is modified. 4 | // See http://www.mathworks.com/help/matlab/ref/fftshift.html 5 | func FFTShiftF(buffer []float64) []float64 { 6 | var tmp float64 7 | size := len(buffer) / 2 8 | for i := 0; i < size; i++ { 9 | tmp = buffer[i] 10 | buffer[i] = buffer[size+i] 11 | buffer[size+i] = tmp 12 | } 13 | return buffer 14 | } 15 | -------------------------------------------------------------------------------- /dsp/analysis/min_max.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import "github.com/mattetti/audio" 4 | 5 | // MinMaxFloat returns the smallest and biggest samples in the buffer 6 | func MinMaxFloat(buf *audio.PCMBuffer) (min, max float64) { 7 | if buf == nil || buf.Len() == 0 { 8 | return 0, 0 9 | } 10 | buf.SwitchPrimaryType(audio.Float) 11 | min = buf.Floats[0] 12 | 13 | for _, v := range buf.Floats { 14 | if v > max { 15 | max = v 16 | } else if v < min { 17 | min = v 18 | } 19 | } 20 | 21 | return min, max 22 | } 23 | -------------------------------------------------------------------------------- /dsp/filters/fir.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import "fmt" 4 | 5 | // FIR represents a Finite Impulse Response filter taking a sinc. 6 | // https://en.wikipedia.org/wiki/Finite_impulse_response 7 | type FIR struct { 8 | Sinc *Sinc 9 | } 10 | 11 | // LowPass applies a low pass filter using the FIR 12 | func (f *FIR) LowPass(input []float64) ([]float64, error) { 13 | return f.Convolve(input, f.Sinc.LowPassCoefs()) 14 | } 15 | 16 | func (f *FIR) HighPass(input []float64) ([]float64, error) { 17 | return f.Convolve(input, f.Sinc.HighPassCoefs()) 18 | } 19 | 20 | // Convolve "mixes" two signals together 21 | // kernels is the imput that is not part of our signal, it might be shorter 22 | // than the origin signal. 23 | func (f *FIR) Convolve(input, kernels []float64) ([]float64, error) { 24 | if f == nil { 25 | return nil, nil 26 | } 27 | if !(len(input) > len(kernels)) { 28 | return nil, fmt.Errorf("provided data set is not greater than the filter weights") 29 | } 30 | 31 | output := make([]float64, len(input)) 32 | for i := 0; i < len(kernels); i++ { 33 | var sum float64 34 | 35 | for j := 0; j < i; j++ { 36 | sum += (input[j] * kernels[len(kernels)-(1+i-j)]) 37 | } 38 | output[i] = sum 39 | } 40 | 41 | for i := len(kernels); i < len(input); i++ { 42 | var sum float64 43 | for j := 0; j < len(kernels); j++ { 44 | sum += (input[i-j] * kernels[j]) 45 | } 46 | output[i] = sum 47 | } 48 | 49 | return output, nil 50 | } 51 | -------------------------------------------------------------------------------- /dsp/filters/fir_test.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mattetti/audio/dsp/windows" 7 | "github.com/mattetti/audio/generator" 8 | ) 9 | 10 | func TestSinc_Convolve(t *testing.T) { 11 | osc := generator.NewOsc(generator.WaveSine, 8000, 10000) 12 | signal := osc.Signal(10000) 13 | 14 | s := &Sinc{ 15 | Taps: 62, 16 | SamplingFreq: 10000, 17 | CutOffFreq: 5000, 18 | Window: windows.Blackman, 19 | } 20 | fir := &FIR{Sinc: s} 21 | 22 | filtered, err := fir.Convolve(signal, fir.Sinc.LowPassCoefs()) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | expected := []float64{ 28 | 0.0, 0.0, 29 | 1.0094628406853492e-19, 6.245659458053641e-20, 2.5624126585886407e-19, -1.1070971733210929e-18, -2.0892090493358173e-18, 3.865086706387968e-20, 4.0687977179264766e-18, -3.5170154530803075e-18, 6.4217194955482514e-18, 9.192015504055974e-18, 4.847319962950073e-18, -2.3587368347966008e-17, -1.1098007002805838e-17, 2.3756098964873828e-17, 3.746195700911826e-17, -4.039839578928172e-17, -4.6881658881701906e-17, 3.314683079224198e-17, 8.712396237059893e-17, -5.904619824855682e-17, -9.955636875046792e-17, -2.8638571726029356e-17, 1.1002682548456403e-16, 6.657294275137425e-17, -3.70783411414936e-17, -1.2283455382615664e-16, -4.18235929931119e-18, 8.454868642543907e-17, 9.289582233031331e-17, -6.405105473151541e-17, 30 | -0.95136, 31 | -0.58816, 0.5881600000000001, 0.95136, -3.70783411414936e-17, -0.95136, -0.58816, 0.5881600000000001, 0.95136, -5.904619824855682e-17, -0.95136, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, 3.7461957009118256e-17, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, 9.192015504055952e-18, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -2.089209049335829e-18, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, 4.067915750455608e-34, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, -1.4030885900110094e-33, -0.9513600000000001, -0.5881600000000001, 0.5881600000000001, 0.9513600000000001, 32 | } 33 | 34 | for i := 0; i < len(expected); i++ { 35 | if !float64Equal(filtered[i], expected[i]) { 36 | t.Fatalf("[%d] expected %v, got %v\n", i, 37 | expected[i], 38 | filtered[i]) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /dsp/filters/sinc.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/mattetti/audio/dsp/windows" 7 | ) 8 | 9 | // Sinc represents a sinc function 10 | // The sinc function also called the "sampling function," is a function that 11 | // arises frequently in signal processing and the theory of Fourier transforms. 12 | // The full name of the function is "sine cardinal," but it is commonly referred to by 13 | // its abbreviation, "sinc." 14 | // http://mathworld.wolfram.com/SincFunction.html 15 | type Sinc struct { 16 | CutOffFreq float64 17 | SamplingFreq int 18 | // Taps are the numbers of samples we go back in time when processing the sync function. 19 | // The tap numbers will affect the shape of the filter. The more taps, the more 20 | // shape but the more delays being injected. 21 | Taps int 22 | Window windows.Function 23 | _lowPassCoefs []float64 24 | _highPassCoefs []float64 25 | } 26 | 27 | // LowPassCoefs returns the coeficients to create a low pass filter 28 | func (s *Sinc) LowPassCoefs() []float64 { 29 | if s == nil { 30 | return nil 31 | } 32 | if s._lowPassCoefs != nil && len(s._lowPassCoefs) > 0 { 33 | return s._lowPassCoefs 34 | } 35 | size := s.Taps + 1 36 | // sample rate is 2 pi radians per second. 37 | // we get the cutt off frequency in radians per second 38 | b := (2 * math.Pi) * s.TransitionFreq() 39 | s._lowPassCoefs = make([]float64, size) 40 | // we use a window of size taps + 1 41 | winData := s.Window(size) 42 | 43 | // we only do half the taps because the coefs are symmetric 44 | // but we fill up all the coefs 45 | for i := 0; i < (s.Taps / 2); i++ { 46 | c := float64(i) - float64(s.Taps)/2 47 | y := math.Sin(c*b) / (math.Pi * c) 48 | s._lowPassCoefs[i] = y * winData[i] 49 | s._lowPassCoefs[size-1-i] = s._lowPassCoefs[i] 50 | } 51 | 52 | // then we do the ones we missed in case we have an odd number of taps 53 | s._lowPassCoefs[s.Taps/2] = 2 * s.TransitionFreq() * winData[s.Taps/2] 54 | return s._lowPassCoefs 55 | } 56 | 57 | // HighPassCoefs returns the coeficients to create a high pass filter 58 | func (s *Sinc) HighPassCoefs() []float64 { 59 | if s == nil { 60 | return nil 61 | } 62 | if s._highPassCoefs != nil && len(s._highPassCoefs) > 0 { 63 | return s._highPassCoefs 64 | } 65 | 66 | // we take the low pass coesf and invert them 67 | size := s.Taps + 1 68 | s._highPassCoefs = make([]float64, size) 69 | lowPassCoefs := s.LowPassCoefs() 70 | winData := s.Window(size) 71 | 72 | for i := 0; i < (s.Taps / 2); i++ { 73 | s._highPassCoefs[i] = -lowPassCoefs[i] 74 | s._highPassCoefs[size-1-i] = s._highPassCoefs[i] 75 | } 76 | s._highPassCoefs[s.Taps/2] = (1 - 2*s.TransitionFreq()) * winData[s.Taps/2] 77 | return s._highPassCoefs 78 | } 79 | 80 | // TransitionFreq returns a ratio of the cutoff frequency and the sample rate. 81 | func (s *Sinc) TransitionFreq() float64 { 82 | if s == nil { 83 | return 0 84 | } 85 | return s.CutOffFreq / float64(s.SamplingFreq) 86 | } 87 | -------------------------------------------------------------------------------- /dsp/filters/sinc_test.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/mattetti/audio/dsp/windows" 8 | ) 9 | 10 | var closeFactor = 1e-8 11 | 12 | func float64Equal(a, b float64) bool { 13 | return math.Abs(a-b) <= closeFactor || math.Abs(1-a/b) <= closeFactor 14 | } 15 | 16 | func TestSinc_TransitionFreq(t *testing.T) { 17 | s := &Sinc{ 18 | Taps: 62, 19 | SamplingFreq: 44100, 20 | CutOffFreq: 10000, 21 | Window: windows.Blackman, 22 | } 23 | if tf := s.TransitionFreq(); tf != 0.22675736961451248 { 24 | t.Fatalf("expected the transition freq to be 0.22675736961451248 but was %f", tf) 25 | } 26 | } 27 | 28 | func TestSinc_LowPassCoefs(t *testing.T) { 29 | s := &Sinc{ 30 | Taps: 62, 31 | SamplingFreq: 44100, 32 | CutOffFreq: 10000, 33 | Window: windows.Blackman, 34 | } 35 | coefs := s.LowPassCoefs() 36 | expected := []float64{ 37 | -2.6242627466020627e-20, -9.308409044081184e-06, -1.891666429827646e-05, 7.930147893322974e-05, 0.00012825996467078055, -0.00018761111755909003, -0.0004157743427201195, 0.00024855585062726743, 0.000965784806320067, -9.669697938843214e-05, -0.001818157533640114, -0.0005258451209523136, 0.002903194559735556, 0.0019501254264326032, -0.003966788901705149, -0.004518429903463843, 0.004508041319221437, 0.008485161279456392, -0.003746405283808051, -0.013904376555168116, 0.0006092595225437504, 0.02054042750631061, 0.006333671273783543, -0.027838706758186078, -0.019241430883053766, 0.034979818093886256, 0.04266396526687624, -0.04101596167330092, -0.0925013134092271, 0.04506098918940999, 0.31359801530436804, 0.4535147392290249, 0.31359801530436804, 0.04506098918940999, -0.0925013134092271, -0.04101596167330092, 0.04266396526687624, 0.034979818093886256, -0.019241430883053766, -0.027838706758186078, 0.006333671273783543, 0.02054042750631061, 0.0006092595225437504, -0.013904376555168116, -0.003746405283808051, 0.008485161279456392, 0.004508041319221437, -0.004518429903463843, -0.003966788901705149, 0.0019501254264326032, 0.002903194559735556, -0.0005258451209523136, -0.001818157533640114, -9.669697938843214e-05, 0.000965784806320067, 0.00024855585062726743, -0.0004157743427201195, -0.00018761111755909003, 0.00012825996467078055, 7.930147893322974e-05, -1.891666429827646e-05, -9.308409044081184e-06, -2.6242627466020627e-20, 38 | } 39 | for i, k := range coefs { 40 | if !float64Equal(k, expected[i]) { 41 | t.Logf("[%d] wrong coef, expected %f, got %f\n", i, expected[i], k) 42 | t.Fail() 43 | } 44 | } 45 | } 46 | 47 | func TestSinc_HighPassCoefs(t *testing.T) { 48 | s := &Sinc{ 49 | Taps: 62, 50 | SamplingFreq: 44100, 51 | CutOffFreq: 10000, 52 | Window: windows.Blackman, 53 | } 54 | coefs := s.HighPassCoefs() 55 | expected := []float64{ 56 | 2.6242627466020627e-20, 9.308409044081184e-06, 1.891666429827646e-05, -7.930147893322974e-05, -0.00012825996467078055, 0.00018761111755909003, 0.0004157743427201195, -0.0002485558506272677, -0.000965784806320067, 9.669697938843214e-05, 0.001818157533640114, 0.0005258451209523136, -0.002903194559735556, -0.0019501254264326032, 0.003966788901705149, 0.004518429903463842, -0.004508041319221437, -0.00848516127945639, 0.003746405283808051, 0.013904376555168116, -0.0006092595225437504, -0.02054042750631061, -0.006333671273783543, 0.027838706758186078, 0.019241430883053766, -0.03497981809388626, -0.04266396526687625, 0.04101596167330092, 0.0925013134092271, -0.04506098918940999, -0.31359801530436804, 0.5464852607709749, -0.31359801530436804, -0.04506098918940999, 0.0925013134092271, 0.04101596167330092, -0.04266396526687625, -0.03497981809388626, 0.019241430883053766, 0.027838706758186078, -0.006333671273783543, -0.02054042750631061, -0.0006092595225437504, 0.013904376555168116, 0.003746405283808051, -0.00848516127945639, -0.004508041319221437, 0.004518429903463842, 0.003966788901705149, -0.0019501254264326032, -0.002903194559735556, 0.0005258451209523136, 0.001818157533640114, 9.669697938843214e-05, -0.000965784806320067, -0.0002485558506272677, 0.0004157743427201195, 0.00018761111755909003, -0.00012825996467078055, -7.930147893322974e-05, 1.891666429827646e-05, 9.308409044081184e-06, 2.6242627466020627e-20, 57 | } 58 | for i, k := range coefs { 59 | if !float64Equal(k, expected[i]) { 60 | t.Logf("[%d] wrong coef, expected %f, got %f\n", i, expected[i], k) 61 | t.Fail() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dsp/windows/blackman.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import "math" 4 | 5 | // Blackman generates a Blackman window of the requested size 6 | // See https://en.wikipedia.org/wiki/Window_function#Blackman_windows 7 | func Blackman(L int) []float64 { 8 | r := make([]float64, L) 9 | LF := float64(L) 10 | alpha := 0.16 11 | a0 := (1 - alpha) / 2.0 12 | a1 := 0.5 13 | a2 := alpha / 2.0 14 | 15 | for i := 0; i < L; i++ { 16 | iF := float64(i) 17 | r[i] = a0 - (a1 * math.Cos((twoPi*iF)/(LF-1))) + (a2 * math.Cos((fourPi*iF)/(LF-1))) 18 | } 19 | return r 20 | } 21 | -------------------------------------------------------------------------------- /dsp/windows/blackman_test.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | var closeFactor = 1e-8 9 | 10 | func float64Equal(a, b float64) bool { 11 | return math.Abs(a-b) <= closeFactor || math.Abs(1-a/b) <= closeFactor 12 | } 13 | 14 | func TestBlackmann(t *testing.T) { 15 | expected := []float64{ 16 | -1.3877787807814457e-17, 0.0003705047306915149, 0.0014885821314992581, 0.0033737620921490546, 0.006058064810356409, 0.00958521624410219, 0.01400956933480732, 0.019394750197489097, 0.025812053403147295, 0.03333861500387118, 0.04205539599650428, 0.05204501241810308, 0.06338945115686861, 0.07616771279672704, 0.09045342435412804, 0.1063124655852105, 0.1238006526254706, 0.1429615220698088, 0.16382425721789776, 0.18640179611981533, 0.2106891582931365, 0.23666202358964922, 0.2642755927224567, 0.29346375448687384, 0.3241385797941454, 0.3561901573658007, 0.38948678039455564, 0.4238754877558361, 0.45918295754596355, 0.4952167449241689, 0.5317668505411229, 0.5686076003402915, 0.605499812310644, 0.6421932209359956, 0.6784291257073802, 0.7139432262128614, 0.7484686030580439, 0.7817388012545962, 0.8134909707872374, 0.8434690178644608, 0.8714267198957117, 0.8971307575265366, 0.9203636180999081, 0.940926326680829, 0.9586409632547708, 0.9733529278493858, 0.9849329190832143, 0.9932785959547453, 0.9983158974810278, 0.9999999999999999, 0.9983158974810278, 0.9932785959547454, 0.9849329190832143, 0.9733529278493859, 0.9586409632547709, 0.940926326680829, 0.9203636180999083, 0.8971307575265368, 0.871426719895712, 0.8434690178644609, 0.8134909707872378, 0.7817388012545962, 0.7484686030580441, 0.7139432262128617, 0.6784291257073805, 0.6421932209359961, 0.6054998123106441, 0.5686076003402917, 0.5317668505411228, 0.4952167449241689, 0.45918295754596383, 0.42387548775583583, 0.38948678039455553, 0.3561901573658007, 0.3241385797941456, 0.2934637544868741, 0.2642755927224572, 0.23666202358964933, 0.21068915829313667, 0.18640179611981533, 0.16382425721789773, 0.1429615220698089, 0.12380065262547083, 0.10631246558521054, 0.09045342435412812, 0.07616771279672714, 0.0633894511568687, 0.05204501241810329, 0.042055395996504416, 0.03333861500387112, 0.025812053403147288, 0.019394750197489118, 0.01400956933480732, 0.009585216244102246, 0.006058064810356409, 0.0033737620921490408, 0.0014885821314992859, 0.00037050473069148715, -1.3877787807814457e-17, 17 | } 18 | winData := Blackman(99) 19 | for i, x := range winData { 20 | if !float64Equal(x, expected[i]) { 21 | t.Logf("[%d] expected %f, got %f\n", i, expected[i], x) 22 | t.Fatal() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dsp/windows/function.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | // Function is an alias type representing window functions. 4 | type Function func(int) []float64 5 | -------------------------------------------------------------------------------- /dsp/windows/hamming.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import "math" 4 | 5 | // Hamming generates a Hamming window of the requested size 6 | // See https://en.wikipedia.org/wiki/Window_function#Hamming_window 7 | func Hamming(L int) []float64 { 8 | r := make([]float64, L) 9 | alpha := 0.54 10 | beta := 1.0 - alpha 11 | Lf := float64(L) 12 | 13 | for i := 0; i < L; i++ { 14 | r[i] = alpha - (beta * math.Cos((twoPi*float64(i))/(Lf-1))) 15 | } 16 | return r 17 | } 18 | -------------------------------------------------------------------------------- /dsp/windows/nuttall.go: -------------------------------------------------------------------------------- 1 | package windows 2 | 3 | import "math" 4 | 5 | var ( 6 | twoPi = math.Pi * 2 7 | fourPi = math.Pi * 4 8 | sixPi = math.Pi * 6 9 | ) 10 | 11 | // Nuttall generates a Blackman-Nutall window 12 | // See https://en.wikipedia.org/wiki/Window_function#Nuttall_window.2C_continuous_first_derivative 13 | func Nuttall(L int) []float64 { 14 | r := make([]float64, L) 15 | LF := float64(L) 16 | for i := 0; i < L; i++ { 17 | iF := float64(i) 18 | r[i] = 0.355768 - 0.487396*math.Cos((twoPi*iF)/(LF-1)) + 0.144232*math.Cos((fourPi*iF)/(LF-1)) - 0.012604*math.Cos((sixPi*iF)/(LF-1)) 19 | } 20 | 21 | return r 22 | } -------------------------------------------------------------------------------- /formats.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "encoding/binary" 4 | 5 | var ( 6 | // AIFF 7 | // MONO 8 | 9 | // FormatMono225008bBE mono 8bit 22.5kHz AIFF like format. 10 | FormatMono225008bBE = &Format{ 11 | NumChannels: 1, 12 | SampleRate: 22500, 13 | BitDepth: 8, 14 | Endianness: binary.BigEndian, 15 | } 16 | // FormatMono2250016bBE mono 16bit 22.5kHz AIFF like format. 17 | FormatMono2250016bBE = &Format{ 18 | NumChannels: 1, 19 | SampleRate: 22500, 20 | BitDepth: 16, 21 | Endianness: binary.BigEndian, 22 | } 23 | // FormatMono441008bBE mono 8bit 44.1kHz AIFF like format. 24 | FormatMono441008bBE = &Format{ 25 | NumChannels: 1, 26 | SampleRate: 44100, 27 | BitDepth: 8, 28 | Endianness: binary.BigEndian, 29 | } 30 | // FormatMono4410016bBE mono 16bit 44.1kHz AIFF like format. 31 | FormatMono4410016bBE = &Format{ 32 | NumChannels: 1, 33 | SampleRate: 44100, 34 | BitDepth: 16, 35 | Endianness: binary.BigEndian, 36 | } 37 | // FormatMono4410024bBE mono 24bit 44.1kHz AIFF like format. 38 | FormatMono4410024bBE = &Format{ 39 | NumChannels: 1, 40 | SampleRate: 44100, 41 | BitDepth: 24, 42 | Endianness: binary.BigEndian, 43 | } 44 | // FormatMono4410032bBE mono 32bit 44.1kHz AIFF like format. 45 | FormatMono4410032bBE = &Format{ 46 | NumChannels: 1, 47 | SampleRate: 44100, 48 | BitDepth: 32, 49 | Endianness: binary.BigEndian, 50 | } 51 | 52 | // STEREO 53 | 54 | // FormatStereo225008bBE Stereo 8bit 22.5kHz AIFF like format. 55 | FormatStereo225008bBE = &Format{ 56 | NumChannels: 2, 57 | SampleRate: 22500, 58 | BitDepth: 8, 59 | Endianness: binary.BigEndian, 60 | } 61 | // FormatStereo2250016bBE Stereo 16bit 22.5kHz AIFF like format. 62 | FormatStereo2250016bBE = &Format{ 63 | NumChannels: 2, 64 | SampleRate: 22500, 65 | BitDepth: 16, 66 | Endianness: binary.BigEndian, 67 | } 68 | // FormatStereo441008bBE Stereo 8bit 44.1kHz AIFF like format. 69 | FormatStereo441008bBE = &Format{ 70 | NumChannels: 2, 71 | SampleRate: 44100, 72 | BitDepth: 8, 73 | Endianness: binary.BigEndian, 74 | } 75 | // FormatStereo4410016bBE Stereo 16bit 44.1kHz AIFF like format. 76 | FormatStereo4410016bBE = &Format{ 77 | NumChannels: 2, 78 | SampleRate: 44100, 79 | BitDepth: 16, 80 | Endianness: binary.BigEndian, 81 | } 82 | // FormatStereo4410024bBE Stereo 24bit 44.1kHz AIFF like format. 83 | FormatStereo4410024bBE = &Format{ 84 | NumChannels: 2, 85 | SampleRate: 44100, 86 | BitDepth: 24, 87 | Endianness: binary.BigEndian, 88 | } 89 | // FormatStereo4410032bBE Stereo 32bit 44.1kHz AIFF like format. 90 | FormatStereo4410032bBE = &Format{ 91 | NumChannels: 2, 92 | SampleRate: 44100, 93 | BitDepth: 32, 94 | Endianness: binary.BigEndian, 95 | } 96 | 97 | // WAV 98 | // MONO 99 | 100 | // FormatMono225008bLE mono 8bit 22.5kHz AIFF like format. 101 | FormatMono225008bLE = &Format{ 102 | NumChannels: 1, 103 | SampleRate: 22500, 104 | BitDepth: 8, 105 | Endianness: binary.LittleEndian, 106 | } 107 | // FormatMono2250016bLE mono 16bit 22.5kHz AIFF like format. 108 | FormatMono2250016bLE = &Format{ 109 | NumChannels: 1, 110 | SampleRate: 22500, 111 | BitDepth: 16, 112 | Endianness: binary.LittleEndian, 113 | } 114 | // FormatMono441008bLE mono 8bit 44.1kHz AIFF like format. 115 | FormatMono441008bLE = &Format{ 116 | NumChannels: 1, 117 | SampleRate: 44100, 118 | BitDepth: 8, 119 | Endianness: binary.LittleEndian, 120 | } 121 | // FormatMono4410016bLE mono 16bit 44.1kHz AIFF like format. 122 | FormatMono4410016bLE = &Format{ 123 | NumChannels: 1, 124 | SampleRate: 44100, 125 | BitDepth: 16, 126 | Endianness: binary.LittleEndian, 127 | } 128 | // FormatMono4410024bLE mono 24bit 44.1kHz AIFF like format. 129 | FormatMono4410024bLE = &Format{ 130 | NumChannels: 1, 131 | SampleRate: 44100, 132 | BitDepth: 24, 133 | Endianness: binary.LittleEndian, 134 | } 135 | // FormatMono4410032bLE mono 32bit 44.1kHz AIFF like format. 136 | FormatMono4410032bLE = &Format{ 137 | NumChannels: 1, 138 | SampleRate: 44100, 139 | BitDepth: 32, 140 | Endianness: binary.LittleEndian, 141 | } 142 | 143 | // STEREO 144 | 145 | // FormatStereo225008bLE Stereo 8bit 22.5kHz AIFF like format. 146 | FormatStereo225008bLE = &Format{ 147 | NumChannels: 2, 148 | SampleRate: 22500, 149 | BitDepth: 8, 150 | Endianness: binary.LittleEndian, 151 | } 152 | // FormatStereo2250016bLE Stereo 16bit 22.5kHz AIFF like format. 153 | FormatStereo2250016bLE = &Format{ 154 | NumChannels: 2, 155 | SampleRate: 22500, 156 | BitDepth: 16, 157 | Endianness: binary.LittleEndian, 158 | } 159 | // FormatStereo441008bLE Stereo 8bit 44.1kHz AIFF like format. 160 | FormatStereo441008bLE = &Format{ 161 | NumChannels: 2, 162 | SampleRate: 44100, 163 | BitDepth: 8, 164 | Endianness: binary.LittleEndian, 165 | } 166 | // FormatStereo4410016bLE Stereo 16bit 44.1kHz AIFF like format. 167 | FormatStereo4410016bLE = &Format{ 168 | NumChannels: 2, 169 | SampleRate: 44100, 170 | BitDepth: 16, 171 | Endianness: binary.LittleEndian, 172 | } 173 | // FormatStereo4410024bLE Stereo 24bit 44.1kHz AIFF like format. 174 | FormatStereo4410024bLE = &Format{ 175 | NumChannels: 2, 176 | SampleRate: 44100, 177 | BitDepth: 24, 178 | Endianness: binary.LittleEndian, 179 | } 180 | // FormatStereo4410032bLE Stereo 32bit 44.1kHz AIFF like format. 181 | FormatStereo4410032bLE = &Format{ 182 | NumChannels: 2, 183 | SampleRate: 44100, 184 | BitDepth: 32, 185 | Endianness: binary.LittleEndian, 186 | } 187 | ) 188 | -------------------------------------------------------------------------------- /generator/cmd/main.go: -------------------------------------------------------------------------------- 1 | // generator example 2 | package main 3 | 4 | import ( 5 | "errors" 6 | "flag" 7 | "os" 8 | 9 | "fmt" 10 | "io" 11 | "strings" 12 | 13 | "github.com/mattetti/audio" 14 | "github.com/mattetti/audio/aiff" 15 | "github.com/mattetti/audio/generator" 16 | "github.com/mattetti/audio/wav" 17 | ) 18 | 19 | var ( 20 | freqFlag = flag.Float64("freq", audio.RootA, "frequency to generate") 21 | biteDepthFlag = flag.Int("biteDepth", 16, "bit size to use when generating the auid file") 22 | durationFlag = flag.Int("duration", 4, "duration of the generated file") 23 | formatFlag = flag.String("format", "wav", "the audio format of the output file") 24 | ) 25 | 26 | func main() { 27 | flag.Parse() 28 | var err error 29 | 30 | freq := *freqFlag 31 | fs := 44100 32 | biteDepth := *biteDepthFlag 33 | 34 | osc := generator.NewOsc(generator.WaveSine, float64(freq), fs) 35 | // our osc generates values from -1 to 1, we need to go back to PCM scale 36 | factor := float64(audio.IntMaxSignedValue(biteDepth)) 37 | osc.Amplitude = factor 38 | data := make([]float64, fs**durationFlag) 39 | buf := audio.NewPCMFloatBuffer(data, audio.FormatMono4410016bBE) 40 | osc.Fill(buf) 41 | 42 | // generate the sound file 43 | var outName string 44 | var format string 45 | switch strings.ToLower(*formatFlag) { 46 | case "aif", "aiff": 47 | format = "aif" 48 | outName = "generated.aiff" 49 | default: 50 | format = "wav" 51 | outName = "generated.wav" 52 | } 53 | 54 | o, err := os.Create(outName) 55 | if err != nil { 56 | panic(err) 57 | } 58 | defer o.Close() 59 | if err := encode(format, buf, o); err != nil { 60 | panic(err) 61 | } 62 | fmt.Println(outName, "generated") 63 | } 64 | 65 | func encode(format string, buf *audio.PCMBuffer, w io.WriteSeeker) error { 66 | switch format { 67 | case "wav": 68 | e := wav.NewEncoder(w, buf.Format.SampleRate, buf.Format.BitDepth, buf.Format.NumChannels, 1) 69 | if err := e.Write(buf); err != nil { 70 | return err 71 | } 72 | return e.Close() 73 | case "aiff": 74 | e := aiff.NewEncoder(w, buf.Format.SampleRate, buf.Format.BitDepth, buf.Format.NumChannels) 75 | if err := e.Write(buf); err != nil { 76 | return err 77 | } 78 | return e.Close() 79 | default: 80 | return errors.New("unknown format") 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import "math" 4 | 5 | // WaveType is an alias type for the type of waveforms that can be generated 6 | type WaveType uint16 7 | 8 | const ( 9 | WaveSine WaveType = iota // 0 10 | WaveTriangle // 1 11 | WaveSaw // 2 12 | WaveSqr //3 13 | ) 14 | 15 | const ( 16 | TwoPi = float64(2 * math.Pi) 17 | ) 18 | 19 | const ( 20 | SineB = 4.0 / math.Pi 21 | SineC = -4.0 / (math.Pi * math.Pi) 22 | Q = 0.775 23 | SineP = 0.225 24 | ) 25 | 26 | // Sine takes an input value from -Pi to Pi 27 | // and returns a value between -1 and 1 28 | func Sine(x32 float64) float64 { 29 | x := float64(x32) 30 | y := SineB*x + SineC*x*(math.Abs(x)) 31 | y = SineP*(y*(math.Abs(y))-y) + y 32 | return float64(y) 33 | } 34 | 35 | const TringleA = 2.0 / math.Pi 36 | 37 | // Triangle takes an input value from -Pi to Pi 38 | // and returns a value between -1 and 1 39 | func Triangle(x float64) float64 { 40 | return float64(TringleA*x) - 1.0 41 | } 42 | 43 | // Square takes an input value from -Pi to Pi 44 | // and returns -1 or 1 45 | func Square(x float64) float64 { 46 | if x >= 0.0 { 47 | return 1 48 | } 49 | return -1.0 50 | } 51 | 52 | const SawtoothA = 1.0 / math.Pi 53 | 54 | // Triangle takes an input value from -Pi to Pi 55 | // and returns a value between -1 and 1 56 | func Sawtooth(x float64) float64 { 57 | return SawtoothA * x 58 | } 59 | -------------------------------------------------------------------------------- /generator/generator_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestSine(t *testing.T) { 9 | testCases := []struct { 10 | in float64 11 | out float64 12 | }{ 13 | 0: {float64(-math.Pi), 0}, 14 | 1: {0.007, 0.006909727339533104}, 15 | 2: {-0.5, -0.47932893655759223}, 16 | 3: {0.1, 0.09895415534087945}, 17 | 4: {1.5862234, 0.9998818440160414}, 18 | 5: {2.0, 0.909795856141705}, 19 | 6: {3.0, 0.14008939955174454}, 20 | 7: {math.Pi, 0}, 21 | } 22 | 23 | for i, tc := range testCases { 24 | if out := Sine(tc.in); !nearlyEqual(out, tc.out, 0.0001) { 25 | t.Logf("[%d] sine(%f) => %.7f != %.7f", i, tc.in, out, tc.out) 26 | t.Fail() 27 | } 28 | } 29 | } 30 | 31 | func nearlyEqual(a, b, epsilon float64) bool { 32 | if a == b { 33 | return true 34 | } 35 | absA := math.Abs(float64(a)) 36 | absB := math.Abs(float64(b)) 37 | diff := math.Abs(float64(a) - float64(b)) 38 | 39 | if a == 0 || b == 0 || diff < 0.0000001 { 40 | return diff < (float64(epsilon)) 41 | } 42 | return diff/(absA+absB) < float64(epsilon) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /generator/osc.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | 8 | "github.com/mattetti/audio" 9 | ) 10 | 11 | // Osc is an oscillator 12 | type Osc struct { 13 | Shape WaveType 14 | Amplitude float64 15 | DcOffset float64 16 | Freq float64 17 | // SampleRate 18 | Fs int 19 | PhaseOffset float64 20 | CurrentPhaseAngle float64 21 | phaseAngleIncr float64 22 | // currentSample allows us to track where we are at in the signal life 23 | // and setup an envelope accordingly 24 | currentSample int 25 | // ADSR 26 | attackInSamples int 27 | } 28 | 29 | // NewOsc returns a new oscillator, note that if you change the phase offset of the returned osc, 30 | // you also need to set the CurrentPhaseAngle 31 | func NewOsc(shape WaveType, hz float64, fs int) *Osc { 32 | return &Osc{Shape: shape, Amplitude: 1, Freq: hz, Fs: fs, phaseAngleIncr: ((hz * TwoPi) / float64(fs))} 33 | } 34 | 35 | // Reset sets the oscillator back to its starting state 36 | func (o *Osc) Reset() { 37 | o.phaseAngleIncr = ((o.Freq * TwoPi) / float64(o.Fs)) 38 | o.currentSample = 0 39 | } 40 | 41 | // SetFreq updates the oscillator frequency 42 | func (o *Osc) SetFreq(hz float64) { 43 | if o.Freq != hz { 44 | o.Freq = hz 45 | o.phaseAngleIncr = ((hz * TwoPi) / float64(o.Fs)) 46 | } 47 | } 48 | 49 | // SetAttackInMs sets the duration for the oscillator to be at full amplitude 50 | // after it starts. 51 | func (o *Osc) SetAttackInMs(ms int) { 52 | if o == nil { 53 | return 54 | } 55 | if ms <= 0 { 56 | o.attackInSamples = 0 57 | return 58 | } 59 | o.attackInSamples = int(float32(o.Fs) / (1000.0 / float32(ms))) 60 | } 61 | 62 | // Signal uses the osc to generate a discreet signal 63 | func (o *Osc) Signal(length int) []float64 { 64 | output := make([]float64, length) 65 | for i := 0; i < length; i++ { 66 | output[i] = o.Sample() 67 | } 68 | return output 69 | } 70 | 71 | // Fill fills up the pass PCMBuffer with the output of the oscillator. 72 | func (o *Osc) Fill(buf *audio.PCMBuffer) error { 73 | if o == nil { 74 | return nil 75 | } 76 | len := buf.Len() 77 | for i := 0; i < len; i++ { 78 | switch buf.DataType { 79 | case audio.Integer: 80 | buf.Ints[i] = int(o.Sample()) 81 | case audio.Float: 82 | buf.Floats[i] = o.Sample() 83 | case audio.Byte: 84 | // TODO: check the format bitdepth and endianess and convert 85 | return errors.New("bytes buffer not yet supported") 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | // Sample returns the next sample generated by the oscillator 92 | func (o *Osc) Sample() (output float64) { 93 | if o == nil { 94 | return 95 | } 96 | o.currentSample++ 97 | if o.CurrentPhaseAngle < -math.Pi { 98 | o.CurrentPhaseAngle += TwoPi 99 | } else if o.CurrentPhaseAngle > math.Pi { 100 | o.CurrentPhaseAngle -= TwoPi 101 | } 102 | 103 | var amp float64 104 | if o.attackInSamples > o.currentSample { 105 | // linear fade in 106 | amp = float64(o.currentSample) * (o.Amplitude / float64(o.attackInSamples)) 107 | } else { 108 | amp = o.Amplitude 109 | } 110 | 111 | switch o.Shape { 112 | case WaveSine: 113 | output = amp*Sine(o.CurrentPhaseAngle) + o.DcOffset 114 | case WaveTriangle: 115 | output = amp*Triangle(o.CurrentPhaseAngle) + o.DcOffset 116 | case WaveSaw: 117 | output = amp*Sawtooth(o.CurrentPhaseAngle) + o.DcOffset 118 | case WaveSqr: 119 | fmt.Println(o.CurrentPhaseAngle) 120 | output = amp*Square(o.CurrentPhaseAngle) + o.DcOffset 121 | } 122 | 123 | o.CurrentPhaseAngle += o.phaseAngleIncr 124 | return output 125 | } 126 | -------------------------------------------------------------------------------- /generator/osc_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mattetti/audio" 7 | ) 8 | 9 | func TestOsc_Signal(t *testing.T) { 10 | osc := NewOsc(WaveSine, audio.RootA, 44100) 11 | if osc.CurrentPhaseAngle != 0 { 12 | t.Fatalf("expected the current phase to be zero") 13 | } 14 | if osc.phaseAngleIncr != 0.06268937721449021 { 15 | t.Fatalf("Wrong phase angle increment") 16 | } 17 | sample := osc.Sample() 18 | if phase := osc.CurrentPhaseAngle; phase != 0.06268937721449021 { 19 | t.Fatalf("wrong phase angle: %f, expected 0.06268937721449021", phase) 20 | } 21 | if sample != 0.0 { 22 | t.Fatalf("wrong first sample: %f expected 0.0", sample) 23 | } 24 | signal := osc.Signal(19) 25 | expected := []float64{0.062001866171879985, 0.12406665714043713, 0.1858716671561314, 0.24710788950751222, 0.3074800165212184, 26 | 0.3667064395619785, 0.42451924903261046, 0.4806642343740216, 0.5349008840652089, 0.5870023856232587, 0.6367556256033469, 27 | 0.6839611895987389, 0.7284333622407899, 0.7700001271989437, 0.8085031671807348, 0.8437978639317864, 0.8757532982358108, 28 | 0.9042522499146113, 0.9291911978280791} 29 | 30 | for i, s := range signal { 31 | if !nearlyEqual(s, expected[i], 0.000001) { 32 | t.Logf("sample %d didn't match, expected: %f got %f\n", i, expected[i], s) 33 | t.Fail() 34 | } 35 | } 36 | 37 | osc = NewOsc(WaveSine, 400, 1000) 38 | signal = osc.Signal(100) 39 | 40 | expected = []float64{0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001, 0, 0.5881600000000001, -0.9513600000000001, 0.9513600000000001, -0.5881600000000001} 41 | for i, s := range signal { 42 | if !nearlyEqual(s, expected[i], 0.00000001) { 43 | t.Logf("sample %d didn't match, expected: %f got %f\n", i, expected[i], s) 44 | t.Fail() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /midi/README.md: -------------------------------------------------------------------------------- 1 | DEPRECATED use [go-audio/midi](https://github.com/go-audio/midi) instead. 2 | -------------------------------------------------------------------------------- /midi/cmd/decoder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/mattetti/audio/midi" 10 | ) 11 | 12 | var ( 13 | fileFlag = flag.String("file", "", "The path to the midi file to decode") 14 | ) 15 | 16 | func main() { 17 | flag.Usage = func() { 18 | fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) 19 | flag.PrintDefaults() 20 | } 21 | flag.Parse() 22 | 23 | f, err := os.Open(*fileFlag) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | defer f.Close() 28 | 29 | decoder := midi.NewDecoder(f) 30 | if err := decoder.Parse(); err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | fmt.Println("format:", decoder.Format) 35 | fmt.Println(decoder.TicksPerQuarterNote, "ticks per quarter") 36 | for _, tr := range decoder.Tracks { 37 | for _, ev := range tr.Events { 38 | fmt.Println(ev) 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /midi/cmd/gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/mattetti/audio/midi" 8 | ) 9 | 10 | func main() { 11 | f, err := os.Create("midi.mid") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | defer func() { 16 | f.Close() 17 | }() 18 | e := midi.NewEncoder(f, midi.SingleTrack, 96) 19 | tr := e.NewTrack() 20 | 21 | // 1 beat with 1 note for nothing 22 | tr.Add(1, midi.NoteOff(0, 60)) 23 | 24 | vel := 90 25 | //C3 to B3 26 | var j float64 27 | for i := 60; i < 72; i++ { 28 | tr.Add(j, midi.NoteOn(0, i, vel)) 29 | tr.Add(1, midi.NoteOff(0, i)) 30 | j = 1 31 | } 32 | tr.Add(1, midi.EndOfTrack()) 33 | 34 | if err := e.Write(); err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /midi/decoder_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestVarint(t *testing.T) { 11 | expecations := []struct { 12 | dec uint32 13 | bytes []byte 14 | }{ 15 | {0, []byte{0}}, 16 | {42, []byte{0x2a}}, 17 | {4610, []byte{0xa4, 0x02}}, 18 | } 19 | 20 | for _, exp := range expecations { 21 | conv := EncodeVarint(exp.dec) 22 | if bytes.Compare(conv, exp.bytes) != 0 { 23 | t.Fatalf("%d was converted to %#v didn't match %#v\n", exp.dec, conv, exp.bytes) 24 | } 25 | } 26 | 27 | for _, exp := range expecations { 28 | conv, _ := DecodeVarint(exp.bytes) 29 | if conv != exp.dec { 30 | t.Fatalf("%#v was converted to %d didn't match %d\n", exp.bytes, conv, exp.dec) 31 | } 32 | } 33 | } 34 | 35 | func TestParsingFile(t *testing.T) { 36 | expectations := []struct { 37 | path string 38 | format uint16 39 | numTracks uint16 40 | ticksPerQuarterNote uint16 41 | timeFormat timeFormat 42 | trackNames []string 43 | bpms []int 44 | }{ 45 | {"fixtures/elise.mid", 1, 4, 960, MetricalTF, []string{"Track 0", "F\xfcr Elise", "http://www.forelise.com/", ""}, []int{69, 0, 0, 0}}, 46 | {"fixtures/elise1track.mid", 0, 1, 480, MetricalTF, []string{"F"}, []int{69}}, 47 | {"fixtures/bossa.mid", 0, 1, 96, MetricalTF, []string{"bossa 1"}, []int{0}}, 48 | {"fixtures/closedHat.mid", 0, 1, 96, MetricalTF, []string{"01 4th Hat Closed Side"}, []int{0}}, 49 | } 50 | 51 | for _, exp := range expectations { 52 | t.Log(exp.path) 53 | path, _ := filepath.Abs(exp.path) 54 | f, err := os.Open(path) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | defer f.Close() 59 | p := NewDecoder(f) 60 | if err := p.Parse(); err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | if p.Format != exp.format { 65 | t.Fatalf("%s of %s didn't match %v, got %v", "format", exp.path, exp.format, p.Format) 66 | } 67 | if p.NumTracks != exp.numTracks { 68 | t.Fatalf("%s of %s didn't match %v, got %v", "numTracks", exp.path, exp.numTracks, p.NumTracks) 69 | } 70 | if p.TicksPerQuarterNote != exp.ticksPerQuarterNote { 71 | t.Fatalf("%s of %s didn't match %v, got %v", "ticksPerQuarterNote", exp.path, exp.ticksPerQuarterNote, p.TicksPerQuarterNote) 72 | } 73 | if p.TimeFormat != exp.timeFormat { 74 | t.Fatalf("%s of %s didn't match %v, got %v", "format", exp.path, exp.timeFormat, p.TimeFormat) 75 | } 76 | 77 | if len(p.Tracks) == 0 { 78 | t.Fatal("Tracks not parsed") 79 | } 80 | t.Logf("%d tracks\n", len(p.Tracks)) 81 | for i, tr := range p.Tracks { 82 | t.Log("track", i) 83 | if tName := tr.Name(); tName != exp.trackNames[i] { 84 | t.Fatalf("expected name of track %d to be %s but got %s (%q)", i, exp.trackNames[i], tName, tName) 85 | } 86 | if bpm := tr.Tempo(); bpm != exp.bpms[i] { 87 | t.Fatalf("expected tempo of track %d to be %d but got %d", i, exp.bpms[i], bpm) 88 | } 89 | for _, ev := range tr.Events { 90 | t.Log(ev) 91 | } 92 | } 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /midi/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIDI package is a midi format parser that parses the content of a midi stream 3 | and asyncronously send the data to a provided channel 4 | or collect the data into an accessible struct, 5 | */ 6 | package midi 7 | -------------------------------------------------------------------------------- /midi/encoder.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "log" 8 | ) 9 | 10 | const ( 11 | SingleTrack uint16 = iota 12 | Syncronous 13 | Asyncronous 14 | ) 15 | 16 | type Encoder struct { 17 | // we need a write seeker because we will update the size at the end 18 | // and need to back to the beginning of the file. 19 | w io.WriteSeeker 20 | 21 | /* 22 | Format describes the tracks format 23 | 24 | 0 - single-track 25 | Format 0 file has a header chunk followed by one track chunk. It 26 | is the most interchangable representation of data. It is very useful 27 | for a simple single-track player in a program which needs to make 28 | synthesizers make sounds, but which is primarily concerned with 29 | something else such as mixers or sound effect boxes. It is very 30 | desirable to be able to produce such a format, even if your program 31 | is track-based, in order to work with these simple programs. On the 32 | other hand, perhaps someone will write a format conversion from 33 | format 1 to format 0 which might be so easy to use in some setting 34 | that it would save you the trouble of putting it into your program. 35 | 36 | 37 | Synchronous multiple tracks means that the tracks will all be vertically synchronous, or in other words, 38 | they all start at the same time, and so can represent different parts in one song. 39 | 1 - multiple tracks, synchronous 40 | Asynchronous multiple tracks do not necessarily start at the same time, and can be completely asynchronous. 41 | 2 - multiple tracks, asynchronous 42 | */ 43 | Format uint16 44 | 45 | // NumTracks represents the number of tracks in the midi file 46 | NumTracks uint16 47 | 48 | // resolution for delta timing 49 | TicksPerQuarterNote uint16 50 | 51 | TimeFormat timeFormat 52 | Tracks []*Track 53 | 54 | size int 55 | } 56 | 57 | func NewEncoder(w io.WriteSeeker, format uint16, ppqn uint16) *Encoder { 58 | return &Encoder{w: w, Format: format, TicksPerQuarterNote: ppqn} 59 | } 60 | 61 | // NewTrack adds and return a new track (not thread safe) 62 | func (e *Encoder) NewTrack() *Track { 63 | t := &Track{ticksPerBeat: e.TicksPerQuarterNote} 64 | e.Tracks = append(e.Tracks, t) 65 | return t 66 | } 67 | 68 | // Write writes the binary representation to the writer 69 | func (e *Encoder) Write() error { 70 | if e == nil { 71 | return errors.New("Can't write a nil encoder") 72 | } 73 | e.writeHeaders() 74 | for _, t := range e.Tracks { 75 | if err := e.encodeTrack(t); err != nil { 76 | return err 77 | } 78 | } 79 | // go back and update body size in header 80 | return nil 81 | } 82 | 83 | func (e *Encoder) writeHeaders() error { 84 | // chunk id [4] headerChunkID 85 | if _, err := e.w.Write(headerChunkID[:]); err != nil { 86 | return err 87 | } 88 | // header size 89 | if err := binary.Write(e.w, binary.BigEndian, uint32(6)); err != nil { 90 | return err 91 | } 92 | // Format 93 | if err := binary.Write(e.w, binary.BigEndian, e.Format); err != nil { 94 | return err 95 | } 96 | // numtracks (not trusting the field value, but checking the actual amount of tracks 97 | if err := binary.Write(e.w, binary.BigEndian, uint16(len(e.Tracks))); err != nil { 98 | return err 99 | } 100 | // division [uint16] <-- contains precision 101 | if err := binary.Write(e.w, binary.BigEndian, e.TicksPerQuarterNote); err != nil { 102 | return err 103 | } 104 | return nil 105 | } 106 | 107 | func (e *Encoder) encodeTrack(t *Track) error { 108 | // chunk id [4] 109 | if _, err := e.w.Write(trackChunkID[:]); err != nil { 110 | return err 111 | } 112 | data, err := t.ChunkData(true) 113 | if err != nil { 114 | return err 115 | } 116 | // chunk size 117 | if err := binary.Write(e.w, binary.BigEndian, uint32(len(data))); err != nil { 118 | log.Fatalf("106 - %v", err) 119 | 120 | return err 121 | } 122 | // chunk data 123 | if _, err := e.w.Write(data); err != nil { 124 | return err 125 | } 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /midi/encoder_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestNewEncoder(t *testing.T) { 12 | w, err := tmpFile() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer func() { 17 | w.Close() 18 | os.Remove(w.Name()) 19 | }() 20 | e := NewEncoder(w, SingleTrack, 96) 21 | tr := e.NewTrack() 22 | // add a C3 at velocity 99, half a beat/quarter note after the start 23 | tr.Add(0.5, NoteOn(1, KeyInt("C", 3), 99)) 24 | // turn off the C3 25 | tr.Add(1, NoteOff(1, KeyInt("C", 3))) 26 | if err := e.Write(); err != nil { 27 | t.Fatal(err) 28 | } 29 | w.Seek(0, 0) 30 | midiData, err := ioutil.ReadAll(w) 31 | if err != nil { 32 | t.Log(err) 33 | } 34 | expected := []byte{ 35 | 0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 00, 00, 00, 0x01, 00, 0x60, 0x4d, 0x54, 36 | 0x72, 0x6b, 0x00, 0x00, 0x00, 0x14, 0x00, 0xff, 0x58, 0x04, 0x04, 0x02, 0x24, 0x08, 0x30, 0x91, 37 | 0x3c, 0x63, 0x60, 0x81, 0x3c, 0x40, 0x00, 0xff, 0x2f, 0x00, 38 | } 39 | if bytes.Compare(midiData, expected) != 0 { 40 | t.Logf("\nExpected:\t%#v\nGot:\t\t%#v\n", expected, midiData) 41 | t.Fatal(fmt.Errorf("Midi binary output didn't match expectations")) 42 | } 43 | } 44 | 45 | func TestNewEncoderWithForcedEnd(t *testing.T) { 46 | w, err := tmpFile() 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | defer func() { 51 | w.Close() 52 | os.Remove(w.Name()) 53 | }() 54 | e := NewEncoder(w, SingleTrack, 96) 55 | tr := e.NewTrack() 56 | // add a C3 at velocity 99, half a beat/quarter note after the start 57 | tr.Add(0.5, NoteOn(1, KeyInt("C", 3), 99)) 58 | // turn off the C3 59 | tr.Add(1, NoteOff(1, KeyInt("C", 3))) 60 | // force the end of track to be later 61 | tr.Add(2, EndOfTrack()) 62 | if err := e.Write(); err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | w.Seek(0, 0) 67 | midiData, err := ioutil.ReadAll(w) 68 | if err != nil { 69 | t.Log(err) 70 | } 71 | expected := []byte{ 72 | 0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 00, 00, 00, 0x01, 00, 0x60, 0x4d, 0x54, 73 | 0x72, 0x6b, 0x00, 0x00, 0x00, 0x15, 0x00, 0xff, 0x58, 0x04, 0x04, 0x02, 0x24, 0x08, 0x30, 0x91, 74 | 0x3c, 0x63, 0x60, 0x81, 0x3c, 0x40, 0x81, 0x40, 0xff, 0x2f, 0x0, 75 | } 76 | if bytes.Compare(midiData, expected) != 0 { 77 | t.Logf("\nExpected:\t%#v\nGot:\t\t%#v\n", expected, midiData) 78 | t.Fatal(fmt.Errorf("Midi binary output didn't match expectations")) 79 | } 80 | } 81 | 82 | func tmpFile() (*os.File, error) { 83 | f, err := ioutil.TempFile("", "midi-test-") 84 | if err != nil { 85 | return nil, err 86 | } 87 | return f, nil 88 | } 89 | -------------------------------------------------------------------------------- /midi/event_mapping.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | // EventMap takes a event byte and returns its matching event name. 4 | // http://www.midi.org/techspecs/midimessages.php 5 | var EventMap = map[byte]string{ 6 | 0x8: "NoteOff", 7 | 0x9: "NoteOn", 8 | 0xA: "AfterTouch", 9 | 0xB: "ControlChange", 10 | 0xC: "ProgramChange", 11 | 0xD: "ChannelAfterTouch", 12 | 0xE: "PitchWheelChange", 13 | 0xF: "Meta", 14 | } 15 | 16 | // EventByteMap takes an event name and returns its equivalent MIDI byte 17 | var EventByteMap = map[string]byte{ 18 | "NoteOff": 0x8, 19 | "NoteOn": 0x9, 20 | "AfterTouch": 0xA, 21 | "ControlChange": 0xB, 22 | "ProgramChange": 0xC, 23 | "ChannelAfterTouch": 0xD, 24 | "PitchWheelChange": 0xE, 25 | "Meta": 0xF, 26 | } 27 | 28 | // MetaCmdMap maps metadata binary command to their names 29 | var MetaCmdMap = map[byte]string{ 30 | 0x0: "Sequence number", 31 | 0x01: "Text event", 32 | 0x02: "Copyright", 33 | 0x03: "Sequence/Track name", 34 | 0x04: "Instrument name", 35 | 0x05: "Lyric", 36 | 0x06: "Marker", 37 | 0x07: "Cue Point", 38 | 0x20: "MIDI Channel Prefix", 39 | 0x2f: "End of Track", 40 | 0x51: "Tempo", 41 | 0x58: "Time Signature", 42 | 0x59: "Key Signature", 43 | 0x7F: "Sequencer specific", 44 | 0x8F: "Timing Clock", 45 | 0xFA: "Start current sequence", 46 | 0xFB: "Continue stopped sequence where left off", 47 | 0xFC: "Stop sequence", 48 | } 49 | 50 | // MetaByteMap maps metadata command names to their binary cmd code 51 | var MetaByteMap = map[string]byte{ 52 | "Sequence number": 0x0, 53 | "Text event": 0x01, 54 | "Copyright": 0x02, 55 | "Sequence/Track name": 0x03, 56 | "Instrument name": 0x04, 57 | "Lyric": 0x05, 58 | "Marker": 0x06, 59 | "Cue Point": 0x07, 60 | "MIDI Channel Prefix": 0x20, 61 | "End of Track": 0x2f, 62 | "Tempo": 0x51, 63 | "Time Signature": 0x58, 64 | "Key Signature": 0x59, 65 | "Sequencer specific": 0x7F, 66 | "Timing Clock": 0x8F, 67 | "Start current sequence": 0xFA, 68 | "Continue stopped sequence where left off": 0xFB, 69 | "Stop sequence": 0xFC, 70 | } 71 | -------------------------------------------------------------------------------- /midi/fixtures/bossa.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/midi/fixtures/bossa.mid -------------------------------------------------------------------------------- /midi/fixtures/closedHat.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/midi/fixtures/closedHat.mid -------------------------------------------------------------------------------- /midi/fixtures/elise.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/midi/fixtures/elise.mid -------------------------------------------------------------------------------- /midi/fixtures/elise1track.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/midi/fixtures/elise1track.mid -------------------------------------------------------------------------------- /midi/grid.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | const ( 4 | // Dur8th is the duration of a 1/8th note in beats 5 | Dur8th = 1.0 / 2.0 6 | // Dur8thT is the duration of a 1/8th triplet note in beats 7 | Dur8thT = 1.0 / 3.0 8 | // Dur16th is the duration of a 1/16th note in beats 9 | Dur16th = Dur8th / 2 10 | // Dur16thT is the duration of a 1/16th note in beats 11 | Dur16thT = Dur8thT / 2 12 | ) 13 | -------------------------------------------------------------------------------- /midi/midi.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | var ( 9 | headerChunkID = [4]byte{0x4D, 0x54, 0x68, 0x64} 10 | trackChunkID = [4]byte{0x4D, 0x54, 0x72, 0x6B} 11 | 12 | // ErrFmtNotSupported is a generic error reporting an unknown format. 13 | ErrFmtNotSupported = errors.New("format not supported") 14 | // ErrUnexpectedData is a generic error reporting that the parser encountered unexpected data. 15 | ErrUnexpectedData = errors.New("unexpected data content") 16 | ) 17 | 18 | func NewDecoder(r io.Reader) *Decoder { 19 | return &Decoder{r: r} 20 | } 21 | 22 | func NewParser(r io.Reader, ch chan *Track) *Decoder { 23 | return &Decoder{r: r, Ch: ch} 24 | } 25 | 26 | // Uint24 converts a uint32 into a uint24 (big endian) 27 | func Uint24(n uint32) []byte { 28 | out := make([]byte, 3) 29 | out[2] = byte(n & 0xFF) 30 | out[1] = byte(n >> 8) 31 | out[0] = byte(n >> 16) 32 | return out 33 | } 34 | -------------------------------------------------------------------------------- /midi/note.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/mattetti/audio" 9 | ) 10 | 11 | var Notes = []string{"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"} 12 | 13 | var notesToInt = map[string]int{ 14 | "C": 0, 15 | "C#": 1, 16 | "DB": 1, 17 | "D": 2, 18 | "D#": 3, 19 | "EB": 3, 20 | "E": 4, 21 | "F": 5, 22 | "F#": 6, 23 | "GB": 6, 24 | "G": 7, 25 | "G#": 8, 26 | "AB": 8, 27 | "A": 9, 28 | "A#": 10, 29 | "BB": 10, 30 | "B": 11, 31 | } 32 | 33 | type Note struct { 34 | Channel int 35 | Key int 36 | Velocity int 37 | } 38 | 39 | // KeyInt converts an A-G note notation to a midi note number value. 40 | func KeyInt(n string, octave int) int { 41 | key := notesToInt[strings.ToUpper(n)] 42 | // octave starts at -2 but first note is at 0 43 | return key + (octave+2)*12 44 | } 45 | 46 | // KeyFreq returns the frequency for the given key/octave combo 47 | // https://en.wikipedia.org/wiki/MIDI_Tuning_Standard#Frequency_values 48 | func KeyFreq(n string, octave int) float64 { 49 | return audio.RootA * math.Pow(2, (float64(KeyInt(n, octave)-69)/12)) 50 | } 51 | 52 | // NoteToFreq returns the frequency of the passed midi note. 53 | func NoteToFreq(note int) float64 { 54 | return audio.RootA * math.Pow(2, (float64(note)-69.0)/12.0) 55 | } 56 | 57 | // NoteToName converts a midi note value into its English name 58 | func NoteToName(note int) string { 59 | key := Notes[note%12] 60 | octave := ((note / 12) | 0) - 2 // The MIDI scale starts at octave = -2 61 | return key + strconv.Itoa(octave) 62 | } 63 | 64 | // FreqToNote reports the associated midi node for a given frequency. 65 | func FreqToNote(freq float64) int { 66 | pitch := 12.0*(math.Log(freq/(440/2.0))/math.Log(2.0)) + 57.0 67 | return int(pitch + 0.00001) 68 | } 69 | -------------------------------------------------------------------------------- /midi/note_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import "testing" 4 | 5 | var epsilon float64 = 0.00000001 6 | 7 | func floatEquals(a, b float64) bool { 8 | if (a-b) < epsilon && (b-a) < epsilon { 9 | return true 10 | } 11 | return false 12 | } 13 | 14 | func TestKeyToInt(t *testing.T) { 15 | testCases := []struct { 16 | key string 17 | octave int 18 | n int 19 | }{ 20 | {"C", 0, 24}, 21 | {"C#", 0, 25}, 22 | {"Db", 0, 25}, 23 | {"D", 0, 26}, 24 | {"E", 0, 28}, 25 | {"F", 0, 29}, 26 | {"G", 0, 31}, 27 | {"A", 0, 33}, 28 | {"B", 0, 35}, 29 | {"C", 3, 60}, 30 | } 31 | 32 | for i, tc := range testCases { 33 | t.Logf("test case %s %d [%d]\n", tc.key, tc.octave, i) 34 | if o := KeyInt(tc.key, tc.octave); o != tc.n { 35 | t.Fatalf("expected %s %d -> %d\ngot\n%d\n", tc.key, tc.octave, tc.n, o) 36 | } 37 | } 38 | } 39 | 40 | func TestKeyFreq(t *testing.T) { 41 | testCases := []struct { 42 | key string 43 | octave int 44 | freq float64 45 | }{ 46 | {"C", 0, 32.70319566257483}, 47 | {"D", 0, 36.70809598967595}, 48 | {"E", 0, 41.20344461410875}, 49 | {"F", 0, 43.653528929125486}, 50 | {"G", 0, 48.99942949771867}, 51 | {"A", 0, 55.00}, 52 | {"B", 0, 61.7354126570155}, 53 | {"A", 3, 440.00}, 54 | {"D#", 3, 311.12698372208087}, 55 | {"C", 3, 261.6255653005986}, 56 | } 57 | 58 | for i, tc := range testCases { 59 | t.Logf("test case %d\n", i) 60 | if o := KeyFreq(tc.key, tc.octave); !floatEquals(o, tc.freq) { 61 | t.Fatalf("expected %s %d -> %v\ngot\n%v\n", tc.key, tc.octave, tc.freq, o) 62 | } 63 | } 64 | } 65 | 66 | func TestFreqToNote(t *testing.T) { 67 | testCases := []struct { 68 | note int 69 | freq float64 70 | }{ 71 | {KeyInt("C", 0), 32.70319566257483}, 72 | {KeyInt("D", 0), 36.70809598967595}, 73 | {KeyInt("E", 0), 41.20344461410875}, 74 | {KeyInt("F", 0), 43.653528929125486}, 75 | {KeyInt("G", 0), 48.99942949771867}, 76 | {KeyInt("A", 0), 55.00}, 77 | {KeyInt("B", 0), 61.7354126570155}, 78 | {KeyInt("A", 3), 440.00}, 79 | {KeyInt("D#", 3), 311.12698372208087}, 80 | {KeyInt("C", 3), 261.6255653005986}, 81 | } 82 | 83 | for i, tc := range testCases { 84 | t.Logf("test case %d\n", i) 85 | if note := FreqToNote(tc.freq); note != tc.note { 86 | t.Fatalf("expected freq %v -> %v\ngot\n%v\n", tc.freq, tc.note, note) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /midi/smpte_offset.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | type SmpteOffset struct { 4 | Hour uint8 5 | Min uint8 6 | Sec uint8 7 | Fr uint8 8 | SubFr uint8 9 | } 10 | -------------------------------------------------------------------------------- /midi/time_signature.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Time signature 9 | // FF 58 04 nn dd cc bb Time Signature 10 | // The time signature is expressed as four numbers. nn and dd 11 | // represent the numerator and denominator of the time signature as it 12 | // would be notated. The denominator is a negative power of two: 2 13 | // represents a quarter-note, 3 represents an eighth-note, etc. 14 | // The cc parameter expresses the number of MIDI clocks in a 15 | // metronome click. The bb parameter expresses the number of 16 | // notated 32nd-notes in a MIDI quarter-note (24 MIDI clocks). This 17 | // was added because there are already multiple programs which allow a 18 | // user to specify that what MIDI thinks of as a quarter-note (24 clocks) 19 | // is to be notated as, or related to in terms of, something else. 20 | type TimeSignature struct { 21 | Numerator uint8 22 | Denominator uint8 23 | ClocksPerTick uint8 24 | ThirtySecondNotesPerQuarter uint8 25 | } 26 | 27 | // Denum returns the notation denominator (which is not how it's stored in MIDI) 28 | func (ts *TimeSignature) Denum() int { 29 | return int(math.Exp2(float64(ts.Denominator))) 30 | } 31 | 32 | func (ts *TimeSignature) String() string { 33 | return fmt.Sprintf("%d/%d - %d clocks per tick - %d", ts.Numerator, ts.Denum(), ts.ClocksPerTick, ts.ThirtySecondNotesPerQuarter) 34 | } 35 | -------------------------------------------------------------------------------- /midi/track.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // Track 9 | // = 10 | // = 11 | // 12 | type Track struct { 13 | Size uint32 14 | Events []*Event 15 | ticksPerBeat uint16 16 | } 17 | 18 | // Add schedules the passed event after x beats (relative to the previous event) 19 | func (t *Track) Add(beatDelta float64, e *Event) { 20 | if t.ticksPerBeat == 0 { 21 | t.ticksPerBeat = 96 22 | } 23 | e.TimeDelta = uint32(beatDelta * float64(t.ticksPerBeat)) 24 | t.Events = append(t.Events, e) 25 | t.Size += uint32(len(EncodeVarint(e.TimeDelta))) + e.Size() 26 | } 27 | 28 | // Tempo returns the tempo of the track if set, 0 otherwise 29 | func (t *Track) Tempo() int { 30 | if t == nil { 31 | return 0 32 | } 33 | tempoEvType := MetaByteMap["Tempo"] 34 | for _, ev := range t.Events { 35 | if ev.Cmd == tempoEvType { 36 | return int(ev.Bpm) 37 | } 38 | } 39 | return 0 40 | } 41 | 42 | func (t *Track) Name() string { 43 | if t == nil { 44 | return "" 45 | } 46 | nameEvType := MetaByteMap["Sequence/Track name"] 47 | for _, ev := range t.Events { 48 | if ev.Cmd == nameEvType { 49 | // trim spaces and null bytes 50 | return strings.TrimRight(strings.TrimSpace(ev.SeqTrackName), "\x00") 51 | } 52 | } 53 | return "" 54 | } 55 | 56 | // ChunkData converts the track and its events into a binary byte slice (chunk 57 | // header included). If endTrack is set to true, the end track metadata will be 58 | // added if not already present. 59 | func (t *Track) ChunkData(endTrack bool) ([]byte, error) { 60 | buff := bytes.NewBuffer(nil) 61 | // time signature 62 | // TODO: don't have 4/4 36, 8 hardcoded 63 | buff.Write([]byte{0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x24, 0x08}) 64 | 65 | if endTrack { 66 | if l := len(t.Events); l > 0 { 67 | if t.Events[l-1].Cmd != MetaByteMap["End of Track"] { 68 | t.Add(0, EndOfTrack()) 69 | } 70 | } 71 | } 72 | for _, e := range t.Events { 73 | if _, err := buff.Write(e.Encode()); err != nil { 74 | return nil, err 75 | } 76 | } 77 | return buff.Bytes(), nil 78 | } 79 | -------------------------------------------------------------------------------- /midi/varint.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | // EncodeVarint returns the varint encoding of x. 4 | func EncodeVarint(x uint32) []byte { 5 | if x>>7 == 0 { 6 | return []byte{ 7 | byte(x), 8 | } 9 | } 10 | 11 | if x>>14 == 0 { 12 | return []byte{ 13 | byte(0x80 | x>>7), 14 | byte(127 & x), 15 | } 16 | } 17 | 18 | if x>>21 == 0 { 19 | return []byte{ 20 | byte(0x80 | x>>14), 21 | byte(0x80 | x>>7), 22 | byte(127 & x), 23 | } 24 | } 25 | 26 | return []byte{ 27 | byte(0x80 | x>>21), 28 | byte(0x80 | x>>14), 29 | byte(0x80 | x>>7), 30 | byte(127 & x), 31 | } 32 | } 33 | 34 | // DecodeVarint reads a varint-encoded integer from the slice. 35 | // It returns the integer and the number of bytes consumed, or 36 | // zero if there is not enough. 37 | func DecodeVarint(buf []byte) (x uint32, n int) { 38 | if len(buf) < 1 { 39 | return 0, 0 40 | } 41 | 42 | if buf[0] <= 0x80 { 43 | return uint32(buf[0]), 1 44 | } 45 | 46 | var b byte 47 | for n, b = range buf { 48 | x = x << 7 49 | x |= uint32(b) & 0x7F 50 | if (b & 0x80) == 0 { 51 | return x, n 52 | } 53 | } 54 | 55 | return x, n 56 | } 57 | -------------------------------------------------------------------------------- /midi/varint_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestEncodeDecodeVarint(t *testing.T) { 9 | testCases := []struct { 10 | input []byte 11 | output uint32 12 | }{ 13 | 0: {[]byte{0x7F}, 127}, 14 | 1: {[]byte{0x81, 0x00}, 128}, 15 | 2: {[]byte{0xC0, 0x00}, 8192}, 16 | 3: {[]byte{0xFF, 0x7F}, 16383}, 17 | 4: {[]byte{0x81, 0x80, 0x00}, 16384}, 18 | 5: {[]byte{0xFF, 0xFF, 0x7F}, 2097151}, 19 | 6: {[]byte{0x81, 0x80, 0x80, 0x00}, 2097152}, 20 | 7: {[]byte{0xC0, 0x80, 0x80, 0x00}, 134217728}, 21 | 8: {[]byte{0xFF, 0xFF, 0xFF, 0x7F}, 268435455}, 22 | 23 | 9: {[]byte{0x82, 0x00}, 256}, 24 | 10: {[]byte{0x81, 0x10}, 144}, 25 | } 26 | 27 | for i, tc := range testCases { 28 | t.Logf("test case %d - %#v\n", i, tc.input) 29 | if o, _ := DecodeVarint(tc.input); o != tc.output { 30 | t.Fatalf("expected %d\ngot\n%d\n", tc.output, o) 31 | } 32 | if encoded := EncodeVarint(tc.output); bytes.Compare(encoded, tc.input) != 0 { 33 | t.Fatalf("%d - expected %#v\ngot\n%#v\n", tc.output, tc.input, encoded) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mp3/cmd/validator/main.go: -------------------------------------------------------------------------------- 1 | // this is a simple tool to validate the mp3 files in a folder 2 | package main 3 | 4 | import ( 5 | "encoding/hex" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/mattetti/audio/mp3" 14 | ) 15 | 16 | var ( 17 | pathFlag = flag.String("path", ".", "Path to look for mp3 files to validate") 18 | ) 19 | 20 | func main() { 21 | flag.Parse() 22 | files, err := ioutil.ReadDir(*pathFlag) 23 | if err != nil { 24 | panic(err) 25 | } 26 | for _, fi := range files { 27 | if strings.HasSuffix(strings.ToLower(fi.Name()), ".mp3") { 28 | f, err := os.Open(filepath.Join(*pathFlag, fi.Name())) 29 | if err != nil { 30 | fmt.Println("error reading", fi.Name()) 31 | panic(err) 32 | } 33 | if !mp3.SeemsValid(f) { 34 | fmt.Printf("%s is not valid\n", fi.Name()) 35 | f.Seek(0, 0) 36 | buf := make([]byte, 128) 37 | f.Read(buf) 38 | fmt.Println(hex.Dump(buf)) 39 | } 40 | f.Close() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mp3/decoder_test.go: -------------------------------------------------------------------------------- 1 | package mp3_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/mattetti/audio/mp3" 9 | ) 10 | 11 | func Test_SeemsValid(t *testing.T) { 12 | testCases := []struct { 13 | input string 14 | isValid bool 15 | }{ 16 | {"fixtures/frame.mp3", false}, 17 | {"fixtures/HousyStab.mp3", true}, 18 | {"../wav/fixtures/bass.wav", false}, 19 | {"fixtures/nullbytes.mp3", true}, 20 | {"fixtures/idv3-24.mp3", true}, 21 | } 22 | 23 | for i, tc := range testCases { 24 | t.Logf("test case %d - %s\n", i, tc.input) 25 | f, err := os.Open(tc.input) 26 | if err != nil { 27 | panic(err) 28 | } 29 | if o := mp3.SeemsValid(f); o != tc.isValid { 30 | t.Fatalf("expected %t\ngot\n%t\n", tc.isValid, o) 31 | } 32 | f.Close() 33 | } 34 | } 35 | 36 | func Test_Decoder_Duration(t *testing.T) { 37 | testCases := []struct { 38 | input string 39 | duration string 40 | }{ 41 | {"fixtures/HousyStab.mp3", "16.483264688s"}, 42 | {"fixtures/slayer.mp3", "28.447345872s"}, 43 | {"fixtures/nullbytes.mp3", "13.505305616s"}, 44 | {"fixtures/idv3-24.mp3", "11.128162848s"}, 45 | {"fixtures/weird_duration.mp3", "5.714122448s"}, 46 | } 47 | 48 | for i, tc := range testCases { 49 | t.Logf("duration test case %d - %s\n", i, tc.input) 50 | f, err := os.Open(tc.input) 51 | if err != nil { 52 | panic(err) 53 | } 54 | d := mp3.New(f) 55 | dur, err := d.Duration() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | if o := fmt.Sprintf("%s", dur); o != tc.duration { 60 | t.Fatalf("expected %s\ngot\n%s\n", tc.duration, o) 61 | } 62 | } 63 | } 64 | 65 | func ExampleDecoder_Duration() { 66 | f, err := os.Open("fixtures/HousyStab.mp3") 67 | if err != nil { 68 | panic(err) 69 | } 70 | d := mp3.New(f) 71 | dur, err := d.Duration() 72 | if err != nil { 73 | panic(err) 74 | } 75 | fmt.Println(dur) 76 | //Output: 16.483264688s 77 | } 78 | -------------------------------------------------------------------------------- /mp3/fixtures/HousyStab.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/mp3/fixtures/HousyStab.mp3 -------------------------------------------------------------------------------- /mp3/fixtures/frame.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/mp3/fixtures/frame.mp3 -------------------------------------------------------------------------------- /mp3/fixtures/idv3-24.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/mp3/fixtures/idv3-24.mp3 -------------------------------------------------------------------------------- /mp3/fixtures/nullbytes.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/mp3/fixtures/nullbytes.mp3 -------------------------------------------------------------------------------- /mp3/fixtures/slayer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/mp3/fixtures/slayer.mp3 -------------------------------------------------------------------------------- /mp3/fixtures/weird_duration.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/mp3/fixtures/weird_duration.mp3 -------------------------------------------------------------------------------- /mp3/frame.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | type Frame struct { 11 | buf []byte 12 | // SkippedBytes is the amount of bytes we had to skip before getting to the frame 13 | SkippedBytes int 14 | // Counter gets incremented if the same frame is reused to parse a file 15 | Counter int 16 | Header FrameHeader 17 | } 18 | 19 | type ( 20 | // FrameVersion is the MPEG version given in the frame header 21 | FrameVersion byte 22 | // FrameLayer is the MPEG layer given in the frame header 23 | FrameLayer byte 24 | // FrameEmphasis is the Emphasis value from the frame header 25 | FrameEmphasis byte 26 | // FrameChannelMode is the Channel mode from the frame header 27 | FrameChannelMode byte 28 | // FrameBitRate is the bit rate from the frame header 29 | FrameBitRate int 30 | // FrameSampleRate is the sample rate from teh frame header 31 | FrameSampleRate int 32 | // FrameSideInfo holds the SideInfo bytes from the frame 33 | FrameSideInfo []byte 34 | ) 35 | 36 | // Duration calculates the time duration of this frame based on the samplerate and number of samples 37 | func (f *Frame) Duration() time.Duration { 38 | if !f.Header.IsValid() { 39 | return 0 40 | } 41 | ms := (1000 / float64(f.Header.SampleRate())) * float64(f.Header.Samples()) 42 | dur := time.Duration(int(float64(time.Millisecond) * ms)) 43 | if dur < 0 { 44 | // we have bad data, let's ignore it 45 | dur = 0 46 | } 47 | return dur 48 | } 49 | 50 | // CRC returns the CRC word stored in this frame 51 | func (f *Frame) CRC() uint16 { 52 | var crc uint16 53 | if !f.Header.Protection() { 54 | return 0 55 | } 56 | crcdata := bytes.NewReader(f.buf[4:6]) 57 | binary.Read(crcdata, binary.BigEndian, &crc) 58 | return crc 59 | } 60 | 61 | // SideInfo returns the side info for this frame 62 | func (f *Frame) SideInfo() FrameSideInfo { 63 | if f.Header.Protection() { 64 | return FrameSideInfo(f.buf[6:]) 65 | } else { 66 | return FrameSideInfo(f.buf[4:]) 67 | } 68 | } 69 | 70 | // Frame returns a string describing this frame, header and side info 71 | func (f *Frame) String() string { 72 | str := "" 73 | str += fmt.Sprintf("Header: \n%s", f.Header) 74 | str += fmt.Sprintf("CRC: %x\n", f.CRC()) 75 | str += fmt.Sprintf("Samples: %v\n", f.Header.Samples()) 76 | str += fmt.Sprintf("Size: %v\n", f.Header.Size()) 77 | str += fmt.Sprintf("Duration: %v\n", f.Duration()) 78 | return str 79 | } 80 | 81 | // NDataBegin is the number of bytes before the frame header at which the sample data begins 82 | // 0 indicates that the data begins after the side channel information. This data is the 83 | // data from the "bit resevoir" and can be up to 511 bytes 84 | func (i FrameSideInfo) NDataBegin() uint16 { 85 | return (uint16(i[0]) << 1 & (uint16(i[1]) >> 7)) 86 | } 87 | -------------------------------------------------------------------------------- /mp3/frame_header.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import "fmt" 4 | 5 | type ( 6 | // FrameHeader represents the entire header of a frame 7 | FrameHeader []byte 8 | ) 9 | 10 | // IsValid tests the basic validity of the frame header by checking the sync word 11 | func (h FrameHeader) IsValid() bool { 12 | if len(h) < 4 { 13 | return false 14 | } 15 | return h[0] == 0xff && h[1]&0xE0 == 0xE0 && h.Version() <= MPEG1 && h.Layer() <= Layer1 16 | } 17 | 18 | // Version returns the MPEG version from the header 19 | func (h FrameHeader) Version() FrameVersion { 20 | if len(h) < 4 { 21 | return 0 22 | } 23 | return FrameVersion((h[1] >> 3) & 0x03) 24 | } 25 | 26 | // Layer returns the MPEG layer from the header 27 | func (h FrameHeader) Layer() FrameLayer { 28 | if len(h) < 4 { 29 | return 0 30 | } 31 | return FrameLayer((h[1] >> 1) & 0x03) 32 | } 33 | 34 | // Protection indicates if there is a CRC present after the header (before the side data) 35 | func (h FrameHeader) Protection() bool { 36 | if len(h) < 4 { 37 | return false 38 | } 39 | return (h[1] & 0x01) != 0x01 40 | } 41 | 42 | // BitRate returns the calculated bit rate from the header 43 | func (h FrameHeader) BitRate() FrameBitRate { 44 | if len(h) < 4 { 45 | return 0 46 | } 47 | bitrateIdx := (h[2] >> 4) & 0x0F 48 | if bitrateIdx == 0x0F { 49 | return ErrInvalidBitrate 50 | } 51 | br := bitrates[h.Version()][h.Layer()][bitrateIdx] * 1000 52 | if br == 0 { 53 | return ErrInvalidBitrate 54 | } 55 | return FrameBitRate(br) 56 | } 57 | 58 | // SampleRate returns the samplerate from the header 59 | func (h FrameHeader) SampleRate() FrameSampleRate { 60 | if len(h) < 4 { 61 | return 0 62 | } 63 | sri := (h[2] >> 2) & 0x03 64 | if sri == 0x03 { 65 | return ErrInvalidSampleRate 66 | } 67 | return FrameSampleRate(sampleRates[h.Version()][sri]) 68 | } 69 | 70 | // Pad returns the pad bit, indicating if there are extra samples 71 | // in this frame to make up the correct bitrate 72 | func (h FrameHeader) Pad() bool { 73 | if len(h) < 4 { 74 | return false 75 | } 76 | return ((h[2] >> 1) & 0x01) == 0x01 77 | } 78 | 79 | // Private retrusn the Private bit from the header 80 | func (h FrameHeader) Private() bool { 81 | if len(h) < 4 { 82 | return false 83 | } 84 | return (h[2] & 0x01) == 0x01 85 | } 86 | 87 | // ChannelMode returns the channel mode from the header 88 | func (h FrameHeader) ChannelMode() FrameChannelMode { 89 | if len(h) < 4 { 90 | return 0 91 | } 92 | return FrameChannelMode((h[3] >> 6) & 0x03) 93 | } 94 | 95 | // CopyRight returns the CopyRight bit from the header 96 | func (h FrameHeader) CopyRight() bool { 97 | if len(h) < 4 { 98 | return false 99 | } 100 | return (h[3]>>3)&0x01 == 0x01 101 | } 102 | 103 | // Original returns the "original content" bit from the header 104 | func (h FrameHeader) Original() bool { 105 | if len(h) < 4 { 106 | return false 107 | } 108 | return (h[3]>>2)&0x01 == 0x01 109 | } 110 | 111 | // Emphasis returns the Emphasis from the header 112 | func (h FrameHeader) Emphasis() FrameEmphasis { 113 | if len(h) < 4 { 114 | return 0 115 | } 116 | return FrameEmphasis((h[3] & 0x03)) 117 | } 118 | 119 | // String dumps the frame header as a string for display purposes 120 | func (h FrameHeader) String() string { 121 | str := "" 122 | str += fmt.Sprintf(" Layer: %v\n", h.Layer()) 123 | str += fmt.Sprintf(" Version: %v\n", h.Version()) 124 | str += fmt.Sprintf(" Protection: %v\n", h.Protection()) 125 | str += fmt.Sprintf(" BitRate: %v\n", h.BitRate()) 126 | str += fmt.Sprintf(" SampleRate: %v\n", h.SampleRate()) 127 | str += fmt.Sprintf(" Pad: %v\n", h.Pad()) 128 | str += fmt.Sprintf(" Private: %v\n", h.Private()) 129 | str += fmt.Sprintf(" ChannelMode: %v\n", h.ChannelMode()) 130 | str += fmt.Sprintf(" CopyRight: %v\n", h.CopyRight()) 131 | str += fmt.Sprintf(" Original: %v\n", h.Original()) 132 | str += fmt.Sprintf(" Emphasis: %v\n", h.Emphasis()) 133 | str += fmt.Sprintf(" Number Samples: %v\n", h.Samples()) 134 | str += fmt.Sprintf(" Size: %v\n", h.Size()) 135 | return str 136 | } 137 | 138 | // Samples is the number of samples contained in this frame 139 | func (h FrameHeader) Samples() int { 140 | if len(h) < 4 { 141 | return 0 142 | } 143 | ftype, ok := samplesPerFrame[h.Version()] 144 | if !ok { 145 | return 0 146 | } 147 | return ftype[h.Layer()] 148 | } 149 | 150 | func (h FrameHeader) Size() int64 { 151 | if !h.IsValid() { 152 | return 0 153 | } 154 | bps := float64(h.Samples()) / 8 155 | fsize := (bps * float64(h.BitRate())) / float64(h.SampleRate()) 156 | if h.Pad() { 157 | fsize += float64(slotSize[h.Layer()]) 158 | } 159 | return int64(fsize) 160 | } 161 | -------------------------------------------------------------------------------- /mp3/id3v1/id3v1.go: -------------------------------------------------------------------------------- 1 | // id3v1 is a package allowing the extraction of id3v1 tags. 2 | // See http://en.wikipedia.org/wiki/ID3#ID3v1 3 | package id3v1 4 | 5 | var ( 6 | HeaderTagID = []byte{0x54, 0x41, 0x47} 7 | ) 8 | 9 | const ( 10 | Size = 128 11 | HeaderCode = "TAG" 12 | ) 13 | -------------------------------------------------------------------------------- /mp3/id3v1/tag.go: -------------------------------------------------------------------------------- 1 | package id3v1 2 | 3 | const ( 4 | // TagSize is the size in bytes of an id3v1 tag 5 | TagSize = 128 6 | ) 7 | 8 | var ( 9 | //TagCode is the byte representation of "TAG" 10 | TagCode = []byte{84, 65, 71} 11 | ) 12 | 13 | // Tag contains the various information stored in a id3 v1 tag. 14 | // Strings are either space or zero-padded. 15 | // Unset string entries are filled using an empty string. ID3v1 is 128 bytes long 16 | type Tag struct { 17 | Title [30]byte 18 | Artist [30]byte 19 | Album [30]byte 20 | Year [4]byte 21 | // The track number is stored in the last two bytes of the comment field. 22 | // If the comment is 29 or 30 characters long, no track number can be stored. 23 | Comment [30]byte 24 | ZeroByte byte 25 | Track byte 26 | Genre byte 27 | } 28 | -------------------------------------------------------------------------------- /mp3/id3v1/tag_plus.go: -------------------------------------------------------------------------------- 1 | package id3v1 2 | 3 | const ( 4 | TagPlusSize = 227 5 | ) 6 | 7 | var ( 8 | TagPlusCode = []byte{84, 65, 71, 43} // "TAG+" 9 | ) 10 | 11 | type TagPlugs struct { 12 | Title [60]byte 13 | Artist [60]byte 14 | Album [60]byte 15 | // 0=unset, 1=slow, 2= medium, 3=fast, 4=hardcore 16 | Speed uint8 17 | // A free-text field for the genre 18 | Genre [30]byte 19 | // the start of the music as mmm:ss 20 | StartTime [6]byte 21 | // the end of the music as mmm:ss 22 | EndTime [6]byte 23 | } 24 | -------------------------------------------------------------------------------- /mp3/id3v2/id3v2.go: -------------------------------------------------------------------------------- 1 | package id3v2 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | // HeaderTagID are the 3 bytes starting the ID3 v2 tag 10 | HeaderTagID = []byte{0x49, 0x44, 0x33} 11 | 12 | // ErrInvalidTagHeader 13 | ErrInvalidTagHeader = errors.New("invalid tag header") 14 | ) 15 | 16 | type Header struct { 17 | Version Version 18 | Flags Flags 19 | Size int 20 | } 21 | 22 | type Version struct { 23 | Major uint8 24 | Revision uint8 25 | } 26 | 27 | type Flags struct { 28 | Unsynchronisation bool 29 | ExtendedHeader bool 30 | ExperimentalIndicator bool 31 | FooterPresent bool 32 | } 33 | 34 | type Frame struct { 35 | Header [10]byte 36 | Data []byte 37 | } 38 | 39 | type Tag struct { 40 | Header *Header 41 | extendedHeader []byte 42 | frameSets map[string][]*Frame 43 | } 44 | 45 | // ReadHeader reads the 10 bytes header and parses the data which gets stored in 46 | // the tag header. 47 | func (t *Tag) ReadHeader(th TagHeader) error { 48 | // id3 tag ID 49 | if !th.IsValidID() { 50 | return ErrInvalidTagHeader 51 | } 52 | // version 53 | t.Header = &Header{ 54 | Version: th.ReadVersion(), 55 | Flags: th.ReadFlags(), 56 | } 57 | size, err := th.ReadSize() 58 | if err != nil { 59 | return err 60 | } 61 | t.Header.Size = size 62 | 63 | return nil 64 | } 65 | 66 | // synchsafe integers 67 | // https://en.wikipedia.org/wiki/Synchsafe 68 | func synchSafe(buf []byte) (int, error) { 69 | n := int(0) 70 | for _, b := range buf { 71 | if (b & (1 << 7)) != 0 { 72 | return 0, fmt.Errorf("invalid synchsafe integer") 73 | } 74 | n |= (n << 7) | int(b) 75 | } 76 | return n, nil 77 | } 78 | -------------------------------------------------------------------------------- /mp3/id3v2/tag_header.go: -------------------------------------------------------------------------------- 1 | package id3v2 2 | 3 | import "bytes" 4 | 5 | // TagHeader is the header content for an id3v2 tag 6 | type TagHeader [10]byte 7 | 8 | // IsValidID checks that the tag header starts by the right ID 9 | func (th TagHeader) IsValidID() bool { 10 | return (bytes.Compare(th[:3], HeaderTagID) == 0) 11 | } 12 | 13 | // ReadVersion extracts the version information. 14 | func (th TagHeader) ReadVersion() Version { 15 | return Version{ 16 | Major: uint8(th[3]), 17 | Revision: uint8(th[4]), 18 | } 19 | } 20 | 21 | // ReadFlags reads the header flags 22 | func (th TagHeader) ReadFlags() Flags { 23 | flags := Flags{} 24 | flags.Unsynchronisation = (th[5] & (1 << 0)) != 0 // 3.1.a 25 | flags.ExtendedHeader = (th[5] & (1 << 1)) != 0 // 3.1.b 26 | flags.ExperimentalIndicator = (th[5] & (1 << 2)) != 0 // 3.1.c 27 | flags.FooterPresent = (th[5] & (1 << 3)) != 0 // 3.1.d 28 | 29 | return flags 30 | } 31 | 32 | // ReadSize reads the size of the tag header 33 | func (th TagHeader) ReadSize() (int, error) { 34 | return synchSafe(th[6:]) 35 | } 36 | -------------------------------------------------------------------------------- /mp3/mp3.go: -------------------------------------------------------------------------------- 1 | // mp3 is a package used to access mp3 information 2 | // See: http://sea-mist.se/fou/cuppsats.nsf/all/857e49b9bfa2d753c125722700157b97/$file/Thesis%20report-%20MP3%20Decoder.pdf 3 | // Uses some code from https://github.com/tcolgate/mp3 under MIT license Tristan Colgate-McFarlane and badgerodon 4 | package mp3 5 | 6 | import ( 7 | "errors" 8 | "io" 9 | ) 10 | 11 | //go:generate stringer -type=FrameVersion 12 | const ( 13 | MPEG25 FrameVersion = iota 14 | MPEGReserved 15 | MPEG2 16 | MPEG1 17 | ) 18 | 19 | //go:generate stringer -type=FrameLayer 20 | const ( 21 | LayerReserved FrameLayer = iota 22 | Layer3 23 | Layer2 24 | Layer1 25 | ) 26 | 27 | //go:generate stringer -type=FrameEmphasis 28 | const ( 29 | EmphNone FrameEmphasis = iota 30 | Emph5015 31 | EmphReserved 32 | EmphCCITJ17 33 | ) 34 | 35 | //go:generate stringer -type=FrameChannelMode 36 | const ( 37 | Stereo FrameChannelMode = iota 38 | JointStereo 39 | DualChannel 40 | SingleChannel 41 | ) 42 | 43 | var ( 44 | // ID31HBytes are the 2 bytes starting the ID3 v1 tag 45 | ID31HBytes = []byte{0xFF, 0xFB} 46 | // XingTAGID Xing vbr tag 47 | XingTAGID = []byte{0x58, 0x69, 0x6E, 0x67} 48 | // InfoTAGID Info cbr tag 49 | InfoTAGID = []byte{0x49, 0x6E, 0x66, 0x6F} 50 | ) 51 | 52 | var ( 53 | // ErrNoSyncBits implies we could not find a valid frame header sync bit before EOF 54 | ErrNoSyncBits = errors.New("EOF before sync bits found") 55 | 56 | // ErrPrematureEOF indicates that the filed ended before a complete frame could be read 57 | ErrPrematureEOF = errors.New("EOF mid stream") 58 | 59 | ErrInvalidHeader = errors.New("invalid header") 60 | 61 | // ErrInvalidBitrate indicates that the header information did not contain a recognized bitrate 62 | ErrInvalidBitrate FrameBitRate = -1 63 | 64 | // ErrInvalidSampleRate indicates that no samplerate could be found for the frame header provided 65 | ErrInvalidSampleRate = FrameSampleRate(-1) 66 | 67 | bitrates = map[FrameVersion]map[FrameLayer][15]int{ 68 | MPEG1: { // MPEG 1 69 | Layer1: {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}, // Layer1 70 | Layer2: {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}, // Layer2 71 | Layer3: {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}, // Layer3 72 | }, 73 | MPEG2: { // MPEG 2, 2.5 74 | Layer1: {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}, // Layer1 75 | Layer2: {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, // Layer2 76 | Layer3: {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, // Layer3 77 | }, 78 | } 79 | 80 | sampleRates = map[FrameVersion][3]int{ 81 | MPEG1: {44100, 48000, 32000}, 82 | MPEG2: {22050, 24000, 16000}, 83 | MPEG25: {11025, 12000, 8000}, 84 | MPEGReserved: {0, 0, 0}, 85 | } 86 | 87 | slotSize = map[FrameLayer]int{ 88 | LayerReserved: 0, 89 | Layer3: 1, 90 | Layer2: 1, 91 | Layer1: 4, 92 | } 93 | 94 | samplesPerFrame = map[FrameVersion]map[FrameLayer]int{ 95 | MPEG1: { 96 | Layer1: 384, 97 | Layer2: 1152, 98 | Layer3: 1152, 99 | }, 100 | MPEG2: { 101 | Layer1: 384, 102 | Layer2: 1152, 103 | Layer3: 576, 104 | }, 105 | } 106 | ) 107 | 108 | func New(r io.Reader) *Decoder { 109 | return &Decoder{r: r} 110 | } 111 | -------------------------------------------------------------------------------- /riff/README.md: -------------------------------------------------------------------------------- 1 | # RIFF parser 2 | 3 | Moved to [https://github.com/go-audio/riff](https://github.com/go-audio/riff) 4 | -------------------------------------------------------------------------------- /riff/chunk.go: -------------------------------------------------------------------------------- 1 | package riff 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "sync" 10 | ) 11 | 12 | // Chunk represents the header and containt of a sub block 13 | // See https://tech.ebu.ch/docs/tech/tech3285.pdf to see how 14 | // audio content is stored in a BWF/WAVE file. 15 | type Chunk struct { 16 | ID [4]byte 17 | Size int 18 | Pos int 19 | R io.Reader 20 | okChan chan bool 21 | Wg *sync.WaitGroup 22 | } 23 | 24 | func (ch *Chunk) DecodeWavHeader(p *Parser) error { 25 | if ch == nil { 26 | return fmt.Errorf("can't decode a nil chunk") 27 | } 28 | if ch.ID == FmtID { 29 | p.wavHeaderSize = uint32(ch.Size) 30 | if err := ch.ReadLE(&p.WavAudioFormat); err != nil { 31 | return err 32 | } 33 | if err := ch.ReadLE(&p.NumChannels); err != nil { 34 | return err 35 | } 36 | if err := ch.ReadLE(&p.SampleRate); err != nil { 37 | return err 38 | } 39 | if err := ch.ReadLE(&p.AvgBytesPerSec); err != nil { 40 | return err 41 | } 42 | if err := ch.ReadLE(&p.BlockAlign); err != nil { 43 | return err 44 | } 45 | if err := ch.ReadLE(&p.BitsPerSample); err != nil { 46 | return err 47 | } 48 | 49 | // if we aren't dealing with a PCM file, we advance to reader to the 50 | // end of the chunck. 51 | if ch.Size > 16 { 52 | extra := make([]byte, ch.Size-16) 53 | ch.ReadLE(&extra) 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | // Done signals the parent parser that we are done reading the chunk 60 | // if the chunk isn't fully read, this code will do so before signaling. 61 | func (ch *Chunk) Done() { 62 | if !ch.IsFullyRead() { 63 | ch.Drain() 64 | } 65 | if ch.Wg != nil { 66 | ch.Wg.Done() 67 | } 68 | } 69 | 70 | // IsFullyRead checks if we're finished reading the chunk 71 | func (ch *Chunk) IsFullyRead() bool { 72 | if ch == nil || ch.R == nil { 73 | return true 74 | } 75 | return ch.Size <= ch.Pos 76 | } 77 | 78 | // Read implements the reader interface 79 | func (ch *Chunk) Read(p []byte) (n int, err error) { 80 | if ch == nil || ch.R == nil { 81 | return 0, errors.New("nil chunk/reader pointer") 82 | } 83 | n, err = ch.R.Read(p) 84 | ch.Pos += n 85 | return n, err 86 | } 87 | 88 | // ReadLE reads the Little Endian chunk data into the passed struct 89 | func (ch *Chunk) ReadLE(dst interface{}) error { 90 | if ch == nil || ch.R == nil { 91 | return errors.New("nil chunk/reader pointer") 92 | } 93 | if ch.IsFullyRead() { 94 | return io.EOF 95 | } 96 | ch.Pos += binary.Size(dst) 97 | return binary.Read(ch.R, binary.LittleEndian, dst) 98 | } 99 | 100 | // ReadBE reads the Big Endian chunk data into the passed struct 101 | func (ch *Chunk) ReadBE(dst interface{}) error { 102 | if ch.IsFullyRead() { 103 | return io.EOF 104 | } 105 | ch.Pos += binary.Size(dst) 106 | return binary.Read(ch.R, binary.LittleEndian, dst) 107 | } 108 | 109 | // ReadByte reads and returns a single byte 110 | func (ch *Chunk) ReadByte() (byte, error) { 111 | if ch.IsFullyRead() { 112 | return 0, io.EOF 113 | } 114 | var r byte 115 | err := ch.ReadLE(&r) 116 | return r, err 117 | } 118 | 119 | // Drain discards the rest of the chunk 120 | func (ch *Chunk) Drain() { 121 | bytesAhead := ch.Size - ch.Pos 122 | for bytesAhead > 0 { 123 | readSize := int64(bytesAhead) 124 | 125 | if _, err := io.CopyN(ioutil.Discard, ch.R, readSize); err != nil { 126 | return 127 | } 128 | bytesAhead -= int(readSize) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /riff/chunk_test.go: -------------------------------------------------------------------------------- 1 | package riff 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | ) 10 | 11 | func TestWavNextChunk(t *testing.T) { 12 | path, _ := filepath.Abs("fixtures/sample.wav") 13 | f, err := os.Open(path) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer f.Close() 18 | c := New(f) 19 | if err := c.ParseHeaders(); err != nil { 20 | t.Fatal(err) 21 | } 22 | // fmt 23 | ch, err := c.NextChunk() 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if ch.ID != FmtID { 28 | t.Fatalf("Expected the next chunk to have an ID of %q but got %q", FmtID, ch.ID) 29 | } 30 | if ch.Size != 16 { 31 | t.Fatalf("Expected the next chunk to have a size of %d but got %d", 16, ch.Size) 32 | } 33 | ch.Done() 34 | // 35 | ch, err = c.NextChunk() 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | if ch.ID != DataFormatID { 40 | t.Fatalf("Expected the next chunk to have an ID of %q but got %q", DataFormatID, ch.ID) 41 | } 42 | if ch.Size != 53958 { 43 | t.Fatalf("Expected the next chunk to have a size of %d but got %d", 53958, ch.Size) 44 | } 45 | if int(c.Size) != (ch.Size + 36) { 46 | t.Fatal("Looks like we have some extra data in this wav file?") 47 | } 48 | } 49 | 50 | func TestNextChunk(t *testing.T) { 51 | path, _ := filepath.Abs("fixtures/sample.wav") 52 | f, err := os.Open(path) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | defer f.Close() 57 | c := New(f) 58 | if err := c.ParseHeaders(); err != nil { 59 | t.Fatal(err) 60 | } 61 | ch, err := c.NextChunk() 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | ch.DecodeWavHeader(c) 66 | 67 | ch, err = c.NextChunk() 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | nextSample := func() []byte { 73 | var s = make([]byte, c.BlockAlign) 74 | if err := ch.ReadLE(s); err != nil { 75 | t.Fatal(err) 76 | } 77 | return s 78 | } 79 | firstSample := nextSample() 80 | if ch.Pos != int(c.BlockAlign) { 81 | t.Fatal("Chunk position wasn't moved as expected") 82 | } 83 | expectedSample := []byte{0, 0} 84 | if bytes.Compare(firstSample, expectedSample) != 0 { 85 | t.Fatalf("First sample doesn't seem right, got %q, expected %q", firstSample, expectedSample) 86 | } 87 | 88 | desideredPos := 1541 89 | bytePos := desideredPos * 2 90 | for ch.Pos < bytePos { 91 | nextSample() 92 | } 93 | s := nextSample() 94 | expectedSample = []byte{0xfe, 0xff} 95 | if bytes.Compare(s, expectedSample) != 0 { 96 | t.Fatalf("1542nd sample doesn't seem right, got %q, expected %q", s, expectedSample) 97 | } 98 | 99 | } 100 | 101 | func ExampleParser_NextChunk() { 102 | // Example showing how to access the sound data 103 | path, _ := filepath.Abs("fixtures/sample.wav") 104 | f, err := os.Open(path) 105 | if err != nil { 106 | panic(err) 107 | } 108 | defer f.Close() 109 | c := New(f) 110 | if err := c.ParseHeaders(); err != nil { 111 | panic(err) 112 | } 113 | 114 | var chunk *Chunk 115 | for err == nil { 116 | chunk, err = c.NextChunk() 117 | if err != nil { 118 | panic(err) 119 | } 120 | if chunk.ID == FmtID { 121 | chunk.DecodeWavHeader(c) 122 | } else if chunk.ID == DataFormatID { 123 | break 124 | } 125 | chunk.Done() 126 | } 127 | soundData := chunk 128 | 129 | nextSample := func() []byte { 130 | s := make([]byte, c.BlockAlign) 131 | if err := soundData.ReadLE(&s); err != nil { 132 | panic(err) 133 | } 134 | return s 135 | } 136 | 137 | // jump to a specific sample since first samples are blank 138 | desideredPos := 1541 139 | bytePos := desideredPos * 2 140 | for i := 0; soundData.Pos < bytePos; i++ { 141 | nextSample() 142 | if i > soundData.Size { 143 | panic(fmt.Errorf("%+v read way too many bytes, we're out of bounds", soundData)) 144 | } 145 | } 146 | 147 | sample := nextSample() 148 | fmt.Printf("1542nd sample: %#X %#X\n", sample[0], sample[1]) 149 | // Output: 150 | // 1542nd sample: 0XFE 0XFF 151 | } 152 | -------------------------------------------------------------------------------- /riff/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Package riff is package implementing a simple Resource Interchange File Format 4 | (RIFF) parser with basic support for sub formats such as WAV. 5 | The goal of this package is to give all the tools needed for a developer 6 | to implement parse any kind of file formats using the RIFF container format. 7 | 8 | Support for PCM wav format was added so the headers are parsed, the duration and the raw sound data 9 | of a wav file can be easily accessed (See the examples below) . 10 | 11 | For more information about RIFF: 12 | https://en.wikipedia.org/wiki/Resource_Interchange_File_Format 13 | 14 | */ 15 | package riff 16 | -------------------------------------------------------------------------------- /riff/fixtures/junkKick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/riff/fixtures/junkKick.wav -------------------------------------------------------------------------------- /riff/fixtures/sample.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/riff/fixtures/sample.avi -------------------------------------------------------------------------------- /riff/fixtures/sample.rmi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/riff/fixtures/sample.rmi -------------------------------------------------------------------------------- /riff/fixtures/sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/riff/fixtures/sample.wav -------------------------------------------------------------------------------- /riff/parser_test.go: -------------------------------------------------------------------------------- 1 | package riff 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestParseHeader(t *testing.T) { 13 | expectations := []struct { 14 | input string 15 | id [4]byte 16 | size uint32 17 | format [4]byte 18 | }{ 19 | {"fixtures/sample.rmi", RiffID, 29632, rmiFormatID}, 20 | {"fixtures/sample.wav", RiffID, 53994, WavFormatID}, 21 | {"fixtures/sample.avi", RiffID, 230256, aviFormatID}, 22 | } 23 | 24 | for _, exp := range expectations { 25 | path, _ := filepath.Abs(exp.input) 26 | f, err := os.Open(path) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer f.Close() 31 | c := New(f) 32 | err = c.ParseHeaders() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | if c.ID != exp.id { 37 | t.Fatalf("%s of %s didn't match %s, got %s", "ID", exp.input, exp.id, c.ID) 38 | } 39 | if c.Size != exp.size { 40 | t.Fatalf("%s of %s didn't match %d, got %d", "BlockSize", exp.input, exp.size, c.Size) 41 | } 42 | if c.Format != exp.format { 43 | t.Fatalf("%s of %s didn't match %q, got %q", "Format", exp.input, exp.format, c.Format) 44 | } 45 | } 46 | } 47 | 48 | func TestParseWavHeaders(t *testing.T) { 49 | expectations := []struct { 50 | input string 51 | headerSize uint32 52 | format uint16 53 | numChans uint16 54 | sampleRate uint32 55 | byteRate uint32 56 | blockAlign uint16 57 | bitsPerSample uint16 58 | }{ 59 | // mono audio files 60 | {"fixtures/sample.wav", 16, 1, 1, 44100, 88200, 2, 16}, 61 | // sterep audio files with junk, bext and more headers 62 | {"fixtures/junkKick.wav", 40, 1, 2, 44100, 176400, 4, 16}, 63 | } 64 | 65 | for _, exp := range expectations { 66 | path, _ := filepath.Abs(exp.input) 67 | t.Log(path) 68 | 69 | f, err := os.Open(path) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | defer f.Close() 74 | c := New(f) 75 | if err := c.ParseHeaders(); err != nil { 76 | t.Fatalf("%s for %s when parsing headers", err, path) 77 | } 78 | ch, err := c.NextChunk() 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | for ; ch != nil; ch, err = c.NextChunk() { 84 | if err != nil { 85 | if err != io.EOF { 86 | t.Fatal(err) 87 | } 88 | break 89 | } 90 | 91 | if bytes.Compare(ch.ID[:], FmtID[:]) == 0 { 92 | ch.DecodeWavHeader(c) 93 | } else { 94 | ch.Done() 95 | } 96 | } 97 | 98 | if c.wavHeaderSize != exp.headerSize { 99 | t.Fatalf("%s didn't match %d, got %d", "header size", exp.headerSize, c.wavHeaderSize) 100 | } 101 | if c.WavAudioFormat != exp.format { 102 | t.Fatalf("%s didn't match %d, got %d", "audio format", exp.format, c.WavAudioFormat) 103 | } 104 | if c.NumChannels != exp.numChans { 105 | t.Fatalf("%s didn't match %d, got %d", "# of channels", exp.numChans, c.NumChannels) 106 | } 107 | 108 | if c.SampleRate != exp.sampleRate { 109 | t.Fatalf("%s didn't match %d, got %d", "SampleRate", exp.sampleRate, c.SampleRate) 110 | } 111 | if c.AvgBytesPerSec != exp.byteRate { 112 | t.Fatalf("%s didn't match %d, got %d", "ByteRate", exp.byteRate, c.AvgBytesPerSec) 113 | } 114 | if c.BlockAlign != exp.blockAlign { 115 | t.Fatalf("%s didn't match %d, got %d", "BlockAlign", exp.blockAlign, c.BlockAlign) 116 | } 117 | if c.BitsPerSample != exp.bitsPerSample { 118 | t.Fatalf("%s didn't match %d, got %d", "BitsPerSample", exp.bitsPerSample, c.BitsPerSample) 119 | } 120 | } 121 | 122 | } 123 | 124 | func TestContainerDuration(t *testing.T) { 125 | expectations := []struct { 126 | input string 127 | dur time.Duration 128 | }{ 129 | {"fixtures/sample.wav", time.Duration(612176870)}, 130 | } 131 | 132 | for _, exp := range expectations { 133 | path, _ := filepath.Abs(exp.input) 134 | f, err := os.Open(path) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | defer f.Close() 139 | c := New(f) 140 | d, err := c.Duration() 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | if d != exp.dur { 145 | t.Fatalf("Container duration of %s didn't match %f, got %f", exp.input, exp.dur.Seconds(), d.Seconds()) 146 | } 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /riff/riff.go: -------------------------------------------------------------------------------- 1 | package riff 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | var ( 11 | RiffID = [4]byte{'R', 'I', 'F', 'F'} 12 | FmtID = [4]byte{'f', 'm', 't', ' '} 13 | // To align RIFF chunks to certain boundaries (i.e. 2048bytes for CD-ROMs) the RIFF specification includes a JUNK chunk. 14 | // Its contents are to be skipped when reading. When writing RIFFs, JUNK chunks should not have odd number as Size. 15 | junkID = [4]byte{'J', 'U', 'N', 'K'} 16 | WavFormatID = [4]byte{'W', 'A', 'V', 'E'} 17 | // DataFormatID is the Wave Data Chunk ID, it contains the digital audio sample data which can be decoded using the format 18 | // and compression method specified in the Wave Format Chunk. If the Compression Code is 1 (uncompressed PCM), then the Wave Data contains raw sample values. 19 | DataFormatID = [4]byte{'d', 'a', 't', 'a'} 20 | rmiFormatID = [4]byte{'R', 'M', 'I', 'D'} 21 | aviFormatID = [4]byte{'A', 'V', 'I', ' '} 22 | // ErrFmtNotSupported is a generic error reporting an unknown format. 23 | ErrFmtNotSupported = errors.New("format not supported") 24 | // ErrUnexpectedData is a generic error reporting that the parser encountered unexpected data. 25 | ErrUnexpectedData = errors.New("unexpected data content") 26 | ) 27 | 28 | // New creates a parser wrapper for a reader. 29 | // Note that the reader doesn't get rewinded as the container is processed. 30 | func New(r io.Reader) *Parser { 31 | return &Parser{r: r, Wg: &sync.WaitGroup{}} 32 | } 33 | 34 | // Duration returns the time duration of the passed reader if the sub format is supported. 35 | func Duration(r io.Reader) (time.Duration, error) { 36 | c := New(r) 37 | if err := c.ParseHeaders(); err != nil { 38 | return 0, err 39 | } 40 | return c.Duration() 41 | } 42 | -------------------------------------------------------------------------------- /riff/riff_test.go: -------------------------------------------------------------------------------- 1 | package riff 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestDuration(t *testing.T) { 12 | expectations := []struct { 13 | input string 14 | dur time.Duration 15 | }{ 16 | {"fixtures/sample.wav", time.Duration(612176870)}, 17 | } 18 | 19 | for _, exp := range expectations { 20 | path, _ := filepath.Abs(exp.input) 21 | f, err := os.Open(path) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | defer f.Close() 26 | d, err := Duration(f) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if d != exp.dur { 31 | t.Fatalf("%s of %s didn't match %f, got %f", "Duration", exp.input, exp.dur.Seconds(), d.Seconds()) 32 | } 33 | } 34 | 35 | } 36 | 37 | func ExampleDuration() { 38 | path, _ := filepath.Abs("fixtures/sample.wav") 39 | f, err := os.Open(path) 40 | if err != nil { 41 | panic(err) 42 | } 43 | defer f.Close() 44 | d, err := Duration(f) 45 | if err != nil { 46 | panic(err) 47 | } 48 | fmt.Printf("File with a duration of %f seconds", d.Seconds()) 49 | // Output: 50 | // File with a duration of 0.612177 seconds 51 | } 52 | -------------------------------------------------------------------------------- /riff/riffinfo/main.go: -------------------------------------------------------------------------------- 1 | // riffinfo is a command line tool used to gather information about riff files. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/mattetti/audio/riff" 13 | ) 14 | 15 | var pathToParse = flag.String("path", ".", "Where to find wav files") 16 | var fileToParse = flag.String("file", "", "The wav file to analyze (instead of a path)") 17 | 18 | func main() { 19 | flag.Usage = func() { 20 | fmt.Fprintf(os.Stderr, "Usage: \n") 21 | flag.PrintDefaults() 22 | } 23 | 24 | flag.Parse() 25 | 26 | if *fileToParse != "" { 27 | analyze(*fileToParse) 28 | } else { 29 | err := filepath.Walk(*pathToParse, walkFn) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | } 35 | 36 | func walkFn(path string, fi os.FileInfo, err error) (e error) { 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | if fi.IsDir() { 41 | filepath.Walk(path, walkFolder) 42 | return 43 | } 44 | if !strings.HasSuffix(fi.Name(), ".wav") || fi.IsDir() { 45 | return 46 | } 47 | return nil 48 | } 49 | 50 | func walkFolder(path string, fi os.FileInfo, err error) (e error) { 51 | if !strings.HasSuffix(fi.Name(), ".wav") || fi.IsDir() { 52 | return 53 | } 54 | analyze(path) 55 | return nil 56 | } 57 | 58 | func analyze(path string) { 59 | fmt.Println(path) 60 | f, err := os.Open(path) 61 | if err != nil { 62 | panic(err) 63 | } 64 | defer f.Close() 65 | c := riff.New(f) 66 | if err := c.ParseHeaders(); err != nil { 67 | log.Fatalf("Can't parse the headers of %s - %s\n", path, err) 68 | } 69 | fmt.Println(c) 70 | } 71 | -------------------------------------------------------------------------------- /transforms/bit_crush.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/mattetti/audio" 7 | ) 8 | 9 | var ( 10 | crusherStepSize = 0.000001 11 | CrusherMinFactor = 1.0 12 | CrusherMaxFactor = 2097152.0 13 | ) 14 | 15 | // BitCrush reduces the resolution of the sample to the target bit depth 16 | // Note that bit crusher effects are usually made of this feature + a decimator 17 | func BitCrush(buf *audio.PCMBuffer, factor float64) { 18 | buf.SwitchPrimaryType(audio.Float) 19 | stepSize := crusherStepSize * factor 20 | for i := 0; i < len(buf.Floats); i++ { 21 | frac, exp := math.Frexp(buf.Floats[i]) 22 | frac = signum(frac) * math.Floor(math.Abs(frac)/stepSize+0.5) * stepSize 23 | buf.Floats[i] = math.Ldexp(frac, exp) 24 | } 25 | } 26 | 27 | func signum(v float64) float64 { 28 | if v >= 0.0 { 29 | return 1.0 30 | } 31 | return -1.0 32 | } 33 | -------------------------------------------------------------------------------- /transforms/decimator.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/mattetti/audio" 7 | "github.com/mattetti/audio/transforms/filters" 8 | ) 9 | 10 | // Decimate drops samples to switch to a lower sample rate. 11 | // Factor is the decimation factor, for instance a factor of 2 of a 44100Hz buffer 12 | // will convert the buffer in a 22500 buffer. 13 | func Decimate(buf *audio.PCMBuffer, factor int) (err error) { 14 | if buf == nil || buf.Format == nil { 15 | return audio.ErrInvalidBuffer 16 | } 17 | 18 | if factor < 0 { 19 | return errors.New("can't use a negative factor") 20 | } 21 | 22 | // apply a low pass filter at the nyquist frequency to avoid 23 | // aliasing. 24 | if err := filters.LowPass(buf, float64(buf.Format.SampleRate)/2); err != nil { 25 | return err 26 | } 27 | 28 | // drop samples to match the decimation factor 29 | newLength := len(buf.Floats) / factor 30 | for i := 0; i < newLength; i++ { 31 | buf.Floats[i] = buf.Floats[i*factor] 32 | } 33 | buf.Floats = buf.Floats[:newLength] 34 | buf.Format.SampleRate /= factor 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /transforms/doc/fullwaverectifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/transforms/doc/fullwaverectifier.png -------------------------------------------------------------------------------- /transforms/doc/source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/transforms/doc/source.png -------------------------------------------------------------------------------- /transforms/filters/filters.go: -------------------------------------------------------------------------------- 1 | // filters implement easy to use audio filters. 2 | package filters 3 | 4 | import ( 5 | "github.com/mattetti/audio" 6 | "github.com/mattetti/audio/dsp/filters" 7 | "github.com/mattetti/audio/dsp/windows" 8 | ) 9 | 10 | // LowPass is a basic LowPass filter cutting off 11 | // CutOffFreq is where the filter would be at -3db. 12 | // TODO: param to say how efficient we want the low pass to be. 13 | // matlab: lpFilt = designfilt('lowpassfir','PassbandFrequency',0.25, ... 14 | // 'StopbandFrequency',0.35,'PassbandRipple',0.5, ... 15 | // 'StopbandAttenuation',65,'DesignMethod','kaiserwin'); 16 | func LowPass(buf *audio.PCMBuffer, cutOffFreq float64) (err error) { 17 | if buf == nil || buf.Format == nil { 18 | return audio.ErrInvalidBuffer 19 | } 20 | s := &filters.Sinc{ 21 | // TODO: find the right taps number to do a proper 22 | // audio low pass based in the sample rate 23 | // there should be a magical function to get that number. 24 | Taps: 62, 25 | SamplingFreq: buf.Format.SampleRate, 26 | CutOffFreq: cutOffFreq, 27 | Window: windows.Hamming, 28 | } 29 | fir := &filters.FIR{Sinc: s} 30 | buf.Floats, err = fir.LowPass(buf.AsFloat64s()) 31 | buf.SwitchPrimaryType(audio.Float) 32 | return err 33 | } 34 | 35 | // HighPass is a basic LowPass filter cutting off 36 | // the audio buffer frequencies below the cutOff frequency. 37 | func HighPass(buf *audio.PCMBuffer, cutOff float64) (err error) { 38 | if buf == nil || buf.Format == nil { 39 | return audio.ErrInvalidBuffer 40 | } 41 | s := &filters.Sinc{ 42 | Taps: 62, 43 | SamplingFreq: buf.Format.SampleRate, 44 | CutOffFreq: cutOff, 45 | Window: windows.Blackman, 46 | } 47 | fir := &filters.FIR{Sinc: s} 48 | buf.Floats, err = fir.HighPass(buf.AsFloat64s()) 49 | buf.SwitchPrimaryType(audio.Float) 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /transforms/mono.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import "github.com/mattetti/audio" 4 | 5 | // MonoDownmix converts the buffer to a mono buffer 6 | // by downmixing the channels together. 7 | func MonoDownmix(buf *audio.PCMBuffer) error { 8 | if buf == nil || buf.Format == nil { 9 | return audio.ErrInvalidBuffer 10 | } 11 | nChans := buf.Format.NumChannels 12 | if nChans < 2 { 13 | return nil 14 | } 15 | nChansF := float64(nChans) 16 | 17 | frameCount := buf.Size() 18 | newData := make([]float64, frameCount) 19 | buf.SwitchPrimaryType(audio.Float) 20 | for i := 0; i < frameCount; i++ { 21 | newData[i] = 0 22 | for j := 0; j < nChans; j++ { 23 | newData[i] += buf.Floats[i*nChans+j] 24 | } 25 | newData[i] /= nChansF 26 | } 27 | buf.Floats = newData 28 | buf.Format.NumChannels = 1 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /transforms/nominal_scale.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "github.com/mattetti/audio" 5 | "github.com/mattetti/audio/dsp/analysis" 6 | ) 7 | 8 | // NominalScale converts the input to a -1.0 / +1.0 scale 9 | func NominalScale(buf *audio.PCMBuffer) error { 10 | if buf == nil || buf.Format == nil { 11 | return audio.ErrInvalidBuffer 12 | } 13 | buf.SwitchPrimaryType(audio.Float) 14 | min, max := analysis.MinMaxFloat(buf) 15 | // check if already in the right scale 16 | if min >= -1 && max <= 1 { 17 | return nil 18 | } 19 | 20 | max = float64(audio.IntMaxSignedValue(buf.Format.BitDepth)) 21 | for i := 0; i < buf.Len(); i++ { 22 | buf.Floats[i] /= max 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /transforms/normalize.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/mattetti/audio" 7 | ) 8 | 9 | // NormalizeMax sets the max value to 1 and normalize the rest of the data. 10 | func NormalizeMax(buf *audio.PCMBuffer) { 11 | if buf == nil { 12 | return 13 | } 14 | buf.SwitchPrimaryType(audio.Float) 15 | max := 0.0 16 | 17 | for i := 0; i < buf.Len(); i++ { 18 | if math.Abs(buf.Floats[i]) > max { 19 | max = math.Abs(buf.Floats[i]) 20 | } 21 | } 22 | 23 | if max != 0.0 { 24 | for i := 0; i < buf.Len(); i++ { 25 | buf.Floats[i] /= max 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /transforms/pcm_scale.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import "github.com/mattetti/audio" 4 | 5 | // PCMScale converts a buffer with audio content from -1 to 1 into 6 | // the PCM scale based on the buffer's bitdepth. 7 | func PCMScale(buf *audio.PCMBuffer) error { 8 | if buf == nil || buf.Format == nil { 9 | return audio.ErrInvalidBuffer 10 | } 11 | buf.SwitchPrimaryType(audio.Float) 12 | factor := float64(audio.IntMaxSignedValue(buf.Format.BitDepth)) 13 | for i := 0; i < buf.Len(); i++ { 14 | buf.Floats[i] *= factor 15 | } 16 | 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /transforms/presenters/csv.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/mattetti/audio" 9 | ) 10 | 11 | // CSV writes the content of the buffer in a CSV file. 12 | // Can be used to plot with R for instance: 13 | // Rscript -e 'png("my_plot.png", height = 768, width = 1024); 14 | // myData <- read.csv("./data.csv"); matplot(myData[,1], type="l")' 15 | func CSV(buf *audio.PCMBuffer, path string) error { 16 | if buf == nil || buf.Format == nil { 17 | return audio.ErrInvalidBuffer 18 | } 19 | csvf, err := os.Create(path) 20 | if err != nil { 21 | return err 22 | } 23 | defer csvf.Close() 24 | csvw := csv.NewWriter(csvf) 25 | buf.SwitchPrimaryType(audio.Float) 26 | row := make([]string, buf.Format.NumChannels) 27 | 28 | for i := 0; i < buf.Format.NumChannels; i++ { 29 | row[i] = fmt.Sprintf("Channel %d", i+1) 30 | } 31 | 32 | if err := csvw.Write(row); err != nil { 33 | return fmt.Errorf("error writing header to csv: %s", err) 34 | } 35 | 36 | totalSize := buf.Len() 37 | for i := 0; i < totalSize; i++ { 38 | for j := 0; j < buf.Format.NumChannels; j++ { 39 | row[j] = fmt.Sprintf("%v", buf.Floats[i*buf.Format.NumChannels+j]) 40 | if i >= totalSize { 41 | break 42 | } 43 | } 44 | if err := csvw.Write(row); err != nil { 45 | return fmt.Errorf("error writing record to csv: %s", err) 46 | } 47 | } 48 | csvw.Flush() 49 | return csvw.Error() 50 | } 51 | -------------------------------------------------------------------------------- /transforms/presenters/gnuplot.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/mattetti/audio" 9 | ) 10 | 11 | // GnuplotBin exports the buffer content as a binary gnuplot file. 12 | func GnuplotBin(buf *audio.PCMBuffer, path string) error { 13 | if buf == nil || buf.Format == nil { 14 | return audio.ErrInvalidBuffer 15 | } 16 | out, err := os.Create(path) 17 | if err != nil { 18 | return err 19 | } 20 | defer out.Close() 21 | for _, s := range buf.AsFloat32s() { 22 | if err = binary.Write(out, binary.BigEndian, s); err != nil { 23 | break 24 | } 25 | } 26 | return err 27 | } 28 | 29 | // GnuplotText exports a gnuplot compatible text file for plotting. 30 | // After normalizing the data (for better plotting) and exported it, 31 | // you can use gnuplot command line, for instance: 32 | // gnuplot -e "plot 'my_data.dat' with line" 33 | func GnuplotText(buf *audio.PCMBuffer, path string) error { 34 | if buf == nil || buf.Format == nil { 35 | return audio.ErrInvalidBuffer 36 | } 37 | out, err := os.Create(path) 38 | if err != nil { 39 | return err 40 | } 41 | defer out.Close() 42 | if _, err = out.WriteString("#"); err != nil { 43 | return err 44 | } 45 | for i := 0; i < buf.Format.NumChannels; i++ { 46 | if _, err = out.WriteString(fmt.Sprintf("%d\t", i+1)); err != nil { 47 | return err 48 | } 49 | } 50 | if _, err = out.WriteString("\n"); err != nil { 51 | return err 52 | } 53 | 54 | buf.SwitchPrimaryType(audio.Float) 55 | for i := 0; i < buf.Size(); i++ { 56 | for j := 0; j < buf.Format.NumChannels; j++ { 57 | out.WriteString(fmt.Sprintf("%f\t", buf.Floats[i*buf.Format.NumChannels+j])) 58 | } 59 | out.WriteString("\n") 60 | } 61 | return err 62 | } 63 | -------------------------------------------------------------------------------- /transforms/quantize.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/mattetti/audio" 7 | ) 8 | 9 | // Quantize quantizes the audio signal to match the target bitDepth 10 | func Quantize(buf *audio.PCMBuffer, bitDepth int) { 11 | if buf == nil { 12 | return 13 | } 14 | max := math.Pow(2, float64(bitDepth)) - 1 15 | 16 | buf.SwitchPrimaryType(audio.Float) 17 | bufLen := buf.Len() 18 | for i := 0; i < bufLen; i++ { 19 | buf.Floats[i] = round((buf.Floats[i]+1)*max)/max - 1.0 20 | } 21 | } 22 | 23 | func round(f float64) float64 { 24 | if f > 0.0 { 25 | return math.Floor(f + 0.5) 26 | } 27 | return math.Ceil(f - 0.5) 28 | } 29 | -------------------------------------------------------------------------------- /transforms/resample.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/mattetti/audio" 7 | "github.com/mattetti/audio/transforms/filters" 8 | ) 9 | 10 | // Resample down or up samples the buffer. Note that the amplitude 11 | // will be affected by upsampling. 12 | func Resample(buf *audio.PCMBuffer, fs float64) error { 13 | if buf == nil || buf.Format == nil { 14 | return audio.ErrInvalidBuffer 15 | } 16 | if buf.Format.SampleRate == int(fs) { 17 | return nil 18 | } 19 | buf.SwitchPrimaryType(audio.Float) 20 | 21 | // downsample 22 | if fs < float64(buf.Format.SampleRate) { 23 | factor := float64(buf.Format.SampleRate) / fs 24 | 25 | // apply a low pass filter at the nyquist frequency to avoid 26 | // aliasing. 27 | if err := filters.LowPass(buf, float64(buf.Format.SampleRate)/2); err != nil { 28 | return err 29 | } 30 | 31 | // drop samples to match the decimation factor 32 | newLength := int(math.Floor(float64(len(buf.Floats)) / factor)) 33 | var targetI int 34 | for i := 0; i < newLength; i++ { 35 | targetI = int(math.Floor(float64(i) * factor)) 36 | buf.Floats[i] = buf.Floats[targetI] 37 | } 38 | buf.Floats = buf.Floats[:newLength] 39 | buf.Format.SampleRate = int(fs) 40 | return nil 41 | } 42 | 43 | // oversample 44 | // Note: oversampling reduces the amplitude 45 | factor := fs / float64(buf.Format.SampleRate) 46 | newLength := int(math.Ceil(float64(len(buf.Floats)) * factor)) 47 | newFloats := make([]float64, newLength) 48 | padding := int( 49 | math.Ceil( 50 | float64(newLength) / 51 | float64(len(buf.Floats)))) 52 | var idx int 53 | for i := 0; i < len(buf.Floats); i++ { 54 | idx = i * padding 55 | if idx >= len(newFloats) { 56 | break 57 | } 58 | newFloats[idx] = buf.Floats[i] 59 | } 60 | buf.Floats = newFloats 61 | buf.Format.SampleRate = int(fs) 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /transforms/transforms.go: -------------------------------------------------------------------------------- 1 | package transforms 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/mattetti/audio" 7 | ) 8 | 9 | // FullWaveRectifier to make all signal positive 10 | // See https://en.wikipedia.org/wiki/Rectifier#Full-wave_rectification 11 | func FullWaveRectifier(buf *audio.PCMBuffer) error { 12 | if buf == nil { 13 | return audio.ErrInvalidBuffer 14 | } 15 | buf.SwitchPrimaryType(audio.Float) 16 | for i := 0; i < buf.Len(); i++ { 17 | buf.Floats[i] = math.Abs(buf.Floats[i]) 18 | } 19 | 20 | return nil 21 | } 22 | 23 | // MonoRMS converts the buffer to mono and apply an RMS treatment. 24 | // rms = sqrt ( (1/n) * (x12 + x22 + … + xn2) ) 25 | // multiplying by 1/n effectively assigns equal weights to all the terms, making it a rectangular window. 26 | // Other window equations can be used instead which would favor terms in the middle of the window. 27 | // This results in even greater accuracy of the RMS value since brand new samples (or old ones at 28 | // the end of the window) have less influence over the signal’s power.) 29 | // TODO: use a sine wave at amplitude of 1: rectication + average = 1.4 (1/square root of 2) 30 | func MonoRMS(b *audio.PCMBuffer, windowSize int) error { 31 | if b == nil { 32 | return audio.ErrInvalidBuffer 33 | } 34 | if b.Len() == 0 { 35 | return nil 36 | } 37 | b.SwitchPrimaryType(audio.Float) 38 | out := []float64{} 39 | winBuf := make([]float64, windowSize) 40 | windowSizeF := float64(windowSize) 41 | 42 | processWindow := func(idx int) { 43 | total := 0.0 44 | for i := 0; i < len(winBuf); i++ { 45 | total += winBuf[idx] * winBuf[idx] 46 | } 47 | v := math.Sqrt((1.0 / windowSizeF) * total) 48 | out = append(out, v) 49 | } 50 | 51 | nbrChans := b.Format.NumChannels 52 | samples := b.AsFloat64s() 53 | 54 | var windowIDX int 55 | // process each frame, convert it to mono and them RMS it 56 | for i := 0; i < len(samples); i++ { 57 | v := samples[i] 58 | if nbrChans > 1 { 59 | for j := 1; j < nbrChans; j++ { 60 | i++ 61 | v += samples[i] 62 | } 63 | v /= float64(nbrChans) 64 | } 65 | winBuf[windowIDX] = v 66 | windowIDX++ 67 | if windowIDX == windowSize || i == (len(samples)-1) { 68 | windowIDX = 0 69 | processWindow(windowIDX) 70 | } 71 | } 72 | 73 | b.Format.NumChannels = 1 74 | b.Format.SampleRate /= windowSize 75 | b.Floats = out 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /vendor/go-dsp/.gitignore: -------------------------------------------------------------------------------- 1 | *.6 2 | *.8 3 | *.out 4 | *.swp 5 | *~ 6 | _* 7 | -------------------------------------------------------------------------------- /vendor/go-dsp/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Matt Jibson 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /vendor/go-dsp/README.md: -------------------------------------------------------------------------------- 1 | # GO-DSP 2 | 3 | go-dsp is a digital signal processing package for the [Go programming language](http://golang.org). 4 | 5 | ## Packages 6 | 7 | * **[dsputils](http://godoc.org/github.com/mjibson/go-dsp/dsputils)** - utilities and data structures for DSP 8 | * **[fft](http://godoc.org/github.com/mjibson/go-dsp/fft)** - fast Fourier transform 9 | * **[spectral](http://godoc.org/github.com/mjibson/go-dsp/spectral)** - power spectral density functions (e.g., Pwelch) 10 | * **[wav](http://godoc.org/github.com/mjibson/go-dsp/wav)** - wav file reader functions 11 | * **[window](http://godoc.org/github.com/mjibson/go-dsp/window)** - window functions (e.g., Hamming, Hann, Bartlett) 12 | 13 | ## Installation and Usage 14 | 15 | ```$ go get github.com/mjibson/go-dsp/fft``` 16 | 17 | ``` 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | 23 | "github.com/mjibson/go-dsp/fft" 24 | ) 25 | 26 | func main() { 27 | fmt.Println(fft.FFTReal([]float64 {1, 2, 3})) 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /vendor/go-dsp/dsputils/compare.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package dsputils 18 | 19 | import ( 20 | "math" 21 | ) 22 | 23 | const ( 24 | closeFactor = 1e-8 25 | ) 26 | 27 | // PrettyClose returns true if the slices a and b are very close, else false. 28 | func PrettyClose(a, b []float64) bool { 29 | if len(a) != len(b) { 30 | return false 31 | } 32 | 33 | for i, c := range a { 34 | if !Float64Equal(c, b[i]) { 35 | return false 36 | } 37 | } 38 | return true 39 | } 40 | 41 | // PrettyCloseC returns true if the slices a and b are very close, else false. 42 | func PrettyCloseC(a, b []complex128) bool { 43 | if len(a) != len(b) { 44 | return false 45 | } 46 | 47 | for i, c := range a { 48 | if !ComplexEqual(c, b[i]) { 49 | return false 50 | } 51 | } 52 | return true 53 | } 54 | 55 | // PrettyClose2 returns true if the matrixes a and b are very close, else false. 56 | func PrettyClose2(a, b [][]complex128) bool { 57 | if len(a) != len(b) { 58 | return false 59 | } 60 | 61 | for i, c := range a { 62 | if !PrettyCloseC(c, b[i]) { 63 | return false 64 | } 65 | } 66 | return true 67 | } 68 | 69 | // PrettyClose2F returns true if the matrixes a and b are very close, else false. 70 | func PrettyClose2F(a, b [][]float64) bool { 71 | if len(a) != len(b) { 72 | return false 73 | } 74 | 75 | for i, c := range a { 76 | if !PrettyClose(c, b[i]) { 77 | return false 78 | } 79 | } 80 | return true 81 | } 82 | 83 | // ComplexEqual returns true if a and b are very close, else false. 84 | func ComplexEqual(a, b complex128) bool { 85 | r_a := real(a) 86 | r_b := real(b) 87 | i_a := imag(a) 88 | i_b := imag(b) 89 | 90 | return Float64Equal(r_a, r_b) && Float64Equal(i_a, i_b) 91 | } 92 | 93 | // Float64Equal returns true if a and b are very close, else false. 94 | func Float64Equal(a, b float64) bool { 95 | return math.Abs(a-b) <= closeFactor || math.Abs(1-a/b) <= closeFactor 96 | } 97 | -------------------------------------------------------------------------------- /vendor/go-dsp/dsputils/dsputils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | // Package dsputils provides functions useful in digital signal processing. 18 | package dsputils 19 | 20 | import ( 21 | "math" 22 | ) 23 | 24 | // ToComplex returns the complex equivalent of the real-valued slice. 25 | func ToComplex(x []float64) []complex128 { 26 | y := make([]complex128, len(x)) 27 | for n, v := range x { 28 | y[n] = complex(v, 0) 29 | } 30 | return y 31 | } 32 | 33 | // IsPowerOf2 returns true if x is a power of 2, else false. 34 | func IsPowerOf2(x int) bool { 35 | return x&(x-1) == 0 36 | } 37 | 38 | // NextPowerOf2 returns the next power of 2 >= x. 39 | func NextPowerOf2(x int) int { 40 | if IsPowerOf2(x) { 41 | return x 42 | } 43 | 44 | return int(math.Pow(2, math.Ceil(math.Log2(float64(x))))) 45 | } 46 | 47 | // ZeroPad returns x with zeros appended to the end to the specified length. 48 | // If len(x) >= length, x is returned, otherwise a new array is returned. 49 | func ZeroPad(x []complex128, length int) []complex128 { 50 | if len(x) >= length { 51 | return x 52 | } 53 | 54 | r := make([]complex128, length) 55 | copy(r, x) 56 | return r 57 | } 58 | 59 | // ZeroPadF returns x with zeros appended to the end to the specified length. 60 | // If len(x) >= length, x is returned, otherwise a new array is returned. 61 | func ZeroPadF(x []float64, length int) []float64 { 62 | if len(x) >= length { 63 | return x 64 | } 65 | 66 | r := make([]float64, length) 67 | copy(r, x) 68 | return r 69 | } 70 | 71 | // ZeroPad2 returns ZeroPad of x, with the length as the next power of 2 >= len(x). 72 | func ZeroPad2(x []complex128) []complex128 { 73 | return ZeroPad(x, NextPowerOf2(len(x))) 74 | } 75 | 76 | // ToComplex2 returns the complex equivalent of the real-valued matrix. 77 | func ToComplex2(x [][]float64) [][]complex128 { 78 | y := make([][]complex128, len(x)) 79 | for n, v := range x { 80 | y[n] = ToComplex(v) 81 | } 82 | return y 83 | } 84 | 85 | // Segment returns segs equal-length slices that are segments of x with noverlap% of overlap. 86 | // The returned slices are not copies of x, but slices into it. 87 | // Trailing entries in x that connot be included in the equal-length segments are discarded. 88 | // noverlap is a percentage, thus 0 <= noverlap <= 1, and noverlap = 0.5 is 50% overlap. 89 | func Segment(x []complex128, segs int, noverlap float64) [][]complex128 { 90 | lx := len(x) 91 | 92 | // determine step, length, and overlap 93 | var overlap, length, step, tot int 94 | for length = lx; length > 0; length-- { 95 | overlap = int(float64(length) * noverlap) 96 | tot = segs*(length-overlap) + overlap 97 | if tot <= lx { 98 | step = length - overlap 99 | break 100 | } 101 | } 102 | 103 | if length == 0 { 104 | panic("too many segments") 105 | } 106 | 107 | r := make([][]complex128, segs) 108 | s := 0 109 | for n := range r { 110 | r[n] = x[s : s+length] 111 | s += step 112 | } 113 | 114 | return r 115 | } 116 | -------------------------------------------------------------------------------- /vendor/go-dsp/dsputils/dsputils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package dsputils 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | type segmentTest struct { 24 | segs int 25 | noverlap float64 26 | slices [][]int 27 | } 28 | 29 | var segmentTests = []segmentTest{ 30 | { 31 | 3, 32 | .5, 33 | [][]int{ 34 | {0, 8}, 35 | {4, 12}, 36 | {8, 16}, 37 | }, 38 | }, 39 | } 40 | 41 | func TestSegment(t *testing.T) { 42 | x := make([]complex128, 16) 43 | for n := range x { 44 | x[n] = complex(float64(n), 0) 45 | } 46 | 47 | for _, st := range segmentTests { 48 | v := Segment(x, st.segs, st.noverlap) 49 | s := make([][]complex128, st.segs) 50 | for i, sl := range st.slices { 51 | s[i] = x[sl[0]:sl[1]] 52 | } 53 | 54 | if !PrettyClose2(v, s) { 55 | t.Error("Segment error: expected:", s, ", output:", v) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /vendor/go-dsp/dsputils/matrix_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package dsputils 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestMakeMatrix(t *testing.T) { 24 | m := MakeMatrix( 25 | []complex128{ 26 | 1, 2, 3, 4, 27 | 5, 6, 7, 8, 28 | 9, 0, 1, 2, 29 | 30 | 3, 4, 5, 6, 31 | 7, 8, 9, 0, 32 | 4, 3, 2, 1}, 33 | []int{2, 3, 4}) 34 | 35 | checkArr(t, m.Dim([]int{1, 0, -1}), ToComplex([]float64{3, 4, 5, 6})) 36 | checkArr(t, m.Dim([]int{0, -1, 2}), ToComplex([]float64{3, 7, 1})) 37 | checkArr(t, m.Dim([]int{-1, 1, 3}), ToComplex([]float64{8, 0})) 38 | 39 | s := ToComplex([]float64{10, 11, 12}) 40 | i := []int{1, -1, 3} 41 | m.SetDim(s, i) 42 | checkArr(t, m.Dim(i), s) 43 | 44 | v := complex(14, 0) 45 | m.SetValue(v, i) 46 | checkFloat(t, m.Value(i), v) 47 | } 48 | 49 | func checkArr(t *testing.T, have, want []complex128) { 50 | if !PrettyCloseC(have, want) { 51 | t.Error("have:", have, "want:", want) 52 | } 53 | } 54 | 55 | func checkFloat(t *testing.T, have, want complex128) { 56 | if !ComplexEqual(have, want) { 57 | t.Error("have:", have, "want:", want) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /vendor/go-dsp/fft/bluestein.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package fft 18 | 19 | import ( 20 | "math" 21 | "sync" 22 | 23 | "go-dsp/dsputils" 24 | ) 25 | 26 | var ( 27 | bluesteinLock sync.RWMutex 28 | bluesteinFactors = map[int][]complex128{} 29 | bluesteinInvFactors = map[int][]complex128{} 30 | ) 31 | 32 | func getBluesteinFactors(input_len int) ([]complex128, []complex128) { 33 | bluesteinLock.RLock() 34 | 35 | if hasBluesteinFactors(input_len) { 36 | defer bluesteinLock.RUnlock() 37 | return bluesteinFactors[input_len], bluesteinInvFactors[input_len] 38 | } 39 | 40 | bluesteinLock.RUnlock() 41 | bluesteinLock.Lock() 42 | defer bluesteinLock.Unlock() 43 | 44 | if !hasBluesteinFactors(input_len) { 45 | bluesteinFactors[input_len] = make([]complex128, input_len) 46 | bluesteinInvFactors[input_len] = make([]complex128, input_len) 47 | 48 | var sin, cos float64 49 | for i := 0; i < input_len; i++ { 50 | if i == 0 { 51 | sin, cos = 0, 1 52 | } else { 53 | sin, cos = math.Sincos(math.Pi / float64(input_len) * float64(i*i)) 54 | } 55 | bluesteinFactors[input_len][i] = complex(cos, sin) 56 | bluesteinInvFactors[input_len][i] = complex(cos, -sin) 57 | } 58 | } 59 | 60 | return bluesteinFactors[input_len], bluesteinInvFactors[input_len] 61 | } 62 | 63 | func hasBluesteinFactors(idx int) bool { 64 | return bluesteinFactors[idx] != nil 65 | } 66 | 67 | // bluesteinFFT returns the FFT calculated using the Bluestein algorithm. 68 | func bluesteinFFT(x []complex128) []complex128 { 69 | lx := len(x) 70 | a := dsputils.ZeroPad(x, dsputils.NextPowerOf2(lx*2-1)) 71 | la := len(a) 72 | factors, invFactors := getBluesteinFactors(lx) 73 | 74 | for n, v := range x { 75 | a[n] = v * invFactors[n] 76 | } 77 | 78 | b := make([]complex128, la) 79 | for i := 0; i < lx; i++ { 80 | b[i] = factors[i] 81 | 82 | if i != 0 { 83 | b[la-i] = factors[i] 84 | } 85 | } 86 | 87 | r := Convolve(a, b) 88 | 89 | for i := 0; i < lx; i++ { 90 | r[i] *= invFactors[i] 91 | } 92 | 93 | return r[:lx] 94 | } 95 | -------------------------------------------------------------------------------- /vendor/go-dsp/fft/radix2.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package fft 18 | 19 | import ( 20 | "math" 21 | "runtime" 22 | "sync" 23 | ) 24 | 25 | var ( 26 | radix2Lock sync.RWMutex 27 | radix2Factors = map[int][]complex128{ 28 | 4: {complex(1, 0), complex(0, -1), complex(-1, 0), complex(0, 1)}, 29 | } 30 | ) 31 | 32 | // EnsureRadix2Factors ensures that all radix 2 factors are computed for inputs 33 | // of length input_len. This is used to precompute needed factors for known 34 | // sizes. Generally should only be used for benchmarks. 35 | func EnsureRadix2Factors(input_len int) { 36 | getRadix2Factors(input_len) 37 | } 38 | 39 | func getRadix2Factors(input_len int) []complex128 { 40 | radix2Lock.RLock() 41 | 42 | if hasRadix2Factors(input_len) { 43 | defer radix2Lock.RUnlock() 44 | return radix2Factors[input_len] 45 | } 46 | 47 | radix2Lock.RUnlock() 48 | radix2Lock.Lock() 49 | defer radix2Lock.Unlock() 50 | 51 | if !hasRadix2Factors(input_len) { 52 | for i, p := 8, 4; i <= input_len; i, p = i<<1, i { 53 | if radix2Factors[i] == nil { 54 | radix2Factors[i] = make([]complex128, i) 55 | 56 | for n, j := 0, 0; n < i; n, j = n+2, j+1 { 57 | radix2Factors[i][n] = radix2Factors[p][j] 58 | } 59 | 60 | for n := 1; n < i; n += 2 { 61 | sin, cos := math.Sincos(-2 * math.Pi / float64(i) * float64(n)) 62 | radix2Factors[i][n] = complex(cos, sin) 63 | } 64 | } 65 | } 66 | } 67 | 68 | return radix2Factors[input_len] 69 | } 70 | 71 | func hasRadix2Factors(idx int) bool { 72 | return radix2Factors[idx] != nil 73 | } 74 | 75 | type fft_work struct { 76 | start, end int 77 | } 78 | 79 | // radix2FFT returns the FFT calculated using the radix-2 DIT Cooley-Tukey algorithm. 80 | func radix2FFT(x []complex128) []complex128 { 81 | lx := len(x) 82 | factors := getRadix2Factors(lx) 83 | 84 | t := make([]complex128, lx) // temp 85 | r := reorderData(x) 86 | 87 | var blocks, stage, s_2 int 88 | 89 | jobs := make(chan *fft_work, lx) 90 | wg := sync.WaitGroup{} 91 | 92 | num_workers := worker_pool_size 93 | if (num_workers) == 0 { 94 | num_workers = runtime.GOMAXPROCS(0) 95 | } 96 | 97 | idx_diff := lx / num_workers 98 | if idx_diff < 2 { 99 | idx_diff = 2 100 | } 101 | 102 | worker := func() { 103 | for work := range jobs { 104 | for nb := work.start; nb < work.end; nb += stage { 105 | if stage != 2 { 106 | for j := 0; j < s_2; j++ { 107 | idx := j + nb 108 | idx2 := idx + s_2 109 | ridx := r[idx] 110 | w_n := r[idx2] * factors[blocks*j] 111 | t[idx] = ridx + w_n 112 | t[idx2] = ridx - w_n 113 | } 114 | } else { 115 | n1 := nb + 1 116 | rn := r[nb] 117 | rn1 := r[n1] 118 | t[nb] = rn + rn1 119 | t[n1] = rn - rn1 120 | } 121 | } 122 | wg.Done() 123 | } 124 | } 125 | 126 | for i := 0; i < num_workers; i++ { 127 | go worker() 128 | } 129 | defer close(jobs) 130 | 131 | for stage = 2; stage <= lx; stage <<= 1 { 132 | blocks = lx / stage 133 | s_2 = stage / 2 134 | 135 | for start, end := 0, stage; ; { 136 | if end-start >= idx_diff || end == lx { 137 | wg.Add(1) 138 | jobs <- &fft_work{start, end} 139 | 140 | if end == lx { 141 | break 142 | } 143 | 144 | start = end 145 | } 146 | 147 | end += stage 148 | } 149 | wg.Wait() 150 | r, t = t, r 151 | } 152 | 153 | return r 154 | } 155 | 156 | // reorderData returns a copy of x reordered for the DFT. 157 | func reorderData(x []complex128) []complex128 { 158 | lx := uint(len(x)) 159 | r := make([]complex128, lx) 160 | s := log2(lx) 161 | 162 | var n uint 163 | for ; n < lx; n++ { 164 | r[reverseBits(n, s)] = x[n] 165 | } 166 | 167 | return r 168 | } 169 | 170 | // log2 returns the log base 2 of v 171 | // from: http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious 172 | func log2(v uint) uint { 173 | var r uint 174 | 175 | for v >>= 1; v != 0; v >>= 1 { 176 | r++ 177 | } 178 | 179 | return r 180 | } 181 | 182 | // reverseBits returns the first s bits of v in reverse order 183 | // from: http://graphics.stanford.edu/~seander/bithacks.html#BitReverseObvious 184 | func reverseBits(v, s uint) uint { 185 | var r uint 186 | 187 | // Since we aren't reversing all the bits in v (just the first s bits), 188 | // we only need the first bit of v instead of a full copy. 189 | r = v & 1 190 | s-- 191 | 192 | for v >>= 1; v != 0; v >>= 1 { 193 | r <<= 1 194 | r |= v & 1 195 | s-- 196 | } 197 | 198 | return r << s 199 | } 200 | -------------------------------------------------------------------------------- /vendor/go-dsp/spectral/pwelch.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package spectral 18 | 19 | import ( 20 | "math" 21 | "math/cmplx" 22 | 23 | "go-dsp/dsputils" 24 | "go-dsp/fft" 25 | "go-dsp/window" 26 | ) 27 | 28 | type PwelchOptions struct { 29 | // NFFT is the number of data points used in each block for the FFT. Must be 30 | // even; a power 2 is most efficient. This should *NOT* be used to get zero 31 | // padding, or the scaling of the result will be incorrect. Use Pad for 32 | // this instead. 33 | // 34 | // The default value is 256. 35 | NFFT int 36 | 37 | // Window is a function that returns an array of window values the length 38 | // of its input parameter. Each segment is scaled by these values. 39 | // 40 | // The default (nil) is window.Hann, from the go-dsp/window package. 41 | Window func(int) []float64 42 | 43 | // Pad is the number of points to which the data segment is padded when 44 | // performing the FFT. This can be different from NFFT, which specifies the 45 | // number of data points used. While not increasing the actual resolution of 46 | // the psd (the minimum distance between resolvable peaks), this can give 47 | // more points in the plot, allowing for more detail. 48 | // 49 | // The value default is 0, which sets Pad equal to NFFT. 50 | Pad int 51 | 52 | // Noverlap is the number of points of overlap between blocks. 53 | // 54 | // The default value is 0 (no overlap). 55 | Noverlap int 56 | 57 | // Specifies whether the resulting density values should be scaled by the 58 | // scaling frequency, which gives density in units of Hz^-1. This allows for 59 | // integration over the returned frequency values. The default is set for 60 | // MATLAB compatibility. Note that this is the opposite of matplotlib style, 61 | // but with equivalent defaults. 62 | // 63 | // The default value is false (enable scaling). 64 | Scale_off bool 65 | } 66 | 67 | // Pwelch estimates the power spectral density of x using Welch's method. 68 | // Fs is the sampling frequency (samples per time unit) of x. Fs is used 69 | // to calculate freqs. 70 | // Returns the power spectral density Pxx and corresponding frequencies freqs. 71 | // Designed to be similar to the matplotlib implementation below. 72 | // Reference: http://matplotlib.org/api/mlab_api.html#matplotlib.mlab.psd 73 | // See also: http://www.mathworks.com/help/signal/ref/pwelch.html 74 | func Pwelch(x []float64, Fs float64, o *PwelchOptions) (Pxx, freqs []float64) { 75 | if len(x) == 0 { 76 | return []float64{}, []float64{} 77 | } 78 | 79 | nfft := o.NFFT 80 | pad := o.Pad 81 | noverlap := o.Noverlap 82 | wf := o.Window 83 | enable_scaling := !o.Scale_off 84 | 85 | if nfft == 0 { 86 | nfft = 256 87 | } 88 | 89 | if wf == nil { 90 | wf = window.Hann 91 | } 92 | 93 | if pad == 0 { 94 | pad = nfft 95 | } 96 | 97 | if len(x) < nfft { 98 | x = dsputils.ZeroPadF(x, nfft) 99 | } 100 | 101 | lp := pad/2 + 1 102 | var scale float64 = 2 103 | 104 | segs := Segment(x, nfft, noverlap) 105 | 106 | Pxx = make([]float64, lp) 107 | for _, x := range segs { 108 | x = dsputils.ZeroPadF(x, pad) 109 | window.Apply(x, wf) 110 | 111 | pgram := fft.FFTReal(x) 112 | 113 | for j := range Pxx { 114 | d := real(cmplx.Conj(pgram[j])*pgram[j]) / float64(len(segs)) 115 | 116 | if j > 0 && j < lp-1 { 117 | d *= scale 118 | } 119 | 120 | Pxx[j] += d 121 | } 122 | } 123 | 124 | w := wf(nfft) 125 | var norm float64 126 | for _, x := range w { 127 | norm += math.Pow(x, 2) 128 | } 129 | 130 | if enable_scaling { 131 | norm *= Fs 132 | } 133 | 134 | for i := range Pxx { 135 | Pxx[i] /= norm 136 | } 137 | 138 | freqs = make([]float64, lp) 139 | coef := Fs / float64(pad) 140 | for i := range freqs { 141 | freqs[i] = float64(i) * coef 142 | } 143 | 144 | return 145 | } 146 | -------------------------------------------------------------------------------- /vendor/go-dsp/spectral/pwelch_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package spectral 18 | 19 | import ( 20 | "testing" 21 | 22 | "go-dsp/dsputils" 23 | ) 24 | 25 | type pwelchTest struct { 26 | fs float64 27 | opts *PwelchOptions 28 | x, p, freqs []float64 29 | } 30 | 31 | var pwelchTests = []pwelchTest{ 32 | { // zero-length input 33 | 0, 34 | &PwelchOptions{}, 35 | []float64{}, 36 | []float64{}, 37 | []float64{}, 38 | }, 39 | { 40 | 2, 41 | &PwelchOptions{}, 42 | []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99}, 43 | []float64{3.66817103e+04, 6.16097526e+04, 3.70964854e+04, 1.76858083e+04, 8.82747121e+03, 5.58636625e+03, 3.86686565e+03, 2.79695091e+03, 2.14687978e+03, 1.68918004e+03, 1.36571705e+03, 1.13024093e+03, 9.48033939e+02, 8.08850444e+02, 6.97809757e+02, 6.08092372e+02, 5.35404251e+02, 4.74620274e+02, 4.24037212e+02, 3.81226909e+02, 3.44548926e+02, 3.13192558e+02, 2.85886182e+02, 2.62122493e+02, 2.41303266e+02, 2.22870690e+02, 2.06594463e+02, 1.92060902e+02, 1.79062190e+02, 1.67411631e+02, 1.56878696e+02, 1.47375046e+02, 1.38742768e+02, 1.30879468e+02, 1.23716560e+02, 1.17146757e+02, 1.11127186e+02, 1.05591138e+02, 1.00482309e+02, 9.57717459e+01, 9.14056404e+01, 8.73592894e+01, 8.36025117e+01, 8.01022290e+01, 7.68443525e+01, 7.38005900e+01, 7.09550385e+01, 6.82933042e+01, 6.57951735e+01, 6.34526724e+01, 6.12504908e+01, 5.91777124e+01, 5.72271084e+01, 5.53860529e+01, 5.36493451e+01, 5.20085636e+01, 5.04559628e+01, 4.89876620e+01, 4.75956980e+01, 4.62762918e+01, 4.50247688e+01, 4.38355570e+01, 4.27063668e+01, 4.16321728e+01, 4.06100428e+01, 3.96373615e+01, 3.87101146e+01, 3.78267782e+01, 3.69842029e+01, 3.61800421e+01, 3.54128094e+01, 3.46796320e+01, 3.39793658e+01, 3.33100629e+01, 3.26698301e+01, 3.20577904e+01, 3.14719152e+01, 3.09112634e+01, 3.03746526e+01, 2.98605643e+01, 2.93684407e+01, 2.88968774e+01, 2.84450603e+01, 2.80122875e+01, 2.75973586e+01, 2.71998759e+01, 2.68188936e+01, 2.64536948e+01, 2.61038720e+01, 2.57684964e+01, 2.54472465e+01, 2.51395088e+01, 2.48446551e+01, 2.45624511e+01, 2.42921985e+01, 2.40336109e+01, 2.37863119e+01, 2.35497603e+01, 2.33238184e+01, 2.31079809e+01, 2.29019795e+01, 2.27056035e+01, 2.25183990e+01, 2.23402769e+01, 2.21708920e+01, 2.20099898e+01, 2.18574728e+01, 2.17129732e+01, 2.15764231e+01, 2.14476081e+01, 2.13262901e+01, 2.12124459e+01, 2.11057929e+01, 2.10062684e+01, 2.09137648e+01, 2.08280657e+01, 2.07491945e+01, 2.06769518e+01, 2.06112729e+01, 2.05521368e+01, 2.04993557e+01, 2.04529802e+01, 2.04128917e+01, 2.03790224e+01, 2.03514209e+01, 2.03299362e+01, 2.03146325e+01, 2.03054705e+01, 1.01511907e+01}, 44 | []float64{0, 0.0078125, 0.015625, 0.0234375, 0.03125, 0.0390625, 0.046875, 0.0546875, 0.0625, 0.0703125, 0.078125, 0.0859375, 0.09375, 0.1015625, 0.109375, 0.1171875, 0.125, 0.1328125, 0.140625, 0.1484375, 0.15625, 0.1640625, 0.171875, 0.1796875, 0.1875, 0.1953125, 0.203125, 0.2109375, 0.21875, 0.2265625, 0.234375, 0.2421875, 0.25, 0.2578125, 0.265625, 0.2734375, 0.28125, 0.2890625, 0.296875, 0.3046875, 0.3125, 0.3203125, 0.328125, 0.3359375, 0.34375, 0.3515625, 0.359375, 0.3671875, 0.375, 0.3828125, 0.390625, 0.3984375, 0.40625, 0.4140625, 0.421875, 0.4296875, 0.4375, 0.4453125, 0.453125, 0.4609375, 0.46875, 0.4765625, 0.484375, 0.4921875, 0.5, 0.5078125, 0.515625, 0.5234375, 0.53125, 0.5390625, 0.546875, 0.5546875, 0.5625, 0.5703125, 0.578125, 0.5859375, 0.59375, 0.6015625, 0.609375, 0.6171875, 0.625, 0.6328125, 0.640625, 0.6484375, 0.65625, 0.6640625, 0.671875, 0.6796875, 0.6875, 0.6953125, 0.703125, 0.7109375, 0.71875, 0.7265625, 0.734375, 0.7421875, 0.75, 0.7578125, 0.765625, 0.7734375, 0.78125, 0.7890625, 0.796875, 0.8046875, 0.8125, 0.8203125, 0.828125, 0.8359375, 0.84375, 0.8515625, 0.859375, 0.8671875, 0.875, 0.8828125, 0.890625, 0.8984375, 0.90625, 0.9140625, 0.921875, 0.9296875, 0.9375, 0.9453125, 0.953125, 0.9609375, 0.96875, 0.9765625, 0.984375, 0.9921875, 1}, 45 | }, 46 | } 47 | 48 | func TestPwelch(t *testing.T) { 49 | 50 | for _, v := range pwelchTests { 51 | p, freqs := Pwelch(v.x, v.fs, v.opts) 52 | if !dsputils.PrettyClose(p, v.p) { 53 | t.Error("Pwelch Pxx error\n input:", v.x, "\n output:", p, "\nexpected:", v.p) 54 | } 55 | 56 | if !dsputils.PrettyClose(freqs, v.freqs) { 57 | t.Error("Pwelch freqs error\n input:", v.x, "\n output:", freqs, "\nexpected:", v.freqs) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /vendor/go-dsp/spectral/spectral.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | // Package spectral provides spectral analysis functions for digital signal processing. 18 | package spectral 19 | 20 | // Segment x segmented into segments of length size with specified noverlap. 21 | // Number of segments returned is (len(x) - size) / (size - noverlap) + 1. 22 | func Segment(x []float64, size, noverlap int) [][]float64 { 23 | stride := size - noverlap 24 | lx := len(x) 25 | 26 | var segments int 27 | if lx == size { 28 | segments = 1 29 | } else if lx > size { 30 | segments = (len(x)-size)/stride + 1 31 | } else { 32 | segments = 0 33 | } 34 | 35 | r := make([][]float64, segments) 36 | for i, offset := 0, 0; i < segments; i++ { 37 | r[i] = make([]float64, size) 38 | 39 | for j := 0; j < size; j++ { 40 | r[i][j] = x[offset+j] 41 | } 42 | 43 | offset += stride 44 | } 45 | 46 | return r 47 | } 48 | -------------------------------------------------------------------------------- /vendor/go-dsp/spectral/spectral_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package spectral 18 | 19 | import ( 20 | "testing" 21 | 22 | "go-dsp/dsputils" 23 | ) 24 | 25 | type segmentTest struct { 26 | size, 27 | noverlap int 28 | out [][]float64 29 | } 30 | 31 | var segmentTests = []segmentTest{ 32 | { 33 | 4, 0, 34 | [][]float64{ 35 | {1, 2, 3, 4}, 36 | {5, 6, 7, 8}, 37 | }, 38 | }, 39 | { 40 | 4, 1, 41 | [][]float64{ 42 | {1, 2, 3, 4}, 43 | {4, 5, 6, 7}, 44 | {7, 8, 9, 10}, 45 | }, 46 | }, 47 | { 48 | 4, 2, 49 | [][]float64{ 50 | {1, 2, 3, 4}, 51 | {3, 4, 5, 6}, 52 | {5, 6, 7, 8}, 53 | {7, 8, 9, 10}, 54 | }, 55 | }, 56 | } 57 | 58 | func TestSegment(t *testing.T) { 59 | x := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 60 | 61 | for _, v := range segmentTests { 62 | o := Segment(x, v.size, v.noverlap) 63 | if !dsputils.PrettyClose2F(o, v.out) { 64 | t.Error("Segment error\n output:", o, "\nexpected:", v.out) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /vendor/go-dsp/wav/float.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/vendor/go-dsp/wav/float.wav -------------------------------------------------------------------------------- /vendor/go-dsp/wav/small.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/vendor/go-dsp/wav/small.wav -------------------------------------------------------------------------------- /vendor/go-dsp/wav/wav.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | // Package wav provides support for the WAV file format. 18 | // 19 | // Supported formats are PCM 8- and 16-bit, and IEEE float. Extended chunks 20 | // (JUNK, bext, and others added by tools like ProTools) are ignored. 21 | package wav 22 | 23 | import ( 24 | "bytes" 25 | "encoding/binary" 26 | "fmt" 27 | "io" 28 | "io/ioutil" 29 | "math" 30 | "time" 31 | ) 32 | 33 | const ( 34 | wavFormatPCM = 1 35 | wavFormatIEEEFloat = 3 36 | ) 37 | 38 | // Header contains Wav fmt chunk data. 39 | type Header struct { 40 | AudioFormat uint16 41 | NumChannels uint16 42 | SampleRate uint32 43 | ByteRate uint32 44 | BlockAlign uint16 45 | BitsPerSample uint16 46 | } 47 | 48 | // Wav reads wav files. 49 | type Wav struct { 50 | Header 51 | // Samples is the total number of available samples. 52 | Samples int 53 | // Duration is the estimated duration based on reported samples. 54 | Duration time.Duration 55 | 56 | r io.Reader 57 | } 58 | 59 | // New reads the WAV header from r. 60 | func New(r io.Reader) (*Wav, error) { 61 | var w Wav 62 | header := make([]byte, 16) 63 | if _, err := io.ReadFull(r, header[:12]); err != nil { 64 | return nil, err 65 | } 66 | if string(header[0:4]) != "RIFF" { 67 | return nil, fmt.Errorf("wav: missing RIFF") 68 | } 69 | if string(header[8:12]) != "WAVE" { 70 | return nil, fmt.Errorf("wav: missing WAVE") 71 | } 72 | hasFmt := false 73 | for { 74 | if _, err := io.ReadFull(r, header[:8]); err != nil { 75 | return nil, err 76 | } 77 | sz := binary.LittleEndian.Uint32(header[4:]) 78 | switch typ := string(header[:4]); typ { 79 | case "fmt ": 80 | if sz < 16 { 81 | return nil, fmt.Errorf("wav: bad fmt size") 82 | } 83 | f := make([]byte, sz) 84 | if _, err := io.ReadFull(r, f); err != nil { 85 | return nil, err 86 | } 87 | if err := binary.Read(bytes.NewBuffer(f), binary.LittleEndian, &w.Header); err != nil { 88 | return nil, err 89 | } 90 | switch w.AudioFormat { 91 | case wavFormatPCM: 92 | case wavFormatIEEEFloat: 93 | default: 94 | return nil, fmt.Errorf("wav: unknown audio format: %02x", w.AudioFormat) 95 | } 96 | hasFmt = true 97 | case "data": 98 | if !hasFmt { 99 | return nil, fmt.Errorf("wav: unexpected fmt chunk") 100 | } 101 | w.Samples = int(sz) / int(w.BitsPerSample) * 8 102 | w.Duration = time.Duration(w.Samples) * time.Second / time.Duration(w.SampleRate) / time.Duration(w.NumChannels) 103 | w.r = io.LimitReader(r, int64(sz)) 104 | return &w, nil 105 | default: 106 | io.CopyN(ioutil.Discard, r, int64(sz)) 107 | } 108 | } 109 | } 110 | 111 | // ReadSamples returns a [n]T, where T is uint8, int16, or float32, based on the 112 | // wav data. n is the number of samples to return. 113 | func (w *Wav) ReadSamples(n int) (interface{}, error) { 114 | var data interface{} 115 | switch w.AudioFormat { 116 | case wavFormatPCM: 117 | switch w.BitsPerSample { 118 | case 8: 119 | data = make([]uint8, n) 120 | case 16: 121 | data = make([]int16, n) 122 | default: 123 | return nil, fmt.Errorf("wav: unknown bits per sample: %v", w.BitsPerSample) 124 | } 125 | case wavFormatIEEEFloat: 126 | data = make([]float32, n) 127 | default: 128 | return nil, fmt.Errorf("wav: unknown audio format") 129 | } 130 | if err := binary.Read(w.r, binary.LittleEndian, data); err != nil { 131 | return nil, err 132 | } 133 | return data, nil 134 | } 135 | 136 | // ReadFloats is like ReadSamples, but it converts any underlying data to a 137 | // float32. 138 | func (w *Wav) ReadFloats(n int) ([]float32, error) { 139 | d, err := w.ReadSamples(n) 140 | if err != nil { 141 | return nil, err 142 | } 143 | var f []float32 144 | switch d := d.(type) { 145 | case []uint8: 146 | f = make([]float32, len(d)) 147 | for i, v := range d { 148 | f[i] = float32(v) / math.MaxUint8 149 | } 150 | case []int16: 151 | f = make([]float32, len(d)) 152 | for i, v := range d { 153 | f[i] = (float32(v) - math.MinInt16) / (math.MaxInt16 - math.MinInt16) 154 | } 155 | case []float32: 156 | f = d 157 | default: 158 | return nil, fmt.Errorf("wav: unknown type: %T", d) 159 | } 160 | return f, nil 161 | } 162 | -------------------------------------------------------------------------------- /vendor/go-dsp/wav/wav_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func checkHeader(b []byte) error { 11 | _, err := New(bytes.NewBuffer(b)) 12 | return err 13 | } 14 | 15 | func TestShortHeaderValidation(t *testing.T) { 16 | shortHeader := []byte{0x52, 0x49, 0x46, 0x46, 0x72, 0x8C, 0x34, 0x00, 0x57, 0x41, 0x56, 0x45} 17 | if err := checkHeader(shortHeader); err == nil { 18 | t.Fatal("Expected short header to fail validation, but validation passed") 19 | } 20 | if err := checkHeader(nil); err == nil { 21 | t.Fatal("Expected nil header to fail validation, but validation passed") 22 | } 23 | } 24 | 25 | func TestInvalidHeaderValidation(t *testing.T) { 26 | validateErrorForMissingHeaderValue := func(err error, value string) { 27 | if err == nil { 28 | t.Fatalf("Invalid header data missing '%s' should fail validation", value) 29 | } 30 | } 31 | 32 | // 44 empty bytes 33 | invalidHeader := make([]byte, 44) 34 | err := checkHeader(invalidHeader) 35 | validateErrorForMissingHeaderValue(err, "RIFF") 36 | 37 | // RIFF and 40 empty bytes 38 | _ = copy(invalidHeader[:4], []byte{0x52, 0x49, 0x46, 0x46}) 39 | err = checkHeader(invalidHeader) 40 | validateErrorForMissingHeaderValue(err, "WAVE") 41 | 42 | // RIFF, WAVE, and 36 empty bytes 43 | _ = copy(invalidHeader[8:12], []byte{0x57, 0x41, 0x56, 0x45}) 44 | err = checkHeader(invalidHeader) 45 | validateErrorForMissingHeaderValue(err, "fmt") 46 | 47 | // RIFF, WAVE, fmt, and 32 empty bytes 48 | _ = copy(invalidHeader[12:16], []byte{0x66, 0x6D, 0x74, 0x20}) 49 | err = checkHeader(invalidHeader) 50 | validateErrorForMissingHeaderValue(err, "data") 51 | } 52 | 53 | type wavTest struct { 54 | w Wav 55 | typ reflect.Type 56 | } 57 | 58 | func TestWav(t *testing.T) { 59 | wavTests := map[string]wavTest{ 60 | "small.wav": { 61 | w: Wav{ 62 | Header: Header{ 63 | AudioFormat: 1, 64 | NumChannels: 1, 65 | SampleRate: 44100, 66 | ByteRate: 88200, 67 | BlockAlign: 2, 68 | BitsPerSample: 16, 69 | }, 70 | Samples: 41888, 71 | Duration: 949841269, 72 | }, 73 | typ: reflect.TypeOf(make([]uint8, 0)), 74 | }, 75 | "float.wav": { 76 | w: Wav{ 77 | Header: Header{ 78 | AudioFormat: 3, 79 | NumChannels: 1, 80 | SampleRate: 44100, 81 | ByteRate: 176400, 82 | BlockAlign: 4, 83 | BitsPerSample: 32, 84 | }, 85 | Samples: 1889280 / 4, 86 | Duration: 10710204081, 87 | }, 88 | typ: reflect.TypeOf(make([]float32, 0)), 89 | }, 90 | } 91 | for name, wt := range wavTests { 92 | f, err := os.Open(name) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | defer f.Close() 97 | w, err := New(f) 98 | if err != nil { 99 | t.Fatalf("%v: %v", name, err) 100 | } 101 | if !eq(*w, wt.w) { 102 | t.Errorf("wavs not equal: %v\ngot: %v\nexpected: %v", name, w, &wt.w) 103 | } 104 | } 105 | } 106 | 107 | func eq(x, y Wav) bool { 108 | x.r, y.r = nil, nil 109 | return x == y 110 | } 111 | -------------------------------------------------------------------------------- /vendor/go-dsp/window/window.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | // Package window provides window functions for digital signal processing. 18 | package window 19 | 20 | import ( 21 | "math" 22 | ) 23 | 24 | // Apply applies the window windowFunction to x. 25 | func Apply(x []float64, windowFunction func(int) []float64) { 26 | for i, w := range windowFunction(len(x)) { 27 | x[i] *= w 28 | } 29 | } 30 | 31 | // Rectangular returns an L-point rectangular window (all values are 1). 32 | func Rectangular(L int) []float64 { 33 | r := make([]float64, L) 34 | 35 | for i := range r { 36 | r[i] = 1 37 | } 38 | 39 | return r 40 | } 41 | 42 | // Hamming returns an L-point symmetric Hamming window. 43 | // Reference: http://www.mathworks.com/help/signal/ref/hamming.html 44 | func Hamming(L int) []float64 { 45 | r := make([]float64, L) 46 | 47 | if L == 1 { 48 | r[0] = 1 49 | } else { 50 | N := L - 1 51 | coef := math.Pi * 2 / float64(N) 52 | for n := 0; n <= N; n++ { 53 | r[n] = 0.54 - 0.46*math.Cos(coef*float64(n)) 54 | } 55 | } 56 | 57 | return r 58 | } 59 | 60 | // Hann returns an L-point Hann window. 61 | // Reference: http://www.mathworks.com/help/signal/ref/hann.html 62 | func Hann(L int) []float64 { 63 | r := make([]float64, L) 64 | 65 | if L == 1 { 66 | r[0] = 1 67 | } else { 68 | N := L - 1 69 | coef := 2 * math.Pi / float64(N) 70 | for n := 0; n <= N; n++ { 71 | r[n] = 0.5 * (1 - math.Cos(coef*float64(n))) 72 | } 73 | } 74 | 75 | return r 76 | } 77 | 78 | // Bartlett returns an L-point Bartlett window. 79 | // Reference: http://www.mathworks.com/help/signal/ref/bartlett.html 80 | func Bartlett(L int) []float64 { 81 | r := make([]float64, L) 82 | 83 | if L == 1 { 84 | r[0] = 1 85 | } else { 86 | N := L - 1 87 | coef := 2 / float64(N) 88 | n := 0 89 | for ; n <= N/2; n++ { 90 | r[n] = coef * float64(n) 91 | } 92 | for ; n <= N; n++ { 93 | r[n] = 2 - coef*float64(n) 94 | } 95 | } 96 | 97 | return r 98 | } 99 | -------------------------------------------------------------------------------- /vendor/go-dsp/window/window_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Matt Jibson 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package window 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/mjibson/go-dsp/dsputils" 23 | ) 24 | 25 | type windowTest struct { 26 | in int 27 | hamming []float64 28 | hann []float64 29 | bartlett []float64 30 | } 31 | 32 | var windowTests = []windowTest{ 33 | { 34 | 1, 35 | []float64{1}, 36 | []float64{1}, 37 | []float64{1}, 38 | }, 39 | { 40 | 5, 41 | []float64{0.08, 0.54, 1, 0.54, 0.08}, 42 | []float64{0, 0.5, 1, 0.5, 0}, 43 | []float64{0, 0.5, 1, 0.5, 0}, 44 | }, 45 | { 46 | 10, 47 | []float64{0.08, 0.18761956, 0.46012184, 0.77, 0.97225861, 0.97225861, 0.77, 0.46012184, 0.18761956, 0.08}, 48 | []float64{0, 0.116977778440511, 0.413175911166535, 0.75, 0.969846310392954, 0.969846310392954, 0.75, 0.413175911166535, 0.116977778440511, 0}, 49 | []float64{0, 0.222222222222222, 0.444444444444444, 0.666666666666667, 0.888888888888889, 0.888888888888889, 0.666666666666667, 0.444444444444444, 0.222222222222222, 0}, 50 | }, 51 | } 52 | 53 | func TestWindowFunctions(t *testing.T) { 54 | for _, v := range windowTests { 55 | o := Hamming(v.in) 56 | if !dsputils.PrettyClose(o, v.hamming) { 57 | t.Error("hamming error\ninput:", v.in, "\noutput:", o, "\nexpected:", v.hamming) 58 | } 59 | 60 | o = Hann(v.in) 61 | if !dsputils.PrettyClose(o, v.hann) { 62 | t.Error("hann error\ninput:", v.in, "\noutput:", o, "\nexpected:", v.hann) 63 | } 64 | 65 | o = Bartlett(v.in) 66 | if !dsputils.PrettyClose(o, v.bartlett) { 67 | t.Error("bartlett error\ninput:", v.in, "\noutput:", o, "\nexpected:", v.bartlett) 68 | } 69 | 70 | o = Rectangular(v.in) 71 | Apply(o, Hamming) 72 | if !dsputils.PrettyClose(o, v.hamming) { 73 | t.Error("apply error\noutput:", o, "\nexpected:", v.hamming) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /wav/encoder_test.go: -------------------------------------------------------------------------------- 1 | package wav_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/mattetti/audio/wav" 8 | ) 9 | 10 | func TestEncoderRoundTrip(t *testing.T) { 11 | os.Mkdir("testOutput", 0777) 12 | testCases := []struct { 13 | in string 14 | out string 15 | desc string 16 | }{ 17 | {"fixtures/kick.wav", "testOutput/kick.wav", "22050 Hz @ 16 bits, 1 channel(s), 44100 avg bytes/sec, duration: 204.172335ms"}, 18 | {"fixtures/kick-16b441k.wav", "testOutput/kick-16b441k.wav", "2 ch, 44100 Hz, 'lpcm' 16-bit little-endian signed integer"}, 19 | {"fixtures/bass.wav", "testOutput/bass.wav", "44100 Hz @ 24 bits, 2 channel(s), 264600 avg bytes/sec, duration: 543.378684ms"}, 20 | } 21 | 22 | for i, tc := range testCases { 23 | t.Logf("%d - in: %s, out: %s\n%s", i, tc.in, tc.out, tc.desc) 24 | in, err := os.Open(tc.in) 25 | if err != nil { 26 | t.Fatalf("couldn't open %s %v", tc.in, err) 27 | } 28 | d := wav.NewDecoder(in) 29 | buf, err := d.FullPCMBuffer() 30 | if err != nil { 31 | t.Fatalf("couldn't read buffer %s %v", tc.in, err) 32 | } 33 | 34 | // pcm := d.PCM() 35 | // numChannels, bitDepth, sampleRate, err := pcm.Info() 36 | // if err != nil { 37 | // t.Fatal(err) 38 | // } 39 | // totalFrames := pcm.Size() 40 | // frames, err := d.FramesInt() 41 | // if err != nil { 42 | // t.Fatal(err) 43 | // } 44 | in.Close() 45 | // t.Logf("%s - total frames %d - total samples %d", tc.in, totalFrames, len(frames)) 46 | 47 | out, err := os.Create(tc.out) 48 | if err != nil { 49 | t.Fatalf("couldn't create %s %v", tc.out, err) 50 | } 51 | 52 | e := wav.NewEncoder(out, 53 | buf.Format.SampleRate, 54 | buf.Format.BitDepth, 55 | buf.Format.NumChannels, 56 | int(d.WavAudioFormat)) 57 | if err := e.Write(buf); err != nil { 58 | t.Fatal(err) 59 | } 60 | if err := e.Close(); err != nil { 61 | t.Fatal(err) 62 | } 63 | out.Close() 64 | 65 | nf, err := os.Open(tc.out) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | nd := wav.NewDecoder(nf) 71 | nBuf, err := nd.FullPCMBuffer() 72 | if err != nil { 73 | t.Fatalf("couldn't extract the PCM from %s - %v", nf.Name(), err) 74 | } 75 | 76 | nf.Close() 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | defer func() { 81 | if err := os.Remove(nf.Name()); err != nil { 82 | panic(err) 83 | } 84 | }() 85 | 86 | if nBuf.Format.SampleRate != buf.Format.SampleRate { 87 | t.Fatalf("sample rate didn't support roundtripping exp: %d, got: %d", buf.Format.SampleRate, nBuf.Format.SampleRate) 88 | } 89 | if nBuf.Format.BitDepth != buf.Format.BitDepth { 90 | t.Fatalf("sample size didn't support roundtripping exp: %d, got: %d", buf.Format.BitDepth, nBuf.Format.BitDepth) 91 | } 92 | if nBuf.Format.NumChannels != buf.Format.NumChannels { 93 | t.Fatalf("the number of channels didn't support roundtripping exp: %d, got: %d", buf.Format.NumChannels, nBuf.Format.NumChannels) 94 | } 95 | if len(nBuf.Ints) != len(buf.Ints) { 96 | t.Fatalf("the reported number of frames didn't support roundtripping, exp: %d, got: %d", len(buf.Ints), len(nBuf.Ints)) 97 | } 98 | for i := 0; i < len(buf.Ints); i++ { 99 | if buf.Ints[i] != nBuf.Ints[i] { 100 | max := len(buf.Ints) 101 | if i+3 < max { 102 | max = i + 3 103 | } 104 | t.Fatalf("frame value at position %d: %d -> %d\ndidn't match new buffer position %d: %d -> %d", i, buf.Ints[:i+1], buf.Ints[i+1:max], i, nBuf.Ints[:i+1], nBuf.Ints[i+1:max]) 105 | } 106 | } 107 | 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /wav/examples_test.go: -------------------------------------------------------------------------------- 1 | package wav_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/mattetti/audio/wav" 9 | ) 10 | 11 | func ExampleDecoder_Duration() { 12 | f, err := os.Open("fixtures/kick.wav") 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | defer f.Close() 17 | dur, err := wav.NewDecoder(f).Duration() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | fmt.Printf("%s duration: %s\n", f.Name(), dur) 22 | // Output: fixtures/kick.wav duration: 204.172335ms 23 | } 24 | 25 | func ExampleDecoder_IsValidFile() { 26 | f, err := os.Open("fixtures/kick.wav") 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | defer f.Close() 31 | fmt.Printf("is this file valid: %t", wav.NewDecoder(f).IsValidFile()) 32 | // Output: is this file valid: true 33 | } 34 | 35 | func ExampleEncoder_Write() { 36 | f, err := os.Open("fixtures/kick.wav") 37 | if err != nil { 38 | panic(fmt.Sprintf("couldn't open audio file - %v", err)) 39 | } 40 | 41 | // Decode the original audio file 42 | // and collect audio content and information. 43 | d := wav.NewDecoder(f) 44 | buf, err := d.FullPCMBuffer() 45 | if err != nil { 46 | panic(err) 47 | } 48 | f.Close() 49 | fmt.Println("Old file ->", d) 50 | 51 | // Destination file 52 | out, err := os.Create("testOutput/kick.wav") 53 | if err != nil { 54 | panic(fmt.Sprintf("couldn't create output file - %v", err)) 55 | } 56 | 57 | // setup the encoder and write all the frames 58 | e := wav.NewEncoder(out, 59 | buf.Format.SampleRate, 60 | buf.Format.BitDepth, 61 | buf.Format.NumChannels, 62 | int(d.WavAudioFormat)) 63 | if err := e.Write(buf); err != nil { 64 | panic(err) 65 | } 66 | // close the encoder to make sure the headers are properly 67 | // set and the data is flushed. 68 | if err := e.Close(); err != nil { 69 | panic(err) 70 | } 71 | out.Close() 72 | 73 | // reopen to confirm things worked well 74 | out, err = os.Open("testOutput/kick.wav") 75 | if err != nil { 76 | panic(err) 77 | } 78 | d2 := wav.NewDecoder(out) 79 | d2.ReadInfo() 80 | fmt.Println("New file ->", d2) 81 | out.Close() 82 | os.Remove(out.Name()) 83 | 84 | // Output: 85 | // Old file -> Format: WAVE - 1 channels @ 22050 / 16 bits - Duration: 0.204172 seconds 86 | // New file -> Format: WAVE - 1 channels @ 22050 / 16 bits - Duration: 0.204172 seconds 87 | } 88 | -------------------------------------------------------------------------------- /wav/fixtures/bass.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/wav/fixtures/bass.wav -------------------------------------------------------------------------------- /wav/fixtures/dirty-kick-24b441k.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/wav/fixtures/dirty-kick-24b441k.wav -------------------------------------------------------------------------------- /wav/fixtures/kick-16b441k.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/wav/fixtures/kick-16b441k.wav -------------------------------------------------------------------------------- /wav/fixtures/kick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/wav/fixtures/kick.wav -------------------------------------------------------------------------------- /wav/fixtures/logicBounce.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/audio/c5379f9b5b61c08fce42156d96c8b2e7e75c2a25/wav/fixtures/logicBounce.wav -------------------------------------------------------------------------------- /wav/wav.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Package wav is a package allowing developers to decode and encode audio PCM 4 | data using the Waveform Audio File Format https://en.wikipedia.org/wiki/WAV 5 | 6 | */ 7 | package wav 8 | --------------------------------------------------------------------------------