vocal_tract_length 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []