├── LICENSE ├── README.md ├── ctc ├── best_path.go ├── ctc.go ├── ctc_test.go ├── prefix_search.go ├── prefix_search_test.go ├── rgradienter.go └── total_cost.go ├── mfcc-graph └── main.go ├── mfcc ├── dct.go ├── dct_test.go ├── fft.go ├── fft_test.go ├── mel.go ├── mel_test.go ├── mfcc.go ├── source.go ├── source_test.go └── velocity.go ├── prototyping ├── bin_matrix.m ├── combiner_matrix.m ├── dct_bins.m ├── even_odd.m ├── fft_bins.m └── power_spec.m ├── recorder ├── assets │ ├── index.html │ ├── jswav.js │ ├── script.js │ └── style.css ├── bindata.go ├── bindata.sh ├── main.go └── server.go └── speechdata └── data.go /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-graph/main.go: -------------------------------------------------------------------------------- 1 | // Command mfcc-graph produces HTML graphs of MFCC 2 | // coefficients for an audio file. 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/unixpickle/speechrecog/mfcc" 14 | "github.com/unixpickle/wav" 15 | ) 16 | 17 | const ( 18 | OutputPerms = 0644 19 | GraphHeight = 200 20 | GraphWidth = 600 21 | ) 22 | 23 | var CepstrumColors = []string{ 24 | "#8c2323", "#000000", "#d9986c", "#e59900", "#add900", "#4a592d", 25 | "#00e699", "#2d98b3", "#0061f2", "#cc00ff", "#cc66b8", "#ff0066", 26 | } 27 | 28 | func main() { 29 | if len(os.Args) != 3 && len(os.Args) != 4 { 30 | fmt.Fprintln(os.Stderr, "Usage: mfcc-graph [--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(`` + "\n") 100 | 101 | timeWidth := GraphWidth / float64(len(coeffs)) 102 | var maxVal, minVal float64 103 | for i, x := range coeffs { 104 | for j, v := range x[1:] { 105 | if (i == 0 && j == 0) || v < minVal { 106 | minVal = v 107 | } 108 | if (i == 0 && j == 0) || v > maxVal { 109 | maxVal = v 110 | } 111 | } 112 | } 113 | 114 | for coeffIdx := 1; coeffIdx < 13; coeffIdx++ { 115 | color := CepstrumColors[coeffIdx-1] 116 | buf.WriteString(` ` + "\n") 130 | } 131 | 132 | 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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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< 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /recorder/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Recorder 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{range .Samples}} 15 | 16 | 17 | {{if .File}} 18 | 24 | {{else}} 25 | 28 | {{end}} 29 | 32 | 33 | {{end}} 34 | 35 |
{{- .Label -}} 19 | 23 | 26 | 27 | 30 | 31 |
36 |
37 | 38 | 39 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /recorder/bindata.sh: -------------------------------------------------------------------------------- 1 | go-bindata assets/... 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------