vocal_tract_length 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1807e5f276002fa0b457c2097660cc3e842856be
4
+ data.tar.gz: e065811735ab6fcaf704942a9e70e0be484d14e3
5
+ SHA512:
6
+ metadata.gz: a6abf0acca943104437bd2d9ce179feed3f87eca7d6600c3070a8df70cd95199400acf6d084d5a8ca4e8f01ee0a8703f06ea333e4c649dc4d679220f37bfa692
7
+ data.tar.gz: 0df030b9089c01e570e5126bc6c19ce734f2c951e015fbce5f77d5f09afc4dea87f2c673929791f16336743a624870658ce06d82816ca6ad9aa6f20c28ae61a6
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in vocal_tract_length.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Max Holder
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Vocal Tract Length
2
+
3
+ This small Sinatra application provides the frontend for users to record their
4
+ voice in order to calculate the length of their vocal tract.
5
+
6
+ See it in action at:
7
+ [http://maxwellholder.com/vocal_tract_length/index.html](http://maxwellholder.com/vocal_tract_length/index.html)
8
+
9
+ # Backend
10
+
11
+ To see the [Praat](http://praat.org) script and the Sinatra extension that provides the
12
+ `/extract_formant1` route, see the
13
+ [sinatra-praat](https://github.com/mxhold/sinatra-praat) project.
14
+
15
+ # How to Run
16
+
17
+ 1. Install a recent version of [Ruby](https://ruby-lang.org)
18
+ 2. Install [Praat](http://praat.org) and add it to your $PATH
19
+ 3. Clone the project: `git clone https://github.com/mxhold/vocal_tract_length`
20
+ 4. Run `bundle install`
21
+ 5. Run `rackup`
22
+ 6. It should be running at
23
+ [http://localhost:9292/vocal_tract_length/index.html](http://localhost:9292/vocal_tract_length/index.html)
24
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/config.ru ADDED
@@ -0,0 +1,3 @@
1
+ require 'vocal_tract_length'
2
+
3
+ run VocalTractLength::App
Binary file
@@ -0,0 +1,174 @@
1
+ <html>
2
+ <head>
3
+ <title>Calculate Your Vocal Tract Length</title>
4
+ <script>
5
+ var audio_context;
6
+ var recorder;
7
+
8
+ window.onload = function init() {
9
+ try {
10
+ // webkit shim
11
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
12
+ navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
13
+ window.URL = window.URL || window.webkitURL;
14
+
15
+ audio_context = new AudioContext;
16
+ navigator.getUserMedia({audio: true}, startUserMedia, function(e) {
17
+ console.log('No live audio input: ' + e);
18
+ });
19
+ } catch (e) {
20
+ alert('No web audio support in this browser!');
21
+ }
22
+
23
+ };
24
+
25
+ function startUserMedia(stream) {
26
+ var input = audio_context.createMediaStreamSource(stream);
27
+ recorder = new Recorder(input);
28
+ document.querySelector("[data-id=allowHint]").style.display = 'none';
29
+ startRecordingButton = document.querySelector("[data-action=startRecording]");
30
+ startRecordingButton.disabled = false;
31
+ startRecordingButton.addEventListener("click", startRecording, false);
32
+ console.log("bound");
33
+ }
34
+
35
+ function startRecording() {
36
+ this.innerHTML = "Recording... "
37
+ this.disabled = true;
38
+ recorder && recorder.record();
39
+
40
+ setTimeout(stopRecording, 2000, this);
41
+ }
42
+
43
+ function stopRecording(button) {
44
+ button.innerHTML = "Record"
45
+ button.disabled = false;
46
+ recorder && recorder.stop();
47
+
48
+ recorder.exportWAV(sendWAV);
49
+ }
50
+
51
+ function calculateVocalTractLength(formant1) {
52
+ return 34400 / (4.0 * formant1);
53
+ }
54
+
55
+ function roundTo(number, places) {
56
+ return Math.round(number * Math.pow(10, places))/Math.pow(10, places);
57
+ }
58
+
59
+ function setFormant1Result(formant1Text) {
60
+ formant1 = parseFloat(formant1Text);
61
+ document.querySelector("[data-id=formant1]").innerHTML = roundTo(formant1, 0);
62
+ document.querySelector("[data-id=vocalTractLength]").innerHTML = roundTo(calculateVocalTractLength(formant1), 1);
63
+ document.querySelector("[data-id=results]").style.display = 'initial';
64
+ }
65
+
66
+ function sendWAV(blob) {
67
+ var formData = new FormData();
68
+ formData.append('data', blob);
69
+ xhr = new XMLHttpRequest();
70
+ xhr.open("POST", "extract_formant1", true);
71
+ xhr.onload = function() {
72
+ setFormant1Result(xhr.responseText);
73
+ }
74
+ xhr.send(formData);
75
+
76
+ recorder.clear();
77
+ }
78
+
79
+
80
+ </script>
81
+ <script src="recorder.js"></script>
82
+ </head>
83
+ <body>
84
+ <nav>
85
+ <a href="/">Home</a>
86
+ <a href="https://github.com/mxhold/vocal_tract_length">Source code</a>
87
+ </nav>
88
+ <section>
89
+ <h1>Calculate Your Vocal Tract Length</h1>
90
+ <p>
91
+ Click <em>Record</em> and produce an unobstructed vowel (the last sound in
92
+ the English word <em>comma</em>) continuously for 2 seconds.
93
+ </p>
94
+ <p data-id="allowHint">
95
+ <em>First, click "Allow" in your browser to let this page to record your
96
+ voice.</em>
97
+ </p>
98
+ <p>
99
+ <button data-action="startRecording" disabled>Record</button>
100
+ </p>
101
+ </section>
102
+ <section data-id="results" style="display: none;">
103
+ <h1>Results</h1>
104
+ <p>
105
+ F<sub>1</sub> = <span data-id="formant1">error</span> Hz
106
+ </p>
107
+ <p>
108
+ Estimated vocal tract length:
109
+ <span data-id="vocalTractLength">error</span> cm
110
+ </p>
111
+ <p>
112
+ Average vocal tract length (female): 14.1 cm<br>
113
+ Average vocal tract length (male): 17 cm<br>
114
+ </p>
115
+ </section>
116
+ <section>
117
+ <h1>About</h1>
118
+ <p>
119
+ If we simplify things significantly, the human vocal tract is really just
120
+ a tube that is closed on one end (where your vocal folds vibrate) and open
121
+ on another end (your lips).
122
+ </p>
123
+ <p>
124
+ Given this model, there is a formula that expresses the relationship
125
+ between resonance frequencies (<em>f</em>, which we is what we are
126
+ measuring here), the speed of sound (<em>v</em>, which we know), and the
127
+ length of the tube (<em>L</em>, which is what we are trying to figure
128
+ out):
129
+ </p>
130
+ <img src="frequency.png" alt="f = nv/2L" height="50">
131
+ <p>
132
+ The <em>n</em> here refers to which resonance frequency we're talking
133
+ about. Vowels will usually have four or more distinguishable resonance
134
+ frequencies, but we're just dealing with the first (F<sub>1</sub>) here,
135
+ so we can replace this with 1. We can also replace <em>v</em> with 34400
136
+ cm/s since we know that's the speed of sound at sea level at room
137
+ temperature (20&deg;C). And since we know the frequency but want to
138
+ determine the length, we can solve for L to get this equation:
139
+ </p>
140
+ <img src="length.png" alt="L = 34400/4f" height="50">
141
+ <h2>How are you measuring F<sub>1</sub>?</h2>
142
+ <p>
143
+ When you hit record, we are using a Javascript library that wraps the
144
+ browser's audio recording API to record a 2 second WAV audio clip.
145
+ Thanks to
146
+ <a href="http://matt-diamond.com/">Matt Diamond's</a>
147
+ <a href="https://github.com/mattdiamond/Recorderjs">Recorderjs project</a>
148
+ for making that library.
149
+ </p>
150
+ <p>
151
+ This data is then sent to the server where it is processed using <a
152
+ href="http://praat.org">Praat</a>, a seriously cool program that can do
153
+ all sorts of phonetic analysis. This gives us the first formant which we
154
+ then plug into the above equation.
155
+ </p>
156
+ <p>
157
+ You can see the source code for this page and the server side Praat script
158
+ on <a href="https://github.com/mxhold/vocal_tract_length">GitHub</a>.
159
+ </p>
160
+ <h2>Is this accurate? Why is the estimate so inconsistent?</h2>
161
+ <p>
162
+ It is pretty hard to get people to produce an unobstructed vowel
163
+ consistently so that is probably the biggest limitation with this method.
164
+ We're also using a simplified model of the vocal tract, likely using a
165
+ less than perfect microphone, and only taking a 2 second sample.
166
+ </p>
167
+ <p>
168
+ The point of this is not to actually give you an accurate measure, but
169
+ rather to show it's possible to calculate the length of the vocal tract
170
+ from a recording, which I think is pretty cool.
171
+ </p>
172
+ </section>
173
+ </body>
174
+ </html>
Binary file
@@ -0,0 +1,89 @@
1
+ (function(window){
2
+
3
+ var WORKER_PATH = 'recorderWorker.js';
4
+
5
+ var Recorder = function(source, cfg){
6
+ var config = cfg || {};
7
+ var bufferLen = config.bufferLen || 4096;
8
+ this.context = source.context;
9
+ this.node = (this.context.createScriptProcessor ||
10
+ this.context.createJavaScriptNode).call(this.context,
11
+ bufferLen, 2, 2);
12
+ var worker = new Worker(config.workerPath || WORKER_PATH);
13
+ worker.postMessage({
14
+ command: 'init',
15
+ config: {
16
+ sampleRate: this.context.sampleRate
17
+ }
18
+ });
19
+ var recording = false,
20
+ currCallback;
21
+
22
+ this.node.onaudioprocess = function(e){
23
+ if (!recording) return;
24
+ worker.postMessage({
25
+ command: 'record',
26
+ buffer: [
27
+ e.inputBuffer.getChannelData(0),
28
+ e.inputBuffer.getChannelData(1)
29
+ ]
30
+ });
31
+ }
32
+
33
+ this.configure = function(cfg){
34
+ for (var prop in cfg){
35
+ if (cfg.hasOwnProperty(prop)){
36
+ config[prop] = cfg[prop];
37
+ }
38
+ }
39
+ }
40
+
41
+ this.record = function(){
42
+ recording = true;
43
+ }
44
+
45
+ this.stop = function(){
46
+ recording = false;
47
+ }
48
+
49
+ this.clear = function(){
50
+ worker.postMessage({ command: 'clear' });
51
+ }
52
+
53
+ this.getBuffer = function(cb) {
54
+ currCallback = cb || config.callback;
55
+ worker.postMessage({ command: 'getBuffer' })
56
+ }
57
+
58
+ this.exportWAV = function(cb, type){
59
+ currCallback = cb || config.callback;
60
+ type = type || config.type || 'audio/wav';
61
+ if (!currCallback) throw new Error('Callback not set');
62
+ worker.postMessage({
63
+ command: 'exportWAV',
64
+ type: type
65
+ });
66
+ }
67
+
68
+ worker.onmessage = function(e){
69
+ var blob = e.data;
70
+ currCallback(blob);
71
+ }
72
+
73
+ source.connect(this.node);
74
+ this.node.connect(this.context.destination); //this should not be necessary
75
+ };
76
+
77
+ Recorder.forceDownload = function(blob, filename){
78
+ var url = (window.URL || window.webkitURL).createObjectURL(blob);
79
+ var link = window.document.createElement('a');
80
+ link.href = url;
81
+ link.download = filename || 'output.wav';
82
+ var click = document.createEvent("Event");
83
+ click.initEvent("click", true, true);
84
+ link.dispatchEvent(click);
85
+ }
86
+
87
+ window.Recorder = Recorder;
88
+
89
+ })(window);
@@ -0,0 +1,131 @@
1
+ var recLength = 0,
2
+ recBuffersL = [],
3
+ recBuffersR = [],
4
+ sampleRate;
5
+
6
+ this.onmessage = function(e){
7
+ switch(e.data.command){
8
+ case 'init':
9
+ init(e.data.config);
10
+ break;
11
+ case 'record':
12
+ record(e.data.buffer);
13
+ break;
14
+ case 'exportWAV':
15
+ exportWAV(e.data.type);
16
+ break;
17
+ case 'getBuffer':
18
+ getBuffer();
19
+ break;
20
+ case 'clear':
21
+ clear();
22
+ break;
23
+ }
24
+ };
25
+
26
+ function init(config){
27
+ sampleRate = config.sampleRate;
28
+ }
29
+
30
+ function record(inputBuffer){
31
+ recBuffersL.push(inputBuffer[0]);
32
+ recBuffersR.push(inputBuffer[1]);
33
+ recLength += inputBuffer[0].length;
34
+ }
35
+
36
+ function exportWAV(type){
37
+ var bufferL = mergeBuffers(recBuffersL, recLength);
38
+ var bufferR = mergeBuffers(recBuffersR, recLength);
39
+ var interleaved = interleave(bufferL, bufferR);
40
+ var dataview = encodeWAV(interleaved);
41
+ var audioBlob = new Blob([dataview], { type: type });
42
+
43
+ this.postMessage(audioBlob);
44
+ }
45
+
46
+ function getBuffer() {
47
+ var buffers = [];
48
+ buffers.push( mergeBuffers(recBuffersL, recLength) );
49
+ buffers.push( mergeBuffers(recBuffersR, recLength) );
50
+ this.postMessage(buffers);
51
+ }
52
+
53
+ function clear(){
54
+ recLength = 0;
55
+ recBuffersL = [];
56
+ recBuffersR = [];
57
+ }
58
+
59
+ function mergeBuffers(recBuffers, recLength){
60
+ var result = new Float32Array(recLength);
61
+ var offset = 0;
62
+ for (var i = 0; i < recBuffers.length; i++){
63
+ result.set(recBuffers[i], offset);
64
+ offset += recBuffers[i].length;
65
+ }
66
+ return result;
67
+ }
68
+
69
+ function interleave(inputL, inputR){
70
+ var length = inputL.length + inputR.length;
71
+ var result = new Float32Array(length);
72
+
73
+ var index = 0,
74
+ inputIndex = 0;
75
+
76
+ while (index < length){
77
+ result[index++] = inputL[inputIndex];
78
+ result[index++] = inputR[inputIndex];
79
+ inputIndex++;
80
+ }
81
+ return result;
82
+ }
83
+
84
+ function floatTo16BitPCM(output, offset, input){
85
+ for (var i = 0; i < input.length; i++, offset+=2){
86
+ var s = Math.max(-1, Math.min(1, input[i]));
87
+ output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
88
+ }
89
+ }
90
+
91
+ function writeString(view, offset, string){
92
+ for (var i = 0; i < string.length; i++){
93
+ view.setUint8(offset + i, string.charCodeAt(i));
94
+ }
95
+ }
96
+
97
+ function encodeWAV(samples){
98
+ var buffer = new ArrayBuffer(44 + samples.length * 2);
99
+ var view = new DataView(buffer);
100
+
101
+ /* RIFF identifier */
102
+ writeString(view, 0, 'RIFF');
103
+ /* RIFF chunk length */
104
+ view.setUint32(4, 36 + samples.length * 2, true);
105
+ /* RIFF type */
106
+ writeString(view, 8, 'WAVE');
107
+ /* format chunk identifier */
108
+ writeString(view, 12, 'fmt ');
109
+ /* format chunk length */
110
+ view.setUint32(16, 16, true);
111
+ /* sample format (raw) */
112
+ view.setUint16(20, 1, true);
113
+ /* channel count */
114
+ view.setUint16(22, 2, true);
115
+ /* sample rate */
116
+ view.setUint32(24, sampleRate, true);
117
+ /* byte rate (sample rate * block align) */
118
+ view.setUint32(28, sampleRate * 4, true);
119
+ /* block align (channel count * bytes per sample) */
120
+ view.setUint16(32, 4, true);
121
+ /* bits per sample */
122
+ view.setUint16(34, 16, true);
123
+ /* data chunk identifier */
124
+ writeString(view, 36, 'data');
125
+ /* data chunk length */
126
+ view.setUint32(40, samples.length * 2, true);
127
+
128
+ floatTo16BitPCM(view, 44, samples);
129
+
130
+ return view;
131
+ }
@@ -0,0 +1,3 @@
1
+ module VocalTractLength
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,10 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/praat'
3
+
4
+ module VocalTractLength
5
+ class App < Sinatra::Base
6
+ register Sinatra::Praat
7
+
8
+ set :public_folder, __dir__ + '/public'
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'vocal_tract_length/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "vocal_tract_length"
8
+ spec.version = VocalTractLength::VERSION
9
+ spec.authors = ["Max Holder"]
10
+ spec.email = ["mxhold@gmail.com"]
11
+ spec.summary = %q{Calculate your vocal tract length by recording your voice}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.7"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_dependency 'sinatra'
23
+ spec.add_dependency 'sinatra-praat'
24
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vocal_tract_length
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Max Holder
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sinatra
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra-praat
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - mxhold@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - config.ru
82
+ - lib/public/frequency.png
83
+ - lib/public/index.html
84
+ - lib/public/length.png
85
+ - lib/public/recorder.js
86
+ - lib/public/recorderWorker.js
87
+ - lib/vocal_tract_length.rb
88
+ - lib/vocal_tract_length/version.rb
89
+ - vocal_tract_length.gemspec
90
+ homepage: ''
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.2.2
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Calculate your vocal tract length by recording your voice
114
+ test_files: []