├── recorder
├── bindata.sh
├── assets
│ ├── style.css
│ ├── index.html
│ ├── script.js
│ └── jswav.js
├── main.go
├── server.go
└── bindata.go
├── prototyping
├── even_odd.m
├── power_spec.m
├── dct_bins.m
├── fft_bins.m
├── bin_matrix.m
└── combiner_matrix.m
├── README.md
├── ctc
├── best_path.go
├── prefix_search_test.go
├── total_cost.go
├── prefix_search.go
├── rgradienter.go
├── ctc_test.go
└── ctc.go
├── mfcc
├── dct.go
├── mel_test.go
├── dct_test.go
├── velocity.go
├── mel.go
├── fft_test.go
├── source_test.go
├── fft.go
├── source.go
└── mfcc.go
├── LICENSE
├── speechdata
└── data.go
└── mfcc-graph
└── main.go
/recorder/bindata.sh:
--------------------------------------------------------------------------------
1 | go-bindata assets/...
2 |
--------------------------------------------------------------------------------
/prototyping/even_odd.m:
--------------------------------------------------------------------------------
1 | function [mat] = even_odd(size)
2 | mat = sparse(size, size);
3 | for i = 1:(size/2)
4 | mat(i, i*2-1) = 1;
5 | mat(i+size/2, i*2) = 1;
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/prototyping/power_spec.m:
--------------------------------------------------------------------------------
1 | function [spec] = power_spec(signal)
2 | fftRes = fft(signal);
3 | spec = zeros(rows(signal)/2+1, 1);
4 | for i = 1:(rows(signal)/2+1)
5 | spec(i) = fftRes(i)*conj(fftRes(i)) / rows(signal);
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/prototyping/dct_bins.m:
--------------------------------------------------------------------------------
1 | function [bins] = dct_bins(signal)
2 | n = rows(signal);
3 | transMat = zeros(n, n);
4 | for i = 1:n
5 | for j = 1:n
6 | transMat(i, j) = cos(pi / n * (j - 0.5) * (i - 1));
7 | end
8 | end
9 | bins = transMat*signal;
10 | end
11 |
--------------------------------------------------------------------------------
/prototyping/fft_bins.m:
--------------------------------------------------------------------------------
1 | function [bins] = fft_bins(data)
2 | n = rows(data);
3 | if n < 4
4 | bins = bin_matrix(n) * data;
5 | else
6 | eo = even_odd(n)*data;
7 | evenOut = fft_bins(eo(1:(n/2)));
8 | oddOut = fft_bins(eo((n/2+1):n));
9 | bins = combiner_matrix(n)*[evenOut; oddOut];
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/prototyping/bin_matrix.m:
--------------------------------------------------------------------------------
1 | function [mat] = bin_matrix(size)
2 | mat = zeros(size, size);
3 | for i = 1:(size/2+1)
4 | for j = 1:size
5 | mat(i, j) = cos(2*pi/size*(i-1)*(j-1));
6 | end
7 | end
8 | for i = 1:(size/2-1)
9 | for j = 1:size
10 | mat(i+size/2+1,j) = sin(2*pi/size*i*(j-1));
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # speechrecog
2 |
3 | This is a set of tools for implementing speech recognition. This is the first time I have played with speech recognition, so I am not exactly sure what will be needed. Nonetheless, here is what I have so far:
4 |
5 | * An [MFCC](https://en.wikipedia.org/wiki/Mel-frequency_cepstrum) package
6 | * A web app for recording and labeling speech samples
7 | * [CTC](http://goo.gl/gyisy9) recurrent neural net training
8 |
9 | # License
10 |
11 | This is under a BSD 2-clause license. See [LICENSE](LICENSE).
12 |
--------------------------------------------------------------------------------
/ctc/best_path.go:
--------------------------------------------------------------------------------
1 | package ctc
2 |
3 | import "github.com/unixpickle/num-analysis/linalg"
4 |
5 | // BestPath performs best path decoding on the sequence.
6 | func BestPath(seq []linalg.Vector) []int {
7 | last := -1
8 | var res []int
9 | for _, vec := range seq {
10 | idx := maxIdx(vec)
11 | if idx == len(vec)-1 {
12 | last = -1
13 | } else if idx != last {
14 | last = idx
15 | res = append(res, idx)
16 | }
17 | }
18 | return res
19 | }
20 |
21 | func maxIdx(vec linalg.Vector) int {
22 | var maxVal float64
23 | var maxIdx int
24 | for i, x := range vec {
25 | if i == 0 || x >= maxVal {
26 | maxVal = x
27 | maxIdx = i
28 | }
29 | }
30 | return maxIdx
31 | }
32 |
--------------------------------------------------------------------------------
/recorder/assets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | text-align: center;
3 | font-family: sans-serif;
4 | }
5 |
6 | audio {
7 | display: inline-block;
8 | width: 210px;
9 | }
10 |
11 | #samples {
12 | border: none;
13 | margin: auto;
14 | }
15 |
16 | #samples, #add-container {
17 | text-align: left;
18 | }
19 |
20 | #samples td {
21 | padding: 5px;
22 | max-width: 300px;
23 | }
24 |
25 | #samples td:first-child {
26 | min-width: 200px;
27 | }
28 |
29 | #samples td:nth-child(2) {
30 | text-align: center;
31 | }
32 |
33 | #samples tr:nth-child(even) {
34 | background-color: #edf1f8;
35 | }
36 |
37 | #samples tr:nth-child(odd) {
38 | background-color: #dae4f0;
39 | }
40 |
41 | #add-container {
42 | margin-top: 10px;
43 | padding: 10px;
44 | background-color: #f0f0f0;
45 | display: inline-block;
46 | }
47 |
--------------------------------------------------------------------------------
/mfcc/dct.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | import "math"
4 |
5 | // dct computes the first n bins of the discrete cosine
6 | // transform of a signal.
7 | func dct(signal []float64, n int) []float64 {
8 | res := make([]float64, n)
9 | baseFreq := math.Pi / float64(len(signal))
10 | for k := 0; k < n; k++ {
11 | initArg := baseFreq * float64(k) * 0.5
12 | curCos := math.Cos(initArg)
13 | curSin := math.Sin(initArg)
14 |
15 | // Double angle formulas to avoid more sin and cos.
16 | addCos := curCos*curCos - curSin*curSin
17 | addSin := 2 * curCos * curSin
18 |
19 | for _, x := range signal {
20 | res[k] += x * curCos
21 | // Angle sum formulas are a lot faster than
22 | // recomputing sines and cosines.
23 | curCos, curSin = curCos*addCos-curSin*addSin, curCos*addSin+addCos*curSin
24 | }
25 | }
26 | return res
27 | }
28 |
--------------------------------------------------------------------------------
/prototyping/combiner_matrix.m:
--------------------------------------------------------------------------------
1 | function [mat] = combiner_matrix(n)
2 | mat = sparse(n, n);
3 | mat(1, 1) = 1;
4 | mat(1, n/2+1) = 1;
5 | for i = 2:(n/4)
6 | mat(i, i) = 1;
7 | mat(i, i+n/2) = cos(2*pi/n*(i-1));
8 | mat(i, i+n/2+n/4) = -sin(2*pi/n*(i-1));
9 | end
10 | mat(n/4+1, n/4+1) = 1;
11 | for i = 1:(n/4)
12 | mirrorRow = n/4 + 1 - i;
13 | mat(n/4+1+i, :) = mat(mirrorRow, :);
14 | mat(n/4+1+i, (n/2+1):n) *= -1;
15 | end
16 | for i = 1:(n/4-1)
17 | rowIdx = n/2 + 1 + i;
18 | mat(rowIdx, n/4+1+i) = 1;
19 | mat(rowIdx, n/2+1+i) = sin(2*pi/n*(i));
20 | mat(rowIdx, n/2+n/4+1+i) = cos(2*pi/n*(i));
21 | end
22 | mat(n-(n/4-1), n-(n/4-1)) = 1;
23 | for i = 1:(n/4-1)
24 | rowIdx = n - n/4 + i + 1;
25 | mat(rowIdx, :) = mat(n-n/4-i+1, :);
26 | mat(rowIdx, 1:(n/2)) *= -1;
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/mfcc/mel_test.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | import (
4 | "math"
5 | "testing"
6 | )
7 |
8 | func TestMelBin(t *testing.T) {
9 | powers := []float64{1, 2, 3, 4, 5, 6, 7}
10 | bin := melBin{startIdx: 1, middleIdx: 3, endIdx: 6}
11 | res := bin.Apply(powers)
12 | expected := 3*0.5 + 4 + 5*2.0/3 + 6*1.0/3
13 | if math.Abs(res-expected) > 1e-5 {
14 | t.Errorf("expected %f got %f", expected, res)
15 | }
16 | }
17 |
18 | func TestMelBinner(t *testing.T) {
19 | binner := newMelBinner(8, 16, 2, 2, 8)
20 | actual := binner.Apply(fftBins{
21 | Cos: []float64{2, 3, 4, 5, 6},
22 | Sin: []float64{0, 0, 0},
23 | })
24 | expected := []float64{4.0 * 4 / 8, 5.0 * 5 / 8}
25 | if len(actual) != len(expected) {
26 | t.Errorf("expected len %d got len %d", len(expected), len(actual))
27 | } else if !slicesClose(actual, expected) {
28 | t.Errorf("expected %v got %v", expected, actual)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/recorder/main.go:
--------------------------------------------------------------------------------
1 | // Command recorder is a web app for recording
2 | // speech clips.
3 | package main
4 |
5 | import (
6 | "flag"
7 | "fmt"
8 | "net/http"
9 | "os"
10 | "strconv"
11 |
12 | "github.com/unixpickle/speechrecog/speechdata"
13 | )
14 |
15 | func main() {
16 | var portNum int
17 | flag.IntVar(&portNum, "port", 80, "HTTP port number")
18 |
19 | flag.Parse()
20 |
21 | if len(flag.Args()) != 1 {
22 | fmt.Fprintln(os.Stderr, "Usage: recorder [flags] data_dir\n\nAvailable flags:")
23 | flag.PrintDefaults()
24 | fmt.Fprintln(os.Stderr)
25 | os.Exit(1)
26 | }
27 |
28 | indexPath := flag.Args()[0]
29 |
30 | if _, err := os.Stat(indexPath); os.IsNotExist(err) {
31 | if err := os.Mkdir(indexPath, speechdata.IndexPerms); err != nil {
32 | fmt.Fprintln(os.Stderr, "Failed to make data dir:", err)
33 | os.Exit(1)
34 | }
35 | }
36 |
37 | index, err := speechdata.LoadIndex(indexPath)
38 | if err != nil {
39 | index = &speechdata.Index{
40 | DirPath: indexPath,
41 | }
42 | err = index.Save()
43 | }
44 | if err != nil {
45 | fmt.Fprintln(os.Stderr, "Failed to create DB:", err)
46 | os.Exit(1)
47 | }
48 |
49 | http.ListenAndServe(":"+strconv.Itoa(portNum), &Server{
50 | Index: index,
51 | })
52 | }
53 |
--------------------------------------------------------------------------------
/mfcc/dct_test.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | import (
4 | "math/rand"
5 | "testing"
6 | )
7 |
8 | const (
9 | dctBenchSignalSize = 26
10 | dctBenchBinCount = 13
11 | )
12 |
13 | func TestDCT(t *testing.T) {
14 | inputs := [][]float64{
15 | []float64{1, 2, 3, 4, 5, 6, 7, 8},
16 | []float64{1, -2, 3, -4, 5, -6},
17 | }
18 | ns := []int{8, 3}
19 | outputs := [][]float64{
20 | []float64{36.000000000000000, -12.884646045410273, -0.000000000000003,
21 | -1.346909601807877, 0.000000000000000, -0.401805807471995,
22 | -0.000000000000031, -0.101404645519244},
23 | []float64{-3.00000000000000, 3.62346663143529, -3.46410161513775},
24 | }
25 | for i, input := range inputs {
26 | actual := dct(input, ns[i])
27 | expected := outputs[i]
28 | if len(actual) != len(expected) {
29 | t.Errorf("%d: expected len %d got len %d", i, len(expected), len(actual))
30 | } else if !slicesClose(actual, expected) {
31 | t.Errorf("%d: expected %v got %v", i, expected, actual)
32 | }
33 | }
34 | }
35 |
36 | func BenchmarkDCT(b *testing.B) {
37 | rand.Seed(123)
38 | input := make([]float64, dctBenchSignalSize)
39 | for i := range input {
40 | input[i] = rand.NormFloat64()
41 | }
42 | b.ResetTimer()
43 | for i := 0; i < b.N; i++ {
44 | dct(input, dctBenchBinCount)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/recorder/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Recorder
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{range .Samples}}
15 |
16 | | {{- .Label -}} |
17 | {{if .File}}
18 |
19 |
23 | |
24 | {{else}}
25 |
26 |
27 | |
28 | {{end}}
29 |
30 |
31 | |
32 |
33 | {{end}}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016-2017, Alexander Nichol.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 | 2. Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 |
--------------------------------------------------------------------------------
/speechdata/data.go:
--------------------------------------------------------------------------------
1 | // Package speechdata facilitates loading and saving
2 | // databases of labeled speech samples.
3 | package speechdata
4 |
5 | import (
6 | "encoding/json"
7 | "io/ioutil"
8 | "path/filepath"
9 | )
10 |
11 | const (
12 | IndexFilename = "index.json"
13 | IndexPerms = 0755
14 | )
15 |
16 | // A Sample stores information about one audio sample.
17 | type Sample struct {
18 | ID string
19 | Label string
20 | File string
21 | }
22 |
23 | // An Index is a listing of a bunch of samples and their
24 | // enclosing directory.
25 | type Index struct {
26 | Samples []Sample
27 | DirPath string `json:"-"`
28 | }
29 |
30 | // LoadIndex loads an index from a data directory.
31 | func LoadIndex(dbPath string) (*Index, error) {
32 | contents, err := ioutil.ReadFile(filepath.Join(dbPath, IndexFilename))
33 | if err != nil {
34 | return nil, err
35 | }
36 | var ind Index
37 | if err := json.Unmarshal(contents, &ind); err != nil {
38 | return nil, err
39 | }
40 | ind.DirPath = dbPath
41 | return &ind, nil
42 | }
43 |
44 | // Save saves the index to its data directory.
45 | func (i *Index) Save() error {
46 | data, err := json.Marshal(i)
47 | if err != nil {
48 | return err
49 | }
50 | return ioutil.WriteFile(filepath.Join(i.DirPath, IndexFilename), data, IndexPerms)
51 | }
52 |
53 | // Clone creates a copy of this index, which should be
54 | // treated as a read-only copy useful for printing
55 | // directory listings.
56 | func (i *Index) Clone() *Index {
57 | res := &Index{DirPath: i.DirPath}
58 | for _, x := range i.Samples {
59 | res.Samples = append(res.Samples, x)
60 | }
61 | return res
62 | }
63 |
--------------------------------------------------------------------------------
/mfcc/velocity.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | // AddVelocities generates a CoeffSource which wraps
4 | // c and augments every vector of coefficients with
5 | // an additional vector of coefficient velocities.
6 | //
7 | // For example, for input coefficients [a,b,c], the
8 | // resulting source would produce coefficients
9 | // [a,b,c,da,db,dc] where d stands for derivative.
10 | func AddVelocities(c CoeffSource) CoeffSource {
11 | return &velocitySource{
12 | Wrapped: c,
13 | }
14 | }
15 |
16 | type velocitySource struct {
17 | Wrapped CoeffSource
18 |
19 | last []float64
20 | lastLast []float64
21 | doneError error
22 | }
23 |
24 | func (v *velocitySource) NextCoeffs() ([]float64, error) {
25 | if v.doneError != nil {
26 | return nil, v.doneError
27 | }
28 |
29 | if v.last == nil {
30 | v.lastLast, v.doneError = v.Wrapped.NextCoeffs()
31 | if v.doneError != nil {
32 | return nil, v.doneError
33 | }
34 | v.last, v.doneError = v.Wrapped.NextCoeffs()
35 | if v.doneError != nil {
36 | augmented := make([]float64, len(v.lastLast)*2)
37 | copy(augmented, v.lastLast)
38 | return augmented, nil
39 | }
40 | res := make([]float64, len(v.lastLast)*2)
41 | copy(res, v.lastLast)
42 | for i, x := range v.lastLast {
43 | res[i+len(v.lastLast)] = v.last[i] - x
44 | }
45 | return res, nil
46 | }
47 |
48 | var next []float64
49 | next, v.doneError = v.Wrapped.NextCoeffs()
50 | if v.doneError != nil {
51 | res := make([]float64, len(v.last)*2)
52 | copy(res, v.last)
53 | for i, x := range v.lastLast {
54 | res[i+len(v.last)] = v.last[i] - x
55 | }
56 | return res, nil
57 | }
58 |
59 | midpointRes := make([]float64, len(v.last)*2)
60 | copy(midpointRes, v.last)
61 | for i, x := range v.lastLast {
62 | midpointRes[i+len(v.last)] = (next[i] - x) / 2
63 | }
64 |
65 | v.lastLast = v.last
66 | v.last = next
67 |
68 | return midpointRes, nil
69 | }
70 |
--------------------------------------------------------------------------------
/mfcc/mel.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | import "math"
4 |
5 | type melBin struct {
6 | startIdx int
7 | middleIdx int
8 | endIdx int
9 | }
10 |
11 | func (m melBin) Apply(powers []float64) float64 {
12 | var res float64
13 | for i := m.startIdx + 1; i < m.middleIdx; i++ {
14 | dist := float64(i-m.startIdx) / float64(m.middleIdx-m.startIdx)
15 | res += dist * powers[i]
16 | }
17 | for i := m.middleIdx; i < m.endIdx; i++ {
18 | dist := float64(i-m.middleIdx) / float64(m.endIdx-m.middleIdx)
19 | res += (1 - dist) * powers[i]
20 | }
21 | return res
22 | }
23 |
24 | type melBinner []melBin
25 |
26 | func newMelBinner(fftSize, sampleRate, binCount int, minFreq, maxFreq float64) melBinner {
27 | if hardMax := float64(sampleRate) / 2; maxFreq > hardMax {
28 | maxFreq = hardMax
29 | }
30 |
31 | minMels, maxMels := hertzToMels(minFreq), hertzToMels(maxFreq)
32 |
33 | points := make([]float64, binCount+2)
34 | points[0] = minMels
35 | for i := 1; i <= binCount; i++ {
36 | points[i] = minMels + float64(i)*(maxMels-minMels)/float64(binCount+1)
37 | }
38 | points[binCount+1] = maxMels
39 |
40 | fftPoints := make([]int, len(points))
41 | for i, m := range points {
42 | fftPoints[i] = hertzToBin(melsToHertz(m), fftSize, sampleRate)
43 | }
44 |
45 | res := make(melBinner, binCount)
46 | for i := range res {
47 | res[i] = melBin{
48 | startIdx: fftPoints[i],
49 | middleIdx: fftPoints[i+1],
50 | endIdx: fftPoints[i+2],
51 | }
52 | }
53 | return res
54 | }
55 |
56 | func (m melBinner) Apply(f fftBins) []float64 {
57 | powers := f.powerSpectrum()
58 | res := make([]float64, len(m))
59 | for i, b := range m {
60 | res[i] = b.Apply(powers)
61 | }
62 | return res
63 | }
64 |
65 | func hertzToMels(h float64) float64 {
66 | return 1125.0 * math.Log(1+h/700)
67 | }
68 |
69 | func melsToHertz(m float64) float64 {
70 | return 700 * (math.Exp(m/1125) - 1)
71 | }
72 |
73 | func hertzToBin(h float64, fftSize, sampleRate int) int {
74 | freqScale := float64(sampleRate) / float64(fftSize)
75 | bin := h / freqScale
76 | bin = math.Min(bin, float64(fftSize)/2)
77 | floorFreq := math.Floor(bin) * freqScale
78 | ceilFreq := math.Ceil(bin) * freqScale
79 | if math.Abs(floorFreq-h) < math.Abs(ceilFreq-h) {
80 | return int(bin)
81 | } else {
82 | return int(math.Ceil(bin))
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/ctc/prefix_search_test.go:
--------------------------------------------------------------------------------
1 | package ctc
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/unixpickle/num-analysis/linalg"
7 | )
8 |
9 | func TestPrefixSearch(t *testing.T) {
10 | var seqs = [][]linalg.Vector{
11 | {
12 | {-9.21034037197618, -0.000100005000333347},
13 | {-0.105360515657826, -2.302585092994046},
14 | {-9.21034037197618, -0.000100005000333347},
15 | {-0.105360515657826, -2.302585092994046},
16 | {-9.21034037197618, -0.000100005000333347},
17 | {-9.21034037197618, -0.000100005000333347},
18 | },
19 | {
20 | {-1.38155105579643e+01, -1.38155105579643e+01, -2.00000199994916e-06},
21 | // The first label is not more likely, but
22 | // after both timesteps it has a 0.64% chance
23 | // of being seen in at least one of the two
24 | // timesteps.
25 | {-0.916290731874155, -13.815510557964274, -0.510827290434046},
26 | {-0.916290731874155, -13.815510557964274, -0.510827290434046},
27 | {-1.38155105579643e+01, -1.38155105579643e+01, -2.00000199994916e-06},
28 | {-1.609437912434100, -0.693147180559945, -1.203972804325936},
29 | },
30 | {
31 | {-1.38155105579643e+01, -1.38155105579643e+01, -2.00000199994916e-06},
32 | {-0.916290731874155, -13.815510557964274, -0.510827290434046},
33 | {-1.38155105579643e+01, -1.38155105579643e+01, -2.00000199994916e-06},
34 | {-1.609437912434100, -0.693147180559945, -1.203972804325936},
35 | },
36 | {
37 | {-0.916290731874155, -13.815510557964274, -0.510827290434046},
38 | {-1.38155105579643e+01, -1.38155105579643e+01, -2.00000199994916e-06},
39 | {-1.609437912434100, -0.693147180559945, -1.203972804325936},
40 | },
41 | }
42 | var outputs = [][]int{
43 | {0, 0},
44 | {0, 1},
45 | {1},
46 | {1},
47 | }
48 | var threshes = []float64{-1e-2, -1e-3, -1e-6, -1e-10}
49 | for _, thresh := range threshes {
50 | for i, seq := range seqs {
51 | actual := PrefixSearch(seq, thresh)
52 | expected := outputs[i]
53 | if !labelingsEqual(actual, expected) {
54 | t.Errorf("thresh %f: seq %d: expected %v got %v", thresh, i,
55 | expected, actual)
56 | }
57 | }
58 | }
59 | }
60 |
61 | func labelingsEqual(l1, l2 []int) bool {
62 | if len(l1) != len(l2) {
63 | return false
64 | }
65 | for i, x := range l1 {
66 | if x != l2[i] {
67 | return false
68 | }
69 | }
70 | return true
71 | }
72 |
--------------------------------------------------------------------------------
/mfcc/fft_test.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | import (
4 | "math/rand"
5 | "testing"
6 | )
7 |
8 | const fftBenchSize = 512
9 |
10 | func TestFFT(t *testing.T) {
11 | inputs := [][]float64{
12 | []float64{0.517450},
13 | []float64{0.517450, -0.515357},
14 | []float64{0.517450, 0.591873, 0.104983, -0.512010},
15 | []float64{0.517450, 0.591873, 0.104983, -0.512010, -0.037091, 0.203369,
16 | 0.452477, -0.452457, 0.873007, 0.134188, -0.515357, 0.864060, 0.838039, 0.618038,
17 | -0.729226, 0.949877},
18 | }
19 | outputs := [][]float64{
20 | []float64{0.517450},
21 | []float64{0.0020930, 1.0328070},
22 | []float64{0.70230, 0.41247, 0.54257, 1.10388},
23 | []float64{3.901220, 0.598021, 0.624881, 1.641404, 2.878528, -1.558631,
24 | 0.554137, -2.103022, -0.892656, -1.616822, -0.303837, 1.961911, 0.697998,
25 | -2.336823, -0.036587, -2.415035},
26 | }
27 | for i, input := range inputs {
28 | expected := outputs[i]
29 | res := fft(input)
30 | actual := append(res.Cos, res.Sin...)
31 | if len(actual) != len(expected) {
32 | t.Errorf("%d: len should be %d but it's %d", i, len(expected), len(actual))
33 | } else if !slicesClose(actual, expected) {
34 | t.Errorf("%d: expected %v but got %v", i, expected, actual)
35 | }
36 | }
37 | }
38 |
39 | func TestFFTPower(t *testing.T) {
40 | inputs := [][]float64{
41 | []float64{0.123},
42 | []float64{1, -2, 3, -2},
43 | []float64{0.517450, 0.591873, 0.104983, -0.512010, -0.037091, 0.203369,
44 | 0.452477, -0.452457, 0.873007, 0.134188, -0.515357, 0.864060, 0.838039, 0.618038,
45 | -0.729226, 0.949877},
46 | }
47 | outputs := [][]float64{
48 | []float64{0.015129},
49 | []float64{0, 1, 16},
50 | []float64{0.951220, 0.185734, 0.030175, 0.408956, 0.548320,
51 | 0.493129, 0.019275, 0.640944, 0.049802},
52 | }
53 | for i, input := range inputs {
54 | actual := fft(input).powerSpectrum()
55 | expected := outputs[i]
56 | if len(actual) != len(expected) {
57 | t.Errorf("%d: expected len %d but got len %d", i, len(expected), len(actual))
58 | } else if !slicesClose(actual, expected) {
59 | t.Errorf("%d: expected %v but got %v", i, expected, actual)
60 | }
61 | }
62 | }
63 |
64 | func BenchmarkFFT(b *testing.B) {
65 | rand.Seed(123)
66 | inputVec := make([]float64, fftBenchSize)
67 | for i := range inputVec {
68 | inputVec[i] = rand.NormFloat64()
69 | }
70 |
71 | b.ResetTimer()
72 | for i := 0; i < b.N; i++ {
73 | fft(inputVec)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ctc/total_cost.go:
--------------------------------------------------------------------------------
1 | package ctc
2 |
3 | import (
4 | "runtime"
5 | "sort"
6 | "sync"
7 |
8 | "github.com/unixpickle/autofunc/seqfunc"
9 | "github.com/unixpickle/num-analysis/linalg"
10 | "github.com/unixpickle/sgd"
11 | )
12 |
13 | // TotalCost returns total CTC cost of a network on
14 | // a batch of samples.
15 | //
16 | // The maxGos argument specifies the maximum number
17 | // of goroutines to run batches on simultaneously.
18 | // If it is 0, GOMAXPROCS is used.
19 | func TotalCost(f seqfunc.RFunc, s sgd.SampleSet, maxBatch, maxGos int) float64 {
20 | if maxGos == 0 {
21 | maxGos = runtime.GOMAXPROCS(0)
22 | }
23 |
24 | s = s.Copy()
25 | sortSampleSet(s)
26 |
27 | subBatches := make(chan sgd.SampleSet, s.Len()/maxBatch+1)
28 | for i := 0; i < s.Len(); i += maxBatch {
29 | bs := maxBatch
30 | if bs > s.Len()-i {
31 | bs = s.Len() - i
32 | }
33 | subBatches <- s.Subset(i, i+bs)
34 | }
35 | close(subBatches)
36 |
37 | var wg sync.WaitGroup
38 | costChan := make(chan float64, 0)
39 | for i := 0; i < maxGos; i++ {
40 | wg.Add(1)
41 | go func() {
42 | defer wg.Done()
43 | for batch := range subBatches {
44 | costChan <- costForBatch(f, batch)
45 | }
46 | }()
47 | }
48 | go func() {
49 | wg.Wait()
50 | close(costChan)
51 | }()
52 |
53 | var sum float64
54 | for c := range costChan {
55 | sum += c
56 | }
57 | return sum
58 | }
59 |
60 | func costForBatch(f seqfunc.RFunc, s sgd.SampleSet) float64 {
61 | inputVecs := make([][]linalg.Vector, s.Len())
62 | for i := 0; i < s.Len(); i++ {
63 | sample := s.GetSample(i).(Sample)
64 | inputVecs[i] = sample.Input
65 | }
66 |
67 | outputs := f.ApplySeqs(seqfunc.ConstResult(inputVecs))
68 |
69 | var sum float64
70 | for i, outSeq := range outputs.OutputSeqs() {
71 | seqVars := sequenceToVars(outSeq)
72 | label := s.GetSample(i).(Sample).Label
73 | sum += LogLikelihood(varsToResults(seqVars), label).Output()[0]
74 | }
75 |
76 | return -sum
77 | }
78 |
79 | // sortSampleSet sorts samples so that the longest
80 | // sequences come first.
81 | func sortSampleSet(s sgd.SampleSet) {
82 | sort.Sort(sampleSorter{s})
83 | }
84 |
85 | type sampleSorter struct {
86 | s sgd.SampleSet
87 | }
88 |
89 | func (s sampleSorter) Len() int {
90 | return s.s.Len()
91 | }
92 |
93 | func (s sampleSorter) Swap(i, j int) {
94 | s.s.Swap(i, j)
95 | }
96 |
97 | func (s sampleSorter) Less(i, j int) bool {
98 | item1 := s.s.GetSample(i).(Sample)
99 | item2 := s.s.GetSample(j).(Sample)
100 | return len(item1.Input) > len(item2.Input)
101 | }
102 |
--------------------------------------------------------------------------------
/mfcc/source_test.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | import (
4 | "io"
5 | "math"
6 | "testing"
7 | )
8 |
9 | type sliceSource struct {
10 | vec []float64
11 | idx int
12 | buffSize int
13 | }
14 |
15 | func (s *sliceSource) ReadSamples(slice []float64) (n int, err error) {
16 | if len(slice) > s.buffSize {
17 | slice = slice[:s.buffSize]
18 | }
19 | n = copy(slice, s.vec[s.idx:])
20 | if n == 0 {
21 | err = io.EOF
22 | }
23 | s.idx += n
24 | return
25 | }
26 |
27 | func TestFramer(t *testing.T) {
28 | var data [11]float64
29 |
30 | source := sliceSource{vec: []float64{1, -1, 0.5, 0.3, 0.2, 1, 0.5}, buffSize: 2}
31 | framedSource := framer{S: &source, Size: 3, Step: 2}
32 |
33 | n, err := framedSource.ReadSamples(data[:])
34 | if err != io.EOF {
35 | t.Errorf("expected EOF error, got %v", err)
36 | }
37 | expected := []float64{1, -1, 0.5, 0.5, 0.3, 0.2, 0.2, 1, 0.5, 0.5}
38 | if n != len(expected) {
39 | t.Errorf("expected %d outputs but got %d", len(expected), n)
40 | } else if !slicesClose(data[:len(expected)], expected) {
41 | t.Errorf("expected slice %v but got %v", expected, data[:len(expected)])
42 | }
43 |
44 | source = sliceSource{vec: []float64{1, -1, 0.5, 0.3, 0.2, 1}, buffSize: 2}
45 | framedSource = framer{S: &source, Size: 3, Step: 3}
46 |
47 | n, err = framedSource.ReadSamples(data[:])
48 | if err != io.EOF {
49 | t.Errorf("expected EOF error, got %v", err)
50 | }
51 | expected = []float64{1, -1, 0.5, 0.3, 0.2, 1}
52 | if n != len(expected) {
53 | t.Errorf("expected %d outputs but got %d", len(expected), n)
54 | } else if !slicesClose(data[:len(expected)], expected) {
55 | t.Errorf("expected slice %v but got %v", expected, data[:len(expected)])
56 | }
57 | }
58 |
59 | func TestrateChanger(t *testing.T) {
60 | var data [20]float64
61 |
62 | source := sliceSource{vec: []float64{1, -1, 0.5, 0.3, 0.2, 1, 0.5}, buffSize: 2}
63 | changer := rateChanger{S: &source, Ratio: 2 + 1e-8}
64 |
65 | n, err := changer.ReadSamples(data[:])
66 | if err != io.EOF {
67 | t.Errorf("expected EOF error, got %v", err)
68 | }
69 | expected := []float64{1, 0, -1, -0.25, 0.5, 0.4, 0.3, 0.25, 0.2, 0.6, 1, 0.75, 0.5}
70 | if n != len(expected) {
71 | t.Errorf("expected %d outputs but got %d", len(expected), n)
72 | } else if !slicesClose(data[:len(expected)], expected) {
73 | t.Errorf("expected slice %v but got %v", expected, data[:len(expected)])
74 | }
75 |
76 | source = sliceSource{vec: []float64{1, -1, 0.5, 0.3, 0.2, 1, 0.5}, buffSize: 2}
77 | changer = rateChanger{S: &source, Ratio: 0.5 + 1e-8}
78 |
79 | n, err = changer.ReadSamples(data[:])
80 | if err != io.EOF {
81 | t.Errorf("expected EOF error, got %v", err)
82 | }
83 | expected = []float64{1, 0.5, 0.2, 0.5}
84 | if n != len(expected) {
85 | t.Errorf("expected %d outputs but got %d", len(expected), n)
86 | } else if !slicesClose(data[:len(expected)], expected) {
87 | t.Errorf("expected slice %v but got %v", expected, data[:len(expected)])
88 | }
89 | }
90 |
91 | func slicesClose(s1, s2 []float64) bool {
92 | for i, x := range s1 {
93 | if math.Abs(s2[i]-x) > 1e-5 {
94 | return false
95 | }
96 | }
97 | return true
98 | }
99 |
--------------------------------------------------------------------------------
/ctc/prefix_search.go:
--------------------------------------------------------------------------------
1 | package ctc
2 |
3 | import (
4 | "math"
5 | "sort"
6 |
7 | "github.com/unixpickle/num-analysis/linalg"
8 | )
9 |
10 | // PrefixSearch performs prefix search decoding on
11 | // the output sequence.
12 | //
13 | // Any blanks with log likelihoods greater than
14 | // or equal to blankThresh will be treated as if
15 | // they have a log likelihood of 0 (i.e. a
16 | // likelihood of 1).
17 | func PrefixSearch(seq []linalg.Vector, blankThresh float64) []int {
18 | var subSeqs [][]linalg.Vector
19 | var subSeq []linalg.Vector
20 | for _, x := range seq {
21 | if x[len(x)-1] > blankThresh {
22 | if len(subSeq) > 0 {
23 | subSeqs = append(subSeqs, subSeq)
24 | subSeq = nil
25 | }
26 | } else {
27 | subSeq = append(subSeq, x)
28 | }
29 | }
30 | if len(subSeq) > 0 {
31 | subSeqs = append(subSeqs, subSeq)
32 | }
33 |
34 | var res []int
35 | for _, sub := range subSeqs {
36 | subRes, _ := prefixSearch(sub, nil, math.Inf(-1), 0)
37 | res = append(res, subRes...)
38 | }
39 | return res
40 | }
41 |
42 | // prefixSearch performs prefix search starting from
43 | // the first element of seq, given the existing prefix
44 | // and the logged probabilities of that prefix with
45 | // and without a terminating blank.
46 | // It returns the best possible prefix and said prefix's
47 | // probability.
48 | func prefixSearch(seq []linalg.Vector, prefix []int, noBlankProb,
49 | blankProb float64) (bestSeq []int, bestProb float64) {
50 | if len(seq) == 0 {
51 | return prefix, addProbabilitiesFloat(noBlankProb, blankProb)
52 | }
53 |
54 | totalProb := addProbabilitiesFloat(noBlankProb, blankProb)
55 |
56 | var exts extensionList
57 | timeVec := seq[0]
58 | for i := 0; i < len(timeVec)-1; i++ {
59 | exts.Labels = append(exts.Labels, i)
60 | if len(prefix) > 0 && i == prefix[len(prefix)-1] {
61 | exts.Probs = append(exts.Probs, timeVec[i]+blankProb)
62 | } else {
63 | exts.Probs = append(exts.Probs, timeVec[i]+totalProb)
64 | }
65 | }
66 |
67 | exts.Labels = append(exts.Labels, -1)
68 | sameBlank := totalProb + timeVec[len(timeVec)-1]
69 | sameNoBlank := math.Inf(-1)
70 | if len(prefix) > 0 {
71 | last := prefix[len(prefix)-1]
72 | sameNoBlank = noBlankProb + timeVec[last]
73 | }
74 | exts.Probs = append(exts.Probs, addProbabilitiesFloat(sameNoBlank, sameBlank))
75 |
76 | sort.Sort(&exts)
77 |
78 | for i, addition := range exts.Labels {
79 | prob := exts.Probs[i]
80 | if i > 0 && prob < bestProb {
81 | continue
82 | }
83 |
84 | var s []int
85 | var p float64
86 | if addition == -1 {
87 | s, p = prefixSearch(seq[1:], prefix, sameNoBlank, sameBlank)
88 | } else {
89 | newPrefix := make([]int, len(prefix)+1)
90 | copy(newPrefix, prefix)
91 | newPrefix[len(prefix)] = addition
92 | s, p = prefixSearch(seq[1:], newPrefix, prob, math.Inf(-1))
93 | }
94 | if i == 0 || p > bestProb {
95 | bestProb = p
96 | bestSeq = s
97 | }
98 | }
99 |
100 | return
101 | }
102 |
103 | type prefixSearchResult struct {
104 | bestSeq []int
105 | logLikelihood float64
106 | }
107 |
108 | type extensionList struct {
109 | Labels []int
110 | Probs []float64
111 | }
112 |
113 | func (e *extensionList) Len() int {
114 | return len(e.Labels)
115 | }
116 |
117 | func (e *extensionList) Swap(i, j int) {
118 | e.Labels[i], e.Labels[j] = e.Labels[j], e.Labels[i]
119 | e.Probs[i], e.Probs[j] = e.Probs[j], e.Probs[i]
120 | }
121 |
122 | func (e *extensionList) Less(i, j int) bool {
123 | return e.Probs[i] > e.Probs[j]
124 | }
125 |
--------------------------------------------------------------------------------
/mfcc/fft.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | import "math"
4 |
5 | // fftBins stores the dot products of a signal with a
6 | // basis of sinusoids.
7 | //
8 | // Let N be len(signal), and assume it is a power of 2.
9 | // The i-th dot product in Cos, where i is between 0 and
10 | // N/2 inclusive, are with is cos(2*pi/N*i).
11 | // The j-th dot product in Sin, where j is between 0 and
12 | // N/2-1 inclusive, are with sin(2*pi/N*i).
13 | type fftBins struct {
14 | Cos []float64
15 | Sin []float64
16 | }
17 |
18 | // fft computes dot products of the signal with
19 | // various sinusoids.
20 | func fft(signal []float64) fftBins {
21 | temp := make([]float64, len(signal))
22 | signalCopy := make([]float64, len(signal))
23 | copy(signalCopy, signal)
24 |
25 | basePeriod := 2 * math.Pi / float64(len(signal))
26 | sines := make([]float64, len(signal)/4)
27 | cosines := make([]float64, len(signal)/4)
28 | for i := range cosines {
29 | if i < 2 || i%100 == 0 {
30 | cosines[i] = math.Cos(basePeriod * float64(i))
31 | sines[i] = math.Sin(basePeriod * float64(i))
32 | } else {
33 | // Angle sum formulas for cosine and sine.
34 | cosines[i] = cosines[i-1]*cosines[1] - sines[i-1]*sines[1]
35 | sines[i] = sines[i-1]*cosines[1] + cosines[i-1]*sines[1]
36 | }
37 | }
38 |
39 | return destructiveFFT(signalCopy, temp, sines, cosines, 0)
40 | }
41 |
42 | func (f fftBins) powerSpectrum() []float64 {
43 | scaleFactor := 1 / float64(len(f.Cos)+len(f.Sin))
44 | n := len(f.Cos)
45 | res := make([]float64, n)
46 | res[0] = f.Cos[0] * f.Cos[0] * scaleFactor
47 | res[n-1] = f.Cos[n-1] * f.Cos[n-1] * scaleFactor
48 | for i, s := range f.Sin {
49 | res[i+1] = (s*s + f.Cos[i+1]*f.Cos[i+1]) * scaleFactor
50 | }
51 | return res
52 | }
53 |
54 | func destructiveFFT(signal []float64, temp []float64, sines, cosines []float64,
55 | depth uint) fftBins {
56 | n := len(signal)
57 | if n == 1 {
58 | return fftBins{Cos: []float64{signal[0]}}
59 | } else if n == 2 {
60 | return fftBins{
61 | Cos: []float64{signal[0] + signal[1], signal[0] - signal[1]},
62 | }
63 | } else if n == 4 {
64 | return fftBins{
65 | Cos: []float64{
66 | signal[0] + signal[1] + signal[2] + signal[3],
67 | signal[0] - signal[2],
68 | signal[0] - signal[1] + signal[2] - signal[3],
69 | },
70 | Sin: []float64{
71 | signal[1] - signal[3],
72 | },
73 | }
74 | } else if n&1 != 0 {
75 | panic("input must be a power of 2")
76 | }
77 |
78 | evenSignal := temp[:n/2]
79 | oddSignal := temp[n/2:]
80 | for i := 0; i < n; i += 2 {
81 | evenSignal[i>>1] = signal[i]
82 | oddSignal[i>>1] = signal[i+1]
83 | }
84 | evenBins := destructiveFFT(evenSignal, signal[:n/2], sines, cosines, depth+1)
85 | oddBins := destructiveFFT(oddSignal, signal[n/2:], sines, cosines, depth+1)
86 |
87 | res := fftBins{
88 | Cos: temp[:n/2+1],
89 | Sin: temp[n/2+1:],
90 | }
91 |
92 | res.Cos[0] = evenBins.Cos[0] + oddBins.Cos[0]
93 | res.Cos[n/2] = evenBins.Cos[0] - oddBins.Cos[0]
94 | res.Cos[n/4] = evenBins.Cos[n/4]
95 | for i := 1; i < n/4; i++ {
96 | oddPart := cosines[i< [--velocity]")
31 | os.Exit(1)
32 | }
33 |
34 | var getVelocity bool
35 | if len(os.Args) == 4 {
36 | if os.Args[3] != "--velocity" {
37 | fmt.Fprintln(os.Stderr, "Unexpected argument:", os.Args[3])
38 | os.Exit(1)
39 | }
40 | getVelocity = true
41 | }
42 |
43 | coeffs, err := readCoeffs(os.Args[1], getVelocity)
44 | if err != nil {
45 | fmt.Fprintln(os.Stderr, "Failed to read MFCCs:", err)
46 | os.Exit(1)
47 | }
48 |
49 | page := createHTML(createSVG(coeffs))
50 |
51 | if err := ioutil.WriteFile(os.Args[2], page, OutputPerms); err != nil {
52 | fmt.Fprintln(os.Stderr, "Failed to write result:", err)
53 | os.Exit(1)
54 | }
55 | }
56 |
57 | func readCoeffs(file string, velocity bool) ([][]float64, error) {
58 | sound, err := wav.ReadSoundFile(os.Args[1])
59 | if err != nil {
60 | return nil, err
61 | }
62 |
63 | var audioData []float64
64 | for i, x := range sound.Samples() {
65 | if i%sound.Channels() == 0 {
66 | audioData = append(audioData, float64(x))
67 | }
68 | }
69 |
70 | mfccSource := mfcc.MFCC(&mfcc.SliceSource{Slice: audioData}, sound.SampleRate(),
71 | &mfcc.Options{Window: time.Millisecond * 20, Overlap: time.Millisecond * 10})
72 | if velocity {
73 | mfccSource = mfcc.AddVelocities(mfccSource)
74 | }
75 |
76 | var coeffs [][]float64
77 | for {
78 | c, err := mfccSource.NextCoeffs()
79 | if err == nil {
80 | if velocity {
81 | coeffs = append(coeffs, c[len(c)/2:])
82 | } else {
83 | coeffs = append(coeffs, c)
84 | }
85 | } else {
86 | break
87 | }
88 | }
89 |
90 | return coeffs, nil
91 | }
92 |
93 | func createSVG(coeffs [][]float64) []byte {
94 | var buf bytes.Buffer
95 | buf.WriteString(`` + "\n")
96 | buf.WriteString(`")
133 | return buf.Bytes()
134 | }
135 |
136 | func createHTML(svgPage []byte) []byte {
137 | var buf bytes.Buffer
138 |
139 | buf.WriteString("\n\n")
140 | buf.WriteString("\nMFCC Graph\n\n\n")
141 | for coeffIdx := 1; coeffIdx < 13; coeffIdx++ {
142 | color := CepstrumColors[coeffIdx-1]
143 | buf.WriteString(` ` +
145 | `\n")
147 | }
148 | buf.Write(svgPage)
149 | buf.WriteString("\n\n")
150 |
151 | return buf.Bytes()
152 | }
153 |
154 | func formatFloat(f float64) string {
155 | return fmt.Sprintf("%.2f", f)
156 | }
157 |
--------------------------------------------------------------------------------
/mfcc/source.go:
--------------------------------------------------------------------------------
1 | package mfcc
2 |
3 | import "io"
4 |
5 | // A Source is a place from which audio sample data can
6 | // be read.
7 | // This interface is very similar to io.Reader, except
8 | // that it deals with samples instead of bytes.
9 | type Source interface {
10 | ReadSamples(s []float64) (n int, err error)
11 | }
12 |
13 | // A SliceSource is a Source which returns pre-determined
14 | // samples from a slice.
15 | type SliceSource struct {
16 | Slice []float64
17 |
18 | // Offset is the current offset into the slice.
19 | // This will be increased as samples are read.
20 | Offset int
21 | }
22 |
23 | func (s *SliceSource) ReadSamples(out []float64) (n int, err error) {
24 | n = copy(out, s.Slice[s.Offset:])
25 | s.Offset += n
26 | if n < len(out) {
27 | err = io.EOF
28 | }
29 | return
30 | }
31 |
32 | // A framer is a Source that wraps another Source and
33 | // generates overlapping windows of sample data.
34 | //
35 | // For instance, if Size is set to 200 and Step is
36 | // set to 100, then samples 0-199 from the wrapped
37 | // source are returned, followed by samples 100-299,
38 | // followed by 200-399, etc.
39 | //
40 | // The Step must not be greater than the Size.
41 | //
42 | // The last frame returned by a framer may be partial,
43 | // i.e. it may be less than Size samples.
44 | type framer struct {
45 | S Source
46 |
47 | Size int
48 | Step int
49 |
50 | doneError error
51 | curCache []float64
52 | nextCache []float64
53 | outWindowProgress int
54 | }
55 |
56 | func (f *framer) ReadSamples(s []float64) (n int, err error) {
57 | if f.doneError != nil {
58 | return 0, f.doneError
59 | }
60 | for i := range s {
61 | var noSample bool
62 | s[i], noSample, err = f.readSample()
63 | if noSample {
64 | break
65 | }
66 | n++
67 | if err != nil {
68 | break
69 | }
70 | }
71 | if err != nil {
72 | f.doneError = err
73 | }
74 | return
75 | }
76 |
77 | func (f *framer) readSample() (sample float64, noSample bool, err error) {
78 | if len(f.curCache) > 0 {
79 | sample = f.curCache[0]
80 | f.curCache = f.curCache[1:]
81 | } else {
82 | var s [1]float64
83 | for {
84 | var n int
85 | n, err = f.S.ReadSamples(s[:])
86 | if n == 1 {
87 | sample = s[0]
88 | break
89 | } else if err != nil {
90 | return 0, true, err
91 | }
92 | }
93 | }
94 | if f.outWindowProgress >= f.Step {
95 | f.nextCache = append(f.nextCache, sample)
96 | }
97 | f.outWindowProgress++
98 | if f.outWindowProgress == f.Size {
99 | f.outWindowProgress = 0
100 | f.curCache = f.nextCache
101 | f.nextCache = nil
102 | }
103 | return
104 | }
105 |
106 | // A rateChanger changes the sample rate of a Source.
107 | //
108 | // The Ratio argument determines the ratio of the new
109 | // sample rate to the old one.
110 | // For example, a Ratio of 2.5 would turn the sample
111 | // rate 22050 to the rate 55125.
112 | type rateChanger struct {
113 | S Source
114 | Ratio float64
115 |
116 | doneError error
117 | started bool
118 | lastSample float64
119 | nextSample float64
120 | midpart float64
121 | }
122 |
123 | func (r *rateChanger) ReadSamples(s []float64) (n int, err error) {
124 | if r.doneError != nil {
125 | return 0, r.doneError
126 | }
127 | for i := range s {
128 | var noSample bool
129 | s[i], noSample, err = r.readSample()
130 | if noSample {
131 | break
132 | }
133 | n++
134 | if err != nil {
135 | break
136 | }
137 | }
138 | if err != nil {
139 | r.doneError = err
140 | }
141 | return
142 | }
143 |
144 | func (r *rateChanger) readSample() (sample float64, noSample bool, err error) {
145 | if !r.started {
146 | noSample, err = r.start()
147 | if noSample {
148 | return
149 | }
150 | }
151 |
152 | if r.midpart > 1 {
153 | readCount := int(r.midpart)
154 | for i := 0; i < readCount; i++ {
155 | noSample, err = r.readNext()
156 | }
157 | if noSample {
158 | return
159 | }
160 | r.midpart -= float64(readCount)
161 | }
162 |
163 | sample = r.lastSample*(1-r.midpart) + r.nextSample*r.midpart
164 | r.midpart += 1 / r.Ratio
165 | return
166 | }
167 |
168 | func (r *rateChanger) start() (noSample bool, err error) {
169 | var samples [2]float64
170 | var n, gotten int
171 | for gotten < 2 {
172 | n, err = r.S.ReadSamples(samples[gotten:])
173 | gotten += n
174 | if err != nil {
175 | break
176 | }
177 | }
178 | if gotten < 2 {
179 | return true, err
180 | }
181 | r.lastSample = samples[0]
182 | r.nextSample = samples[1]
183 | r.started = true
184 | return
185 | }
186 |
187 | func (r *rateChanger) readNext() (noSample bool, err error) {
188 | var samples [1]float64
189 | var n int
190 | for {
191 | n, err = r.S.ReadSamples(samples[:])
192 | if n == 1 {
193 | break
194 | } else if err != nil {
195 | return true, err
196 | }
197 | }
198 | r.lastSample = r.nextSample
199 | r.nextSample = samples[0]
200 | return
201 | }
202 |
--------------------------------------------------------------------------------
/ctc/rgradienter.go:
--------------------------------------------------------------------------------
1 | package ctc
2 |
3 | import (
4 | "github.com/unixpickle/autofunc"
5 | "github.com/unixpickle/autofunc/seqfunc"
6 | "github.com/unixpickle/num-analysis/linalg"
7 | "github.com/unixpickle/sgd"
8 | "github.com/unixpickle/weakai/neuralnet"
9 | )
10 |
11 | // A Sample is a labeled training sample.
12 | type Sample struct {
13 | Input []linalg.Vector
14 | Label []int
15 | }
16 |
17 | // RGradienter computes gradients for sgd.SampleSets
18 | // full of Samples.
19 | type RGradienter struct {
20 | SeqFunc seqfunc.RFunc
21 | Learner sgd.Learner
22 |
23 | // MaxConcurrency is the maximum number of goroutines
24 | // to use simultaneously.
25 | MaxConcurrency int
26 |
27 | // MaxSubBatch is the maximum batch size to pass to
28 | // the SeqFunc in one call.
29 | MaxSubBatch int
30 |
31 | helper *neuralnet.GradHelper
32 | }
33 |
34 | func (r *RGradienter) Gradient(s sgd.SampleSet) autofunc.Gradient {
35 | s = s.Copy()
36 | sortSampleSet(s)
37 | return r.makeHelper().Gradient(s)
38 | }
39 |
40 | func (r *RGradienter) RGradient(v autofunc.RVector, s sgd.SampleSet) (autofunc.Gradient,
41 | autofunc.RGradient) {
42 | s = s.Copy()
43 | sortSampleSet(s)
44 | return r.makeHelper().RGradient(v, s)
45 | }
46 |
47 | func (r *RGradienter) makeHelper() *neuralnet.GradHelper {
48 | if r.helper != nil {
49 | r.helper.MaxConcurrency = r.MaxConcurrency
50 | r.helper.MaxSubBatch = r.MaxSubBatch
51 | return r.helper
52 | }
53 | return &neuralnet.GradHelper{
54 | MaxConcurrency: r.MaxConcurrency,
55 | MaxSubBatch: r.MaxSubBatch,
56 | Learner: r.Learner,
57 | CompGrad: r.compGrad,
58 | CompRGrad: r.compRGrad,
59 | }
60 | }
61 |
62 | func (r *RGradienter) compGrad(g autofunc.Gradient, s sgd.SampleSet) {
63 | inputVars := make([][]*autofunc.Variable, s.Len())
64 | for i := 0; i < s.Len(); i++ {
65 | sample := s.GetSample(i).(Sample)
66 | inputVars[i] = sequenceToVars(sample.Input)
67 | }
68 |
69 | outputs := r.SeqFunc.ApplySeqs(seqfunc.VarResult(inputVars))
70 |
71 | var upstream [][]linalg.Vector
72 | for i, outSeq := range outputs.OutputSeqs() {
73 | seqVars := sequenceToVars(outSeq)
74 | grad := autofunc.NewGradient(seqVars)
75 | label := s.GetSample(i).(Sample).Label
76 | cost := autofunc.Scale(LogLikelihood(varsToResults(seqVars), label), -1)
77 | cost.PropagateGradient(linalg.Vector{1}, grad)
78 |
79 | upstreamSeq := make([]linalg.Vector, len(seqVars))
80 | for i, variable := range seqVars {
81 | upstreamSeq[i] = grad[variable]
82 | }
83 | upstream = append(upstream, upstreamSeq)
84 | }
85 |
86 | outputs.PropagateGradient(upstream, g)
87 | }
88 |
89 | func (r *RGradienter) compRGrad(rv autofunc.RVector, rg autofunc.RGradient,
90 | g autofunc.Gradient, s sgd.SampleSet) {
91 | inputVars := make([][]*autofunc.Variable, s.Len())
92 | for i := 0; i < s.Len(); i++ {
93 | sample := s.GetSample(i).(Sample)
94 | inputVars[i] = sequenceToVars(sample.Input)
95 | }
96 |
97 | outputs := r.SeqFunc.ApplySeqsR(rv, seqfunc.VarRResult(rv, inputVars))
98 |
99 | var upstream [][]linalg.Vector
100 | var upstreamR [][]linalg.Vector
101 | for i, outSeq := range outputs.OutputSeqs() {
102 | seqRVars := sequenceToRVars(outSeq, outputs.ROutputSeqs()[i])
103 | params := varsInRVars(seqRVars)
104 | grad := autofunc.NewGradient(params)
105 | rgrad := autofunc.NewRGradient(params)
106 | label := s.GetSample(i).(Sample).Label
107 | cost := autofunc.ScaleR(LogLikelihoodR(rvarsToRResults(seqRVars), label), -1)
108 | cost.PropagateRGradient(linalg.Vector{1}, linalg.Vector{0}, rgrad, grad)
109 |
110 | upstreamSeq := make([]linalg.Vector, len(params))
111 | upstreamSeqR := make([]linalg.Vector, len(params))
112 | for i, variable := range params {
113 | upstreamSeq[i] = grad[variable]
114 | upstreamSeqR[i] = rgrad[variable]
115 | }
116 | upstream = append(upstream, upstreamSeq)
117 | upstreamR = append(upstreamR, upstreamSeqR)
118 | }
119 |
120 | outputs.PropagateRGradient(upstream, upstreamR, rg, g)
121 | }
122 |
123 | func sequenceToVars(seq []linalg.Vector) []*autofunc.Variable {
124 | res := make([]*autofunc.Variable, len(seq))
125 | for i, vec := range seq {
126 | res[i] = &autofunc.Variable{Vector: vec}
127 | }
128 | return res
129 | }
130 |
131 | func sequenceToRVars(seq, seqR []linalg.Vector) []*autofunc.RVariable {
132 | if seqR == nil && len(seq) > 0 {
133 | seqR = make([]linalg.Vector, len(seq))
134 | zeroVec := make(linalg.Vector, len(seq[0]))
135 | for i := range seqR {
136 | seqR[i] = zeroVec
137 | }
138 | }
139 | res := make([]*autofunc.RVariable, len(seq))
140 | for i, vec := range seq {
141 | res[i] = &autofunc.RVariable{
142 | Variable: &autofunc.Variable{Vector: vec},
143 | ROutputVec: seqR[i],
144 | }
145 | }
146 | return res
147 | }
148 |
149 | func varsToResults(vars []*autofunc.Variable) []autofunc.Result {
150 | res := make([]autofunc.Result, len(vars))
151 | for i, v := range vars {
152 | res[i] = v
153 | }
154 | return res
155 | }
156 |
157 | func rvarsToRResults(vars []*autofunc.RVariable) []autofunc.RResult {
158 | res := make([]autofunc.RResult, len(vars))
159 | for i, v := range vars {
160 | res[i] = v
161 | }
162 | return res
163 | }
164 |
165 | func varsInRVars(vars []*autofunc.RVariable) []*autofunc.Variable {
166 | res := make([]*autofunc.Variable, len(vars))
167 | for i, x := range vars {
168 | res[i] = x.Variable
169 | }
170 | return res
171 | }
172 |
--------------------------------------------------------------------------------
/mfcc/mfcc.go:
--------------------------------------------------------------------------------
1 | // Package mfcc can compute Mel-frequency cepstrum
2 | // coefficients from raw sample data.
3 | //
4 | // For more information about MFCC, see Wikipedia:
5 | // https://en.wikipedia.org/wiki/Mel-frequency_cepstrum
6 | package mfcc
7 |
8 | import (
9 | "math"
10 | "time"
11 | )
12 |
13 | const (
14 | DefaultWindow = time.Millisecond * 20
15 | DefaultOverlap = time.Millisecond * 10
16 |
17 | DefaultFFTSize = 512
18 | DefaultLowFreq = 300
19 | DefaultHighFreq = 8000
20 | DefaultMelCount = 26
21 | DefaultKeepCount = 13
22 | )
23 |
24 | // Options stores all of the configuration options for
25 | // computing MFCCs.
26 | type Options struct {
27 | // Window is the amount of time represented in each
28 | // MFCC frame.
29 | // If this is 0, DefaultWindow is used.
30 | Window time.Duration
31 |
32 | // Overlap is the amount of overlapping time between
33 | // adjacent windows.
34 | // If this is 0, DefaultOverlap is used.
35 | Overlap time.Duration
36 |
37 | // DisableOverlap can be set to disable overlap.
38 | // If this is set to true, Overlap is ignored.
39 | DisableOverlap bool
40 |
41 | // FFTSize is the number of FFT bins to compute for
42 | // each window.
43 | // This must be a power of 2.
44 | // If this is 0, DefaultFFTSize is used.
45 | //
46 | // It may be noted that the FFT size influences the
47 | // upsampling/downsampling behavior of the converter.
48 | FFTSize int
49 |
50 | // LowFreq is the minimum frequency for Mel banks.
51 | // If this is 0, DefaultLowFreq is used.
52 | LowFreq float64
53 |
54 | // HighFreq is the maximum frequency for Mel banks.
55 | // If this is 0, DefaultHighFreq is used.
56 | // In practice, this may be bounded by the FFT window
57 | // size.
58 | HighFreq float64
59 |
60 | // MelCount is the number of Mel banks to compute.
61 | // If this is 0, DefaultMelCount is used.
62 | MelCount int
63 |
64 | // KeepCount is the number of MFCCs to keep after the
65 | // discrete cosine transform is complete.
66 | // If this is 0, DefaultKeepCount is used.
67 | KeepCount int
68 | }
69 |
70 | // CoeffSource computes MFCCs (or augmented MFCCs) from an
71 | // underlying audio source.
72 | type CoeffSource interface {
73 | // NextCoeffs returns the next batch of coefficients,
74 | // or an error if the underlying Source ended with one.
75 | //
76 | // This will never return a non-nil batch along with
77 | // an error.
78 | NextCoeffs() ([]float64, error)
79 | }
80 |
81 | // MFCC generates a CoeffSource that computes the MFCCs
82 | // of the given Source.
83 | //
84 | // After source returns its first error, the last window
85 | // will be padded with zeroes and used to compute a final
86 | // batch of MFCCs before returning the error.
87 | func MFCC(source Source, sampleRate int, options *Options) CoeffSource {
88 | if options == nil {
89 | options = &Options{}
90 | }
91 |
92 | windowTime := options.Window
93 | if windowTime == 0 {
94 | windowTime = DefaultWindow
95 | }
96 | windowSeconds := float64(windowTime) / float64(time.Second)
97 |
98 | fftSize := intOrDefault(options.FFTSize, DefaultFFTSize)
99 | newSampleRate := int(float64(fftSize)/windowSeconds + 0.5)
100 |
101 | overlapTime := options.Overlap
102 | if options.DisableOverlap {
103 | overlapTime = 0
104 | } else if overlapTime == 0 {
105 | overlapTime = DefaultOverlap
106 | }
107 | overlapSeconds := float64(overlapTime) / float64(time.Second)
108 | overlapSamples := int(overlapSeconds*float64(newSampleRate) + 0.5)
109 | if overlapSamples >= fftSize {
110 | overlapSamples = fftSize - 1
111 | }
112 |
113 | binCount := intOrDefault(options.MelCount, DefaultMelCount)
114 | minFreq := floatOrDefault(options.LowFreq, DefaultLowFreq)
115 | maxFreq := floatOrDefault(options.HighFreq, DefaultHighFreq)
116 |
117 | return &coeffChan{
118 | windowedSource: &framer{
119 | S: &rateChanger{
120 | S: source,
121 | Ratio: float64(newSampleRate) / float64(sampleRate),
122 | },
123 | Size: fftSize,
124 | Step: fftSize - overlapSamples,
125 | },
126 | windowSize: fftSize,
127 | binner: newMelBinner(fftSize, newSampleRate, binCount, minFreq, maxFreq),
128 | keepCount: intOrDefault(options.KeepCount, DefaultKeepCount),
129 | }
130 | }
131 |
132 | type coeffChan struct {
133 | windowedSource Source
134 | windowSize int
135 | binner melBinner
136 | keepCount int
137 |
138 | doneError error
139 | }
140 |
141 | func (c *coeffChan) NextCoeffs() ([]float64, error) {
142 | if c.doneError != nil {
143 | return nil, c.doneError
144 | }
145 |
146 | buf := make([]float64, c.windowSize)
147 | var have int
148 | for have < len(buf) && c.doneError == nil {
149 | n, err := c.windowedSource.ReadSamples(buf[have:])
150 | if err != nil {
151 | c.doneError = err
152 | }
153 | have += n
154 | }
155 | if have == 0 && c.doneError != nil {
156 | return nil, c.doneError
157 | }
158 |
159 | // ReadSamples can use the buffer as scratch space,
160 | // just like io.Reader.
161 | for i := have; i < len(buf); i++ {
162 | buf[i] = 0
163 | }
164 |
165 | banks := c.binner.Apply(fft(buf))
166 | for i, x := range banks {
167 | banks[i] = math.Log(x)
168 | }
169 | return dct(banks, c.keepCount), nil
170 | }
171 |
172 | func intOrDefault(val, def int) int {
173 | if val == 0 {
174 | return def
175 | } else {
176 | return val
177 | }
178 | }
179 |
180 | func floatOrDefault(val, def float64) float64 {
181 | if val == 0 {
182 | return def
183 | } else {
184 | return val
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/recorder/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/hex"
6 | "errors"
7 | "html/template"
8 | "io"
9 | "math/rand"
10 | "net/http"
11 | "os"
12 | "path"
13 | "path/filepath"
14 | "strings"
15 | "sync"
16 | "time"
17 |
18 | "github.com/unixpickle/speechrecog/speechdata"
19 | )
20 |
21 | const (
22 | AssetPrefix = "assets/"
23 | IndexPath = AssetPrefix + "index.html"
24 | StylePath = AssetPrefix + "style.css"
25 | )
26 |
27 | var (
28 | IndexTemplate *template.Template
29 | )
30 |
31 | func init() {
32 | rand.Seed(time.Now().UnixNano())
33 | templateData, err := Asset(IndexPath)
34 | if err != nil {
35 | panic(err)
36 | }
37 | IndexTemplate = template.Must(template.New("index").Parse(string(templateData)))
38 | }
39 |
40 | type Server struct {
41 | DataLock sync.RWMutex
42 | Index *speechdata.Index
43 | }
44 |
45 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
46 | switch r.URL.Path {
47 | case "/":
48 | w.Header().Set("Content-Type", "text/html")
49 | s.DataLock.RLock()
50 | idx := s.Index.Clone()
51 | s.DataLock.RUnlock()
52 | IndexTemplate.Execute(w, idx)
53 | case "/recording.wav":
54 | soundFile, err := s.openSoundFile(r)
55 | defer soundFile.Close()
56 | if err != nil {
57 | http.NotFound(w, r)
58 | } else {
59 | defer soundFile.Close()
60 | w.Header().Set("Content-Type", "audio/x-wav")
61 | w.Header().Set("Content-Disposition", "inline; filename=recording.wav")
62 | http.ServeContent(w, r, "recording.wav", time.Now(), soundFile)
63 | }
64 | case "/style.css":
65 | contents, _ := Asset(StylePath)
66 | w.Header().Set("Content-Type", "text/css")
67 | w.Write(contents)
68 | case "/script.js", "/jswav.js":
69 | contents, _ := Asset(path.Join(AssetPrefix, r.URL.Path))
70 | w.Header().Set("Content-Type", "application/javascript")
71 | w.Write(contents)
72 | case "/add":
73 | msg, err := s.addLabel(r)
74 | writeAPIResponse(w, msg, err)
75 | case "/delete":
76 | writeAPIResponse(w, "success", s.deleteEntry(r))
77 | case "/upload":
78 | writeAPIResponse(w, "success", s.upload(r))
79 | default:
80 | http.NotFound(w, r)
81 | }
82 | }
83 |
84 | func (s *Server) openSoundFile(r *http.Request) (*os.File, error) {
85 | query := r.URL.Query()
86 | id := query.Get("id")
87 |
88 | s.DataLock.RLock()
89 | defer s.DataLock.RUnlock()
90 |
91 | for _, x := range s.Index.Samples {
92 | if x.ID == id {
93 | if x.File == "" {
94 | return nil, errors.New("no recording for sample")
95 | }
96 | return os.Open(filepath.Join(s.Index.DirPath, x.File))
97 | }
98 | }
99 |
100 | return nil, errors.New("invalid ID: " + id)
101 | }
102 |
103 | func (s *Server) addLabel(r *http.Request) (id string, err error) {
104 | query := r.URL.Query()
105 | label := query.Get("label")
106 |
107 | if label == "" {
108 | return "", errors.New("cannot add empty label")
109 | }
110 |
111 | s.DataLock.Lock()
112 | defer s.DataLock.Unlock()
113 |
114 | id = randomID()
115 | s.Index.Samples = append(s.Index.Samples, speechdata.Sample{
116 | ID: id,
117 | Label: label,
118 | })
119 | if err := s.Index.Save(); err != nil {
120 | s.Index.Samples = s.Index.Samples[:len(s.Index.Samples)-1]
121 | return "", err
122 | }
123 | return id, nil
124 | }
125 |
126 | func (s *Server) deleteEntry(r *http.Request) error {
127 | query := r.URL.Query()
128 | id := query.Get("id")
129 |
130 | s.DataLock.Lock()
131 | defer s.DataLock.Unlock()
132 |
133 | for i, x := range s.Index.Samples {
134 | if x.ID == id {
135 | backup := s.Index.Clone()
136 | copy(s.Index.Samples[i:], s.Index.Samples[i+1:])
137 | s.Index.Samples = s.Index.Samples[:len(s.Index.Samples)-1]
138 | if err := s.Index.Save(); err != nil {
139 | s.Index = backup
140 | return err
141 | }
142 | if x.File != "" {
143 | os.Remove(filepath.Join(s.Index.DirPath, x.File))
144 | }
145 | return nil
146 | }
147 | }
148 |
149 | return errors.New("ID not found: " + id)
150 | }
151 |
152 | func (s *Server) upload(r *http.Request) error {
153 | query := r.URL.Query()
154 | id := query.Get("id")
155 |
156 | if id == "" {
157 | return errors.New("missing id field")
158 | }
159 |
160 | destName := randomID()
161 | destPath := filepath.Join(s.Index.DirPath, destName)
162 |
163 | f, err := os.Create(destPath)
164 | defer f.Close()
165 | if err != nil {
166 | return err
167 | }
168 | b64 := base64.NewDecoder(base64.StdEncoding, r.Body)
169 | if _, err := io.Copy(f, b64); err != nil {
170 | os.Remove(destPath)
171 | return err
172 | }
173 |
174 | s.DataLock.Lock()
175 | defer s.DataLock.Unlock()
176 |
177 | for i, x := range s.Index.Samples {
178 | if x.ID == id {
179 | oldFile := s.Index.Samples[i].File
180 | if oldFile != "" {
181 | os.Remove(destPath)
182 | return errors.New("entry already has a recording")
183 | }
184 | s.Index.Samples[i].File = destName
185 | if err := s.Index.Save(); err != nil {
186 | s.Index.Samples[i].File = ""
187 | os.Remove(destPath)
188 | }
189 | return nil
190 | }
191 | }
192 |
193 | os.Remove(destPath)
194 | return errors.New("unknown id: " + id)
195 | }
196 |
197 | func writeAPIResponse(w http.ResponseWriter, successMsg string, err error) {
198 | w.Header().Set("Content-Type", "text/plain")
199 | if err != nil {
200 | w.WriteHeader(http.StatusBadRequest)
201 | w.Write([]byte("Error: " + err.Error()))
202 | } else {
203 | w.Write([]byte(successMsg))
204 | }
205 | }
206 |
207 | func randomID() string {
208 | var buf [16]byte
209 | for i := 0; i < len(buf); i++ {
210 | buf[i] = byte(rand.Intn(0x100))
211 | }
212 | return strings.ToLower(hex.EncodeToString(buf[:]))
213 | }
214 |
--------------------------------------------------------------------------------
/recorder/assets/script.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | var AJAX_DONE = 4;
4 | var HTTP_OK = 200;
5 | var ENTER_KEYCODE = 13;
6 | var ESCAPE_KEYCODE = 27;
7 |
8 | var addButton = null;
9 | var currentRecorder = null;
10 | var currentRecordButton = null;
11 |
12 | function initialize() {
13 | addButton = document.getElementById('add-button');
14 | addButton.addEventListener('click', addLabel);
15 |
16 | window.addEventListener('keyup', function(e) {
17 | if (e.which === ESCAPE_KEYCODE) {
18 | e.preventDefault();
19 | cancelRecording();
20 | }
21 | });
22 |
23 | document.getElementById('add-content').addEventListener('keyup', function(e) {
24 | e.preventDefault();
25 | if (e.which === ENTER_KEYCODE) {
26 | addLabel();
27 | }
28 | });
29 |
30 | var buttonClasses = ['record-button', 'delete-button'];
31 | var buttonRegistrars = [registerRecordButton, registerDeleteButton];
32 | for (var i = 0, len = buttonClasses.length; i < len; ++i) {
33 | var className = buttonClasses[i];
34 | var reg = buttonRegistrars[i];
35 | var buttons = document.getElementsByClassName(className);
36 | for (var j = 0, len1 = buttons.length; j < len1; ++j) {
37 | reg(buttons[j]);
38 | }
39 | }
40 | }
41 |
42 | function addLabel() {
43 | cancelRecording();
44 | var labelField = document.getElementById('add-content');
45 | var label = labelField.value;
46 | var addURL = '/add?label=' + encodeURIComponent(label);
47 | addButton.disabled = true;
48 | getURL(addURL, function(err, id) {
49 | addButton.disabled = false;
50 | if (!err) {
51 | cancelRecording();
52 | labelField.value = null;
53 | addNewRow(id, label);
54 | } else {
55 | showError(err);
56 | }
57 | });
58 | }
59 |
60 | function showError(err) {
61 | alert(err);
62 | }
63 |
64 | function addNewRow(id, label) {
65 | var element = document.createElement('tr');
66 | element.setAttribute('label-id', id);
67 |
68 | var labelCol = document.createElement('td');
69 | labelCol.textContent = label;
70 |
71 | var recordCol = document.createElement('td');
72 | var recordButton = document.createElement('button');
73 | recordButton.className = 'record-button';
74 | recordButton.textContent = 'Record';
75 | recordCol.appendChild(recordButton);
76 |
77 | var deleteCol = document.createElement('td');
78 | var deleteButton = document.createElement('button');
79 | deleteButton.className = 'delete-button';
80 | deleteButton.textContent = 'Delete';
81 | deleteCol.appendChild(deleteButton);
82 |
83 | element.appendChild(labelCol);
84 | element.appendChild(recordCol);
85 | element.appendChild(deleteCol);
86 |
87 | document.getElementById('samples-body').appendChild(element);
88 | registerRecordButton(recordButton);
89 | registerDeleteButton(deleteButton);
90 | }
91 |
92 | function showAudioInRow(row) {
93 | var id = row.getAttribute('label-id');
94 | var oldCol = row.getElementsByTagName('td')[1];
95 |
96 | var newCol = document.createElement('td');
97 | var audioTag = document.createElement('audio');
98 | audioTag.controls = true;
99 | audioTag.preload = 'none';
100 | var sourceTag = document.createElement('source');
101 | sourceTag.src = '/recording.wav?id=' + encodeURIComponent(id);
102 | sourceTag.type = 'audio/x-wav';
103 | audioTag.appendChild(sourceTag);
104 | newCol.appendChild(audioTag);
105 |
106 | row.insertBefore(newCol, oldCol);
107 | row.removeChild(oldCol);
108 | }
109 |
110 | function registerRecordButton(button) {
111 | var id = idForButton(button);
112 | button.addEventListener('click', function() {
113 | if (button.textContent === 'Done') {
114 | currentRecorder.stop();
115 | button.textContent = 'Record';
116 | return;
117 | }
118 | cancelRecording();
119 | button.textContent = 'Done';
120 | currentRecordButton = button;
121 | currentRecorder = new window.jswav.Recorder();
122 | currentRecorder.ondone = function(sound) {
123 | currentRecorder = null;
124 | currentRecordButton = null;
125 | button.textContent = 'Record';
126 | uploadRecording(id, sound, function(err, data) {
127 | if (err) {
128 | showError(err);
129 | } else {
130 | showAudioInRow(rowForButton(button));
131 | }
132 | });
133 | };
134 | currentRecorder.onerror = function(err) {
135 | button.textContent = 'Record';
136 | currentRecorder = null;
137 | currentRecordButton = null;
138 | showError(err);
139 | };
140 | currentRecorder.start();
141 | });
142 | }
143 |
144 | function registerDeleteButton(button) {
145 | var id = idForButton(button);
146 | button.addEventListener('click', function() {
147 | cancelRecording();
148 | var url = '/delete?id=' + encodeURIComponent(id);
149 | getURL(url, function(err) {
150 | if (err) {
151 | showError(err);
152 | } else {
153 | cancelRecording();
154 | var row = rowForButton(button);
155 | row.parentElement.removeChild(row);
156 | }
157 | });
158 | });
159 | }
160 |
161 | function rowForButton(button) {
162 | return button.parentElement.parentElement;
163 | }
164 |
165 | function idForButton(button) {
166 | return rowForButton(button).getAttribute('label-id');
167 | }
168 |
169 | function cancelRecording() {
170 | if (currentRecorder !== null) {
171 | currentRecorder.ondone = null;
172 | currentRecorder.onerror = null;
173 | currentRecorder.stop();
174 | currentRecorder = null;
175 | currentRecordButton.textContent = 'Record';
176 | currentRecordButton = null;
177 | }
178 | }
179 |
180 | function uploadRecording(id, sound, callback) {
181 | var xhr = new XMLHttpRequest();
182 | xhr.onreadystatechange = function() {
183 | if (xhr.readyState === AJAX_DONE) {
184 | if (xhr.status === HTTP_OK) {
185 | callback(null, xhr.responseText);
186 | } else {
187 | callback('Error '+xhr.status+': '+xhr.responseText, null);
188 | }
189 | }
190 | };
191 | xhr.open('POST', '/upload?id='+encodeURIComponent(id));
192 | xhr.send(sound.base64());
193 | }
194 |
195 | function getURL(reqURL, callback) {
196 | var xhr = new XMLHttpRequest();
197 | xhr.onreadystatechange = function() {
198 | if (xhr.readyState === AJAX_DONE) {
199 | if (xhr.status === HTTP_OK) {
200 | callback(null, xhr.responseText);
201 | } else {
202 | callback('Error '+xhr.status+': '+xhr.responseText, null);
203 | }
204 | }
205 | };
206 | xhr.open('GET', reqURL);
207 | xhr.send(null);
208 | }
209 |
210 | window.addEventListener('load', initialize);
211 |
212 | })();
213 |
--------------------------------------------------------------------------------
/ctc/ctc_test.go:
--------------------------------------------------------------------------------
1 | package ctc
2 |
3 | import (
4 | "math"
5 | "math/rand"
6 | "testing"
7 |
8 | "github.com/unixpickle/autofunc"
9 | "github.com/unixpickle/autofunc/functest"
10 | "github.com/unixpickle/num-analysis/linalg"
11 | )
12 |
13 | const (
14 | testSymbolCount = 5
15 | testPrecision = 1e-5
16 |
17 | benchLabelLen = 50
18 | benchSeqLen = 500
19 | benchSymbolCount = 30
20 | )
21 |
22 | var gradTestInputs = []*autofunc.Variable{
23 | &autofunc.Variable{Vector: []float64{-1.58522, -1.38379, -0.92827, -1.90226}},
24 | &autofunc.Variable{Vector: []float64{-2.87357, -2.75353, -1.11873, -0.59220}},
25 | &autofunc.Variable{Vector: []float64{-1.23140, -1.08975, -1.89920, -1.50451}},
26 | &autofunc.Variable{Vector: []float64{-1.44935, -1.51638, -1.59394, -1.07105}},
27 | &autofunc.Variable{Vector: []float64{-2.15367, -1.80056, -2.75221, -0.42320}},
28 | }
29 |
30 | var gradTestLabels = []int{2, 0, 1}
31 |
32 | type logLikelihoodTestFunc struct{}
33 |
34 | func (_ logLikelihoodTestFunc) Apply(in autofunc.Result) autofunc.Result {
35 | resVec := make([]autofunc.Result, len(gradTestInputs))
36 | for i, x := range gradTestInputs {
37 | resVec[i] = x
38 | }
39 | return LogLikelihood(resVec, gradTestLabels)
40 | }
41 |
42 | func (_ logLikelihoodTestFunc) ApplyR(rv autofunc.RVector, in autofunc.RResult) autofunc.RResult {
43 | resVec := make([]autofunc.RResult, len(gradTestInputs))
44 | for i, x := range gradTestInputs {
45 | resVec[i] = autofunc.NewRVariable(x, rv)
46 | }
47 | return LogLikelihoodR(resVec, gradTestLabels)
48 | }
49 |
50 | func TestLogLikelihoodOutputs(t *testing.T) {
51 | for i := 0; i < 11; i++ {
52 | labelLen := 5 + rand.Intn(5)
53 | if i == 10 {
54 | labelLen = 0
55 | }
56 | seqLen := labelLen + rand.Intn(5)
57 | label := make([]int, labelLen)
58 | for i := range label {
59 | label[i] = rand.Intn(testSymbolCount)
60 | }
61 | seq, resSeq, rresSeq := createTestSequence(seqLen, testSymbolCount)
62 | expected := exactLikelihood(seq, label, -1)
63 | actual := math.Exp(LogLikelihood(resSeq, label).Output()[0])
64 | rActual := math.Exp(LogLikelihoodR(rresSeq, label).Output()[0])
65 | if math.Abs(actual-expected)/math.Abs(expected) > testPrecision {
66 | t.Errorf("LogLikelihood gave log(%e) but expected log(%e)",
67 | actual, expected)
68 | }
69 | if math.Abs(rActual-expected)/math.Abs(expected) > testPrecision {
70 | t.Errorf("LogLikelihoodR gave log(%e) but expected log(%e)",
71 | rActual, expected)
72 | }
73 | }
74 | }
75 |
76 | func TestLoglikelihoodChecks(t *testing.T) {
77 | gradTestRVector := autofunc.RVector{}
78 |
79 | for _, in := range gradTestInputs {
80 | rVec := make(linalg.Vector, len(in.Vector))
81 | for i := range rVec {
82 | rVec[i] = rand.NormFloat64()
83 | }
84 | gradTestRVector[in] = rVec
85 | }
86 |
87 | test := functest.RFuncChecker{
88 | F: logLikelihoodTestFunc{},
89 | Vars: gradTestInputs,
90 | Input: gradTestInputs[0],
91 | RV: gradTestRVector,
92 | }
93 | test.FullCheck(t)
94 | }
95 |
96 | func TestLoglikelihoodRConsistency(t *testing.T) {
97 | label := make([]int, benchLabelLen)
98 | for i := range label {
99 | label[i] = rand.Intn(testSymbolCount)
100 | }
101 | _, resSeq, rresSeq := createTestSequence(benchSeqLen, benchSymbolCount)
102 |
103 | grad := autofunc.Gradient{}
104 | gradFromR := autofunc.Gradient{}
105 | for _, s := range resSeq {
106 | zeroVec := make(linalg.Vector, len(s.Output()))
107 | grad[s.(*autofunc.Variable)] = zeroVec
108 | zeroVec = make(linalg.Vector, len(s.Output()))
109 | gradFromR[s.(*autofunc.Variable)] = zeroVec
110 | }
111 |
112 | ll := LogLikelihood(resSeq, label)
113 | ll.PropagateGradient(linalg.Vector{1}, grad)
114 |
115 | llR := LogLikelihoodR(rresSeq, label)
116 | llR.PropagateRGradient(linalg.Vector{1}, linalg.Vector{0}, autofunc.RGradient{},
117 | gradFromR)
118 |
119 | for variable, gradVec := range grad {
120 | rgradVec := gradFromR[variable]
121 | for i, x := range gradVec {
122 | y := rgradVec[i]
123 | if math.IsNaN(x) || math.IsNaN(y) || math.Abs(x-y) > testPrecision {
124 | t.Errorf("grad value has %e but grad (R) has %e", x, y)
125 | }
126 | }
127 | }
128 | }
129 |
130 | func BenchmarkLogLikelihood(b *testing.B) {
131 | label := make([]int, benchLabelLen)
132 | for i := range label {
133 | label[i] = rand.Intn(testSymbolCount)
134 | }
135 | _, resSeq, _ := createTestSequence(benchSeqLen, benchSymbolCount)
136 |
137 | b.ResetTimer()
138 | for i := 0; i < b.N; i++ {
139 | LogLikelihood(resSeq, label)
140 | }
141 | }
142 |
143 | func BenchmarkLogLikelihoodGradient(b *testing.B) {
144 | label := make([]int, benchLabelLen)
145 | for i := range label {
146 | label[i] = rand.Intn(testSymbolCount)
147 | }
148 | _, resSeq, _ := createTestSequence(benchSeqLen, benchSymbolCount)
149 |
150 | grad := autofunc.Gradient{}
151 | for _, s := range resSeq {
152 | zeroVec := make(linalg.Vector, len(s.Output()))
153 | grad[s.(*autofunc.Variable)] = zeroVec
154 | }
155 |
156 | b.ResetTimer()
157 | for i := 0; i < b.N; i++ {
158 | ll := LogLikelihood(resSeq, label)
159 | ll.PropagateGradient(linalg.Vector{1}, grad)
160 | }
161 | }
162 |
163 | func BenchmarkLogLikelihoodR(b *testing.B) {
164 | label := make([]int, benchLabelLen)
165 | for i := range label {
166 | label[i] = rand.Intn(testSymbolCount)
167 | }
168 | _, _, rresSeq := createTestSequence(benchSeqLen, benchSymbolCount)
169 |
170 | b.ResetTimer()
171 | for i := 0; i < b.N; i++ {
172 | LogLikelihoodR(rresSeq, label)
173 | }
174 | }
175 |
176 | func BenchmarkLogLikelihoodRGradient(b *testing.B) {
177 | label := make([]int, benchLabelLen)
178 | for i := range label {
179 | label[i] = rand.Intn(testSymbolCount)
180 | }
181 | _, _, rresSeq := createTestSequence(benchSeqLen, benchSymbolCount)
182 |
183 | grad := autofunc.Gradient{}
184 | rgrad := autofunc.RGradient{}
185 | for _, s := range rresSeq {
186 | zeroVec := make(linalg.Vector, len(s.Output()))
187 | grad[s.(*autofunc.RVariable).Variable] = zeroVec
188 | zeroVec = make(linalg.Vector, len(s.Output()))
189 | rgrad[s.(*autofunc.RVariable).Variable] = zeroVec
190 | }
191 |
192 | b.ResetTimer()
193 | for i := 0; i < b.N; i++ {
194 | ll := LogLikelihoodR(rresSeq, label)
195 | ll.PropagateRGradient(linalg.Vector{1}, linalg.Vector{0}, rgrad, grad)
196 | }
197 | }
198 |
199 | func createTestSequence(seqLen, symCount int) (seq []linalg.Vector,
200 | res []autofunc.Result, rres []autofunc.RResult) {
201 | res = make([]autofunc.Result, seqLen)
202 | rres = make([]autofunc.RResult, seqLen)
203 | seq = make([]linalg.Vector, seqLen)
204 | for i := range seq {
205 | seq[i] = make(linalg.Vector, symCount+1)
206 | var probSum float64
207 | for j := range seq[i] {
208 | seq[i][j] = rand.Float64()
209 | probSum += seq[i][j]
210 | }
211 | for j := range seq[i] {
212 | seq[i][j] /= probSum
213 | }
214 | logVec := make(linalg.Vector, len(seq[i]))
215 | res[i] = &autofunc.Variable{
216 | Vector: logVec,
217 | }
218 | for j := range logVec {
219 | logVec[j] = math.Log(seq[i][j])
220 | }
221 | rres[i] = &autofunc.RVariable{
222 | Variable: res[i].(*autofunc.Variable),
223 | ROutputVec: make(linalg.Vector, len(logVec)),
224 | }
225 | }
226 | return
227 | }
228 |
229 | func exactLikelihood(seq []linalg.Vector, label []int, lastSymbol int) float64 {
230 | if len(seq) == 0 {
231 | if len(label) == 0 {
232 | return 1
233 | } else {
234 | return 0
235 | }
236 | }
237 |
238 | next := seq[0]
239 | blank := len(next) - 1
240 |
241 | var res float64
242 | res += next[blank] * exactLikelihood(seq[1:], label, -1)
243 | if lastSymbol >= 0 {
244 | res += next[lastSymbol] * exactLikelihood(seq[1:], label, lastSymbol)
245 | }
246 | if len(label) > 0 && label[0] != lastSymbol {
247 | res += next[label[0]] * exactLikelihood(seq[1:], label[1:], label[0])
248 | }
249 | return res
250 | }
251 |
--------------------------------------------------------------------------------
/recorder/assets/jswav.js:
--------------------------------------------------------------------------------
1 | // Code from https://github.com/unixpickle/jswav
2 | (function() {
3 |
4 | function Recorder() {
5 | this.ondone = null;
6 | this.onerror = null;
7 | this.onstart = null;
8 | this.channels = 2;
9 | this._started = false;
10 | this._stopped = false;
11 | this._stream = null;
12 | }
13 |
14 | Recorder.prototype.start = function() {
15 | if (this._started) {
16 | throw new Error('Recorder was already started.');
17 | }
18 | this._started = true;
19 | getUserMedia(function(err, stream) {
20 | if (this._stopped) {
21 | return;
22 | }
23 | if (err !== null) {
24 | if (this.onerror !== null) {
25 | this.onerror(err);
26 | }
27 | return;
28 | }
29 | addStopMethod(stream);
30 | this._stream = stream;
31 | try {
32 | this._handleStream();
33 | } catch (e) {
34 | this._stream.stop();
35 | this._stopped = true;
36 | if (this.onerror !== null) {
37 | this.onerror(e);
38 | }
39 | }
40 | }.bind(this));
41 | };
42 |
43 | Recorder.prototype.stop = function() {
44 | if (!this._started) {
45 | throw new Error('Recorder was not started.');
46 | }
47 | if (this._stopped) {
48 | return;
49 | }
50 | this._stopped = true;
51 | if (this._stream !== null) {
52 | var stream = this._stream;
53 | this._stream.stop();
54 | // Firefox does not fire the onended event.
55 | setTimeout(function() {
56 | if (stream.onended) {
57 | stream.onended();
58 | }
59 | }, 500);
60 | }
61 | };
62 |
63 | Recorder.prototype._handleStream = function() {
64 | var context = getAudioContext();
65 | var source = context.createMediaStreamSource(this._stream);
66 | var wavNode = new window.jswav.WavNode(context, this.channels);
67 | source.connect(wavNode.node);
68 | wavNode.node.connect(context.destination);
69 | this._stream.onended = function() {
70 | this._stream.onended = null;
71 | source.disconnect(wavNode.node);
72 | wavNode.node.disconnect(context.destination);
73 | if (this.ondone !== null) {
74 | this.ondone(wavNode.sound());
75 | }
76 | }.bind(this);
77 | if (this.onstart !== null) {
78 | this.onstart();
79 | }
80 | };
81 |
82 | function getUserMedia(cb) {
83 | var gum = (navigator.getUserMedia || navigator.webkitGetUserMedia ||
84 | navigator.mozGetUserMedia || navigator.msGetUserMedia);
85 | if (!gum) {
86 | setTimeout(function() {
87 | cb('getUserMedia() is not available.', null);
88 | }, 10);
89 | return;
90 | }
91 | gum.call(navigator, {audio: true, video: false},
92 | function(stream) {
93 | cb(null, stream);
94 | },
95 | function(err) {
96 | cb(err, null);
97 | }
98 | );
99 | }
100 |
101 | function addStopMethod(stream) {
102 | if ('undefined' === typeof stream.stop) {
103 | stream.stop = function() {
104 | var tracks = this.getTracks();
105 | for (var i = 0, len = tracks.length; i < len; ++i) {
106 | tracks[i].stop();
107 | }
108 | };
109 | }
110 | }
111 |
112 | var reusableAudioContext = null;
113 |
114 | function getAudioContext() {
115 | if (reusableAudioContext !== null) {
116 | return reusableAudioContext;
117 | }
118 | var AudioContext = (window.AudioContext || window.webkitAudioContext);
119 | reusableAudioContext = new AudioContext();
120 | return reusableAudioContext;
121 | }
122 |
123 | if (!window.jswav) {
124 | window.jswav = {};
125 | }
126 | window.jswav.Recorder = Recorder;
127 |
128 | })();
129 | (function() {
130 |
131 | function Header(view) {
132 | this.view = view;
133 | }
134 |
135 | Header.prototype.getBitsPerSample = function() {
136 | return this.view.getUint16(34, true);
137 | };
138 |
139 | Header.prototype.getChannels = function() {
140 | return this.view.getUint16(22, true);
141 | };
142 |
143 | Header.prototype.getDuration = function() {
144 | return this.getSampleCount() / this.getSampleRate();
145 | };
146 |
147 | Header.prototype.getSampleCount = function() {
148 | var bps = this.getBitsPerSample() * this.getChannels() / 8;
149 | return this.view.getUint32(40, true) / bps;
150 | };
151 |
152 | Header.prototype.getSampleRate = function() {
153 | return this.view.getUint32(24, true);
154 | };
155 |
156 | Header.prototype.setDefaults = function() {
157 | this.view.setUint32(0, 0x46464952, true); // RIFF
158 | this.view.setUint32(8, 0x45564157, true); // WAVE
159 | this.view.setUint32(12, 0x20746d66, true); // "fmt "
160 | this.view.setUint32(16, 0x10, true); // length of "fmt"
161 | this.view.setUint16(20, 1, true); // format = PCM
162 | this.view.setUint32(36, 0x61746164, true); // "data"
163 | };
164 |
165 | Header.prototype.setFields = function(count, rate, bitsPerSample, channels) {
166 | totalSize = count * (bitsPerSample / 8) * channels;
167 |
168 | this.view.setUint32(4, totalSize + 36, true); // size of "RIFF"
169 | this.view.setUint16(22, channels, true); // channel count
170 | this.view.setUint32(24, rate, true); // sample rate
171 | this.view.setUint32(28, rate * channels * bitsPerSample / 8,
172 | true); // byte rate
173 | this.view.setUint16(32, bitsPerSample * channels / 8, true); // block align
174 | this.view.setUint16(34, bitsPerSample, true); // bits per sample
175 | this.view.setUint32(40, totalSize, true); // size of "data"
176 | };
177 |
178 | function Sound(buffer) {
179 | this.buffer = buffer;
180 | this._view = new DataView(buffer);
181 | this.header = new Header(this._view);
182 | }
183 |
184 | Sound.fromBase64 = function(str) {
185 | var raw = window.atob(str);
186 | var buffer = new ArrayBuffer(raw.length);
187 | var bytes = new Uint8Array(buffer);
188 | for (var i = 0; i < raw.length; ++i) {
189 | bytes[i] = raw.charCodeAt(i);
190 | }
191 | return new Sound(buffer);
192 | };
193 |
194 | Sound.prototype.average = function(start, end) {
195 | var startIdx = this.indexForTime(start);
196 | var endIdx = this.indexForTime(end);
197 | if (endIdx-startIdx === 0) {
198 | return 0;
199 | }
200 | var sum = 0;
201 | var channels = this.header.getChannels();
202 | for (var i = startIdx; i < endIdx; ++i) {
203 | for (var j = 0; j < channels; ++j) {
204 | sum += Math.abs(this.getSample(i, j));
205 | }
206 | }
207 | return sum / (channels*(endIdx-startIdx));
208 | };
209 |
210 | Sound.prototype.base64 = function() {
211 | var binary = '';
212 | var bytes = new Uint8Array(this.buffer);
213 | for (var i = 0, len = bytes.length; i < len; ++i) {
214 | binary += String.fromCharCode(bytes[i]);
215 | }
216 | return window.btoa(binary);
217 | };
218 |
219 | Sound.prototype.crop = function(start, end) {
220 | var startIdx = this.indexForTime(start);
221 | var endIdx = this.indexForTime(end);
222 |
223 | // Figure out a bunch of math
224 | var channels = this.header.getChannels();
225 | var bps = this.header.getBitsPerSample();
226 | var copyCount = endIdx - startIdx;
227 | var blockSize = channels * bps / 8;
228 | var copyBytes = blockSize * copyCount;
229 |
230 | // Create a new buffer
231 | var buffer = new ArrayBuffer(copyBytes + 44);
232 | var view = new DataView(buffer);
233 |
234 | // Setup the header
235 | var header = new Header(view);
236 | header.setDefaults();
237 | header.setFields(copyCount, this.header.getSampleRate(), bps, channels);
238 |
239 | // Copy the sample data
240 | var bufferSource = startIdx*blockSize + 44;
241 | for (var i = 0; i < copyBytes; ++i) {
242 | view.setUint8(i+44, this._view.getUint8(bufferSource+i));
243 | }
244 |
245 | return new Sound(buffer);
246 | };
247 |
248 | Sound.prototype.getSample = function(idx, channel) {
249 | if ('undefined' === typeof channel) {
250 | // Default value of channel is 0.
251 | channel = 0;
252 | }
253 | var bps = this.header.getBitsPerSample();
254 | var channels = this.header.getChannels();
255 | if (bps === 8) {
256 | var offset = 44 + idx*channels + channel;
257 | return (this._view.getUint8(offset)-0x80) / 0x80;
258 | } else if (bps === 16) {
259 | var offset = 44 + idx*channels*2 + channel*2;
260 | return this._view.getInt16(offset, true) / 0x8000;
261 | } else {
262 | return NaN;
263 | }
264 | };
265 |
266 | Sound.prototype.histogram = function(num) {
267 | var duration = this.header.getDuration();
268 | var timeSlice = duration / num;
269 | var result = [];
270 | for (var i = 0; i < num; ++i) {
271 | result.push(this.average(i*timeSlice, (i+1)*timeSlice));
272 | }
273 | return result;
274 | };
275 |
276 | Sound.prototype.indexForTime = function(time) {
277 | var samples = this.header.getSampleCount();
278 | var duration = this.header.getDuration();
279 | var rawIdx = Math.floor(samples * time / duration);
280 | return Math.min(Math.max(rawIdx, 0), samples);
281 | };
282 |
283 | if (!window.jswav) {
284 | window.jswav = {};
285 | }
286 | window.jswav.Sound = Sound;
287 | window.jswav.Header = Header;
288 |
289 | })();
290 | (function() {
291 |
292 | function WavNode(context, ch) {
293 | this.node = null;
294 | this._buffers = [];
295 | this._sampleCount = 0;
296 | this._sampleRate = 0;
297 | this._channels = 0;
298 | if (context.createScriptProcessor) {
299 | this.node = context.createScriptProcessor(1024, ch, ch);
300 | } else if (context.createJavaScriptNode) {
301 | this.node = context.createJavaScriptNode(1024, ch, ch);
302 | } else {
303 | throw new Error('No javascript processing node available.');
304 | }
305 | this.node.onaudioprocess = function(event) {
306 | var input = event.inputBuffer;
307 | if (this._sampleRate === 0) {
308 | this._sampleRate = Math.round(input.sampleRate);
309 | }
310 | if (this._channels === 0) {
311 | this._channels = input.numberOfChannels;
312 | }
313 |
314 | // Interleave the audio data
315 | var sampleCount = input.length;
316 | this._sampleCount += sampleCount;
317 | var buffer = new ArrayBuffer(sampleCount * this._channels * 2);
318 | var view = new DataView(buffer);
319 | var x = 0;
320 | for (var i = 0; i < sampleCount; ++i) {
321 | for (var j = 0; j < this._channels; ++j) {
322 | var value = Math.round(input.getChannelData(j)[i] * 0x8000);
323 | view.setInt16(x, value, true);
324 | x += 2;
325 | }
326 | }
327 | this._buffers.push(buffer);
328 |
329 | // If I don't do this, the entire thing backs up after a few buffers.
330 | event.outputBuffer = event.inputBuffer;
331 | }.bind(this);
332 | }
333 |
334 | WavNode.prototype.sound = function() {
335 | // Setup the buffer
336 | var buffer = new ArrayBuffer(44 + this._sampleCount*this._channels*2);
337 | var view = new DataView(buffer);
338 |
339 | // Setup the header
340 | var header = new window.jswav.Header(view);
341 | header.setDefaults();
342 | header.setFields(this._sampleCount, this._sampleRate, 16, this._channels);
343 |
344 | // Copy the raw data
345 | var byteIdx = 44;
346 | for (var i = 0; i < this._buffers.length; ++i) {
347 | var aBuffer = this._buffers[i];
348 | var aView = new DataView(aBuffer);
349 | var len = aBuffer.byteLength;
350 | for (var j = 0; j < len; ++j) {
351 | view.setUint8(byteIdx++, aView.getUint8(j));
352 | }
353 | }
354 |
355 | return new window.jswav.Sound(buffer);
356 | };
357 |
358 | if (!window.jswav) {
359 | window.jswav = {};
360 | }
361 | window.jswav.WavNode = WavNode;
362 |
363 | })();
364 |
--------------------------------------------------------------------------------
/ctc/ctc.go:
--------------------------------------------------------------------------------
1 | // Package ctc implements Connectionist Temporal
2 | // Classification for training models (typically
3 | // neural networks) to predict output sequences.
4 | //
5 | // For more on CTC, check out the paper:
6 | // ftp://ftp.idsia.ch/pub/juergen/icml2006.pdf.
7 | package ctc
8 |
9 | import (
10 | "math"
11 |
12 | "github.com/unixpickle/autofunc"
13 | "github.com/unixpickle/num-analysis/linalg"
14 | )
15 |
16 | // LogLikelihood computes the log likelihood of the
17 | // label given an output sequence of the logs of
18 | // output probabilities.
19 | //
20 | // The last entry of each output vector is the log of
21 | // the probability of the blank symbol.
22 | //
23 | // Each element in the label is an index corresponding
24 | // to elements of the output vectors (e.g. a label 2
25 | // corresponds to whatever symbol is represented by
26 | // element 2 of the output vectors).
27 | //
28 | // The result is only valid so long as the label slice
29 | // is not changed by the caller.
30 | func LogLikelihood(seq []autofunc.Result, label []int) autofunc.Result {
31 | if len(seq) == 0 {
32 | if len(label) == 0 {
33 | return &autofunc.Variable{Vector: []float64{0}}
34 | } else {
35 | return &autofunc.Variable{Vector: []float64{math.Inf(-1)}}
36 | }
37 | }
38 |
39 | // positionProbs stores the log probabilities of
40 | // being at every position in the blank-infused
41 | // label, where blanks are injected at the start
42 | // and end of the label, and between entries.
43 | var positionProbs autofunc.Result
44 |
45 | initProbs := make(linalg.Vector, len(label)*2+1)
46 | initProbs[0] = 0
47 | for i := 1; i < len(initProbs); i++ {
48 | initProbs[i] = math.Inf(-1)
49 | }
50 | positionProbs = &autofunc.Variable{
51 | Vector: initProbs,
52 | }
53 |
54 | for _, inputRes := range seq {
55 | input := inputRes.Output()
56 | last := positionProbs.Output()
57 | newProbs := make(linalg.Vector, len(positionProbs.Output()))
58 | newProbs[0] = last[0] + input[len(input)-1]
59 | for i := 2; i < len(label)*2+1; i += 2 {
60 | newProbs[i] = addProbabilitiesFloat(last[i-1], last[i]) +
61 | input[len(input)-1]
62 | }
63 | for i := 1; i < len(label)*2+1; i += 2 {
64 | labelIdx := (i - 1) / 2
65 | positionSum := addProbabilitiesFloat(last[i], last[i-1])
66 | if labelIdx > 0 && label[labelIdx-1] != label[labelIdx] {
67 | positionSum = addProbabilitiesFloat(positionSum,
68 | last[i-2])
69 | }
70 | newProbs[i] = input[label[labelIdx]] + positionSum
71 | }
72 | positionProbs = &logLikelihoodStep{
73 | OutputVec: newProbs,
74 | LastProbs: positionProbs,
75 | SeqIn: inputRes,
76 | Label: label,
77 | }
78 | }
79 |
80 | // May occur if the label is empty.
81 | if len(positionProbs.Output()) == 1 {
82 | return vectorEntry(positionProbs, -1)
83 | }
84 |
85 | return addProbabilities(vectorEntry(positionProbs, -1), vectorEntry(positionProbs, -2))
86 | }
87 |
88 | // LogLikelihoodR is like LogLikelihood, but with
89 | // r-operator support.
90 | func LogLikelihoodR(seq []autofunc.RResult, label []int) autofunc.RResult {
91 | if len(seq) == 0 {
92 | if len(label) == 0 {
93 | return &autofunc.RVariable{
94 | Variable: &autofunc.Variable{Vector: []float64{0}},
95 | ROutputVec: []float64{0},
96 | }
97 | } else {
98 | return &autofunc.RVariable{
99 | Variable: &autofunc.Variable{Vector: []float64{0}},
100 | ROutputVec: []float64{math.Inf(-1)},
101 | }
102 | }
103 | }
104 |
105 | var positionProbs autofunc.RResult
106 |
107 | initProbs := make(linalg.Vector, len(label)*2+1)
108 | initProbs[0] = 0
109 | for i := 1; i < len(initProbs); i++ {
110 | initProbs[i] = math.Inf(-1)
111 | }
112 | positionProbs = &autofunc.RVariable{
113 | Variable: &autofunc.Variable{Vector: initProbs},
114 | ROutputVec: make(linalg.Vector, len(initProbs)),
115 | }
116 |
117 | for _, inputRes := range seq {
118 | input := inputRes.Output()
119 | last := positionProbs.Output()
120 | inputR := inputRes.ROutput()
121 | lastR := positionProbs.ROutput()
122 | newProbs := make(linalg.Vector, len(positionProbs.Output()))
123 | newProbsR := make(linalg.Vector, len(positionProbs.Output()))
124 | newProbs[0] = last[0] + input[len(input)-1]
125 | newProbsR[0] = lastR[0] + inputR[len(input)-1]
126 | for i := 2; i < len(label)*2+1; i += 2 {
127 | newProbs[i], newProbsR[i] = addProbabilitiesFloatR(last[i-1], lastR[i-1],
128 | last[i], lastR[i])
129 | newProbs[i] += input[len(input)-1]
130 | newProbsR[i] += inputR[len(input)-1]
131 | }
132 | for i := 1; i < len(label)*2+1; i += 2 {
133 | labelIdx := (i - 1) / 2
134 | posSum, posSumR := addProbabilitiesFloatR(last[i-1], lastR[i-1],
135 | last[i], lastR[i])
136 | if labelIdx > 0 && label[labelIdx-1] != label[labelIdx] {
137 | posSum, posSumR = addProbabilitiesFloatR(last[i-2], lastR[i-2],
138 | posSum, posSumR)
139 | }
140 | newProbs[i] = input[label[labelIdx]] + posSum
141 | newProbsR[i] = inputR[label[labelIdx]] + posSumR
142 | }
143 | positionProbs = &logLikelihoodRStep{
144 | OutputVec: newProbs,
145 | ROutputVec: newProbsR,
146 | LastProbs: positionProbs,
147 | SeqIn: inputRes,
148 | Label: label,
149 | }
150 | }
151 |
152 | // May occur if the label is empty.
153 | if len(positionProbs.Output()) == 1 {
154 | return vectorEntryR(positionProbs, -1)
155 | }
156 |
157 | return addProbabilitiesR(vectorEntryR(positionProbs, -1), vectorEntryR(positionProbs, -2))
158 | }
159 |
160 | type logLikelihoodStep struct {
161 | OutputVec linalg.Vector
162 | LastProbs autofunc.Result
163 | SeqIn autofunc.Result
164 | Label []int
165 | }
166 |
167 | func (l *logLikelihoodStep) Output() linalg.Vector {
168 | return l.OutputVec
169 | }
170 |
171 | func (l *logLikelihoodStep) Constant(g autofunc.Gradient) bool {
172 | return l.SeqIn.Constant(g) && l.LastProbs.Constant(g)
173 | }
174 |
175 | func (l *logLikelihoodStep) PropagateGradient(upstream linalg.Vector, g autofunc.Gradient) {
176 | if l.Constant(g) {
177 | return
178 | }
179 |
180 | last := l.LastProbs.Output()
181 | input := l.SeqIn.Output()
182 |
183 | lastGrad := make(linalg.Vector, len(last))
184 | inputGrad := make(linalg.Vector, len(input))
185 |
186 | lastGrad[0] = upstream[0]
187 | inputGrad[len(inputGrad)-1] = upstream[0]
188 |
189 | for i := 2; i < len(l.Label)*2+1; i += 2 {
190 | inputGrad[len(inputGrad)-1] += upstream[i]
191 | da, db := productSumPartials(last[i-1], last[i], upstream[i])
192 | lastGrad[i-1] += da
193 | lastGrad[i] += db
194 | }
195 | for i := 1; i < len(l.Label)*2+1; i += 2 {
196 | labelIdx := (i - 1) / 2
197 | inputGrad[l.Label[labelIdx]] += upstream[i]
198 | if labelIdx > 0 && l.Label[labelIdx-1] != l.Label[labelIdx] {
199 | a := addProbabilitiesFloat(last[i-2], last[i-1])
200 | b := last[i]
201 | da, db := productSumPartials(a, b, upstream[i])
202 | lastGrad[i] += db
203 | da, db = productSumPartials(last[i-2], last[i-1], da)
204 | lastGrad[i-2] += da
205 | lastGrad[i-1] += db
206 | } else {
207 | da, db := productSumPartials(last[i-1], last[i], upstream[i])
208 | lastGrad[i-1] += da
209 | lastGrad[i] += db
210 | }
211 | }
212 |
213 | if !l.LastProbs.Constant(g) {
214 | l.LastProbs.PropagateGradient(lastGrad, g)
215 | }
216 | if !l.SeqIn.Constant(g) {
217 | l.SeqIn.PropagateGradient(inputGrad, g)
218 | }
219 | }
220 |
221 | type logLikelihoodRStep struct {
222 | OutputVec linalg.Vector
223 | ROutputVec linalg.Vector
224 | LastProbs autofunc.RResult
225 | SeqIn autofunc.RResult
226 | Label []int
227 | }
228 |
229 | func (l *logLikelihoodRStep) Output() linalg.Vector {
230 | return l.OutputVec
231 | }
232 |
233 | func (l *logLikelihoodRStep) ROutput() linalg.Vector {
234 | return l.ROutputVec
235 | }
236 |
237 | func (l *logLikelihoodRStep) Constant(rg autofunc.RGradient, g autofunc.Gradient) bool {
238 | return l.SeqIn.Constant(rg, g) && l.LastProbs.Constant(rg, g)
239 | }
240 |
241 | func (l *logLikelihoodRStep) PropagateRGradient(upstream, upstreamR linalg.Vector,
242 | rg autofunc.RGradient, g autofunc.Gradient) {
243 | if l.Constant(rg, g) {
244 | return
245 | }
246 |
247 | last := l.LastProbs.Output()
248 | lastR := l.LastProbs.ROutput()
249 | input := l.SeqIn.Output()
250 |
251 | lastGrad := make(linalg.Vector, len(last))
252 | lastGradR := make(linalg.Vector, len(last))
253 | inputGrad := make(linalg.Vector, len(input))
254 | inputGradR := make(linalg.Vector, len(input))
255 |
256 | lastGrad[0] = upstream[0]
257 | lastGradR[0] = upstreamR[0]
258 | inputGrad[len(inputGrad)-1] = upstream[0]
259 | inputGradR[len(inputGrad)-1] = upstreamR[0]
260 |
261 | for i := 2; i < len(l.Label)*2+1; i += 2 {
262 | inputGrad[len(inputGrad)-1] += upstream[i]
263 | inputGradR[len(inputGrad)-1] += upstreamR[i]
264 | da, daR, db, dbR := productSumPartialsR(last[i-1], lastR[i-1], last[i], lastR[i],
265 | upstream[i], upstreamR[i])
266 | lastGrad[i-1] += da
267 | lastGrad[i] += db
268 | lastGradR[i-1] += daR
269 | lastGradR[i] += dbR
270 | }
271 | for i := 1; i < len(l.Label)*2+1; i += 2 {
272 | labelIdx := (i - 1) / 2
273 | inputGrad[l.Label[labelIdx]] += upstream[i]
274 | inputGradR[l.Label[labelIdx]] += upstreamR[i]
275 | if labelIdx > 0 && l.Label[labelIdx-1] != l.Label[labelIdx] {
276 | a, aR := addProbabilitiesFloatR(last[i-2], lastR[i-2], last[i-1], lastR[i-1])
277 | b, bR := last[i], lastR[i]
278 | da, daR, db, dbR := productSumPartialsR(a, aR, b, bR, upstream[i], upstreamR[i])
279 | lastGrad[i] += db
280 | lastGradR[i] += dbR
281 | da, daR, db, dbR = productSumPartialsR(last[i-2], lastR[i-2], last[i-1],
282 | lastR[i-1], da, daR)
283 | lastGrad[i-2] += da
284 | lastGrad[i-1] += db
285 | lastGradR[i-2] += daR
286 | lastGradR[i-1] += dbR
287 | } else {
288 | da, daR, db, dbR := productSumPartialsR(last[i-1], lastR[i-1], last[i],
289 | lastR[i], upstream[i], upstreamR[i])
290 | lastGrad[i-1] += da
291 | lastGradR[i-1] += daR
292 | lastGrad[i] += db
293 | lastGradR[i] += dbR
294 | }
295 | }
296 |
297 | if !l.LastProbs.Constant(rg, g) {
298 | l.LastProbs.PropagateRGradient(lastGrad, lastGradR, rg, g)
299 | }
300 | if !l.SeqIn.Constant(rg, g) {
301 | l.SeqIn.PropagateRGradient(inputGrad, inputGradR, rg, g)
302 | }
303 | }
304 |
305 | func productSumPartials(a, b, upstream float64) (da, db float64) {
306 | if math.IsInf(a, -1) && math.IsInf(b, -1) {
307 | return
308 | }
309 | denomLog := addProbabilitiesFloat(a, b)
310 | daLog := a - denomLog
311 | dbLog := b - denomLog
312 | da = upstream * math.Exp(daLog)
313 | db = upstream * math.Exp(dbLog)
314 | return
315 | }
316 |
317 | func productSumPartialsR(a, aR, b, bR, upstream, upstreamR float64) (da, daR, db, dbR float64) {
318 | if math.IsInf(a, -1) && math.IsInf(b, -1) {
319 | return
320 | }
321 | denomLog, denomLogR := addProbabilitiesFloatR(a, aR, b, bR)
322 | daExp := math.Exp(a - denomLog)
323 | dbExp := math.Exp(b - denomLog)
324 | da = upstream * daExp
325 | db = upstream * dbExp
326 | daR = upstreamR*daExp + upstream*daExp*(aR-denomLogR)
327 | dbR = upstreamR*dbExp + upstream*dbExp*(bR-denomLogR)
328 | return
329 | }
330 |
331 | // vectorEntry returns a Result for the i-th entry in
332 | // an autofunc.Result.
333 | // If i is negative, then the length of the vector is
334 | // added to it.
335 | func vectorEntry(vec autofunc.Result, i int) autofunc.Result {
336 | if i < 0 {
337 | i += len(vec.Output())
338 | }
339 | return autofunc.Slice(vec, i, i+1)
340 | }
341 |
342 | func vectorEntryR(vec autofunc.RResult, i int) autofunc.RResult {
343 | if i < 0 {
344 | i += len(vec.Output())
345 | }
346 | return autofunc.SliceR(vec, i, i+1)
347 | }
348 |
349 | // addProbabilities adds two probabilities given their
350 | // logarithms and returns the new log probability.
351 | func addProbabilities(p1, p2 autofunc.Result) autofunc.Result {
352 | if math.IsInf(p1.Output()[0], -1) {
353 | return p2
354 | } else if math.IsInf(p2.Output()[0], -1) {
355 | return p1
356 | }
357 | normalizer := math.Max(p1.Output()[0], p2.Output()[0])
358 | offset1 := autofunc.AddScaler(p1, -normalizer)
359 | offset2 := autofunc.AddScaler(p2, -normalizer)
360 | exp := autofunc.Exp{}
361 | exp1 := exp.Apply(offset1)
362 | exp2 := exp.Apply(offset2)
363 | sumLog := autofunc.Log{}.Apply(autofunc.Add(exp1, exp2))
364 | return autofunc.AddScaler(sumLog, normalizer)
365 | }
366 |
367 | func addProbabilitiesR(p1, p2 autofunc.RResult) autofunc.RResult {
368 | if math.IsInf(p1.Output()[0], -1) {
369 | return p2
370 | } else if math.IsInf(p2.Output()[0], -1) {
371 | return p1
372 | }
373 | normalizer := math.Max(p1.Output()[0], p2.Output()[0])
374 | offset1 := autofunc.AddScalerR(p1, -normalizer)
375 | offset2 := autofunc.AddScalerR(p2, -normalizer)
376 | exp := autofunc.Exp{}
377 | exp1 := exp.ApplyR(autofunc.RVector{}, offset1)
378 | exp2 := exp.ApplyR(autofunc.RVector{}, offset2)
379 | sumLog := autofunc.Log{}.ApplyR(autofunc.RVector{}, autofunc.AddR(exp1, exp2))
380 | return autofunc.AddScalerR(sumLog, normalizer)
381 | }
382 |
383 | func addProbabilitiesFloat(a, b float64) float64 {
384 | if math.IsInf(a, -1) {
385 | return b
386 | } else if math.IsInf(b, -1) {
387 | return a
388 | }
389 | normalizer := math.Max(a, b)
390 | exp1 := math.Exp(a - normalizer)
391 | exp2 := math.Exp(b - normalizer)
392 | return math.Log(exp1+exp2) + normalizer
393 | }
394 |
395 | func addProbabilitiesFloatR(a, aR, b, bR float64) (res, resR float64) {
396 | if math.IsInf(a, -1) {
397 | return b, bR
398 | } else if math.IsInf(b, -1) {
399 | return a, aR
400 | }
401 | normalizer := math.Max(a, b)
402 | exp1 := math.Exp(a - normalizer)
403 | exp2 := math.Exp(b - normalizer)
404 | res = math.Log(exp1+exp2) + normalizer
405 | resR = (exp1*aR + exp2*bR) / (exp1 + exp2)
406 | return
407 | }
408 |
--------------------------------------------------------------------------------
/recorder/bindata.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "fmt"
7 | "io"
8 | "strings"
9 | "os"
10 | "time"
11 | "io/ioutil"
12 | "path"
13 | "path/filepath"
14 | )
15 |
16 | func bindata_read(data []byte, name string) ([]byte, error) {
17 | gz, err := gzip.NewReader(bytes.NewBuffer(data))
18 | if err != nil {
19 | return nil, fmt.Errorf("Read %q: %v", name, err)
20 | }
21 |
22 | var buf bytes.Buffer
23 | _, err = io.Copy(&buf, gz)
24 | gz.Close()
25 |
26 | if err != nil {
27 | return nil, fmt.Errorf("Read %q: %v", name, err)
28 | }
29 |
30 | return buf.Bytes(), nil
31 | }
32 |
33 | type asset struct {
34 | bytes []byte
35 | info os.FileInfo
36 | }
37 |
38 | type bindata_file_info struct {
39 | name string
40 | size int64
41 | mode os.FileMode
42 | modTime time.Time
43 | }
44 |
45 | func (fi bindata_file_info) Name() string {
46 | return fi.name
47 | }
48 | func (fi bindata_file_info) Size() int64 {
49 | return fi.size
50 | }
51 | func (fi bindata_file_info) Mode() os.FileMode {
52 | return fi.mode
53 | }
54 | func (fi bindata_file_info) ModTime() time.Time {
55 | return fi.modTime
56 | }
57 | func (fi bindata_file_info) IsDir() bool {
58 | return false
59 | }
60 | func (fi bindata_file_info) Sys() interface{} {
61 | return nil
62 | }
63 |
64 | var _assets_index_html = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x94\x54\x4d\x8f\xdb\x20\x10\xbd\xef\xaf\x98\x72\xb7\xe9\xb1\xaa\x70\xaa\x4a\xab\x4a\x95\x7a\x6a\x4f\x3d\x12\x98\x5d\xb3\x25\x60\xc1\x78\x3f\x64\xf9\xbf\x77\x30\x76\xd6\x51\x2a\x45\x3d\xc5\xc3\x9b\x8f\xf7\x98\x47\xd4\x07\x1b\x0d\xbd\x0d\x08\x3d\x9d\xfc\xe1\x4e\xd5\x1f\x00\xd5\xa3\xb6\xe5\x83\x3f\x4f\x48\x1a\x4c\xaf\x53\x46\xea\xc4\x48\x0f\xcd\x27\xb1\x42\xe4\xc8\xe3\xe1\x27\x9a\x98\x2c\x26\x25\x6b\x7c\x57\x41\xef\xc2\x1f\x48\xe8\x3b\x91\xe9\xcd\x63\xee\x11\x49\x40\x99\xd6\x09\xc2\x57\x92\x26\x67\x01\x7d\xc2\x87\x4e\xc8\x25\xa5\x2d\x27\x6b\xeb\x6c\x92\x1b\x08\x72\x32\x8c\x3e\xe5\x17\xfd\xdc\x3e\x31\xa8\x64\x05\xfe\x95\x55\x83\xab\x34\x25\x37\x31\xea\x18\xed\xdb\x46\x5d\x1f\x3d\x82\xb3\xcc\x4e\x9f\x06\xa6\x27\xc0\xa0\xf7\x79\xd0\xc6\x85\xc7\x4e\x7c\xac\xf1\xa0\xad\x5d\xe3\x5a\x58\x4a\x4b\x9b\x7d\x69\x53\x0e\xce\x38\xc0\x34\x25\x1d\x1e\x11\xda\x5f\x15\x9f\xe7\x33\xa4\x28\x81\xd7\x47\xf4\x4d\xa9\x9f\xa6\xf6\xfb\xfd\x3c\xef\x4a\x4b\x86\x3d\x70\x87\x06\xda\x1f\x25\x0f\x9a\x79\x66\x09\x64\xf7\x39\xd3\xe4\x1e\xa0\xfd\xe6\x3c\xee\x5a\xd7\xd2\x5d\xc8\x07\x7a\xb4\x2e\x82\x89\x81\x52\xf4\x19\x06\x5e\x47\xd4\x3c\x38\xc4\x80\xe2\x32\xb7\xdc\x66\x1c\x93\xc1\xf5\x36\xd3\xb2\x55\xd6\xde\xf2\xdd\x7f\x61\xb6\x1b\xd9\x75\x85\x4b\x6b\xf9\xda\x30\x7a\xd5\xe9\x37\x37\x82\x63\x8a\x2f\x19\x13\xd8\x88\x19\x42\xe4\x2d\x8d\xc3\x10\x13\x01\xf5\x08\x95\x18\x7a\x3c\x61\xa0\xf6\x92\xb4\x5c\xc0\x8b\x3b\xb9\xd2\x8f\x3e\xdf\xd2\x7e\x1c\x89\x62\x00\xe3\x75\xce\x9d\xa8\x72\x9a\x7a\x28\x56\xcf\x2a\x59\xe3\x1b\xb3\x82\xfd\xaf\x51\x96\x65\x11\x9e\x47\xdd\x2f\xe1\xed\x51\x1c\xa5\xbd\x87\xf6\x63\x19\xdb\x79\x57\x2e\xe6\x5d\x03\xeb\x9e\x17\x2f\xb2\x51\x9b\xb2\x68\xed\x02\xa6\x77\xb3\x2e\x66\x3b\x7c\xb5\xb6\xda\xee\xb3\x92\xf5\x64\xc3\x5d\x18\x46\xba\x68\xc0\x0b\x79\x2f\x5f\x95\x6d\xf8\xa6\x89\xfb\x5d\x0a\x52\x92\x79\xd4\xd7\x56\x89\xf2\xab\x5b\xfe\x4b\xfe\x06\x00\x00\xff\xff\x75\x0b\x09\x6b\x63\x04\x00\x00")
65 |
66 | func assets_index_html_bytes() ([]byte, error) {
67 | return bindata_read(
68 | _assets_index_html,
69 | "assets/index.html",
70 | )
71 | }
72 |
73 | func assets_index_html() (*asset, error) {
74 | bytes, err := assets_index_html_bytes()
75 | if err != nil {
76 | return nil, err
77 | }
78 |
79 | info := bindata_file_info{name: "assets/index.html", size: 1123, mode: os.FileMode(420), modTime: time.Unix(1469122891, 0)}
80 | a := &asset{bytes: bytes, info: info}
81 | return a, nil
82 | }
83 |
84 | var _assets_jswav_js = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xbc\x1a\x5d\x73\xdb\x36\xf2\x3d\xbf\x02\xc9\x8b\x29\x99\xa1\x25\x45\x51\x7d\xd5\xf9\x21\x71\x9a\x36\x37\x97\x5c\x26\xea\x35\x0f\x9d\x4e\x07\x22\x21\x8b\x0e\x45\x6a\x40\xc8\x96\x9b\xea\xbf\xdf\xe2\x7b\xc1\x0f\xd9\x71\xe7\x9a\xce\x58\x12\xb0\xbb\xd8\xef\x0f\xa0\x67\x67\xe4\xb2\xca\x18\x59\xf1\x6a\x43\xd6\x42\x6c\xeb\xef\xcf\xce\xae\x72\xb1\xde\x2d\x93\xb4\xda\x9c\xed\xca\x7c\xbf\xcd\xd3\x2f\x05\x3b\xbb\xae\x6f\xe9\xcd\x93\x68\xb5\x2b\x53\x91\x57\x65\x34\x20\x5f\x9f\x3c\x21\xc4\xfe\x26\x9f\x58\x5a\xf1\x8c\x71\xb5\x41\xe0\x9f\x58\xe7\x75\x52\x95\x59\x55\x32\x72\x41\xca\x5d\x51\xcc\xf1\x3a\xe3\xbc\xe2\x5d\x1b\xb5\xa0\x5c\xb4\x37\xd2\x35\x2d\x4b\x56\xd4\xb0\x33\x41\xcb\xbf\x2b\x70\x96\xc1\xf2\x8a\x16\x35\x0b\xb7\xaa\xed\xb6\x6f\x8b\x33\xba\x41\xa7\x1c\xa4\x30\x56\x86\x64\xcb\x2b\x51\x89\xbb\x2d\x4b\x2c\x37\x81\xdc\x92\x4e\xbe\x22\x51\xc0\x81\xdd\x90\x47\xf0\xea\x96\x94\xec\x96\xfc\x20\x85\x8c\x4e\x2c\x5d\x72\x4b\x6b\x42\x0b\x38\x3a\xbb\x23\x06\x2d\x39\x19\x68\xc6\x0e\x9d\x42\x09\xbe\x33\x8c\x5f\x31\xf1\xdf\x9a\xf1\xf7\x2c\xcb\xa9\x37\x03\xa8\x31\x26\x5a\x1a\xcf\x00\xe6\x4d\xa9\xc0\x6f\x11\xc2\x99\xd8\xf1\x72\x6e\x7e\x1f\x10\x0a\xd0\x22\x4f\x2f\xb4\x4e\x30\x86\x23\x67\xad\xd6\x05\x14\x1a\x56\xd2\x1a\xcc\xdd\xe6\xe1\x9e\xd3\x69\x96\x2d\x80\xd3\xf7\x4c\xac\xab\x2c\x32\xe2\xcc\x9f\x20\xba\xde\x62\xfa\x8b\xdb\xe4\x77\x88\x09\x0d\x0a\x9e\x92\x15\x6c\xa1\xe0\x22\x47\xe6\x40\x52\x2a\xd2\x35\x88\x39\x68\x61\x68\x9a\x89\xd4\x56\x84\xd8\x6e\xba\x91\x37\xc6\xa3\xd5\xd2\xa1\x14\xfd\x79\x48\x96\x79\x99\x29\x82\x03\x05\x74\x98\xf7\xba\x64\xb5\xed\xf3\xc8\xa7\x8f\x72\xc9\xb2\x12\x3d\xee\x78\xc4\x93\xb0\x25\x0f\x9d\x51\xe7\xd5\x85\xc9\x28\x33\xb6\x55\x75\x43\x39\x71\x36\xc6\xb0\x5d\x6e\xd0\xb0\x14\xa4\xb1\xb7\x39\x67\xab\x6a\x4f\xb2\x8a\x69\x79\x56\xb0\x00\x38\x8c\x80\xea\xcb\x0c\xf8\x61\x37\xac\x14\x89\xc1\xa8\x99\xf8\x39\xdf\xb0\x6a\x27\xa2\x96\x1e\x2d\xc3\xe6\x28\x83\x1f\x1a\x35\xdc\x8b\xba\xac\x1a\x93\x97\xa3\x11\x52\x66\xaf\x3d\x03\x7f\xed\x32\xac\x54\x4d\x5a\x95\x82\xed\x65\x26\x82\x3c\xf0\x6a\x97\xe5\xd5\xa5\x5e\xb1\x67\x2b\xfd\x55\x3b\x9e\xca\x6c\x6b\xa0\x93\x14\x48\x0a\xa6\x52\x86\x26\xbf\x50\x10\x81\x2d\x10\x3e\x64\xf8\x0f\xb2\x1c\x5c\x28\x4f\xb9\x05\x7f\xac\x6e\x13\x95\xf8\x93\xcf\x7a\x2b\x32\x94\xe3\x30\x29\x1b\x1a\xfa\x7c\xa8\x1c\xb0\x98\x8a\xc8\x90\x4b\x4a\xf8\x63\x20\xf0\x92\x83\xb3\xdc\x66\xac\x16\x79\x49\xa5\xec\x83\x76\xb2\xb6\xca\xee\xd2\x50\x2f\xa4\x2f\x22\x8e\xbb\x2c\xaf\x8f\x30\xd8\x60\x11\x01\xf7\x73\x19\xa4\x02\x55\xef\xba\x32\x01\xda\x77\x07\x03\x4b\x10\xf3\x83\xc1\xbc\x37\x15\x34\xe2\xc7\x96\xc7\xf6\x01\x78\x3b\x6a\xb9\x9d\x2b\xd3\x41\x15\x49\x97\xd8\xc5\xae\x76\xd2\xfb\xa2\x92\xde\xe4\x57\x54\x54\x3c\xc1\xb0\xe4\xcf\x3f\x89\xdf\xb9\x65\xcb\x2f\xb9\xf8\x31\xdc\x37\x8c\x78\xa8\x4d\xf5\xc7\x8f\xbd\x24\x36\x35\xde\x43\x72\x3e\x05\x3e\xbc\x58\xf7\x05\x6a\xba\x8c\x4e\x02\x99\x06\x24\xd7\x09\x80\xde\xd0\xbc\xa0\xcb\x82\x25\x27\xb1\xd6\xd5\xdc\x87\xe6\x78\xe4\x7e\xb5\xf3\x18\x30\x90\xa4\xb4\x28\xbc\x2a\x62\xf2\x95\xca\x98\xfb\x5e\xe5\xb4\x98\xdc\xe4\x19\x83\x1f\xaa\xad\x38\xc4\x86\x90\xe3\xaf\x59\x8e\x15\x97\x92\x03\x57\xa9\x3d\x27\x4d\x5c\x59\x37\x43\x44\x55\xe1\x43\xfe\xd5\xe7\xc0\xb5\x2d\xce\xb8\x9d\x65\x14\x55\x87\x13\xf0\x36\xb6\xca\x4b\x96\x9d\x90\x0b\xf0\x20\x99\x7e\xaa\x15\x41\x39\x15\x29\xde\x2f\x76\x47\x9c\x76\x1a\xc1\x69\xfa\xa5\xb6\x29\x1b\x2c\xf1\xb3\x5a\xc0\x19\x71\x05\xa5\x31\x92\xb0\x39\x80\x8d\x62\x52\xb0\x52\x15\x07\x09\x97\xc0\x8f\x2b\xb1\x9e\xc3\xde\x3f\xe5\xc6\x9c\x9c\x9e\xe6\x8d\xfa\xa9\x00\x7f\xcd\x7f\x6b\x95\x67\x97\x6a\x91\xb7\x4b\x7d\xc8\xb3\x38\xdb\xd5\xd2\xf8\x38\x55\xba\x7c\xd0\x08\x88\x30\x9d\x22\x7d\x75\x12\x69\x87\x9e\xf6\xa0\xce\x23\xb1\x5b\x49\xb6\x1a\xec\x44\x26\xc7\x06\xcb\x10\x26\x66\x59\x87\x19\xde\x34\xd2\xf7\x49\x07\x59\xbb\xab\x36\xdc\xc3\xa0\x52\x9a\x0a\x3d\x9c\xf2\xad\x7c\x78\x0d\xce\xf8\x7a\xd0\x28\xe1\x46\xe2\xfa\x89\x0b\x57\xe2\x40\xcf\x87\x81\x64\xa1\x7f\x68\xf8\x09\x3a\x61\x18\x19\x6e\x72\x76\x1b\x8c\x0d\x72\x01\x28\xc9\x0f\xc7\xa0\x86\x45\x75\x13\x2c\xf7\x3a\x17\xf5\x47\xc6\x17\x74\xb3\x2d\x58\x97\x9b\x1a\xc9\x1d\x51\x95\xd3\xf2\x52\x8c\x67\xd1\x8b\x69\xac\xa2\x19\x35\x5b\x5d\x47\x5c\xfa\xb9\xe3\x5b\xa8\x4f\x26\x0f\xa2\xfe\x66\xc7\x55\x25\xb9\x8f\x3a\x80\x6a\x21\x2f\xa1\x62\x48\x27\x3d\x6b\xac\x7f\x82\x3a\x1f\xdd\x73\x18\xa2\xd0\xd7\x66\x2c\xb7\x38\x96\x03\xf5\x02\xdc\xd0\xed\x58\xad\x28\x4e\xce\xe7\x47\xb5\xf1\x62\x12\x4d\x47\x46\x1b\x00\x0d\x47\x3c\x84\x4d\x29\xd0\xb7\xe8\x1c\x4e\x99\x3c\xc0\xa2\x50\x4f\xde\xb0\x15\xdd\x15\xa2\xd3\xa2\x9e\x6c\xed\xc8\x02\xef\xa3\xfd\x74\x06\xff\xfd\xe3\xa5\xb3\xaa\xec\x39\x3f\xbd\x7b\xfb\xb6\x17\xeb\x5c\x61\xbd\x7c\x39\x9b\x8e\x5f\x7e\x87\xb1\x3e\xbf\xfa\xe5\x87\x5e\xac\xf1\x44\xa2\x4d\x46\xdf\x4d\x67\xd9\x6c\x86\xd1\x9e\xad\x36\x82\x3c\xeb\x47\x9c\x49\xc4\xf1\x08\xa3\xe8\xdc\x4a\x20\xc3\x4b\xe4\x1e\x5c\xe9\xab\x80\x35\xc6\x88\x90\xb1\x37\x54\xfa\xc8\xc7\xcb\xf7\xbd\x27\xbe\x50\x27\xce\xc6\xc0\xea\x78\x36\x0d\x58\xcd\xa8\xa0\xcf\x8e\xda\xe0\x6d\xce\x8a\x2c\xb0\x40\x2a\xdd\x32\x26\x10\x0e\x50\x5f\x97\xd8\xf1\x62\xe2\xda\x4c\x6b\xa5\x4a\xd0\x62\x91\xff\xa1\x5b\x5d\xe9\xce\x43\x12\x05\x38\xd2\x2b\xa5\xbf\x5a\x4c\xc5\x48\xb7\x1c\x92\x75\x47\xef\x94\xbc\x08\xb4\x5e\xcb\x45\xa9\x40\x69\xeb\x23\x1a\x9c\x78\x26\x31\xba\x59\xd3\x4c\xf6\x72\x20\xfd\x56\xcb\x8d\x0e\xd6\x62\xc8\xe5\x7e\xbc\x73\x8d\x87\xe4\x84\xaf\x2d\x3d\xc4\x6e\x54\xb6\xc4\x97\x77\xe2\x18\x69\x99\x1c\x27\x0d\x23\xe0\x33\x24\x4d\x4c\xad\xa8\xd2\x2f\x84\x16\xf9\x55\xd9\x4f\x6f\xda\x32\x2a\x22\x00\x1b\x64\x0b\xb5\x43\x0b\xdd\x6f\xa9\x11\x32\x55\xa7\x95\x42\xc7\x73\x65\x66\xa1\xda\xec\xe5\x6e\xb5\x62\x3c\x88\x74\xbd\x04\x5e\xa4\xbf\xe0\xa1\xc3\x94\x20\x59\x52\xdf\x00\xd5\x5f\xe0\xa7\xa5\x80\xc0\xd6\xca\xb7\x0d\x9c\xa9\x66\x1e\xdf\xb7\x68\x8a\x83\x44\x5e\xb3\xbd\xa6\x35\x9b\x4d\xb1\xe7\x43\xa7\x85\x53\x30\xa7\xf2\x5c\x53\x5c\xa1\xf9\x5c\x2a\x00\x3f\xa4\x39\x9e\x55\xb5\xe7\x9c\xde\xbd\x56\x2b\x11\x20\x9a\x76\x0a\x43\x83\xa9\x6b\x03\x2c\xd5\x78\xae\x30\x42\x49\xc2\x06\x4d\xf7\x62\x9e\x58\xa3\x25\x53\x04\xa1\x1b\x03\x50\x09\x03\x4e\xc1\xe5\x15\xe2\x2b\x11\xe5\xc1\xcd\x81\xc9\xd3\xf2\xdc\x40\xfd\x3e\x37\x6b\x9d\xf8\xb4\x40\x6f\x18\xa7\x57\x2c\x54\x0d\x4c\x33\x31\x81\x69\x0e\x6b\x48\xad\xbe\xcb\xf6\xb6\x52\x81\xaa\xd8\xfe\x6d\xc5\xe5\xa0\xa0\x51\x90\x02\x00\xb7\x0f\x54\x92\xf5\x73\x87\x06\x7c\xee\x89\x43\x9f\x37\x6a\x35\x79\xa3\x66\x47\x57\xab\xa9\x69\xe4\x0f\x44\x17\x95\xc8\x45\xc2\xa2\xd9\xa1\x77\x7b\xae\x56\xbf\x66\xa6\xa1\x7a\x07\x7f\xad\xed\x74\x0d\x80\x2e\xc1\x01\xe8\x35\x6e\x9c\x25\x5f\xa7\x17\xe4\x3d\x15\xeb\x84\x2e\xeb\x28\x6c\x17\xa2\x3c\x26\xd7\xad\xb9\x13\x1b\x4e\xe2\x9f\x91\xc8\xd2\x1f\x36\xf5\x33\x38\x62\xc9\x65\xcb\xc7\x83\x1e\x03\xa6\x67\x7e\x07\xbb\x27\x27\xf7\xfa\x29\x8a\xd2\x4e\x67\xb5\xd3\x84\x42\xbf\x67\x98\x30\xe7\x82\x52\x16\x82\xe7\xe5\x95\x0a\xc7\x4b\xe3\xbf\x91\xf5\xeb\x2e\x27\x36\xc1\xb8\x14\x15\x8d\x34\x95\x23\xc2\xa7\x3c\x9c\x98\xfe\x0e\x1f\x56\x90\xea\x16\xec\x6a\xc7\x21\x15\xee\x60\xfc\x85\x44\x51\xa6\xaa\xfa\x43\x2d\x5f\x3f\xc2\x3d\x1b\xfd\xa0\x07\x6c\xb4\x85\xc8\xf5\xab\xed\x9d\x6d\x2f\x0d\xcf\xcf\xbd\x63\x7b\xa2\xb2\x60\xd8\xf2\x8d\x0a\xd7\xb6\xf6\xcd\xa4\x25\xf7\xda\xf8\x85\xc7\x19\xfa\x63\xbc\xe0\x97\xea\xa2\x0b\x84\x96\xfe\xa3\xfd\xe5\xfe\x84\xe9\xe9\x9f\x92\xe9\x14\xc9\x71\x34\xf7\xdb\x23\x17\xe0\x1b\x5b\x75\xc3\xa8\x35\xe3\xb0\xbb\x6a\x82\xab\x06\xc4\x6c\xe3\x46\x34\x6a\xed\xe8\xf6\x28\x72\x82\xc6\x4d\x13\xe0\xa6\x3f\x96\x9a\x43\x4d\x12\xd2\x0a\xe0\x2b\x0e\x4d\x37\x21\xcb\x63\x43\x2d\x0b\x7b\x65\x68\xcd\x34\xf4\x9a\x96\x5a\xe9\xaf\x10\x4e\x7b\x8d\x30\xc3\x45\xfb\x3c\xca\x4f\xa7\xd3\x18\x95\x54\xdb\xae\x9f\x47\xf8\x7c\xc0\x77\x71\xf7\xc8\xea\xe1\x94\x82\x63\x2f\xcf\xf6\x4e\x31\x0f\xb8\x08\x69\x40\x2a\x1d\x1a\x23\x81\xca\x8a\x9d\x6a\x32\x6c\x4f\x97\xd7\x64\x64\xaf\x93\xed\xda\x45\xbb\x3e\x7c\x63\x04\x3d\x38\x3a\xa5\x18\x8a\x36\xb0\x7f\x1e\x5e\xa3\x57\xab\x15\xe8\x1f\x48\x4c\xa7\x60\x43\xd0\xc1\xd0\x91\x3d\xb5\x27\x84\xb7\x5f\x24\xea\xb2\x90\xa6\x33\x78\x3e\xda\x9f\x8f\xe4\xe4\x26\x3f\x8d\x78\x04\x88\xb1\x80\x87\xf1\xec\xa1\x4c\x0c\x27\x9e\x8d\xe1\xa4\xc1\x48\xc8\xc7\x3b\xd5\x3c\x6a\x52\x7e\x82\x94\x7c\x8c\x42\x4e\x1a\x55\xfa\x03\xfd\xd0\xbc\x01\x6d\xfa\x0b\x9c\x23\xaa\x2b\x1e\xde\xb8\x97\xfe\xea\x51\xca\x90\xf9\xe9\xbc\x61\x0d\x3b\xb7\x63\xdb\x09\xc8\xc7\x8b\x22\x57\xd1\xe4\x30\xcf\x08\xd0\xf4\x30\x9c\xd5\xd2\x99\x2e\xc8\xaf\xbf\xf5\x47\x96\xc4\x08\x63\x4a\xa3\x25\xdb\x5d\xbd\xd6\x96\x32\xbd\x52\x94\x0f\xdd\xa9\x31\x81\x60\x1b\x0f\xfc\xc2\xa0\xab\x94\x69\x4a\xfd\x61\x84\x8b\x0b\xd6\x8c\xa4\x1a\xd4\x2f\xe5\xba\x1d\x7e\x1a\x5c\x53\xcc\x1f\xa9\x4a\xe8\x2c\x75\xc1\x53\xed\xcb\xaa\xa8\x2a\x1e\xd9\x13\x87\x4a\xd1\xa0\x58\x4b\x32\xbc\xe4\x52\x18\x9b\xbc\x8c\xf4\x17\xba\x8f\x34\x31\x18\x56\x21\x53\x1a\x22\x28\x8f\xfc\xc5\x7b\x2f\xa5\x3f\xd8\x56\x9f\xf3\xe6\xee\x4f\xb6\x14\xe8\x2f\x0f\xb8\x11\x6b\xbd\xb3\xa4\xeb\x60\x62\x29\xcd\xfb\x4c\xf8\x38\xfe\xbb\x4e\x8f\x35\x72\x2c\xf3\x20\x12\x5c\xf9\x8c\xda\x5b\xe6\x9a\x25\xd8\x41\x49\x68\xe4\x93\x4d\xf8\xa6\xb4\x48\x79\xbe\x15\x1f\x79\x95\xb2\xba\xae\x78\xe3\x1d\xc2\x70\x79\x14\x25\x1a\x8f\xe4\xe0\x9b\xae\x95\x8c\xad\xbc\x12\xe2\xfe\x8b\xde\x50\x8d\x2f\xb5\xf3\x90\xd3\x42\x8c\x23\x87\x79\x52\x8d\x27\xd2\x0f\x15\xb9\x06\x22\xb5\x22\x42\xb6\x9a\x6f\xe8\x1b\x89\x3a\x0f\x3d\x2f\xb4\x1f\xf0\xd5\xdb\x51\x55\xaa\x37\x03\x83\x88\x83\x49\x3d\x45\x86\xe9\x32\x2f\xb7\x3b\xd5\x36\xa9\x57\x4a\xf5\xeb\x35\x9a\x49\x83\x27\x54\x64\xb8\x70\x46\xe9\x34\xad\x8a\x03\xae\x8a\xa8\x22\x9b\xf8\xdd\x46\xff\x8f\x0f\xf1\x3e\xd0\x7d\x04\xf2\x11\x4d\x15\x92\xd6\x92\xf1\xff\xac\x2e\xdd\x8d\x8b\x25\xed\x6b\x29\xa4\x73\xc6\x0b\x06\xc9\x4b\x75\x25\x4a\x3d\xbe\x29\xc1\x89\xc5\x7a\xac\xa6\x6d\xfa\xfa\x27\x6d\x19\x35\x1c\xb4\xf4\xe8\xe7\x1c\x51\xeb\xed\xfd\x30\xfa\xb0\x29\xd3\x90\x4c\x06\x98\xca\xbd\x77\x01\x1a\x6c\xef\x03\xa6\x3b\xb1\x63\x26\x9b\x0f\x1d\x5d\xc3\x5d\xc8\x56\x6b\xc4\x33\xcc\xa9\xc6\xa4\xc3\xce\xbe\x69\x90\x4c\x47\xd7\x03\x39\xb5\x0f\x4d\xf5\x44\xcf\x28\xbe\x65\xd3\xe5\x16\x52\xa5\xa2\x89\x2e\x53\xed\xbf\xbd\x54\xf5\xa4\xe3\x05\x26\x30\x8d\x49\x46\xba\x5c\x85\x7d\xb3\x76\x83\x15\x79\x47\xb2\xaa\x3c\x11\xf0\x57\xe1\xc4\xca\x1f\xc0\xf3\xf5\xa3\xbd\x0c\xb2\xa5\x7a\x57\x82\x0e\x9b\xae\xc0\x69\xa0\xb3\x5f\xb9\xce\xbe\xb6\x6d\x97\x0e\x16\x98\x76\x5c\xb4\xf4\x47\x50\xf3\x51\x55\xf9\xa5\xc9\xb6\xf8\x6e\xd2\xe4\xf3\xd6\xe4\x1a\x74\xfc\x0f\x9d\x30\x54\xef\xd3\xf2\xd7\x61\x68\xd7\xe1\xe4\xff\x33\x7a\x74\x54\xa2\x47\xce\x21\x2d\x09\xe2\x56\xa2\x89\x89\xbc\x81\x0e\x05\xeb\x9a\x44\xe4\xed\x56\x38\x86\xc0\x08\xa1\x6b\xfd\xb1\x61\x23\x74\xac\xce\x8b\x29\x89\x41\x9d\x17\x04\x08\xe0\xf8\x38\x4e\x95\x56\x9b\x4a\x36\xa8\x41\x40\xeb\xbb\x05\xb3\x93\x48\x4e\xff\x1d\x64\xa2\xae\x98\x35\xf7\x0e\x41\xa0\x86\x23\x91\x91\xf8\xf4\x34\xd6\x9c\xf8\x86\xbb\xe3\x46\xa6\x39\x0d\xb5\x9b\x8f\xf6\x68\xf4\x17\x5b\x9a\xcf\xee\xff\x02\x31\xdf\x5c\xdb\xf2\xbf\x00\x00\x00\xff\xff\x76\x3d\x74\x68\x2f\x28\x00\x00")
85 |
86 | func assets_jswav_js_bytes() ([]byte, error) {
87 | return bindata_read(
88 | _assets_jswav_js,
89 | "assets/jswav.js",
90 | )
91 | }
92 |
93 | func assets_jswav_js() (*asset, error) {
94 | bytes, err := assets_jswav_js_bytes()
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | info := bindata_file_info{name: "assets/jswav.js", size: 10287, mode: os.FileMode(420), modTime: time.Unix(1468021116, 0)}
100 | a := &asset{bytes: bytes, info: info}
101 | return a, nil
102 | }
103 |
104 | var _assets_script_js = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x58\x5b\x6f\xdb\x36\x14\x7e\xcf\xaf\x60\x9f\x24\x23\x8e\xda\x74\xc5\x06\xcc\x33\x8a\xc4\x71\xd7\xae\x59\x52\xb8\x29\xd0\x21\x08\x0a\x5a\x3a\x89\x99\x32\xa2\x4b\x51\x71\xb3\x21\xff\x7d\xbc\x88\x12\x49\x53\x4a\x02\x14\x7b\x9a\x81\x20\xb6\xf4\x9d\xc3\x73\xf9\xce\x45\x4a\x2f\xeb\x32\x17\x84\x95\xe9\x08\xfd\xb3\xb3\x83\xd0\x2d\xe6\xe8\xe0\x8f\x83\xcf\x5f\x8e\x4e\x4f\xe6\x68\x8a\x5e\x4d\x9a\x8b\x6f\xcf\xce\x3e\x7c\x39\x7d\x2f\x2f\xbd\x7c\xf1\xc2\x5e\x9c\x9f\x9c\xcd\x17\x5f\xde\xcf\xff\x9a\x9d\x1e\x29\xf4\xfe\x4f\xed\x9d\x8f\xb3\x83\x0f\x73\xe7\xd6\xcb\x5f\x26\x56\x3f\x2e\x8a\xc3\x5a\x08\x56\xca\xcb\x65\x4d\xa9\x95\xc9\x6b\xce\xa1\x14\x0b\xc8\x19\x2f\x80\x0f\xdd\x0d\xe4\x25\xc4\x3a\x82\x48\x49\x04\xc1\x94\xfc\x0d\xda\x27\x24\x3f\xee\x81\x05\xcb\xeb\x1b\xa9\x27\xbb\x02\x31\xa7\xa0\xbe\x1e\xde\xbd\x2b\xd2\x44\x82\xf6\x96\x1a\x95\x8c\x26\xbe\x58\x26\xbf\xcd\x6f\x25\xf2\x98\x54\x02\x4a\xe0\x69\x92\x53\x92\x7f\x4d\xc6\x0a\x73\x8c\x97\x40\x47\xda\x08\x84\x36\xa4\x2c\xd8\x26\x22\xf0\x15\xee\xea\xb5\x14\x68\x03\x0e\xd6\x3a\x84\xc8\x25\x4a\x21\xdb\xac\x48\xbe\x42\xd3\xe9\x34\x08\x5e\x87\x43\x08\xb2\x35\x07\xa5\xf8\x08\x2e\x71\x4d\x45\xda\x98\xaa\x3e\x39\x2e\x73\xa0\x26\x3e\xa4\xbc\xea\x6e\xdd\xeb\xff\xf7\xd6\xc4\xc1\x08\xe4\xac\x94\x06\x8b\x64\xf4\x44\x17\xfa\x0d\xdb\x72\xce\xe5\x8c\xeb\x9b\x0d\x65\xaf\xe1\x8a\x05\x26\x43\x33\x8a\xab\x0a\x2a\x99\xce\xf3\x84\x6b\x87\x6d\xea\xc6\x28\x29\x80\x82\x00\x7b\xe1\x62\x12\xc8\x2e\xe0\x4a\x7a\xc4\x31\xd7\xe2\x5c\xff\x02\xee\xd2\x6a\x8c\xec\xd5\x23\xad\xca\x5c\x6d\x14\x5d\x32\x8e\x52\xa5\x8d\x48\xf1\x17\x63\x44\x41\xb1\xca\x33\x2b\x93\xd7\xae\xc4\x6a\x22\x21\xbf\xa9\xfb\x13\xb4\xbb\x4b\x3a\x4f\x35\x99\x15\xf2\x04\xdf\x40\x28\x7b\x4e\x2e\x26\x0e\x4e\x1a\xd2\x22\x3a\xc3\x03\x90\xb9\x5d\xc5\xc9\x5d\x1d\xde\xcd\xec\x61\x69\x7b\x6c\x1b\xe2\xd6\x9d\xeb\xd6\x9d\xfd\xf6\xc4\xce\x93\x6b\xe3\xc9\xbe\x72\xe5\xda\x4d\x9a\x34\x30\x6d\xc0\xe7\xd7\x17\x61\xe6\x76\xd4\x9f\x5b\x9c\x5d\x8e\x1b\x1d\x3d\xa4\x55\x16\x51\x05\x7c\x43\x80\x16\x0f\x95\x6d\x4b\xda\x40\x58\xca\x75\x4a\xb2\x5b\x4c\x6b\xe8\x10\x52\xf0\xd3\xe2\x58\x42\x92\xe7\xf2\xeb\x6b\x0d\x9c\x26\x68\x17\x41\x99\xb3\x02\x3e\x2d\xde\xcd\xd8\xcd\x9a\x95\x52\x71\x4a\x9b\x12\xf7\xbb\x42\x41\x2a\xbc\xa4\xa0\xcc\x13\xdc\xaa\x96\xf6\x49\xb5\xa9\xd1\xee\xd6\x0a\xe7\x63\x44\x8a\x2e\x76\x51\x3d\x97\x98\x56\xe0\x56\xce\x33\x29\xe7\xc6\xbb\xb7\xc8\xd1\x96\xab\x4e\xff\x6c\x4f\x3c\x81\xcd\x82\x6d\x52\x52\x8c\x91\xeb\x93\x4c\x13\x02\x79\xb2\x73\x50\xb5\x62\x9b\x39\xe7\x8c\x2b\xcb\x23\x15\x19\x66\xd6\xc7\xdb\xc6\x4b\x81\x8b\x56\xc1\x16\x15\xb6\xac\x69\xc4\x54\x7a\xc0\x24\xd9\x4d\x7d\xce\x01\x0b\x68\xb2\x9f\x26\x82\xdb\x84\x37\xd8\xac\x02\x71\x20\x04\x27\x92\x90\x90\x26\x5a\xe5\x1e\x29\x12\x1d\x77\xa7\x87\xe8\x1b\x33\x46\x87\x74\x17\x56\xb7\x05\x67\x02\xbe\x8b\x99\x21\x9a\xe5\x95\xa3\xd3\x74\xa1\xc7\x2a\xed\x24\xb6\xe7\x52\x20\xe4\x8f\x24\x57\x28\x73\x9b\x48\xd0\x06\x23\x68\xdf\xfe\xc4\x50\xc8\x03\x2a\x2f\xf1\x7a\x0d\x65\x31\x5b\x11\x5a\xa4\xae\xb8\x1b\x3f\xd3\x60\x9f\xe2\x6b\xe1\xf4\xd1\x47\xfb\xea\x0a\xf9\xbe\xfa\x1d\x3e\x82\x0e\x7c\x35\x6d\xdc\x03\x86\xbe\xba\xe2\xd6\x57\x4b\x2b\x17\x67\xe9\x10\x30\x6f\x3b\x6c\xc3\x98\xd6\x88\x07\x87\x72\x85\x6f\xd6\x14\xaa\xbd\x25\x2b\xee\xd4\x54\x76\x94\x34\x8a\x5b\x6a\x6c\x4f\xb2\x30\x85\x2e\xce\x9d\x6d\xa1\xfb\xb1\xe2\x3e\xa8\x0b\xc2\xde\x95\xaa\x60\x39\xdb\xb8\xa5\x4a\x54\xe7\x92\xd7\x94\xed\xb1\xfa\x73\x88\xc0\x68\x53\x23\x0d\xbc\x1b\x52\x67\xf8\x4a\x8f\x28\xcd\x9c\xf3\xfd\x0b\x87\x6f\x25\x6c\x9e\x42\x36\xac\x0c\x95\xea\x06\x04\x34\xa4\x5d\xf3\x1a\x7c\xa6\xc6\x08\x67\xb4\xf2\xfa\x79\x7b\x57\xee\x37\x94\x61\xe5\x6a\x52\xca\xa9\x90\x74\x07\x56\xac\xe6\x39\x0c\x9f\x68\x30\xf6\xc8\x56\x22\xab\x78\xae\x47\x10\xb7\x1d\x3d\xdb\xe0\xdb\xd7\xa4\xe8\x9b\x44\xba\x93\xf9\x2a\xc4\xdd\x5a\x57\x85\xb6\xf4\xf9\xf7\x3d\xa9\x20\x09\x6c\x77\x59\xd3\x0a\x36\x8a\x4c\x74\x3d\x88\x95\xb3\xe4\x54\xc9\x22\x65\x25\x7b\xf9\x21\xc8\x85\x01\x52\x23\x33\x6e\xd2\x69\x89\x25\x51\x1c\x6e\xd8\x2d\x18\x2d\xce\x4d\x9f\x4d\x51\xa6\x9a\x62\xde\xa2\x15\x29\xde\x30\xee\x43\xcc\x69\xcb\x87\x36\x73\xef\xc9\xa6\x1b\xa8\xcb\x48\x87\x98\xaa\x1e\xa1\x72\xea\x4d\x5a\xff\x69\x24\xab\x04\x5b\xbb\xe3\x36\xa6\x28\x68\xab\xa6\xdc\x44\xcd\x4b\x7f\x7e\x0e\x8c\xf1\xb8\xd6\xa3\x8e\x70\xa8\xe7\x41\xc8\x08\x46\x31\xe6\x51\x0a\x36\xf6\xe1\xe4\xba\x92\x14\xc9\xec\xbd\xee\xec\xd0\x63\x26\xd1\xa5\xa2\x56\x1b\x4a\xc9\x9d\xb2\x18\x08\xd2\xd6\xca\x31\xf4\xd0\xf6\xc4\x40\xd6\x6b\x55\x7e\x5d\xc8\xd4\xd6\xa0\xed\x09\x97\xac\x02\x0b\xec\xda\xd8\x3c\x82\xf8\x7b\x94\x2e\xa1\xe8\x8a\xa3\xd3\x14\x6e\x43\x16\xee\x37\xc1\x2d\x6a\xfa\x4a\xda\xef\xf7\xdd\xfe\xd4\x1f\x6b\x50\x96\xb8\xc1\x0e\x2c\x7e\x64\x98\x7e\x44\x46\x7a\x76\xbf\x3e\xdb\x2b\x81\x79\xfb\xc0\x17\xdb\x0d\xa3\x23\xe7\xbf\x28\xf8\xde\x32\x53\xc7\xd5\x9c\xea\xd6\x6b\x66\xdf\x63\x7a\x6e\xbb\xdd\x4b\xd1\x71\x6f\xa2\xa2\x6c\xeb\xe3\x5a\x84\x69\x03\x2b\x7e\xb3\x39\xb2\x8d\x99\xa0\x3d\x91\x32\x1f\xd5\x8e\xd7\x58\x25\xaa\x19\x42\x5e\x73\x56\x23\xdc\xb1\x62\xc7\x27\x6a\x34\x89\x91\xf3\x1a\xbb\x4d\x8f\xb3\x19\xf2\x0f\xf5\x7e\x6d\x6b\x8d\xa4\xdb\x57\x1a\x3b\x76\x70\xd1\xf0\xf5\x6f\x05\xb3\xd1\xae\x92\x14\x96\xca\xb3\xa9\x29\x04\x87\x3f\x7d\x1d\xd1\xad\x97\xfe\x52\x1e\x42\xf9\xe3\x64\xb8\x6a\x23\x35\xfb\x40\x1f\x78\xa8\xca\xb7\x9f\xcc\x07\xda\x6b\x8e\x29\x5d\xe2\xfc\xab\x5b\xab\xdf\x57\x76\xa8\x7c\xfe\xf3\xf8\xad\x10\xeb\x05\x7c\xab\xa1\x6a\xdb\x80\xbc\x2f\x03\x21\x77\xa0\xe2\x4e\xb6\x07\x01\xf9\x0a\x97\x57\xde\x2c\xf1\xc7\xb2\xc2\x6b\xf4\x47\x85\xd6\x23\xb9\x7d\x0d\x19\x16\x97\xc2\x2a\xa5\x75\xa5\x71\xcd\x9b\xc9\x51\x50\x44\xc6\xe6\x54\xf9\x3c\x46\x46\x7d\x25\x0b\xba\x82\x33\x19\xb9\x87\x2a\xb0\x11\x4e\x74\xc9\xa2\x64\xb7\x3b\x72\x37\xf9\xb5\xf9\xed\xea\x1b\x1b\xe2\x44\x2a\xca\x84\xdb\x09\x8a\x5c\xb3\xd2\xe4\xc3\xe9\xc7\x33\xf5\xae\xea\xb9\x09\xbb\x6e\x3e\xbb\xf1\xd6\xe3\x04\xb4\x92\x1b\x9a\x99\xc1\xd9\x12\x57\xf0\xf3\xab\x74\x14\xa1\x7c\xd3\xa3\x38\x7c\xd3\x6f\x20\xfe\xcf\xde\x0f\xcf\xde\xef\x73\x95\x3c\x13\xe0\x30\x3d\xad\x26\x9d\x94\xde\xd7\xc1\x2a\xeb\xea\xc5\x44\xfb\xb2\x5a\xed\xda\xf7\x23\x95\x80\x7f\x03\x00\x00\xff\xff\xac\xf0\xd4\x0c\x94\x17\x00\x00")
105 |
106 | func assets_script_js_bytes() ([]byte, error) {
107 | return bindata_read(
108 | _assets_script_js,
109 | "assets/script.js",
110 | )
111 | }
112 |
113 | func assets_script_js() (*asset, error) {
114 | bytes, err := assets_script_js_bytes()
115 | if err != nil {
116 | return nil, err
117 | }
118 |
119 | info := bindata_file_info{name: "assets/script.js", size: 6036, mode: os.FileMode(420), modTime: time.Unix(1469122911, 0)}
120 | a := &asset{bytes: bytes, info: info}
121 | return a, nil
122 | }
123 |
124 | var _assets_style_css = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x7c\x91\xd1\x6a\xf3\x30\x0c\x85\xef\xf3\x14\x86\xde\xfc\x3f\xcc\x90\x76\x1b\x0c\xf7\x69\x94\x48\x4e\x45\x1d\x39\xd8\xea\xd6\x32\xfa\xee\x73\xe2\x15\x5a\xda\x0e\xdf\x89\xf3\x1d\x1d\x1f\x75\x11\x4f\xe6\xbb\x31\x46\xe9\xa8\x16\x02\x0f\xe2\x4c\x4f\xa2\x94\xb6\x65\xea\xa3\xa8\xf5\x30\x72\x38\x39\x93\x41\xb2\xcd\x94\xd8\x6f\x9b\x73\xd3\xc0\x01\x39\x2e\x2c\x72\x9e\x02\x14\x05\x4b\x60\x21\xdb\x85\xd8\xef\x67\xfc\x8b\x51\x77\xce\x6c\xd6\xed\x74\x5c\x98\x55\x86\x71\x0a\x94\x17\xac\x8b\x09\x29\x39\x23\x51\x68\x56\x8f\x90\x06\x2e\xeb\xe1\xa0\xf1\x46\xfd\x62\x56\x80\x68\xfb\x12\x06\x8a\x7f\xba\x0b\x1c\xc8\xeb\xad\xbf\xe2\x22\x9a\x0a\xc7\x32\x38\xf3\x3e\x07\x98\x57\x1c\xed\x6f\xa8\xd7\xf6\x2e\x94\xa2\xf3\x9c\xb2\xda\x7e\xc7\xa1\x1a\x8c\x2c\x17\x60\xf3\x10\x10\xdd\x55\xf9\xbf\xcd\xff\xa7\x45\xde\x30\xe9\x8a\xa1\x4f\x92\x8a\x75\xd0\xef\x87\x14\x0f\x32\xff\x33\xc4\x52\xcb\x8a\xd0\xaf\xfd\xc7\x1f\x74\x44\x7c\x0a\x23\xd0\x9b\x6f\x2b\x7c\x5f\x5e\xad\xda\x6a\x9c\x9c\xa9\xc7\xb9\xea\xea\x32\x78\xe0\xea\xdb\xf9\x6d\x9f\xdf\xfc\xdc\xfc\x04\x00\x00\xff\xff\x95\xbf\x79\x36\x52\x02\x00\x00")
125 |
126 | func assets_style_css_bytes() ([]byte, error) {
127 | return bindata_read(
128 | _assets_style_css,
129 | "assets/style.css",
130 | )
131 | }
132 |
133 | func assets_style_css() (*asset, error) {
134 | bytes, err := assets_style_css_bytes()
135 | if err != nil {
136 | return nil, err
137 | }
138 |
139 | info := bindata_file_info{name: "assets/style.css", size: 594, mode: os.FileMode(420), modTime: time.Unix(1468020159, 0)}
140 | a := &asset{bytes: bytes, info: info}
141 | return a, nil
142 | }
143 |
144 | // Asset loads and returns the asset for the given name.
145 | // It returns an error if the asset could not be found or
146 | // could not be loaded.
147 | func Asset(name string) ([]byte, error) {
148 | cannonicalName := strings.Replace(name, "\\", "/", -1)
149 | if f, ok := _bindata[cannonicalName]; ok {
150 | a, err := f()
151 | if err != nil {
152 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
153 | }
154 | return a.bytes, nil
155 | }
156 | return nil, fmt.Errorf("Asset %s not found", name)
157 | }
158 |
159 | // MustAsset is like Asset but panics when Asset would return an error.
160 | // It simplifies safe initialization of global variables.
161 | func MustAsset(name string) []byte {
162 | a, err := Asset(name)
163 | if (err != nil) {
164 | panic("asset: Asset(" + name + "): " + err.Error())
165 | }
166 |
167 | return a
168 | }
169 |
170 | // AssetInfo loads and returns the asset info for the given name.
171 | // It returns an error if the asset could not be found or
172 | // could not be loaded.
173 | func AssetInfo(name string) (os.FileInfo, error) {
174 | cannonicalName := strings.Replace(name, "\\", "/", -1)
175 | if f, ok := _bindata[cannonicalName]; ok {
176 | a, err := f()
177 | if err != nil {
178 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
179 | }
180 | return a.info, nil
181 | }
182 | return nil, fmt.Errorf("AssetInfo %s not found", name)
183 | }
184 |
185 | // AssetNames returns the names of the assets.
186 | func AssetNames() []string {
187 | names := make([]string, 0, len(_bindata))
188 | for name := range _bindata {
189 | names = append(names, name)
190 | }
191 | return names
192 | }
193 |
194 | // _bindata is a table, holding each asset generator, mapped to its name.
195 | var _bindata = map[string]func() (*asset, error){
196 | "assets/index.html": assets_index_html,
197 | "assets/jswav.js": assets_jswav_js,
198 | "assets/script.js": assets_script_js,
199 | "assets/style.css": assets_style_css,
200 | }
201 |
202 | // AssetDir returns the file names below a certain
203 | // directory embedded in the file by go-bindata.
204 | // For example if you run go-bindata on data/... and data contains the
205 | // following hierarchy:
206 | // data/
207 | // foo.txt
208 | // img/
209 | // a.png
210 | // b.png
211 | // then AssetDir("data") would return []string{"foo.txt", "img"}
212 | // AssetDir("data/img") would return []string{"a.png", "b.png"}
213 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error
214 | // AssetDir("") will return []string{"data"}.
215 | func AssetDir(name string) ([]string, error) {
216 | node := _bintree
217 | if len(name) != 0 {
218 | cannonicalName := strings.Replace(name, "\\", "/", -1)
219 | pathList := strings.Split(cannonicalName, "/")
220 | for _, p := range pathList {
221 | node = node.Children[p]
222 | if node == nil {
223 | return nil, fmt.Errorf("Asset %s not found", name)
224 | }
225 | }
226 | }
227 | if node.Func != nil {
228 | return nil, fmt.Errorf("Asset %s not found", name)
229 | }
230 | rv := make([]string, 0, len(node.Children))
231 | for name := range node.Children {
232 | rv = append(rv, name)
233 | }
234 | return rv, nil
235 | }
236 |
237 | type _bintree_t struct {
238 | Func func() (*asset, error)
239 | Children map[string]*_bintree_t
240 | }
241 | var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
242 | "assets": &_bintree_t{nil, map[string]*_bintree_t{
243 | "index.html": &_bintree_t{assets_index_html, map[string]*_bintree_t{
244 | }},
245 | "jswav.js": &_bintree_t{assets_jswav_js, map[string]*_bintree_t{
246 | }},
247 | "script.js": &_bintree_t{assets_script_js, map[string]*_bintree_t{
248 | }},
249 | "style.css": &_bintree_t{assets_style_css, map[string]*_bintree_t{
250 | }},
251 | }},
252 | }}
253 |
254 | // Restore an asset under the given directory
255 | func RestoreAsset(dir, name string) error {
256 | data, err := Asset(name)
257 | if err != nil {
258 | return err
259 | }
260 | info, err := AssetInfo(name)
261 | if err != nil {
262 | return err
263 | }
264 | err = os.MkdirAll(_filePath(dir, path.Dir(name)), os.FileMode(0755))
265 | if err != nil {
266 | return err
267 | }
268 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
269 | if err != nil {
270 | return err
271 | }
272 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
273 | if err != nil {
274 | return err
275 | }
276 | return nil
277 | }
278 |
279 | // Restore assets under the given directory recursively
280 | func RestoreAssets(dir, name string) error {
281 | children, err := AssetDir(name)
282 | if err != nil { // File
283 | return RestoreAsset(dir, name)
284 | } else { // Dir
285 | for _, child := range children {
286 | err = RestoreAssets(dir, path.Join(name, child))
287 | if err != nil {
288 | return err
289 | }
290 | }
291 | }
292 | return nil
293 | }
294 |
295 | func _filePath(dir, name string) string {
296 | cannonicalName := strings.Replace(name, "\\", "/", -1)
297 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
298 | }
299 |
300 |
--------------------------------------------------------------------------------