├── .gitignore
├── LICENSE
├── README.md
├── css
├── bootstrap.min.css
└── style.css
├── example
├── Blue-headed-Vireo-BirdNET-Test.wav
├── Song_Sparrow.mp3
├── Soundscape.wav
└── White-crowned_Sparrow.mp3
├── fonts
└── MaterialIcons-Regular.woff2
├── img
├── icon
│ ├── icon.icns
│ └── icon.png
└── logo
│ └── CLO_logo_inverted.png
├── index.html
├── js
├── birdnet.js
├── labels.js
├── ui.js
├── wavesurfer.drawer.extended.js
├── wavesurfer.min.js
├── wavesurfer.regions.min.js
├── wavesurfer.spectrogram.min.js
└── wavesurfer.timeline.min.js
├── main.js
├── model
├── group1-shard1of9.bin
├── group1-shard2of9.bin
├── group1-shard3of9.bin
├── group1-shard4of9.bin
├── group1-shard5of9.bin
├── group1-shard6of9.bin
├── group1-shard7of9.bin
├── group1-shard8of9.bin
├── group1-shard9of9.bin
└── model.json
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Exports
2 | /dist
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 |
12 | # Diagnostic reports (https://nodejs.org/api/report.html)
13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14 |
15 | # Runtime data
16 | pids
17 | *.pid
18 | *.seed
19 | *.pid.lock
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 |
43 | # Dependency directories
44 | node_modules/
45 | jspm_packages/
46 |
47 | # TypeScript v1 declaration files
48 | typings/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Microbundle cache
60 | .rpt2_cache/
61 | .rts2_cache_cjs/
62 | .rts2_cache_es/
63 | .rts2_cache_umd/
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variables file
75 | .env
76 | .env.test
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 |
81 | # Next.js build output
82 | .next
83 |
84 | # Nuxt.js build / generate output
85 | .nuxt
86 | dist
87 |
88 | # Gatsby files
89 | .cache/
90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
91 | # https://nextjs.org/blog/next-9-1#public-directory-support
92 | # public
93 |
94 | # vuepress build output
95 | .vuepress/dist
96 |
97 | # Serverless directories
98 | .serverless/
99 |
100 | # FuseBox cache
101 | .fusebox/
102 |
103 | # DynamoDB Local files
104 | .dynamodb/
105 |
106 | # TernJS port file
107 | .tern-port
108 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Stefan Kahl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This repo is currently under development.
2 |
3 | # BirdNET-Electron
4 |
5 | Electron app for sound file analysis with BirdNET.
6 |
7 | Author: Stefan Kahl
8 |
9 | Contact: stefan.kahl@cs.tu-chemnitz.de
10 |
11 | Website: https://birdnet.cornell.edu/
12 |
13 | Please cite as (PDF coming soon):
14 |
15 | ```
16 | @phdthesis{kahl2019identifying,
17 | title={{Identifying Birds by Sound: Large-scale Acoustic Event Recognition for Avian Activity Monitoring}},
18 | author={Kahl, Stefan},
19 | year={2019},
20 | school={Chemnitz University of Technology}
21 | }
22 | ```
23 |
24 | ## Application setup
25 |
26 | First, clone the project and install all dependencies:
27 |
28 | ```
29 | git clone https://github.com/kahst/BirdNET-Electron.git
30 | cd BirdNET-Electron
31 | npm install
32 | ```
33 |
34 | Next, launch the app with:
35 |
36 | ```
37 | npm start
38 | ```
39 |
40 | ## Development setup
41 |
42 | Setting up the project requires Node.js, which we need to install first.
43 |
44 | After that, we can initialize the source directory with:
45 |
46 | ```
47 | npm init
48 | ```
49 |
50 | Follow the prompt to setup ```package.json```.
51 |
52 | Now, we need to install electron with:
53 |
54 | ```
55 | npm install --save-dev electron
56 | ```
57 |
58 | BirdNET requires Tensorflow.js which we install with:
59 |
60 | ```
61 | npm install @tensorflow/tfjs
62 | ```
63 |
64 | Install Bootstrap and its dependencies:
65 |
66 | ```
67 | npm install bootstrap
68 | npm install jquery
69 | npm install popper.js
70 | ```
71 |
72 | This app also needs some additional packages that we have to install.
73 |
74 | ```
75 | npm install audio-loader
76 | npm install audio-resampler
77 | npm install array-normalize
78 | npm install colormap
79 | ```
80 |
81 | In order to package the app for stand-alone applications, we need electron-packager:
82 |
83 | ```
84 | npm install electron-packager --save-dev
85 | ```
86 |
87 | We can now add the export script in the package.json:
88 |
89 | ```
90 | "scripts": {
91 | "start": "electron .",
92 | "export": "electron-packager . --out dist --overwrite"
93 | }
94 | ```
95 |
96 | After that, we can export the app with:
97 |
98 | ```
99 | npm run export
100 | ```
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/css/style.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | padding-top: 28px;
4 | padding-bottom: 18px;
5 | background-color: #f8f9fa;
6 | overflow: hidden;
7 | }
8 |
9 | .border-10 {
10 | border-width:10px !important;
11 | }
12 |
13 | .h-33 {height: 33%;}
14 | .h-40 {height: 40%;}
15 | .h-85 {height: 85%;}
16 |
17 | @font-face {
18 | font-family: 'Material Icons';
19 | font-style: normal;
20 | font-weight: 400;
21 | src: local('Material Icons'),
22 | local('MaterialIcons-Regular'),
23 | url(../fonts/MaterialIcons-Regular.woff2) format('woff2')
24 | }
25 |
26 | .material-icons {
27 | font-family: 'Material Icons';
28 | font-weight: normal;
29 | font-style: normal;
30 | font-size: 24px; /* Preferred icon size */
31 | display: inline-block;
32 | line-height: 1;
33 | text-transform: none;
34 | letter-spacing: normal;
35 | word-wrap: normal;
36 | white-space: nowrap;
37 | direction: ltr;
38 | vertical-align: middle;
39 | padding-bottom: 0px;
40 |
41 | /* Support for all WebKit browsers. */
42 | -webkit-font-smoothing: antialiased;
43 | }
--------------------------------------------------------------------------------
/example/Blue-headed-Vireo-BirdNET-Test.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/example/Blue-headed-Vireo-BirdNET-Test.wav
--------------------------------------------------------------------------------
/example/Song_Sparrow.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/example/Song_Sparrow.mp3
--------------------------------------------------------------------------------
/example/Soundscape.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/example/Soundscape.wav
--------------------------------------------------------------------------------
/example/White-crowned_Sparrow.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/example/White-crowned_Sparrow.mp3
--------------------------------------------------------------------------------
/fonts/MaterialIcons-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/fonts/MaterialIcons-Regular.woff2
--------------------------------------------------------------------------------
/img/icon/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/img/icon/icon.icns
--------------------------------------------------------------------------------
/img/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/img/icon/icon.png
--------------------------------------------------------------------------------
/img/logo/CLO_logo_inverted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/img/logo/CLO_logo_inverted.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | BirdNET Sound Analysis
30 |
31 |
32 |
33 |
34 |
35 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
Load an audio file for analysis by clickingon File > Open audio file in the top menu.
74 |
75 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
92 |
95 |
98 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | # |
110 | Timestamp |
111 | Common name |
112 | Scientific name |
113 | Confidence |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
129 |
--------------------------------------------------------------------------------
/js/birdnet.js:
--------------------------------------------------------------------------------
1 | // Imports
2 | const tf = require('@tensorflow/tfjs');
3 | const load = require('audio-loader')
4 | const resampler = require('audio-resampler');
5 | const normalize = require('array-normalize')
6 | const colormap = require('colormap')
7 |
8 | const MODEL_JSON = 'model/model.json'
9 | const CONFIG = {
10 |
11 | sampleRate: 48000,
12 | specLength: 3,
13 | sigmoid: 1.0,
14 | minConfidence: 0.15,
15 |
16 | }
17 |
18 | let MODEL = null;
19 | var AUDIO_DATA = [];
20 | var RESULTS = [];
21 |
22 | var WAVESURFER = null;
23 | var CURRENT_ADUIO_BUFFER = null;
24 | var WS_ZOOM = 0;
25 |
26 | ///////////////////////// Build SimpleSpecLayer /////////////////////////
27 | class SimpleSpecLayer extends tf.layers.Layer {
28 | constructor(config) {
29 | super(config);
30 |
31 | // For now, let's work with hard coded values to avoid strange errors when reading the config
32 | this.spec_shape = [257, 384];
33 | this.frame_length = 512;
34 | this.frame_step = 374;
35 | }
36 |
37 | build(inputShape) {
38 | this.mag_scale = this.addWeight('magnitude_scaling', [], 'float32', tf.initializers.constant({value: 1.0}));
39 | }
40 |
41 | computeOutputShape(inputShape) { return [inputShape[0], this.spec_shape[0], this.spec_shape[1], 1]; }
42 |
43 | call(input, kwargs) {
44 |
45 | // Perform STFT
46 | var spec = tf.signal.stft(input[0].squeeze(),
47 | this.frame_length,
48 | this.frame_step)
49 |
50 | // Cast from complex to float
51 | spec = tf.cast(spec, 'float32');
52 |
53 | // Convert to power spectrogram
54 | spec = tf.pow(spec, 2.0)
55 |
56 | // Convert magnitudes using nonlinearity
57 | spec = tf.pow(spec, tf.div(1.0, tf.add(1.0, tf.exp(this.mag_scale.read()))))
58 |
59 | // Normalize values between 0 and 1
60 | //spec = tf.div(tf.sub(spec, tf.min(spec)), tf.max(spec));
61 |
62 | // Swap axes to fit output shape
63 | spec = tf.transpose(spec)
64 |
65 | // Add channel axis
66 | spec = tf.expandDims(spec, -1)
67 |
68 | // Add batch axis
69 | spec = tf.expandDims(spec, 0)
70 |
71 | return spec
72 |
73 | }
74 |
75 | static get className() { return 'SimpleSpecLayer'; }
76 | }
77 |
78 | tf.serialization.registerClass(SimpleSpecLayer);
79 |
80 | ///////////////////////// Build GlobalExpPool2D Layer /////////////////////////
81 | function logmeanexp(x, axis, keepdims, sharpness) {
82 | xmax = tf.max(x, axis, true);
83 | xmax2 = tf.max(x, axis, keepdims);
84 | x = tf.mul(sharpness, tf.sub(x, xmax));
85 | y = tf.log(tf.mean(tf.exp(x), axis, keepdims));
86 | y = tf.add(tf.div(y, sharpness), xmax2);
87 | return y
88 | }
89 |
90 | class GlobalLogExpPooling2D extends tf.layers.Layer {
91 | constructor(config) {
92 | super(config);
93 | }
94 |
95 | build(inputShape) {
96 | this.sharpness = this.addWeight('sharpness', [1], 'float32', tf.initializers.constant({value: 2.0}));
97 | }
98 |
99 | computeOutputShape(inputShape) { return [inputShape[0], inputShape[3]]; }
100 |
101 | call(input, kwargs) {
102 |
103 | return logmeanexp(input[0], [1, 2], false, this.sharpness.read());//.read().dataSync()[0]);
104 |
105 | }
106 |
107 | static get className() { return 'GlobalLogExpPooling2D'; }
108 | }
109 |
110 | tf.serialization.registerClass(GlobalLogExpPooling2D);
111 |
112 | ///////////////////////// Build Sigmoid Layer /////////////////////////
113 | class SigmoidLayer extends tf.layers.Layer {
114 | constructor(config) {
115 | super(config);
116 | this.config = config;
117 | }
118 |
119 | build(inputShape) {
120 | this.kernel = this.addWeight('scale_factor', [1], 'float32', tf.initializers.constant({value: 1.0}));
121 | }
122 |
123 | computeOutputShape(inputShape) { return inputShape; }
124 |
125 | call(input, kwargs) {
126 |
127 | return tf.sigmoid(tf.mul(input[0], CONFIG.sigmoid))
128 |
129 | }
130 |
131 | static get className() { return 'SigmoidLayer'; }
132 | }
133 |
134 | tf.serialization.registerClass(SigmoidLayer);
135 |
136 | async function loadModel() {
137 |
138 | // Load model
139 | if (MODEL == null) {
140 | console.log('Loading model...');
141 | MODEL = await tf.loadLayersModel(MODEL_JSON);
142 | //CONFIG.labels = MODEL.getLayer('SIGMOID').config.labels;
143 | CONFIG.labels = LABELS;
144 | console.log('...done loading model!');
145 | }
146 |
147 | }
148 |
149 | async function predict(audioData, model) {
150 |
151 | const audioTensor = tf.tensor1d(audioData)
152 | RESULTS = [];
153 |
154 | // Slice and expand
155 | var cunkLength = CONFIG.sampleRate * CONFIG.specLength;
156 | for (var i = 0; i < audioTensor.shape[0] - cunkLength; i += CONFIG.sampleRate) {
157 |
158 | if (i + cunkLength > audioTensor.shape[0]) i = audioTensor.shape[0] - cunkLength;
159 | const chunkTensor = audioTensor.slice(i, cunkLength).expandDims(0);
160 |
161 | // Make prediction
162 | const prediction = model.predict(chunkTensor);
163 |
164 | // Get label
165 | const index = prediction.argMax(1).dataSync()[0];
166 | const score = prediction.dataSync()[index];
167 |
168 | console.log(index, CONFIG.labels[index], score);
169 |
170 | if (score >= CONFIG.minConfidence) {
171 | RESULTS.push({
172 |
173 | timestamp: timestampFromSeconds(i / CONFIG.sampleRate) + ' - ' + timestampFromSeconds((i + cunkLength) / CONFIG.sampleRate),
174 | sname: CONFIG.labels[index].split('_')[0],
175 | cname: CONFIG.labels[index].split('_')[1],
176 | score: score
177 |
178 | });
179 | }
180 | }
181 | }
182 |
183 |
184 | function loadAudioFile(filePath) {
185 |
186 | // Hide load hint and show spinnner
187 | hideAll();
188 | showElement('loadFileHint');
189 | showElement('loadFileHintSpinner');
190 | showElement('loadFileHintLog');
191 |
192 | // load one file
193 | log('loadFileHintLog', 'Loading file...');
194 | load(filePath).then(function (buffer) {
195 |
196 | // Resample
197 | log('loadFileHintLog', 'Analyzing...');
198 | resampler(buffer, CONFIG.sampleRate, async function(event) {
199 |
200 | // Get raw audio data
201 | AUDIO_DATA = event.getAudioBuffer().getChannelData(0);
202 |
203 | // Normalize audio data
204 | //AUDIO_DATA = normalize(AUDIO_DATA)
205 |
206 | // Predict
207 | predict(AUDIO_DATA, MODEL);
208 |
209 | //Hide center div when done
210 | hideElement('loadFileHint');
211 |
212 | // Draw and show spectrogram
213 | drawSpectrogram(buffer);
214 |
215 | // Show results
216 | showResults();
217 |
218 | });
219 |
220 | });
221 |
222 | }
223 |
224 | function drawSpectrogram(audioBuffer) {
225 |
226 | // Set global buffer
227 | CURRENT_ADUIO_BUFFER = audioBuffer;
228 |
229 | // Show waveform container
230 | showElement('specContainer', false, true);
231 | showElement('specTimeline', false, true);
232 |
233 | // Setup waveform and spec views
234 | var options = {
235 | container: '#specContainer',
236 | backgroundColor: '#363a40',
237 | waveColor: '#fff',
238 | cursorColor: '#fff',
239 | progressColor: '#4b79fa',
240 | cursorWidth: 2,
241 | normalize: true,
242 | fillParent: true,
243 | responsive: true,
244 | height: 512,
245 | fftSamples: 1024,
246 | minPxPerSec: 50,
247 | colorMap: colormap({
248 | colormap: 'viridis',
249 | nshades: 256,
250 | format: 'rgb',
251 | alpha: 1
252 | }),
253 | hideScrollbar: false,
254 | visualization: 'spectrogram',
255 | plugins: []
256 | };
257 |
258 | // Create wavesurfer object
259 | WAVESURFER = WaveSurfer.create(options);
260 | WAVESURFER.enableDragSelection({});
261 |
262 | // Load audio file
263 | WAVESURFER.loadDecodedBuffer(CURRENT_ADUIO_BUFFER);
264 |
265 | // Set initial zoom level
266 | WS_ZOOM = $('#specContainer').width() / WAVESURFER.getDuration();
267 |
268 | // Set click event that removes all regions
269 | $('#specContainer').mousedown(function (e) { WAVESURFER.clearRegions(); });
270 |
271 | // Resize canvas of spec and labels
272 | adjustSpecHeight(false);
273 |
274 | // Show controls
275 | showElement('controlsWrapper');
276 |
277 | }
278 |
279 | function adjustSpecHeight(redraw) {
280 |
281 | if (redraw && WAVESURFER != null) WAVESURFER.drawBuffer();
282 |
283 | $('#specContainer wave, canvas').each(function() {
284 | $( this ).height($('body').height() * 0.40);
285 | });
286 |
287 | $('#resultTableContainer').height($('#contentWrapper').height() - $('#specContainer').height() - $('#controlsWrapper').height() - 47);
288 |
289 | }
290 |
291 | function zoomSpecIn() {
292 |
293 | WS_ZOOM += 50;
294 | WAVESURFER.zoom(WS_ZOOM);
295 | }
296 |
297 | function zoomSpecOut() {
298 |
299 | WS_ZOOM -= 50;
300 | WAVESURFER.zoom(WS_ZOOM);
301 |
302 | }
303 |
304 | function showResults() {
305 |
306 | console.log(RESULTS);
307 | showElement('resultTableContainer');
308 |
309 | // Remove old results
310 | $('#resultTableBody').empty();
311 |
312 | // Add new results
313 | for (var i = 0 ; i < RESULTS.length; i++) {
314 |
315 | var tr = "" + (i + 1) + " | ";
316 | tr += "" + RESULTS[i].timestamp + " | ";
317 | tr += "" + RESULTS[i].cname + " | ";
318 | tr += "" + RESULTS[i].sname + " | ";
319 | tr += "" + RESULTS[i].score + " | ";
320 | tr += "
";
321 |
322 | $('#resultTableBody').append(tr);
323 |
324 | }
325 |
326 | }
327 |
328 | function timestampFromSeconds(seconds) {
329 |
330 | var date = new Date(1970,0,1);
331 | date.setSeconds(seconds);
332 | return date.toTimeString().replace(/.*(\d{2}:\d{2}).*/, "$1");
333 |
334 | }
335 |
336 |
337 |
--------------------------------------------------------------------------------
/js/labels.js:
--------------------------------------------------------------------------------
1 | const LABELS = ["Acanthis cabaret_Lesser Redpoll", "Acanthis flammea_Common Redpoll", "Acanthis hornemanni_Hoary Redpoll", "Accipiter cooperii_Cooper's Hawk", "Accipiter gentilis_Northern Goshawk", "Accipiter nisus_Eurasian Sparrowhawk", "Accipiter striatus_Sharp-shinned Hawk", "Acridotheres tristis_Common Myna", "Acrocephalus agricola_Paddyfield Warbler", "Acrocephalus arundinaceus_Great Reed Warbler", "Acrocephalus baeticatus_African Reed Warbler", "Acrocephalus dumetorum_Blyth's Reed Warbler", "Acrocephalus melanopogon_Moustached Warbler", "Acrocephalus paludicola_Aquatic Warbler", "Acrocephalus palustris_Marsh Warbler", "Acrocephalus schoenobaenus_Sedge Warbler", "Acrocephalus scirpaceus_Eurasian Reed Warbler", "Actitis hypoleucos_Common Sandpiper", "Actitis macularius_Spotted Sandpiper", "Aechmophorus occidentalis_Western Grebe", "Aegithalos caudatus_Long-tailed Tit", "Aegolius acadicus_Northern Saw-whet Owl", "Aegolius funereus_Boreal Owl", "Aeronautes saxatalis_White-throated Swift", "Agelaius phoeniceus_Red-winged Blackbird", "Agelaius tricolor_Tricolored Blackbird", "Aimophila ruficeps_Rufous-crowned Sparrow", "Aix galericulata_Mandarin Duck", "Aix sponsa_Wood Duck", "Alaemon alaudipes_Greater Hoopoe-Lark", "Alauda arvensis_Eurasian Skylark", "Alauda leucoptera_White-winged Lark", "Alaudala rufescens_Lesser Short-toed Lark", "Alca torda_Razorbill", "Alcedo atthis_Common Kingfisher", "Alectoris barbara_Barbary Partridge", "Alectoris chukar_Chukar", "Alectoris graeca_Rock Partridge", "Alectoris rufa_Red-legged Partridge", "Alopochen aegyptiaca_Egyptian Goose", "Amazilia yucatanensis_Buff-bellied Hummingbird", "Amazona viridigenalis_Red-crowned Parrot", "Ammodramus savannarum_Grasshopper Sparrow", "Ammomanes cinctura_Bar-tailed Lark", "Ammomanes deserti_Desert Lark", "Ammospiza leconteii_LeConte's Sparrow", "Ammospiza maritima_Seaside Sparrow", "Ammospiza nelsoni_Nelson's Sparrow", "Amphispiza bilineata_Black-throated Sparrow", "Anas acuta_Northern Pintail", "Anas crecca_Green-winged Teal", "Anas platyrhynchos_Mallard", "Anhinga anhinga_Anhinga", "Anser albifrons_Greater White-fronted Goose", "Anser anser_Graylag Goose", "Anser brachyrhynchus_Pink-footed Goose", "Anser caerulescens_Snow Goose", "Anser canagicus_Emperor Goose", "Anser erythropus_Lesser White-fronted Goose", "Anser fabalis_Taiga Bean-Goose", "Anser indicus_Bar-headed Goose", "Anser rossii_Ross's Goose", "Anser serrirostris_Tundra Bean-Goose", "Anthropoides virgo_Demoiselle Crane", "Anthus campestris_Tawny Pipit", "Anthus cervinus_Red-throated Pipit", "Anthus gustavi_Pechora Pipit", "Anthus hodgsoni_Olive-backed Pipit", "Anthus petrosus_Rock Pipit", "Anthus pratensis_Meadow Pipit", "Anthus richardi_Richard's Pipit", "Anthus rubescens_American Pipit", "Anthus spinoletta_Water Pipit", "Anthus spragueii_Sprague's Pipit", "Anthus trivialis_Tree Pipit", "Antigone canadensis_Sandhill Crane", "Antrostomus arizonae_Mexican Whip-poor-will", "Antrostomus carolinensis_Chuck-will's-widow", "Antrostomus vociferus_Eastern Whip-poor-will", "Aphelocoma californica_California Scrub-Jay", "Aphelocoma coerulescens_Florida Scrub-Jay", "Aphelocoma insularis_Island Scrub-Jay", "Aphelocoma wollweberi_Mexican Jay", "Aphelocoma woodhouseii_Woodhouse's Scrub-Jay", "Apus affinis_Little Swift", "Apus apus_Common Swift", "Apus melba_Alpine Swift", "Apus pallidus_Pallid Swift", "Aquila chrysaetos_Golden Eagle", "Aramus guarauna_Limpkin", "Archilochus alexandri_Black-chinned Hummingbird", "Archilochus colubris_Ruby-throated Hummingbird", "Ardea alba_Great Egret", "Ardea cinerea_Gray Heron", "Ardea herodias_Great Blue Heron", "Ardea purpurea_Purple Heron", "Ardenna grisea_Sooty Shearwater", "Ardeola ralloides_Squacco Heron", "Arenaria interpres_Ruddy Turnstone", "Arenaria melanocephala_Black Turnstone", "Arremonops rufivirgatus_Olive Sparrow", "Artemisiospiza belli_Bell's Sparrow", "Artemisiospiza nevadensis_Sagebrush Sparrow", "Asio flammeus_Short-eared Owl", "Asio otus_Long-eared Owl", "Athene cunicularia_Burrowing Owl", "Athene noctua_Little Owl", "Auriparus flaviceps_Verdin", "Aythya americana_Redhead", "Aythya collaris_Ring-necked Duck", "Aythya ferina_Common Pochard", "Aythya fuligula_Tufted Duck", "Aythya marila_Greater Scaup", "Aythya nyroca_Ferruginous Duck", "Baeolophus atricristatus_Black-crested Titmouse", "Baeolophus bicolor_Tufted Titmouse", "Baeolophus inornatus_Oak Titmouse", "Baeolophus ridgwayi_Juniper Titmouse", "Baeolophus wollweberi_Bridled Titmouse", "Bartramia longicauda_Upland Sandpiper", "Bombycilla cedrorum_Cedar Waxwing", "Bombycilla garrulus_Bohemian Waxwing", "Bonasa umbellus_Ruffed Grouse", "Botaurus lentiginosus_American Bittern", "Botaurus stellaris_Great Bittern", "Brachyramphus marmoratus_Marbled Murrelet", "Branta bernicla_Brant", "Branta canadensis_Canada Goose", "Branta hutchinsii_Cackling Goose", "Branta leucopsis_Barnacle Goose", "Bubo ascalaphus_Pharaoh Eagle-Owl", "Bubo bubo_Eurasian Eagle-Owl", "Bubo scandiacus_Snowy Owl", "Bubo virginianus_Great Horned Owl", "Bubulcus ibis_Cattle Egret", "Bucanetes githagineus_Trumpeter Finch", "Bucephala albeola_Bufflehead", "Bucephala clangula_Common Goldeneye", "Burhinus oedicnemus_Eurasian Thick-knee", "Buteo albonotatus_Zone-tailed Hawk", "Buteo brachyurus_Short-tailed Hawk", "Buteo buteo_Common Buzzard", "Buteo jamaicensis_Red-tailed Hawk", "Buteo lagopus_Rough-legged Hawk", "Buteo lineatus_Red-shouldered Hawk", "Buteo plagiatus_Gray Hawk", "Buteo platypterus_Broad-winged Hawk", "Buteo rufinus_Long-legged Buzzard", "Buteo swainsoni_Swainson's Hawk", "Buteogallus anthracinus_Common Black Hawk", "Butorides virescens_Green Heron", "Calamospiza melanocorys_Lark Bunting", "Calandrella brachydactyla_Greater Short-toed Lark", "Calcarius lapponicus_Lapland Longspur", "Calcarius ornatus_Chestnut-collared Longspur", "Calcarius pictus_Smith's Longspur", "Calidris alba_Sanderling", "Calidris alpina_Dunlin", "Calidris bairdii_Baird's Sandpiper", "Calidris canutus_Red Knot", "Calidris falcinellus_Broad-billed Sandpiper", "Calidris ferruginea_Curlew Sandpiper", "Calidris fuscicollis_White-rumped Sandpiper", "Calidris himantopus_Stilt Sandpiper", "Calidris maritima_Purple Sandpiper", "Calidris mauri_Western Sandpiper", "Calidris melanotos_Pectoral Sandpiper", "Calidris minuta_Little Stint", "Calidris minutilla_Least Sandpiper", "Calidris ptilocnemis_Rock Sandpiper", "Calidris pugnax_Ruff", "Calidris pusilla_Semipalmated Sandpiper", "Calidris temminckii_Temminck's Stint", "Calidris virgata_Surfbird", "Calliope calliope_Siberian Rubythroat", "Callipepla californica_California Quail", "Callipepla gambelii_Gambel's Quail", "Callipepla squamata_Scaled Quail", "Calonectris diomedea_Cory's Shearwater", "Calypte anna_Anna's Hummingbird", "Calypte costae_Costa's Hummingbird", "Camptostoma imberbe_Northern Beardless-Tyrannulet", "Campylorhynchus brunneicapillus_Cactus Wren", "Caprimulgus europaeus_Eurasian Nightjar", "Caprimulgus ruficollis_Red-necked Nightjar", "Caracara cheriway_Crested Caracara", "Cardellina canadensis_Canada Warbler", "Cardellina pusilla_Wilson's Warbler", "Cardellina rubrifrons_Red-faced Warbler", "Cardinalis cardinalis_Northern Cardinal", "Cardinalis sinuatus_Pyrrhuloxia", "Carduelis carduelis_European Goldfinch", "Carduelis citrinella_Citril Finch", "Carduelis corsicana_Corsican Finch", "Carpodacus erythrinus_Common Rosefinch", "Carpospiza brachydactyla_Pale Rockfinch", "Catharus bicknelli_Bicknell's Thrush", "Catharus fuscescens_Veery", "Catharus guttatus_Hermit Thrush", "Catharus minimus_Gray-cheeked Thrush", "Catharus ustulatus_Swainson's Thrush", "Catherpes mexicanus_Canyon Wren", "Cecropis daurica_Red-rumped Swallow", "Centrocercus urophasianus_Greater Sage-Grouse", "Centronyx bairdii_Baird's Sparrow", "Centronyx henslowii_Henslow's Sparrow", "Cepphus columba_Pigeon Guillemot", "Cepphus grylle_Black Guillemot", "Cercotrichas galactotes_Rufous-tailed Scrub-Robin", "Certhia americana_Brown Creeper", "Certhia brachydactyla_Short-toed Treecreeper", "Certhia familiaris_Eurasian Treecreeper", "Ceryle rudis_Pied Kingfisher", "Cettia cetti_Cetti's Warbler", "Chaetura pelagica_Chimney Swift", "Chaetura vauxi_Vaux's Swift", "Chamaea fasciata_Wrentit", "Charadrius alexandrinus_Kentish Plover", "Charadrius dubius_Little Ringed Plover", "Charadrius hiaticula_Common Ringed Plover", "Charadrius leschenaultii_Greater Sand-Plover", "Charadrius melodus_Piping Plover", "Charadrius morinellus_Eurasian Dotterel", "Charadrius nivosus_Snowy Plover", "Charadrius semipalmatus_Semipalmated Plover", "Charadrius vociferus_Killdeer", "Charadrius wilsonia_Wilson's Plover", "Chersophilus duponti_Dupont's Lark", "Chlidonias hybrida_Whiskered Tern", "Chlidonias leucopterus_White-winged Tern", "Chlidonias niger_Black Tern", "Chloris chloris_European Greenfinch", "Chloroceryle americana_Green Kingfisher", "Chondestes grammacus_Lark Sparrow", "Chordeiles acutipennis_Lesser Nighthawk", "Chordeiles gundlachii_Antillean Nighthawk", "Chordeiles minor_Common Nighthawk", "Chroicocephalus genei_Slender-billed Gull", "Chroicocephalus philadelphia_Bonaparte's Gull", "Chroicocephalus ridibundus_Black-headed Gull", "Ciconia ciconia_White Stork", "Ciconia nigra_Black Stork", "Cinclus cinclus_White-throated Dipper", "Cinclus mexicanus_American Dipper", "Circaetus gallicus_Short-toed Snake-Eagle", "Circus aeruginosus_Eurasian Marsh-Harrier", "Circus hudsonius_Northern Harrier", "Circus macrourus_Pallid Harrier", "Circus pygargus_Montagu's Harrier", "Cisticola juncidis_Zitting Cisticola", "Cistothorus palustris_Marsh Wren", "Cistothorus platensis_Sedge Wren", "Clamator glandarius_Great Spotted Cuckoo", "Clanga clanga_Greater Spotted Eagle", "Clanga pomarina_Lesser Spotted Eagle", "Clangula hyemalis_Long-tailed Duck", "Coccothraustes coccothraustes_Hawfinch", "Coccothraustes vespertinus_Evening Grosbeak", "Coccyzus americanus_Yellow-billed Cuckoo", "Coccyzus erythropthalmus_Black-billed Cuckoo", "Coccyzus minor_Mangrove Cuckoo", "Colaptes auratus_Northern Flicker", "Colaptes chrysoides_Gilded Flicker", "Colinus virginianus_Northern Bobwhite", "Columba livia_Rock Pigeon", "Columba oenas_Stock Dove", "Columba palumbus_Common Wood-Pigeon", "Columbina inca_Inca Dove", "Contopus cooperi_Olive-sided Flycatcher", "Contopus pertinax_Greater Pewee", "Contopus sordidulus_Western Wood-Pewee", "Contopus virens_Eastern Wood-Pewee", "Coracias garrulus_European Roller", "Coragyps atratus_Black Vulture", "Corvus brachyrhynchos_American Crow", "Corvus caurinus_Northwestern Crow", "Corvus corax_Common Raven", "Corvus cornix_Hooded Crow", "Corvus corone_Carrion Crow", "Corvus cryptoleucus_Chihuahuan Raven", "Corvus frugilegus_Rook", "Corvus monedula_Eurasian Jackdaw", "Corvus ossifragus_Fish Crow", "Corvus ruficollis_Brown-necked Raven", "Coturnicops noveboracensis_Yellow Rail", "Coturnix coturnix_Common Quail", "Crex crex_Corn Crake", "Crotophaga sulcirostris_Groove-billed Ani", "Cuculus canorus_Common Cuckoo", "Cyanistes caeruleus_Eurasian Blue Tit", "Cyanistes cyanus_Azure Tit", "Cyanistes teneriffae_African Blue Tit", "Cyanocitta cristata_Blue Jay", "Cyanocitta stelleri_Steller's Jay", "Cyanocorax yncas_Green Jay", "Cyanopica cooki_Iberian Magpie", "Cygnus atratus_Black Swan", "Cygnus buccinator_Trumpeter Swan", "Cygnus columbianus_Tundra Swan", "Cygnus cygnus_Whooper Swan", "Cygnus olor_Mute Swan", "Cynanthus latirostris_Broad-billed Hummingbird", "Cyrtonyx montezumae_Montezuma Quail", "Delichon urbicum_Common House-Martin", "Dendragapus fuliginosus_Sooty Grouse", "Dendragapus obscurus_Dusky Grouse", "Dendrocopos leucotos_White-backed Woodpecker", "Dendrocopos major_Great Spotted Woodpecker", "Dendrocopos syriacus_Syrian Woodpecker", "Dendrocoptes medius_Middle Spotted Woodpecker", "Dendrocygna autumnalis_Black-bellied Whistling-Duck", "Dendrocygna bicolor_Fulvous Whistling-Duck", "Dolichonyx oryzivorus_Bobolink", "Dryobates albolarvatus_White-headed Woodpecker", "Dryobates arizonae_Arizona Woodpecker", "Dryobates borealis_Red-cockaded Woodpecker", "Dryobates minor_Lesser Spotted Woodpecker", "Dryobates nuttallii_Nuttall's Woodpecker", "Dryobates pubescens_Downy Woodpecker", "Dryobates scalaris_Ladder-backed Woodpecker", "Dryobates villosus_Hairy Woodpecker", "Dryocopus martius_Black Woodpecker", "Dryocopus pileatus_Pileated Woodpecker", "Dumetella carolinensis_Gray Catbird", "Egretta caerulea_Little Blue Heron", "Egretta garzetta_Little Egret", "Egretta thula_Snowy Egret", "Egretta tricolor_Tricolored Heron", "Elanoides forficatus_Swallow-tailed Kite", "Elanus axillaris_Black-shouldered Kite", "Elanus leucurus_White-tailed Kite", "Emberiza bruniceps_Red-headed Bunting", "Emberiza buchanani_Gray-necked Bunting", "Emberiza caesia_Cretzschmar's Bunting", "Emberiza calandra_Corn Bunting", "Emberiza cia_Rock Bunting", "Emberiza cineracea_Cinereous Bunting", "Emberiza cirlus_Cirl Bunting", "Emberiza citrinella_Yellowhammer", "Emberiza hortulana_Ortolan Bunting", "Emberiza melanocephala_Black-headed Bunting", "Emberiza pusilla_Little Bunting", "Emberiza rustica_Rustic Bunting", "Emberiza sahari_House Bunting", "Emberiza schoeniclus_Reed Bunting", "Empidonax alnorum_Alder Flycatcher", "Empidonax difficilis_Pacific-slope Flycatcher", "Empidonax flaviventris_Yellow-bellied Flycatcher", "Empidonax fulvifrons_Buff-breasted Flycatcher", "Empidonax hammondii_Hammond's Flycatcher", "Empidonax minimus_Least Flycatcher", "Empidonax oberholseri_Dusky Flycatcher", "Empidonax occidentalis_Cordilleran Flycatcher", "Empidonax traillii_Willow Flycatcher", "Empidonax virescens_Acadian Flycatcher", "Empidonax wrightii_Gray Flycatcher", "Eremophila alpestris_Horned Lark", "Erithacus rubecula_European Robin", "Estrilda astrild_Common Waxbill", "Eudocimus albus_White Ibis", "Eugenes fulgens_Rivoli's Hummingbird", "Euphagus carolinus_Rusty Blackbird", "Euphagus cyanocephalus_Brewer's Blackbird", "Falcipennis canadensis_Spruce Grouse", "Falco columbarius_Merlin", "Falco eleonorae_Eleonora's Falcon", "Falco femoralis_Aplomado Falcon", "Falco naumanni_Lesser Kestrel", "Falco peregrinus_Peregrine Falcon", "Falco sparverius_American Kestrel", "Falco subbuteo_Eurasian Hobby", "Falco tinnunculus_Eurasian Kestrel", "Falco vespertinus_Red-footed Falcon", "Ficedula albicollis_Collared Flycatcher", "Ficedula hypoleuca_European Pied Flycatcher", "Ficedula parva_Red-breasted Flycatcher", "Ficedula semitorquata_Semicollared Flycatcher", "Ficedula speculigera_Atlas Flycatcher", "Francolinus francolinus_Black Francolin", "Fratercula arctica_Atlantic Puffin", "Fregata magnificens_Magnificent Frigatebird", "Fringilla coelebs_Common Chaffinch", "Fringilla montifringilla_Brambling", "Fulica americana_American Coot", "Fulica atra_Eurasian Coot", "Fulmarus glacialis_Northern Fulmar", "Galerida cristata_Crested Lark", "Galerida theklae_Thekla's Lark", "Gallinago delicata_Wilson's Snipe", "Gallinago gallinago_Common Snipe", "Gallinago media_Great Snipe", "Gallinula chloropus_Eurasian Moorhen", "Gallinula galeata_Common Gallinule", "Gallus gallus_Red Junglefowl", "Garrulus glandarius_Eurasian Jay", "Gavia arctica_Arctic Loon", "Gavia immer_Common Loon", "Gavia pacifica_Pacific Loon", "Gavia stellata_Red-throated Loon", "Gelochelidon nilotica_Gull-billed Tern", "Geococcyx californianus_Greater Roadrunner", "Geothlypis formosa_Kentucky Warbler", "Geothlypis philadelphia_Mourning Warbler", "Geothlypis tolmiei_MacGillivray's Warbler", "Geothlypis trichas_Common Yellowthroat", "Geronticus eremita_Northern Bald Ibis", "Glareola nordmanni_Black-winged Pratincole", "Glareola pratincola_Collared Pratincole", "Glaucidium gnoma_Northern Pygmy-Owl", "Glaucidium passerinum_Eurasian Pygmy-Owl", "Grus grus_Common Crane", "Gymnorhinus cyanocephalus_Pinyon Jay", "Gyps fulvus_Eurasian Griffon", "Haematopus bachmani_Black Oystercatcher", "Haematopus ostralegus_Eurasian Oystercatcher", "Haematopus palliatus_American Oystercatcher", "Haemorhous cassinii_Cassin's Finch", "Haemorhous mexicanus_House Finch", "Haemorhous purpureus_Purple Finch", "Halcyon smyrnensis_White-throated Kingfisher", "Haliaeetus albicilla_White-tailed Eagle", "Haliaeetus leucocephalus_Bald Eagle", "Helmitheros vermivorum_Worm-eating Warbler", "Hieraaetus pennatus_Booted Eagle", "Himantopus himantopus_Black-winged Stilt", "Himantopus mexicanus_Black-necked Stilt", "Hippolais icterina_Icterine Warbler", "Hippolais languida_Upcher's Warbler", "Hippolais olivetorum_Olive-tree Warbler", "Hippolais polyglotta_Melodious Warbler", "Hirundo rustica_Barn Swallow", "Histrionicus histrionicus_Harlequin Duck", "Human_Human", "Hydrobates pelagicus_European Storm-Petrel", "Hydrocoloeus minutus_Little Gull", "Hydroprogne caspia_Caspian Tern", "Hylocichla mustelina_Wood Thrush", "Ichthyaetus audouinii_Audouin's Gull", "Ichthyaetus ichthyaetus_Pallas's Gull", "Ichthyaetus melanocephalus_Mediterranean Gull", "Icteria virens_Yellow-breasted Chat", "Icterus bullockii_Bullock's Oriole", "Icterus cucullatus_Hooded Oriole", "Icterus galbula_Baltimore Oriole", "Icterus graduacauda_Audubon's Oriole", "Icterus gularis_Altamira Oriole", "Icterus parisorum_Scott's Oriole", "Icterus spurius_Orchard Oriole", "Ictinia mississippiensis_Mississippi Kite", "Iduna caligata_Booted Warbler", "Iduna opaca_Western Olivaceous Warbler", "Iduna pallida_Eastern Olivaceous Warbler", "Iduna rama_Sykes's Warbler", "Irania gutturalis_White-throated Robin", "Ixobrychus exilis_Least Bittern", "Ixobrychus minutus_Little Bittern", "Ixoreus naevius_Varied Thrush", "Junco hyemalis_Dark-eyed Junco", "Junco phaeonotus_Yellow-eyed Junco", "Jynx torquilla_Eurasian Wryneck", "Ketupa zeylonensis_Brown Fish-Owl", "Lagonosticta senegala_Red-billed Firefinch", "Lagopus lagopus_Willow Ptarmigan", "Lagopus leucura_White-tailed Ptarmigan", "Lagopus muta_Rock Ptarmigan", "Lanius borealis_Northern Shrike", "Lanius collurio_Red-backed Shrike", "Lanius excubitor_Great Gray Shrike", "Lanius isabellinus_Isabelline Shrike", "Lanius ludovicianus_Loggerhead Shrike", "Lanius minor_Lesser Gray Shrike", "Lanius nubicus_Masked Shrike", "Lanius phoenicuroides_Red-tailed Shrike", "Lanius senator_Woodchat Shrike", "Larus argentatus_Herring Gull", "Larus cachinnans_Caspian Gull", "Larus californicus_California Gull", "Larus canus_Mew Gull", "Larus delawarensis_Ring-billed Gull", "Larus fuscus_Lesser Black-backed Gull", "Larus glaucescens_Glaucous-winged Gull", "Larus heermanni_Heermann's Gull", "Larus hyperboreus_Glaucous Gull", "Larus marinus_Great Black-backed Gull", "Larus michahellis_Yellow-legged Gull", "Larus occidentalis_Western Gull", "Laterallus jamaicensis_Black Rail", "Leptotila verreauxi_White-tipped Dove", "Leucophaeus atricilla_Laughing Gull", "Leucophaeus pipixcan_Franklin's Gull", "Leucosticte atrata_Black Rosy-Finch", "Leucosticte australis_Brown-capped Rosy-Finch", "Leucosticte tephrocotis_Gray-crowned Rosy-Finch", "Limnodromus griseus_Short-billed Dowitcher", "Limnodromus scolopaceus_Long-billed Dowitcher", "Limnothlypis swainsonii_Swainson's Warbler", "Limosa fedoa_Marbled Godwit", "Limosa haemastica_Hudsonian Godwit", "Limosa lapponica_Bar-tailed Godwit", "Limosa limosa_Black-tailed Godwit", "Linaria cannabina_Eurasian Linnet", "Linaria flavirostris_Twite", "Locustella lanceolata_Lanceolated Warbler", "Locustella luscinioides_Savi's Warbler", "Locustella naevia_Common Grasshopper-Warbler", "Lophodytes cucullatus_Hooded Merganser", "Lophophanes cristatus_Crested Tit", "Loxia curvirostra_Red Crossbill", "Loxia leucoptera_White-winged Crossbill", "Loxia pytyopsittacus_Parrot Crossbill", "Loxia sinesciuris_Cassia Crossbill", "Lullula arborea_Wood Lark", "Luscinia luscinia_Thrush Nightingale", "Luscinia megarhynchos_Common Nightingale", "Luscinia svecica_Bluethroat", "Lymnocryptes minimus_Jack Snipe", "Mareca americana_American Wigeon", "Mareca penelope_Eurasian Wigeon", "Mareca strepera_Gadwall", "Megaceryle alcyon_Belted Kingfisher", "Megaceryle torquata_Ringed Kingfisher", "Megascops asio_Eastern Screech-Owl", "Megascops kennicottii_Western Screech-Owl", "Megascops trichopsis_Whiskered Screech-Owl", "Melanerpes aurifrons_Golden-fronted Woodpecker", "Melanerpes carolinus_Red-bellied Woodpecker", "Melanerpes erythrocephalus_Red-headed Woodpecker", "Melanerpes formicivorus_Acorn Woodpecker", "Melanerpes lewis_Lewis's Woodpecker", "Melanerpes uropygialis_Gila Woodpecker", "Melanitta nigra_Common Scoter", "Melanocorypha bimaculata_Bimaculated Lark", "Melanocorypha calandra_Calandra Lark", "Melanocorypha yeltoniensis_Black Lark", "Meleagris gallopavo_Wild Turkey", "Melospiza georgiana_Swamp Sparrow", "Melospiza lincolnii_Lincoln's Sparrow", "Melospiza melodia_Song Sparrow", "Melozone aberti_Abert's Towhee", "Melozone crissalis_California Towhee", "Melozone fusca_Canyon Towhee", "Mergus merganser_Common Merganser", "Mergus serrator_Red-breasted Merganser", "Merops apiaster_European Bee-eater", "Merops persicus_Blue-cheeked Bee-eater", "Micrathene whitneyi_Elf Owl", "Milvus migrans_Black Kite", "Milvus milvus_Red Kite", "Mimus polyglottos_Northern Mockingbird", "Mniotilta varia_Black-and-white Warbler", "Molothrus ater_Brown-headed Cowbird", "Monticola saxatilis_Rufous-tailed Rock-Thrush", "Monticola solitarius_Blue Rock-Thrush", "Montifringilla nivalis_White-winged Snowfinch", "Morus bassanus_Northern Gannet", "Motacilla alba_White Wagtail", "Motacilla cinerea_Gray Wagtail", "Motacilla citreola_Citrine Wagtail", "Motacilla flava_Western Yellow Wagtail", "Motacilla tschutschensis_Eastern Yellow Wagtail", "Muscicapa striata_Spotted Flycatcher", "Myadestes townsendi_Townsend's Solitaire", "Mycteria americana_Wood Stork", "Myiarchus cinerascens_Ash-throated Flycatcher", "Myiarchus crinitus_Great Crested Flycatcher", "Myiarchus tuberculifer_Dusky-capped Flycatcher", "Myiarchus tyrannulus_Brown-crested Flycatcher", "Myioborus pictus_Painted Redstart", "Myiodynastes luteiventris_Sulphur-bellied Flycatcher", "Myiopsitta monachus_Monk Parakeet", "Netta rufina_Red-crested Pochard", "Noise_Noise", "Non-Bird_Non-Bird", "Nucifraga caryocatactes_Eurasian Nutcracker", "Nucifraga columbiana_Clark's Nutcracker", "Numenius americanus_Long-billed Curlew", "Numenius arquata_Eurasian Curlew", "Numenius phaeopus_Whimbrel", "Nyctanassa violacea_Yellow-crowned Night-Heron", "Nycticorax nycticorax_Black-crowned Night-Heron", "Nyctidromus albicollis_Common Pauraque", "Oceanodroma castro_Band-rumped Storm-Petrel", "Oceanodroma leucorhoa_Leach's Storm-Petrel", "Oena capensis_Namaqua Dove", "Oenanthe deserti_Desert Wheatear", "Oenanthe finschii_Finsch's Wheatear", "Oenanthe hispanica_Black-eared Wheatear", "Oenanthe isabellina_Isabelline Wheatear", "Oenanthe leucopyga_White-crowned Wheatear", "Oenanthe leucura_Black Wheatear", "Oenanthe moesta_Red-rumped Wheatear", "Oenanthe oenanthe_Northern Wheatear", "Oenanthe pleschanka_Pied Wheatear", "Onychoprion aleuticus_Aleutian Tern", "Onychoprion fuscatus_Sooty Tern", "Oporornis agilis_Connecticut Warbler", "Oreortyx pictus_Mountain Quail", "Oreoscoptes montanus_Sage Thrasher", "Oriolus oriolus_Eurasian Golden Oriole", "Ortalis vetula_Plain Chachalaca", "Otus brucei_Pallid Scops-Owl", "Otus scops_Eurasian Scops-Owl", "Oxyura jamaicensis_Ruddy Duck", "Pandion haliaetus_Osprey", "Panurus biarmicus_Bearded Reedling", "Parabuteo unicinctus_Harris's Hawk", "Parkesia motacilla_Louisiana Waterthrush", "Parkesia noveboracensis_Northern Waterthrush", "Parus major_Great Tit", "Passer domesticus_House Sparrow", "Passer hispaniolensis_Spanish Sparrow", "Passer italiae_Italian Sparrow", "Passer moabiticus_Dead Sea Sparrow", "Passer montanus_Eurasian Tree Sparrow", "Passer simplex_Desert Sparrow", "Passerculus sandwichensis_Savannah Sparrow", "Passerella iliaca_Fox Sparrow", "Passerina amoena_Lazuli Bunting", "Passerina caerulea_Blue Grosbeak", "Passerina ciris_Painted Bunting", "Passerina cyanea_Indigo Bunting", "Passerina versicolor_Varied Bunting", "Pastor roseus_Rosy Starling", "Patagioenas fasciata_Band-tailed Pigeon", "Patagioenas flavirostris_Red-billed Pigeon", "Pelecanus occidentalis_Brown Pelican", "Pelecanus onocrotalus_Great White Pelican", "Perdix perdix_Gray Partridge", "Periparus ater_Coal Tit", "Perisoreus canadensis_Canada Jay", "Perisoreus infaustus_Siberian Jay", "Pernis apivorus_European Honey-buzzard", "Petrochelidon fulva_Cave Swallow", "Petrochelidon pyrrhonota_Cliff Swallow", "Petronia petronia_Rock Sparrow", "Peucaea aestivalis_Bachman's Sparrow", "Peucaea botterii_Botteri's Sparrow", "Peucaea carpalis_Rufous-winged Sparrow", "Peucaea cassinii_Cassin's Sparrow", "Peucedramus taeniatus_Olive Warbler", "Phainopepla nitens_Phainopepla", "Phalacrocorax auritus_Double-crested Cormorant", "Phalacrocorax brasilianus_Neotropic Cormorant", "Phalacrocorax carbo_Great Cormorant", "Phalaenoptilus nuttallii_Common Poorwill", "Phalaropus fulicarius_Red Phalarope", "Phalaropus lobatus_Red-necked Phalarope", "Phasianus colchicus_Ring-necked Pheasant", "Pheucticus ludovicianus_Rose-breasted Grosbeak", "Pheucticus melanocephalus_Black-headed Grosbeak", "Phoebastria nigripes_Black-footed Albatross", "Phoenicopterus roseus_Greater Flamingo", "Phoenicurus moussieri_Moussier's Redstart", "Phoenicurus ochruros_Black Redstart", "Phoenicurus phoenicurus_Common Redstart", "Phylloscopus bonelli_Western Bonelli's Warbler", "Phylloscopus borealis_Arctic Warbler", "Phylloscopus collybita_Common Chiffchaff", "Phylloscopus fuscatus_Dusky Warbler", "Phylloscopus ibericus_Iberian Chiffchaff", "Phylloscopus inornatus_Yellow-browed Warbler", "Phylloscopus nitidus_Green Warbler", "Phylloscopus orientalis_Eastern Bonelli's Warbler", "Phylloscopus proregulus_Pallas's Leaf Warbler", "Phylloscopus sibilatrix_Wood Warbler", "Phylloscopus sindianus_Mountain Chiffchaff", "Phylloscopus trochiloides_Greenish Warbler", "Phylloscopus trochilus_Willow Warbler", "Pica hudsonia_Black-billed Magpie", "Pica nuttalli_Yellow-billed Magpie", "Pica pica_Eurasian Magpie", "Picoides arcticus_Black-backed Woodpecker", "Picoides dorsalis_American Three-toed Woodpecker", "Picoides tridactylus_Eurasian Three-toed Woodpecker", "Picus canus_Gray-headed Woodpecker", "Picus vaillantii_Levaillant's Woodpecker", "Picus viridis_Eurasian Green Woodpecker", "Pinicola enucleator_Pine Grosbeak", "Pipilo chlorurus_Green-tailed Towhee", "Pipilo erythrophthalmus_Eastern Towhee", "Pipilo maculatus_Spotted Towhee", "Piranga flava_Hepatic Tanager", "Piranga ludoviciana_Western Tanager", "Piranga olivacea_Scarlet Tanager", "Piranga rubra_Summer Tanager", "Pitangus sulphuratus_Great Kiskadee", "Platalea ajaja_Roseate Spoonbill", "Platalea leucorodia_Eurasian Spoonbill", "Plectrophenax nivalis_Snow Bunting", "Plegadis chihi_White-faced Ibis", "Plegadis falcinellus_Glossy Ibis", "Pluvialis apricaria_European Golden-Plover", "Pluvialis dominica_American Golden-Plover", "Pluvialis fulva_Pacific Golden-Plover", "Pluvialis squatarola_Black-bellied Plover", "Podiceps auritus_Horned Grebe", "Podiceps cristatus_Great Crested Grebe", "Podiceps grisegena_Red-necked Grebe", "Podiceps nigricollis_Eared Grebe", "Podilymbus podiceps_Pied-billed Grebe", "Poecile atricapillus_Black-capped Chickadee", "Poecile carolinensis_Carolina Chickadee", "Poecile cinctus_Gray-headed Chickadee", "Poecile gambeli_Mountain Chickadee", "Poecile hudsonicus_Boreal Chickadee", "Poecile lugubris_Sombre Tit", "Poecile montanus_Willow Tit", "Poecile palustris_Marsh Tit", "Poecile rufescens_Chestnut-backed Chickadee", "Poecile sclateri_Mexican Chickadee", "Polioptila caerulea_Blue-gray Gnatcatcher", "Polioptila californica_California Gnatcatcher", "Polioptila melanura_Black-tailed Gnatcatcher", "Pooecetes gramineus_Vesper Sparrow", "Porphyrio martinica_Purple Gallinule", "Porphyrio poliocephalus_Gray-headed Swamphen", "Porphyrio porphyrio_Western Swamphen", "Porzana carolina_Sora", "Porzana porzana_Spotted Crake", "Prinia gracilis_Graceful Prinia", "Progne subis_Purple Martin", "Protonotaria citrea_Prothonotary Warbler", "Prunella collaris_Alpine Accentor", "Prunella modularis_Dunnock", "Prunella ocularis_Radde's Accentor", "Psaltriparus minimus_Bushtit", "Psilorhinus morio_Brown Jay", "Psiloscops flammeolus_Flammulated Owl", "Psittacara holochlorus_Green Parakeet", "Psittacula eupatria_Alexandrine Parakeet", "Psittacula krameri_Rose-ringed Parakeet", "Pterocles alchata_Pin-tailed Sandgrouse", "Pterocles coronatus_Crowned Sandgrouse", "Pterocles exustus_Chestnut-bellied Sandgrouse", "Pterocles orientalis_Black-bellied Sandgrouse", "Pterocles senegallus_Spotted Sandgrouse", "Ptyonoprogne fuligula_Rock Martin", "Ptyonoprogne rupestris_Eurasian Crag-Martin", "Puffinus puffinus_Manx Shearwater", "Pycnonotus barbatus_Common Bulbul", "Pycnonotus jocosus_Red-whiskered Bulbul", "Pycnonotus xanthopygos_White-spectacled Bulbul", "Pyrocephalus rubinus_Vermilion Flycatcher", "Pyrrhocorax graculus_Yellow-billed Chough", "Pyrrhocorax pyrrhocorax_Red-billed Chough", "Pyrrhula pyrrhula_Eurasian Bullfinch", "Quiscalus major_Boat-tailed Grackle", "Quiscalus mexicanus_Great-tailed Grackle", "Quiscalus quiscula_Common Grackle", "Rallus aquaticus_Water Rail", "Rallus crepitans_Clapper Rail", "Rallus elegans_King Rail", "Rallus limicola_Virginia Rail", "Rallus obsoletus_Ridgway's Rail", "Recurvirostra americana_American Avocet", "Recurvirostra avosetta_Pied Avocet", "Regulus calendula_Ruby-crowned Kinglet", "Regulus ignicapilla_Common Firecrest", "Regulus regulus_Goldcrest", "Regulus satrapa_Golden-crowned Kinglet", "Remiz pendulinus_Eurasian Penduline-Tit", "Rhodospiza obsoleta_Desert Finch", "Rhodostethia rosea_Ross's Gull", "Rhynchophanes mccownii_McCown's Longspur", "Riparia riparia_Bank Swallow", "Rissa tridactyla_Black-legged Kittiwake", "Rostrhamus sociabilis_Snail Kite", "Rynchops niger_Black Skimmer", "Salpinctes obsoletus_Rock Wren", "Saxicola maurus_Siberian Stonechat", "Saxicola rubetra_Whinchat", "Saxicola rubicola_European Stonechat", "Sayornis nigricans_Black Phoebe", "Sayornis phoebe_Eastern Phoebe", "Sayornis saya_Say's Phoebe", "Scolopax minor_American Woodcock", "Scolopax rusticola_Eurasian Woodcock", "Scotocerca inquieta_Scrub Warbler", "Seiurus aurocapilla_Ovenbird", "Selasphorus calliope_Calliope Hummingbird", "Selasphorus platycercus_Broad-tailed Hummingbird", "Selasphorus rufus_Rufous Hummingbird", "Selasphorus sasin_Allen's Hummingbird", "Serinus pusillus_Fire-fronted Serin", "Serinus serinus_European Serin", "Setophaga americana_Northern Parula", "Setophaga caerulescens_Black-throated Blue Warbler", "Setophaga castanea_Bay-breasted Warbler", "Setophaga cerulea_Cerulean Warbler", "Setophaga chrysoparia_Golden-cheeked Warbler", "Setophaga citrina_Hooded Warbler", "Setophaga coronata_Yellow-rumped Warbler", "Setophaga discolor_Prairie Warbler", "Setophaga dominica_Yellow-throated Warbler", "Setophaga fusca_Blackburnian Warbler", "Setophaga graciae_Grace's Warbler", "Setophaga kirtlandii_Kirtland's Warbler", "Setophaga magnolia_Magnolia Warbler", "Setophaga nigrescens_Black-throated Gray Warbler", "Setophaga occidentalis_Hermit Warbler", "Setophaga palmarum_Palm Warbler", "Setophaga pensylvanica_Chestnut-sided Warbler", "Setophaga petechia_Yellow Warbler", "Setophaga pinus_Pine Warbler", "Setophaga pitiayumi_Tropical Parula", "Setophaga ruticilla_American Redstart", "Setophaga striata_Blackpoll Warbler", "Setophaga tigrina_Cape May Warbler", "Setophaga townsendi_Townsend's Warbler", "Setophaga virens_Black-throated Green Warbler", "Sialia currucoides_Mountain Bluebird", "Sialia mexicana_Western Bluebird", "Sialia sialis_Eastern Bluebird", "Sitta canadensis_Red-breasted Nuthatch", "Sitta carolinensis_White-breasted Nuthatch", "Sitta europaea_Eurasian Nuthatch", "Sitta ledanti_Algerian Nuthatch", "Sitta neumayer_Western Rock Nuthatch", "Sitta pusilla_Brown-headed Nuthatch", "Sitta pygmaea_Pygmy Nuthatch", "Sitta tephronota_Eastern Rock Nuthatch", "Somateria mollissima_Common Eider", "Somateria spectabilis_King Eider", "Spatula clypeata_Northern Shoveler", "Spatula discors_Blue-winged Teal", "Spatula querquedula_Garganey", "Sphyrapicus nuchalis_Red-naped Sapsucker", "Sphyrapicus ruber_Red-breasted Sapsucker", "Sphyrapicus thyroideus_Williamson's Sapsucker", "Sphyrapicus varius_Yellow-bellied Sapsucker", "Spinus lawrencei_Lawrence's Goldfinch", "Spinus pinus_Pine Siskin", "Spinus psaltria_Lesser Goldfinch", "Spinus spinus_Eurasian Siskin", "Spinus tristis_American Goldfinch", "Spiza americana_Dickcissel", "Spizella atrogularis_Black-chinned Sparrow", "Spizella breweri_Brewer's Sparrow", "Spizella pallida_Clay-colored Sparrow", "Spizella passerina_Chipping Sparrow", "Spizella pusilla_Field Sparrow", "Spizelloides arborea_American Tree Sparrow", "Stelgidopteryx serripennis_Northern Rough-winged Swallow", "Stercorarius longicaudus_Long-tailed Jaeger", "Stercorarius maccormicki_South Polar Skua", "Stercorarius parasiticus_Parasitic Jaeger", "Stercorarius skua_Great Skua", "Sterna dougallii_Roseate Tern", "Sterna forsteri_Forster's Tern", "Sterna hirundo_Common Tern", "Sterna paradisaea_Arctic Tern", "Sternula albifrons_Little Tern", "Sternula antillarum_Least Tern", "Streptopelia decaocto_Eurasian Collared-Dove", "Streptopelia senegalensis_Laughing Dove", "Streptopelia turtur_European Turtle-Dove", "Strix aluco_Tawny Owl", "Strix nebulosa_Great Gray Owl", "Strix uralensis_Ural Owl", "Strix varia_Barred Owl", "Sturnella magna_Eastern Meadowlark", "Sturnella neglecta_Western Meadowlark", "Sturnus unicolor_Spotless Starling", "Sturnus vulgaris_European Starling", "Surnia ulula_Northern Hawk Owl", "Sylvia atricapilla_Eurasian Blackcap", "Sylvia borin_Garden Warbler", "Sylvia cantillans_Subalpine Warbler", "Sylvia communis_Greater Whitethroat", "Sylvia conspicillata_Spectacled Warbler", "Sylvia crassirostris_Eastern Orphean Warbler", "Sylvia curruca_Lesser Whitethroat", "Sylvia deserticola_Tristram's Warbler", "Sylvia hortensis_Western Orphean Warbler", "Sylvia melanocephala_Sardinian Warbler", "Sylvia mystacea_Menetries's Warbler", "Sylvia nana_Asian Desert Warbler", "Sylvia nisoria_Barred Warbler", "Sylvia sarda_Marmora's Warbler", "Sylvia subalpina_Moltoni's Warbler", "Sylvia undata_Dartford Warbler", "Tachybaptus dominicus_Least Grebe", "Tachybaptus ruficollis_Little Grebe", "Tachycineta bicolor_Tree Swallow", "Tachycineta thalassina_Violet-green Swallow", "Tadorna ferruginea_Ruddy Shelduck", "Tadorna tadorna_Common Shelduck", "Tarsiger cyanurus_Red-flanked Bluetail", "Tchagra senegalus_Black-crowned Tchagra", "Tetrao tetrix_Black Grouse", "Tetrao urogallus_Western Capercaillie", "Tetrastes bonasia_Hazel Grouse", "Tetrax tetrax_Little Bustard", "Thalasseus elegans_Elegant Tern", "Thalasseus maximus_Royal Tern", "Thalasseus sandvicensis_Sandwich Tern", "Thryomanes bewickii_Bewick's Wren", "Thryothorus ludovicianus_Carolina Wren", "Tichodroma muraria_Wallcreeper", "Toxostoma bendirei_Bendire's Thrasher", "Toxostoma crissale_Crissal Thrasher", "Toxostoma curvirostre_Curve-billed Thrasher", "Toxostoma lecontei_LeConte's Thrasher", "Toxostoma longirostre_Long-billed Thrasher", "Toxostoma redivivum_California Thrasher", "Toxostoma rufum_Brown Thrasher", "Tringa erythropus_Spotted Redshank", "Tringa flavipes_Lesser Yellowlegs", "Tringa glareola_Wood Sandpiper", "Tringa incana_Wandering Tattler", "Tringa melanoleuca_Greater Yellowlegs", "Tringa nebularia_Common Greenshank", "Tringa ochropus_Green Sandpiper", "Tringa semipalmata_Willet", "Tringa solitaria_Solitary Sandpiper", "Tringa stagnatilis_Marsh Sandpiper", "Tringa totanus_Common Redshank", "Troglodytes aedon_House Wren", "Troglodytes hiemalis_Winter Wren", "Troglodytes pacificus_Pacific Wren", "Troglodytes troglodytes_Eurasian Wren", "Trogon elegans_Elegant Trogon", "Turdoides fulva_Fulvous Chatterer", "Turdus grayi_Clay-colored Thrush", "Turdus iliacus_Redwing", "Turdus merula_Eurasian Blackbird", "Turdus migratorius_American Robin", "Turdus philomelos_Song Thrush", "Turdus pilaris_Fieldfare", "Turdus torquatus_Ring Ouzel", "Turdus viscivorus_Mistle Thrush", "Tympanuchus cupido_Greater Prairie-Chicken", "Tympanuchus pallidicinctus_Lesser Prairie-Chicken", "Tympanuchus phasianellus_Sharp-tailed Grouse", "Tyrannus couchii_Couch's Kingbird", "Tyrannus crassirostris_Thick-billed Kingbird", "Tyrannus dominicensis_Gray Kingbird", "Tyrannus forficatus_Scissor-tailed Flycatcher", "Tyrannus melancholicus_Tropical Kingbird", "Tyrannus tyrannus_Eastern Kingbird", "Tyrannus verticalis_Western Kingbird", "Tyrannus vociferans_Cassin's Kingbird", "Tyto alba_Barn Owl", "Upupa epops_Eurasian Hoopoe", "Uria aalge_Common Murre", "Uria lomvia_Thick-billed Murre", "Vanellus indicus_Red-wattled Lapwing", "Vanellus leucurus_White-tailed Lapwing", "Vanellus spinosus_Spur-winged Lapwing", "Vanellus vanellus_Northern Lapwing", "Vermivora chrysoptera_Golden-winged Warbler", "Vermivora cyanoptera_Blue-winged Warbler", "Vireo altiloquus_Black-whiskered Vireo", "Vireo atricapilla_Black-capped Vireo", "Vireo bellii_Bell's Vireo", "Vireo cassinii_Cassin's Vireo", "Vireo flavifrons_Yellow-throated Vireo", "Vireo gilvus_Warbling Vireo", "Vireo griseus_White-eyed Vireo", "Vireo huttoni_Hutton's Vireo", "Vireo olivaceus_Red-eyed Vireo", "Vireo philadelphicus_Philadelphia Vireo", "Vireo plumbeus_Plumbeous Vireo", "Vireo solitarius_Blue-headed Vireo", "Vireo vicinior_Gray Vireo", "Xanthocephalus xanthocephalus_Yellow-headed Blackbird", "Xema sabini_Sabine's Gull", "Xenus cinereus_Terek Sandpiper", "Zapornia parva_Little Crake", "Zenaida asiatica_White-winged Dove", "Zenaida macroura_Mourning Dove", "Zonotrichia albicollis_White-throated Sparrow", "Zonotrichia atricapilla_Golden-crowned Sparrow", "Zonotrichia leucophrys_White-crowned Sparrow", "Zonotrichia querula_Harris's Sparrow"];
--------------------------------------------------------------------------------
/js/ui.js:
--------------------------------------------------------------------------------
1 | const { dialog } = require('electron').remote;
2 | const remote = require('electron').remote;
3 |
4 | async function showFileDialog() {
5 |
6 | // Show file dialog to select audio file
7 | const fileDialog = await dialog.showOpenDialog({
8 |
9 | filters: [{name: 'Audio Files', extensions: ['mp3', 'wav'] }],
10 | properties: ['openFile']
11 | });
12 |
13 | // Load audio file
14 | if (fileDialog.filePaths.length > 0) loadAudioFile(fileDialog.filePaths[0]);
15 |
16 | }
17 |
18 | function exitApplication() {
19 |
20 | remote.getCurrentWindow().close()
21 |
22 | }
23 |
24 | function showElement(id, makeFlex=true, empty=false) {
25 |
26 | $('#' + id).removeClass('d-none');
27 | if (makeFlex) $('#' + id).addClass('d-flex');
28 | if (empty) $('#' + id).empty();
29 |
30 | }
31 |
32 | function hideElement(id) {
33 |
34 | $('#' + id).removeClass('d-flex');
35 | $('#' + id).addClass('d-none');
36 |
37 | }
38 |
39 | function hideAll() {
40 |
41 | // File hint div
42 | hideElement('loadFileHint');
43 | hideElement('loadFileHintText');
44 | hideElement('loadFileHintSpinner');
45 | hideElement('loadFileHintLog')
46 |
47 | // Waveform and spec
48 | hideElement('waveformContainer');
49 | hideElement('specContainer');
50 |
51 | // Controls
52 | hideElement('controlsWrapper');
53 |
54 | // Result table
55 | hideElement('resultTableContainer');
56 |
57 | }
58 |
59 | function log(element, text) {
60 |
61 | $('#' + element).html('' + text);
62 |
63 | }
64 |
65 |
66 | ///////////////////////// DO AFTER LOAD ////////////////////////////
67 | window.onload = function () {
68 |
69 | // Set footer year
70 | $('#year').text(new Date().getFullYear());
71 |
72 | // Load model
73 | loadModel()
74 |
75 | };
76 |
77 | /*
78 | $(window).resize(function() {
79 | adjustSpecHeight(true);
80 | });
81 | */
82 |
83 | $(function() {
84 | var $window = $(window);
85 | var width = $window.width();
86 | var height = $window.height();
87 |
88 | setInterval(function () {
89 | if ((width != $window.width()) || (height != $window.height())) {
90 | width = $window.width();
91 | height = $window.height();
92 |
93 | adjustSpecHeight(true);
94 | }
95 | }, 1000);
96 | });
97 |
98 |
--------------------------------------------------------------------------------
/js/wavesurfer.drawer.extended.js:
--------------------------------------------------------------------------------
1 | /*! wavesurfer.js 1.1.1 (Mon, 04 Apr 2016 09:49:47 GMT)
2 | * https://github.com/katspaugh/wavesurfer.js
3 | * @license CC-BY-3.0 */
4 |
5 | 'use strict';
6 |
7 | /**
8 | * Purpose:
9 | * Add methods getFrequencyRGB, getFrequencies, resample, drawSpectrogram
10 | * to WaveSurfer.Drawer.Canvas. These methods are modified versions from the the
11 | * spectrogram plugin (https://github.com/katspaugh/wavesurfer.js/blob/master/plugin/wavesurfer.spectrogram.js)
12 | * to allow the wavesurfer drawer to draw a spectrogram representation when this.params.visualization is
13 | * set to "spectrogram"
14 | * Dependencies:
15 | * WaveSurfer (lib/wavesurfer.min.js & lib/wavesurfer.spectrogram.min.js)
16 | */
17 | WaveSurfer.util.extend(WaveSurfer.Drawer.Canvas, {
18 |
19 | // Takes in integer 0-255 and maps it to rgb string
20 | getFrequencyRGB: function(colorValue) {
21 | if (this.params.colorMap) {
22 | // If the wavesurfer has a specified colour map
23 | var rgb = this.params.colorMap[colorValue];
24 | return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')';
25 | } else {
26 | // If not just use gray scale
27 | return 'rgb(' + colorValue + ',' + colorValue + ',' + colorValue + ')';
28 | }
29 |
30 | },
31 |
32 | getFrequencies: function(buffer) {
33 | var fftSamples = this.params.fftSamples || 512;
34 | var channelOne = Array.prototype.slice.call(buffer.getChannelData(0));
35 | var bufferLength = buffer.length;
36 | var sampleRate = buffer.sampleRate;
37 | var frequencies = [];
38 |
39 | if (! buffer) {
40 | this.fireEvent('error', 'Web Audio buffer is not available');
41 | return;
42 | }
43 |
44 | var noverlap = this.params.noverlap;
45 | if (! noverlap) {
46 | var uniqueSamplesPerPx = buffer.length / this.width;
47 | noverlap = Math.max(0, Math.round(fftSamples - uniqueSamplesPerPx));
48 | }
49 |
50 | var fft = new WaveSurfer.FFT(fftSamples, sampleRate);
51 |
52 | var maxSlicesCount = Math.floor(bufferLength/ (fftSamples - noverlap));
53 |
54 | var currentOffset = 0;
55 |
56 | while (currentOffset + fftSamples < channelOne.length) {
57 | var segment = channelOne.slice(currentOffset, currentOffset + fftSamples);
58 | var spectrum = fft.calculateSpectrum(segment);
59 | var length = fftSamples / 2 + 1;
60 | var array = new Uint8Array(length);
61 | for (var j = 0; j < length; j++) {
62 | array[j] = Math.max(-255, Math.log10(spectrum[j])*45);
63 | }
64 | frequencies.push(array);
65 | currentOffset += (fftSamples - noverlap);
66 | }
67 |
68 | return frequencies;
69 | },
70 |
71 | resample: function(oldMatrix) {
72 | var columnsNumber = this.width;
73 | var newMatrix = [];
74 |
75 | var oldPiece = 1 / oldMatrix.length;
76 | var newPiece = 1 / columnsNumber;
77 |
78 | for (var i = 0; i < columnsNumber; i++) {
79 | var column = new Array(oldMatrix[0].length);
80 |
81 | for (var j = 0; j < oldMatrix.length; j++) {
82 | var oldStart = j * oldPiece;
83 | var oldEnd = oldStart + oldPiece;
84 | var newStart = i * newPiece;
85 | var newEnd = newStart + newPiece;
86 |
87 | var overlap = (oldEnd <= newStart || newEnd <= oldStart) ?
88 | 0 :
89 | Math.min(Math.max(oldEnd, newStart), Math.max(newEnd, oldStart)) -
90 | Math.max(Math.min(oldEnd, newStart), Math.min(newEnd, oldStart));
91 |
92 | if (overlap > 0) {
93 | for (var k = 0; k < oldMatrix[0].length; k++) {
94 | if (column[k] == null) {
95 | column[k] = 0;
96 | }
97 | column[k] += (overlap / newPiece) * oldMatrix[j][k];
98 | }
99 | }
100 | }
101 |
102 | var intColumn = new Uint8Array(oldMatrix[0].length);
103 |
104 | for (var k = 0; k < oldMatrix[0].length; k++) {
105 | intColumn[k] = column[k];
106 | }
107 |
108 | newMatrix.push(intColumn);
109 | }
110 |
111 | return newMatrix;
112 | },
113 |
114 | drawSpectrogram: function (buffer) {
115 | var pixelRatio = this.params.pixelRatio;
116 | var length = buffer.duration;
117 | var height = (this.params.fftSamples / 2) * pixelRatio;
118 | var frequenciesData = this.getFrequencies(buffer);
119 |
120 | var pixels = this.resample(frequenciesData);
121 |
122 | var heightFactor = pixelRatio;
123 |
124 | for (var i = 0; i < pixels.length; i++) {
125 | for (var j = 0; j < pixels[i].length; j++) {
126 | this.waveCc.fillStyle = this.getFrequencyRGB(pixels[i][j]);
127 | this.waveCc.fillRect(i, height - j * heightFactor, 1, heightFactor);
128 | }
129 | }
130 | }
131 | });
132 |
133 | /**
134 | * Override the method WaveSurfer.drawBuffer to pass in the this.backend.buffer to
135 | * WaveSurfer.Drawer.drawPeaks since the buffer is needed to draw the spectrogram
136 | */
137 | WaveSurfer.util.extend(WaveSurfer, {
138 | drawBuffer: function () {
139 | var nominalWidth = Math.round(
140 | this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio
141 | );
142 | var parentWidth = this.drawer.getWidth();
143 | var width = nominalWidth;
144 |
145 | // Fill container
146 | if (this.params.fillParent && (!this.params.scrollParent || nominalWidth < parentWidth)) {
147 | width = parentWidth;
148 | }
149 |
150 | var peaks = this.backend.getPeaks(width);
151 | this.drawer.drawPeaks(peaks, width, this.backend.buffer);
152 | this.fireEvent('redraw', peaks, width);
153 | },
154 | });
155 |
156 | /**
157 | * Override the methods WaveSurfer.Drawer.drawPeaks to support invisible and
158 | * spectrogram representations
159 | */
160 | WaveSurfer.util.extend(WaveSurfer.Drawer, {
161 | drawPeaks: function (peaks, length, buffer) {
162 | this.resetScroll();
163 | this.setWidth(length);
164 | var visualization = this.params.visualization;
165 | if (visualization === 'invisible') {
166 | //draw nothing
167 | } else if (visualization === 'spectrogram' && buffer) {
168 | this.drawSpectrogram(buffer);
169 | } else {
170 | this.params.barWidth ?
171 | this.drawBars(peaks) :
172 | this.drawWave(peaks);
173 | }
174 | }
175 | });
--------------------------------------------------------------------------------
/js/wavesurfer.min.js:
--------------------------------------------------------------------------------
1 | /*! wavesurfer.js 1.1.1 (Mon, 04 Apr 2016 09:49:47 GMT)
2 | * https://github.com/katspaugh/wavesurfer.js
3 | * @license CC-BY-3.0 */
4 | !function (a, b) {
5 | "function" == typeof define && define.amd ? define("wavesurfer", [], function () {
6 | return a.WaveSurfer = b()
7 | }) : "object" == typeof exports ? module.exports = b() : a.WaveSurfer = b()
8 | }(this, function () {
9 | "use strict";
10 | var a = {
11 | defaultParams: {
12 | height: 128,
13 | waveColor: "#999",
14 | progressColor: "#555",
15 | cursorColor: "#333",
16 | cursorWidth: 1,
17 | skipLength: 2,
18 | minPxPerSec: 20,
19 | pixelRatio: window.devicePixelRatio || screen.deviceXDPI / screen.logicalXDPI,
20 | fillParent: !0,
21 | scrollParent: !1,
22 | hideScrollbar: !1,
23 | normalize: !1,
24 | audioContext: null,
25 | container: null,
26 | dragSelection: !0,
27 | loopSelection: !0,
28 | audioRate: 1,
29 | interact: !0,
30 | splitChannels: !1,
31 | channel: -1,
32 | mediaContainer: null,
33 | mediaControls: !1,
34 | renderer: "Canvas",
35 | backend: "WebAudio",
36 | mediaType: "audio",
37 | autoCenter: !0
38 | }, init: function (b) {
39 | if (this.params = a.util.extend({}, this.defaultParams, b), this.container = "string" == typeof b.container ? document.querySelector(this.params.container) : this.params.container, !this.container)throw new Error("Container element not found");
40 | if (null == this.params.mediaContainer ? this.mediaContainer = this.container : "string" == typeof this.params.mediaContainer ? this.mediaContainer = document.querySelector(this.params.mediaContainer) : this.mediaContainer = this.params.mediaContainer, !this.mediaContainer)throw new Error("Media Container element not found");
41 | this.savedVolume = 0, this.isMuted = !1, this.tmpEvents = [], this.currentAjax = null, this.createDrawer(), this.createBackend()
42 | }, createDrawer: function () {
43 | var b = this;
44 | this.drawer = Object.create(a.Drawer[this.params.renderer]), this.drawer.init(this.container, this.params), this.drawer.on("redraw", function () {
45 | b.drawBuffer(), b.drawer.progress(b.backend.getPlayedPercents())
46 | }), this.drawer.on("click", function (a, c) {
47 | setTimeout(function () {
48 | b.seekTo(c)
49 | }, 0)
50 | }), this.drawer.on("scroll", function (a) {
51 | b.fireEvent("scroll", a)
52 | })
53 | }, createBackend: function () {
54 | var b = this;
55 | this.backend && this.backend.destroy(), "AudioElement" == this.params.backend && (this.params.backend = "MediaElement"), "WebAudio" != this.params.backend || a.WebAudio.supportsWebAudio() || (this.params.backend = "MediaElement"), this.backend = Object.create(a[this.params.backend]), this.backend.init(this.params), this.backend.on("finish", function () {
56 | b.fireEvent("finish")
57 | }), this.backend.on("play", function () {
58 | b.fireEvent("play")
59 | }), this.backend.on("pause", function () {
60 | b.fireEvent("pause")
61 | }), this.backend.on("audioprocess", function (a) {
62 | b.fireEvent("audioprocess", a)
63 | })
64 | }, startAnimationLoop: function () {
65 | var a = this, b = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame, c = function () {
66 | if (!a.backend.isPaused()) {
67 | var d = a.backend.getPlayedPercents();
68 | a.drawer.progress(d), a.fireEvent("audioprocess", a.getCurrentTime()), b(c)
69 | }
70 | };
71 | c()
72 | }, getDuration: function () {
73 | return this.backend.getDuration()
74 | }, getCurrentTime: function () {
75 | return this.backend.getCurrentTime()
76 | }, play: function (a, b) {
77 | this.backend.play(a, b), this.startAnimationLoop()
78 | }, pause: function () {
79 | this.backend.pause()
80 | }, playPause: function () {
81 | this.backend.isPaused() ? this.play() : this.pause()
82 | }, isPlaying: function () {
83 | return !this.backend.isPaused()
84 | }, skipBackward: function (a) {
85 | this.skip(-a || -this.params.skipLength)
86 | }, skipForward: function (a) {
87 | this.skip(a || this.params.skipLength)
88 | }, skip: function (a) {
89 | var b = this.getCurrentTime() || 0, c = this.getDuration() || 1;
90 | b = Math.max(0, Math.min(c, b + (a || 0))), this.seekAndCenter(b / c)
91 | }, seekAndCenter: function (a) {
92 | this.seekTo(a), this.drawer.recenter(a)
93 | }, seekTo: function (a) {
94 | var b = this.backend.isPaused(), c = this.params.scrollParent;
95 | b && (this.params.scrollParent = !1), this.backend.seekTo(a * this.getDuration()), this.drawer.progress(this.backend.getPlayedPercents()), b || (this.backend.pause(), this.backend.play()), this.params.scrollParent = c, this.fireEvent("seek", a)
96 | }, stop: function () {
97 | this.pause(), this.seekTo(0), this.drawer.progress(0)
98 | }, setVolume: function (a) {
99 | this.backend.setVolume(a)
100 | }, setPlaybackRate: function (a) {
101 | this.backend.setPlaybackRate(a)
102 | }, toggleMute: function () {
103 | this.isMuted ? (this.backend.setVolume(this.savedVolume), this.isMuted = !1) : (this.savedVolume = this.backend.getVolume(), this.backend.setVolume(0), this.isMuted = !0)
104 | }, toggleScroll: function () {
105 | this.params.scrollParent = !this.params.scrollParent, this.drawBuffer()
106 | }, toggleInteraction: function () {
107 | this.params.interact = !this.params.interact
108 | }, drawBuffer: function () {
109 | var a = Math.round(this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio), b = this.drawer.getWidth(), c = a;
110 | this.params.fillParent && (!this.params.scrollParent || b > a) && (c = b);
111 | var d = this.backend.getPeaks(c);
112 | this.drawer.drawPeaks(d, c), this.fireEvent("redraw", d, c)
113 | }, zoom: function (a) {
114 | this.params.minPxPerSec = a, this.params.scrollParent = !0, this.drawBuffer(), this.seekAndCenter(this.getCurrentTime() / this.getDuration()), this.fireEvent("zoom", a)
115 | }, setChannel: function (a) {
116 | this.params.channel = a, this.drawer.clearWave(), this.drawBuffer(), this.backend.setChannel(a)
117 | }, loadArrayBuffer: function (a) {
118 | this.decodeArrayBuffer(a, function (a) {
119 | this.loadDecodedBuffer(a)
120 | }.bind(this))
121 | }, loadDecodedBuffer: function (a) {
122 | this.backend.load(a), this.drawBuffer(), this.fireEvent("ready")
123 | }, loadBlob: function (a) {
124 | var b = this, c = new FileReader;
125 | c.addEventListener("progress", function (a) {
126 | b.onProgress(a)
127 | }), c.addEventListener("load", function (a) {
128 | b.loadArrayBuffer(a.target.result)
129 | }), c.addEventListener("error", function () {
130 | b.fireEvent("error", "Error reading file")
131 | }), c.readAsArrayBuffer(a), this.empty()
132 | }, load: function (a, b) {
133 | switch (this.params.backend) {
134 | case"WebAudio":
135 | return this.loadBuffer(a);
136 | case"MediaElement":
137 | return this.loadMediaElement(a, b)
138 | }
139 | }, loadBuffer: function (a) {
140 | return this.empty(), this.getArrayBuffer(a, this.loadArrayBuffer.bind(this))
141 | }, loadMediaElement: function (a, b) {
142 | this.empty(), this.backend.load(a, this.mediaContainer, b), this.tmpEvents.push(this.backend.once("canplay", function () {
143 | this.drawBuffer(), this.fireEvent("ready")
144 | }.bind(this)), this.backend.once("error", function (a) {
145 | this.fireEvent("error", a)
146 | }.bind(this))), !b && this.backend.supportsWebAudio() && this.getArrayBuffer(a, function (a) {
147 | this.decodeArrayBuffer(a, function (a) {
148 | this.backend.buffer = a, this.drawBuffer()
149 | }.bind(this))
150 | }.bind(this))
151 | }, decodeArrayBuffer: function (a, b) {
152 | this.backend.decodeArrayBuffer(a, this.fireEvent.bind(this, "decoded"), this.fireEvent.bind(this, "error", "Error decoding audiobuffer")), this.tmpEvents.push(this.once("decoded", b))
153 | }, getArrayBuffer: function (b, c) {
154 | var d = this, e = a.util.ajax({url: b, responseType: "arraybuffer"});
155 | return this.currentAjax = e, this.tmpEvents.push(e.on("progress", function (a) {
156 | d.onProgress(a)
157 | }), e.on("success", function (a, b) {
158 | c(a), d.currentAjax = null
159 | }), e.on("error", function (a) {
160 | d.fireEvent("error", "XHR error: " + a.target.statusText), d.currentAjax = null
161 | })), e
162 | }, onProgress: function (a) {
163 | if (a.lengthComputable)var b = a.loaded / a.total; else b = a.loaded / (a.loaded + 1e6);
164 | this.fireEvent("loading", Math.round(100 * b), a.target)
165 | }, exportPCM: function (a, b, c) {
166 | a = a || 1024, b = b || 1e4, c = c || !1;
167 | var d = this.backend.getPeaks(a, b), e = [].map.call(d, function (a) {
168 | return Math.round(a * b) / b
169 | }), f = JSON.stringify(e);
170 | return c || window.open("data:application/json;charset=utf-8," + encodeURIComponent(f)), f
171 | }, cancelAjax: function () {
172 | this.currentAjax && (this.currentAjax.xhr.abort(), this.currentAjax = null)
173 | }, clearTmpEvents: function () {
174 | this.tmpEvents.forEach(function (a) {
175 | a.un()
176 | })
177 | }, empty: function () {
178 | this.backend.isPaused() || (this.stop(), this.backend.disconnectSource()), this.cancelAjax(), this.clearTmpEvents(), this.drawer.progress(0), this.drawer.setWidth(0), this.drawer.drawPeaks({length: this.drawer.getWidth()}, 0)
179 | }, destroy: function () {
180 | this.fireEvent("destroy"), this.cancelAjax(), this.clearTmpEvents(), this.unAll(), this.backend.destroy(), this.drawer.destroy()
181 | }
182 | };
183 | return a.create = function (b) {
184 | var c = Object.create(a);
185 | return c.init(b), c
186 | }, a.util = {
187 | extend: function (a) {
188 | var b = Array.prototype.slice.call(arguments, 1);
189 | return b.forEach(function (b) {
190 | Object.keys(b).forEach(function (c) {
191 | a[c] = b[c]
192 | })
193 | }), a
194 | }, min: function (a) {
195 | var b = +(1 / 0);
196 | for (var c in a)a[c] < b && (b = a[c]);
197 | return b
198 | }, max: function (a) {
199 | var b = -(1 / 0);
200 | for (var c in a)a[c] > b && (b = a[c]);
201 | return b
202 | }, getId: function () {
203 | return "wavesurfer_" + Math.random().toString(32).substring(2)
204 | }, ajax: function (b) {
205 | var c = Object.create(a.Observer), d = new XMLHttpRequest, e = !1;
206 | return d.open(b.method || "GET", b.url, !0), d.responseType = b.responseType || "json", d.addEventListener("progress", function (a) {
207 | c.fireEvent("progress", a), a.lengthComputable && a.loaded == a.total && (e = !0)
208 | }), d.addEventListener("load", function (a) {
209 | e || c.fireEvent("progress", a), c.fireEvent("load", a), 200 == d.status || 206 == d.status ? c.fireEvent("success", d.response, a) : c.fireEvent("error", a)
210 | }), d.addEventListener("error", function (a) {
211 | c.fireEvent("error", a)
212 | }), d.send(), c.xhr = d, c
213 | }
214 | }, a.Observer = {
215 | on: function (a, b) {
216 | this.handlers || (this.handlers = {});
217 | var c = this.handlers[a];
218 | return c || (c = this.handlers[a] = []), c.push(b), {name: a, callback: b, un: this.un.bind(this, a, b)}
219 | }, un: function (a, b) {
220 | if (this.handlers) {
221 | var c = this.handlers[a];
222 | if (c)if (b)for (var d = c.length - 1; d >= 0; d--)c[d] == b && c.splice(d, 1); else c.length = 0
223 | }
224 | }, unAll: function () {
225 | this.handlers = null
226 | }, once: function (a, b) {
227 | var c = this, d = function () {
228 | b.apply(this, arguments), setTimeout(function () {
229 | c.un(a, d)
230 | }, 0)
231 | };
232 | return this.on(a, d)
233 | }, fireEvent: function (a) {
234 | if (this.handlers) {
235 | var b = this.handlers[a], c = Array.prototype.slice.call(arguments, 1);
236 | b && b.forEach(function (a) {
237 | a.apply(null, c)
238 | })
239 | }
240 | }
241 | }, a.util.extend(a, a.Observer), a.WebAudio = {
242 | scriptBufferSize: 256,
243 | PLAYING_STATE: 0,
244 | PAUSED_STATE: 1,
245 | FINISHED_STATE: 2,
246 | supportsWebAudio: function () {
247 | return !(!window.AudioContext && !window.webkitAudioContext)
248 | },
249 | getAudioContext: function () {
250 | return a.WebAudio.audioContext || (a.WebAudio.audioContext = new (window.AudioContext || window.webkitAudioContext)), a.WebAudio.audioContext
251 | },
252 | getOfflineAudioContext: function (b) {
253 | return a.WebAudio.offlineAudioContext || (a.WebAudio.offlineAudioContext = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 2, b)), a.WebAudio.offlineAudioContext
254 | },
255 | init: function (b) {
256 | this.params = b, this.ac = b.audioContext || this.getAudioContext(), this.lastPlay = this.ac.currentTime, this.startPosition = 0, this.scheduledPause = null, this.states = [Object.create(a.WebAudio.state.playing), Object.create(a.WebAudio.state.paused), Object.create(a.WebAudio.state.finished)], this.createVolumeNode(), this.createScriptNode(), this.createAnalyserNode(), this.setState(this.PAUSED_STATE), this.setPlaybackRate(this.params.audioRate)
257 | },
258 | disconnectFilters: function () {
259 | this.filters && (this.filters.forEach(function (a) {
260 | a && a.disconnect()
261 | }), this.filters = null, this.analyser.connect(this.splitter))
262 | },
263 | setState: function (a) {
264 | this.state !== this.states[a] && (this.state = this.states[a], this.state.init.call(this))
265 | },
266 | setFilter: function () {
267 | this.setFilters([].slice.call(arguments))
268 | },
269 | setFilters: function (a) {
270 | this.disconnectFilters(), a && a.length && (this.filters = a, this.analyser.disconnect(), a.reduce(function (a, b) {
271 | return a.connect(b), b
272 | }, this.analyser).connect(this.splitter))
273 | },
274 | createScriptNode: function () {
275 | this.ac.createScriptProcessor ? this.scriptNode = this.ac.createScriptProcessor(this.scriptBufferSize) : this.scriptNode = this.ac.createJavaScriptNode(this.scriptBufferSize), this.scriptNode.connect(this.ac.destination)
276 | },
277 | addOnAudioProcess: function () {
278 | var a = this;
279 | this.scriptNode.onaudioprocess = function () {
280 | var b = a.getCurrentTime();
281 | b >= a.getDuration() ? (a.setState(a.FINISHED_STATE), a.fireEvent("pause")) : b >= a.scheduledPause ? (a.setState(a.PAUSED_STATE), a.fireEvent("pause")) : a.state === a.states[a.PLAYING_STATE] && a.fireEvent("audioprocess", b)
282 | }
283 | },
284 | removeOnAudioProcess: function () {
285 | this.scriptNode.onaudioprocess = null
286 | },
287 | createChannelNodes: function () {
288 | var a = this.buffer.numberOfChannels;
289 | this.splitter = this.ac.createChannelSplitter(a), this.merger = this.ac.createChannelMerger(a), this.setChannel(this.params.channel), this.analyser.disconnect(), this.analyser.connect(this.splitter), this.merger.connect(this.gainNode)
290 | },
291 | setChannel: function (a) {
292 | var b = this.buffer.numberOfChannels;
293 | this.splitter.disconnect();
294 | for (var c = 0; b > c; c++)this.splitter.connect(this.merger, -1 === a ? c : a, c)
295 | },
296 | createAnalyserNode: function () {
297 | this.analyser = this.ac.createAnalyser(), this.analyser.connect(this.gainNode)
298 | },
299 | createVolumeNode: function () {
300 | this.ac.createGain ? this.gainNode = this.ac.createGain() : this.gainNode = this.ac.createGainNode(), this.gainNode.connect(this.ac.destination)
301 | },
302 | setVolume: function (a) {
303 | this.gainNode.gain.value = a
304 | },
305 | getVolume: function () {
306 | return this.gainNode.gain.value
307 | },
308 | decodeArrayBuffer: function (a, b, c) {
309 | this.offlineAc || (this.offlineAc = this.getOfflineAudioContext(this.ac ? this.ac.sampleRate : 44100)), this.offlineAc.decodeAudioData(a, function (a) {
310 | b(a)
311 | }.bind(this), c)
312 | },
313 | getPeaks: function (a) {
314 | for (var b = this.buffer.length / a, c = ~~(b / 10) || 1, d = this.buffer.numberOfChannels, e = [], f = [], g = 0; d > g; g++)for (var h = e[g] = [], i = this.buffer.getChannelData(g), j = 0; a > j; j++) {
315 | for (var k = ~~(j * b), l = ~~(k + b), m = 0, n = 0, o = k; l > o; o += c) {
316 | var p = i[o];
317 | p > n && (n = p), m > p && (m = p)
318 | }
319 | h[2 * j] = n, h[2 * j + 1] = m, (0 == g || n > f[2 * j]) && (f[2 * j] = n), (0 == g || m < f[2 * j + 1]) && (f[2 * j + 1] = m)
320 | }
321 | return this.params.splitChannels || this.params.channel > -1 ? e : f
322 | },
323 | getPlayedPercents: function () {
324 | return this.state.getPlayedPercents.call(this)
325 | },
326 | disconnectSource: function () {
327 | this.source && this.source.disconnect()
328 | },
329 | destroy: function () {
330 | this.isPaused() || this.pause(), this.unAll(), this.buffer = null, this.disconnectFilters(), this.disconnectSource(), this.gainNode.disconnect(), this.scriptNode.disconnect(), this.merger.disconnect(), this.splitter.disconnect(), this.analyser.disconnect()
331 | },
332 | load: function (a) {
333 | this.startPosition = 0, this.lastPlay = this.ac.currentTime, this.buffer = a, this.createSource(), this.createChannelNodes()
334 | },
335 | createSource: function () {
336 | this.disconnectSource(), this.source = this.ac.createBufferSource(), this.source.start = this.source.start || this.source.noteGrainOn, this.source.stop = this.source.stop || this.source.noteOff, this.source.playbackRate.value = this.playbackRate, this.source.buffer = this.buffer, this.source.connect(this.analyser)
337 | },
338 | isPaused: function () {
339 | return this.state !== this.states[this.PLAYING_STATE]
340 | },
341 | getDuration: function () {
342 | return this.buffer ? this.buffer.duration : 0
343 | },
344 | seekTo: function (a, b) {
345 | return this.scheduledPause = null, null == a && (a = this.getCurrentTime(), a >= this.getDuration() && (a = 0)), null == b && (b = this.getDuration()), this.startPosition = a, this.lastPlay = this.ac.currentTime, this.state === this.states[this.FINISHED_STATE] && this.setState(this.PAUSED_STATE), {
346 | start: a,
347 | end: b
348 | }
349 | },
350 | getPlayedTime: function () {
351 | return (this.ac.currentTime - this.lastPlay) * this.playbackRate
352 | },
353 | play: function (a, b) {
354 | this.createSource();
355 | var c = this.seekTo(a, b);
356 | a = c.start, b = c.end, this.scheduledPause = b, this.source.start(0, a, b - a), this.setState(this.PLAYING_STATE), this.fireEvent("play")
357 | },
358 | pause: function () {
359 | this.scheduledPause = null, this.startPosition += this.getPlayedTime(), this.source && this.source.stop(0), this.setState(this.PAUSED_STATE), this.fireEvent("pause")
360 | },
361 | getCurrentTime: function () {
362 | return this.state.getCurrentTime.call(this)
363 | },
364 | setPlaybackRate: function (a) {
365 | a = a || 1, this.isPaused() ? this.playbackRate = a : (this.pause(), this.playbackRate = a, this.play())
366 | }
367 | }, a.WebAudio.state = {}, a.WebAudio.state.playing = {
368 | init: function () {
369 | this.addOnAudioProcess()
370 | }, getPlayedPercents: function () {
371 | var a = this.getDuration();
372 | return this.getCurrentTime() / a || 0
373 | }, getCurrentTime: function () {
374 | return this.startPosition + this.getPlayedTime()
375 | }
376 | }, a.WebAudio.state.paused = {
377 | init: function () {
378 | this.removeOnAudioProcess()
379 | }, getPlayedPercents: function () {
380 | var a = this.getDuration();
381 | return this.getCurrentTime() / a || 0
382 | }, getCurrentTime: function () {
383 | return this.startPosition
384 | }
385 | }, a.WebAudio.state.finished = {
386 | init: function () {
387 | this.removeOnAudioProcess(), this.fireEvent("finish")
388 | }, getPlayedPercents: function () {
389 | return 1
390 | }, getCurrentTime: function () {
391 | return this.getDuration()
392 | }
393 | }, a.util.extend(a.WebAudio, a.Observer), a.MediaElement = Object.create(a.WebAudio), a.util.extend(a.MediaElement, {
394 | init: function (a) {
395 | this.params = a, this.media = {
396 | currentTime: 0, duration: 0, paused: !0, playbackRate: 1, play: function () {
397 | }, pause: function () {
398 | }
399 | }, this.mediaType = a.mediaType.toLowerCase(), this.elementPosition = a.elementPosition, this.setPlaybackRate(this.params.audioRate)
400 | }, load: function (a, b, c) {
401 | var d = this, e = document.createElement(this.mediaType);
402 | e.controls = this.params.mediaControls, e.autoplay = this.params.autoplay || !1, e.preload = "auto", e.src = a, e.style.width = "100%", e.addEventListener("error", function () {
403 | d.fireEvent("error", "Error loading media element")
404 | }), e.addEventListener("canplay", function () {
405 | d.fireEvent("canplay")
406 | }), e.addEventListener("ended", function () {
407 | d.fireEvent("finish")
408 | }), e.addEventListener("timeupdate", function () {
409 | d.fireEvent("audioprocess", d.getCurrentTime())
410 | });
411 | var f = b.querySelector(this.mediaType);
412 | f && b.removeChild(f), b.appendChild(e), this.media = e, this.peaks = c, this.onPlayEnd = null, this.buffer = null, this.setPlaybackRate(this.playbackRate)
413 | }, isPaused: function () {
414 | return !this.media || this.media.paused
415 | }, getDuration: function () {
416 | var a = this.media.duration;
417 | return a >= 1 / 0 && (a = this.media.seekable.end()), a
418 | }, getCurrentTime: function () {
419 | return this.media && this.media.currentTime
420 | }, getPlayedPercents: function () {
421 | return this.getCurrentTime() / this.getDuration() || 0
422 | }, setPlaybackRate: function (a) {
423 | this.playbackRate = a || 1, this.media.playbackRate = this.playbackRate
424 | }, seekTo: function (a) {
425 | null != a && (this.media.currentTime = a), this.clearPlayEnd()
426 | }, play: function (a, b) {
427 | this.seekTo(a), this.media.play(), b && this.setPlayEnd(b), this.fireEvent("play")
428 | }, pause: function () {
429 | this.media && this.media.pause(), this.clearPlayEnd(), this.fireEvent("pause")
430 | }, setPlayEnd: function (a) {
431 | var b = this;
432 | this.onPlayEnd = function (c) {
433 | c >= a && (b.pause(), b.seekTo(a))
434 | }, this.on("audioprocess", this.onPlayEnd)
435 | }, clearPlayEnd: function () {
436 | this.onPlayEnd && (this.un("audioprocess", this.onPlayEnd), this.onPlayEnd = null)
437 | }, getPeaks: function (b) {
438 | return this.buffer ? a.WebAudio.getPeaks.call(this, b) : this.peaks || []
439 | }, getVolume: function () {
440 | return this.media.volume
441 | }, setVolume: function (a) {
442 | this.media.volume = a
443 | }, destroy: function () {
444 | this.pause(), this.unAll(), this.media && this.media.parentNode && this.media.parentNode.removeChild(this.media), this.media = null
445 | }
446 | }), a.AudioElement = a.MediaElement, a.Drawer = {
447 | init: function (a, b) {
448 | this.container = a, this.params = b, this.width = 0, this.height = b.height * this.params.pixelRatio, this.lastPos = 0, this.initDrawer(b), this.createWrapper(), this.createElements()
449 | }, createWrapper: function () {
450 | this.wrapper = this.container.appendChild(document.createElement("wave")), this.style(this.wrapper, {
451 | display: "block",
452 | position: "relative",
453 | userSelect: "none",
454 | webkitUserSelect: "none",
455 | height: this.params.height + "px"
456 | }), (this.params.fillParent || this.params.scrollParent) && this.style(this.wrapper, {
457 | width: "100%",
458 | overflowX: this.params.hideScrollbar ? "hidden" : "auto",
459 | overflowY: "hidden"
460 | }), this.setupWrapperEvents()
461 | }, handleEvent: function (a) {
462 | a.preventDefault();
463 | var b, c = this.wrapper.getBoundingClientRect(), d = this.width, e = this.getWidth();
464 | return !this.params.fillParent && e > d ? (b = (a.clientX - c.left) * this.params.pixelRatio / d || 0, b > 1 && (b = 1)) : b = (a.clientX - c.left + this.wrapper.scrollLeft) / this.wrapper.scrollWidth || 0, b
465 | }, setupWrapperEvents: function () {
466 | var a = this;
467 | this.wrapper.addEventListener("click", function (b) {
468 | var c = a.wrapper.offsetHeight - a.wrapper.clientHeight;
469 | if (0 != c) {
470 | var d = a.wrapper.getBoundingClientRect();
471 | if (b.clientY >= d.bottom - c)return
472 | }
473 | a.params.interact && a.fireEvent("click", b, a.handleEvent(b))
474 | }), this.wrapper.addEventListener("scroll", function (b) {
475 | a.fireEvent("scroll", b)
476 | })
477 | }, drawPeaks: function (a, b) {
478 | this.resetScroll(), this.setWidth(b), this.params.barWidth ? this.drawBars(a) : this.drawWave(a)
479 | }, style: function (a, b) {
480 | return Object.keys(b).forEach(function (c) {
481 | a.style[c] !== b[c] && (a.style[c] = b[c])
482 | }), a
483 | }, resetScroll: function () {
484 | null !== this.wrapper && (this.wrapper.scrollLeft = 0)
485 | }, recenter: function (a) {
486 | var b = this.wrapper.scrollWidth * a;
487 | this.recenterOnPosition(b, !0)
488 | }, recenterOnPosition: function (a, b) {
489 | var c = this.wrapper.scrollLeft, d = ~~(this.wrapper.clientWidth / 2), e = a - d, f = e - c, g = this.wrapper.scrollWidth - this.wrapper.clientWidth;
490 | if (0 != g) {
491 | if (!b && f >= -d && d > f) {
492 | var h = 5;
493 | f = Math.max(-h, Math.min(h, f)), e = c + f
494 | }
495 | e = Math.max(0, Math.min(g, e)), e != c && (this.wrapper.scrollLeft = e)
496 | }
497 | }, getWidth: function () {
498 | return Math.round(this.container.clientWidth * this.params.pixelRatio)
499 | }, setWidth: function (a) {
500 | a != this.width && (this.width = a, this.params.fillParent || this.params.scrollParent ? this.style(this.wrapper, {width: ""}) : this.style(this.wrapper, {width: ~~(this.width / this.params.pixelRatio) + "px"}), this.updateSize())
501 | }, setHeight: function (a) {
502 | a != this.height && (this.height = a, this.style(this.wrapper, {height: ~~(this.height / this.params.pixelRatio) + "px"}), this.updateSize())
503 | }, progress: function (a) {
504 | var b = 1 / this.params.pixelRatio, c = Math.round(a * this.width) * b;
505 | if (c < this.lastPos || c - this.lastPos >= b) {
506 | if (this.lastPos = c, this.params.scrollParent && this.params.autoCenter) {
507 | var d = ~~(this.wrapper.scrollWidth * a);
508 | this.recenterOnPosition(d)
509 | }
510 | this.updateProgress(a)
511 | }
512 | }, destroy: function () {
513 | this.unAll(), this.wrapper && (this.container.removeChild(this.wrapper), this.wrapper = null)
514 | }, initDrawer: function () {
515 | }, createElements: function () {
516 | }, updateSize: function () {
517 | }, drawWave: function (a, b) {
518 | }, clearWave: function () {
519 | }, updateProgress: function (a) {
520 | }
521 | }, a.util.extend(a.Drawer, a.Observer), a.Drawer.Canvas = Object.create(a.Drawer), a.util.extend(a.Drawer.Canvas, {
522 | createElements: function () {
523 | var a = this.wrapper.appendChild(this.style(document.createElement("canvas"), {
524 | position: "absolute",
525 | zIndex: 1,
526 | left: 0,
527 | top: 0,
528 | bottom: 0
529 | }));
530 | if (this.waveCc = a.getContext("2d"), this.progressWave = this.wrapper.appendChild(this.style(document.createElement("wave"), {
531 | position: "absolute",
532 | zIndex: 2,
533 | left: 0,
534 | top: 0,
535 | bottom: 0,
536 | overflow: "hidden",
537 | width: "0",
538 | display: "none",
539 | boxSizing: "border-box",
540 | borderRightStyle: "solid",
541 | borderRightWidth: this.params.cursorWidth + "px",
542 | borderRightColor: this.params.cursorColor
543 | })), this.params.waveColor != this.params.progressColor) {
544 | var b = this.progressWave.appendChild(document.createElement("canvas"));
545 | this.progressCc = b.getContext("2d")
546 | }
547 | }, updateSize: function () {
548 | var a = Math.round(this.width / this.params.pixelRatio);
549 | this.waveCc.canvas.width = this.width, this.waveCc.canvas.height = this.height, this.style(this.waveCc.canvas, {width: a + "px"}), this.style(this.progressWave, {display: "block"}), this.progressCc && (this.progressCc.canvas.width = this.width, this.progressCc.canvas.height = this.height, this.style(this.progressCc.canvas, {width: a + "px"})), this.clearWave()
550 | }, clearWave: function () {
551 | this.waveCc.clearRect(0, 0, this.width, this.height), this.progressCc && this.progressCc.clearRect(0, 0, this.width, this.height)
552 | }, drawBars: function (a, b) {
553 | if (a[0] instanceof Array) {
554 | var c = a;
555 | if (this.params.splitChannels)return this.setHeight(c.length * this.params.height * this.params.pixelRatio), void c.forEach(this.drawBars, this);
556 | if (this.params.channel > -1) {
557 | if (this.params.channel >= c.length)throw new Error("Channel doesn't exist");
558 | a = c[this.params.channel]
559 | } else a = c[0]
560 | }
561 | var d = [].some.call(a, function (a) {
562 | return 0 > a
563 | });
564 | d && (a = [].filter.call(a, function (a, b) {
565 | return b % 2 == 0
566 | }));
567 | var e = .5 / this.params.pixelRatio, f = this.width, g = this.params.height * this.params.pixelRatio, h = g * b || 0, i = g / 2, j = a.length, k = this.params.barWidth * this.params.pixelRatio, l = Math.max(this.params.pixelRatio, ~~(k / 2)), m = k + l, n = 1;
568 | this.params.normalize && (n = Math.max.apply(Math, a));
569 | var o = j / f;
570 | this.waveCc.fillStyle = this.params.waveColor, this.progressCc && (this.progressCc.fillStyle = this.params.progressColor), [this.waveCc, this.progressCc].forEach(function (b) {
571 | if (b)for (var c = 0; f > c; c += m) {
572 | var d = Math.round(a[Math.floor(c * o)] / n * i);
573 | b.fillRect(c + e, i - d + h, k + e, 2 * d)
574 | }
575 | }, this)
576 | }, drawWave: function (a, b) {
577 | if (a[0] instanceof Array) {
578 | var c = a;
579 | if (this.params.splitChannels)return this.setHeight(c.length * this.params.height * this.params.pixelRatio), void c.forEach(this.drawWave, this);
580 | if (this.params.channel > -1) {
581 | if (this.params.channel >= c.length)throw new Error("Channel doesn't exist");
582 | a = c[this.params.channel]
583 | } else a = c[0]
584 | }
585 | var d = [].some.call(a, function (a) {
586 | return 0 > a
587 | });
588 | if (!d) {
589 | for (var e = [], f = 0, g = a.length; g > f; f++)e[2 * f] = a[f], e[2 * f + 1] = -a[f];
590 | a = e
591 | }
592 | var h = .5 / this.params.pixelRatio, i = this.params.height * this.params.pixelRatio, j = i * b || 0, k = i / 2, l = ~~(a.length / 2), m = 1;
593 | this.params.fillParent && this.width != l && (m = this.width / l);
594 | var n = 1;
595 | if (this.params.normalize) {
596 | var o = Math.max.apply(Math, a), p = Math.min.apply(Math, a);
597 | n = -p > o ? -p : o
598 | }
599 | this.waveCc.fillStyle = this.params.waveColor, this.progressCc && (this.progressCc.fillStyle = this.params.progressColor), [this.waveCc, this.progressCc].forEach(function (b) {
600 | if (b) {
601 | b.beginPath(), b.moveTo(h, k + j);
602 | for (var c = 0; l > c; c++) {
603 | var d = Math.round(a[2 * c] / n * k);
604 | b.lineTo(c * m + h, k - d + j)
605 | }
606 | for (var c = l - 1; c >= 0; c--) {
607 | var d = Math.round(a[2 * c + 1] / n * k);
608 | b.lineTo(c * m + h, k - d + j)
609 | }
610 | b.closePath(), b.fill(), b.fillRect(0, k + j - h, this.width, h)
611 | }
612 | }, this)
613 | }, updateProgress: function (a) {
614 | var b = Math.round(this.width * a) / this.params.pixelRatio;
615 | this.style(this.progressWave, {width: b + "px"})
616 | }
617 | }), a.Drawer.MultiCanvas = Object.create(a.Drawer), a.util.extend(a.Drawer.MultiCanvas, {
618 | initDrawer: function (a) {
619 | if (this.maxCanvasWidth = null != a.maxCanvasWidth ? a.maxCanvasWidth : 4e3, this.maxCanvasElementWidth = Math.round(this.maxCanvasWidth / this.params.pixelRatio), this.maxCanvasWidth <= 1)throw"maxCanvasWidth must be greater than 1.";
620 | if (this.maxCanvasWidth % 2 == 1)throw"maxCanvasWidth must be an even number.";
621 | this.hasProgressCanvas = this.params.waveColor != this.params.progressColor, this.halfPixel = .5 / this.params.pixelRatio, this.canvases = []
622 | }, createElements: function () {
623 | this.progressWave = this.wrapper.appendChild(this.style(document.createElement("wave"), {
624 | position: "absolute",
625 | zIndex: 2,
626 | left: 0,
627 | top: 0,
628 | bottom: 0,
629 | overflow: "hidden",
630 | width: "0",
631 | display: "none",
632 | boxSizing: "border-box",
633 | borderRightStyle: "solid",
634 | borderRightWidth: this.params.cursorWidth + "px",
635 | borderRightColor: this.params.cursorColor
636 | })), this.addCanvas()
637 | }, updateSize: function () {
638 | for (var a = Math.round(this.width / this.params.pixelRatio), b = Math.ceil(a / this.maxCanvasElementWidth); this.canvases.length < b;)this.addCanvas();
639 | for (; this.canvases.length > b;)this.removeCanvas();
640 | for (var c in this.canvases) {
641 | var d = this.maxCanvasWidth + 2 * Math.ceil(this.params.pixelRatio / 2);
642 | c == this.canvases.length - 1 && (d = this.width - this.maxCanvasWidth * (this.canvases.length - 1)), this.updateDimensions(this.canvases[c], d, this.height), this.clearWave(this.canvases[c])
643 | }
644 | }, addCanvas: function () {
645 | var a = {}, b = this.maxCanvasElementWidth * this.canvases.length;
646 | a.wave = this.wrapper.appendChild(this.style(document.createElement("canvas"), {
647 | position: "absolute",
648 | zIndex: 1,
649 | left: b + "px",
650 | top: 0,
651 | bottom: 0
652 | })), a.waveCtx = a.wave.getContext("2d"), this.hasProgressCanvas && (a.progress = this.progressWave.appendChild(this.style(document.createElement("canvas"), {
653 | position: "absolute",
654 | left: b + "px",
655 | top: 0,
656 | bottom: 0
657 | })), a.progressCtx = a.progress.getContext("2d")), this.canvases.push(a)
658 | }, removeCanvas: function () {
659 | var a = this.canvases.pop();
660 | a.wave.parentElement.removeChild(a.wave), this.hasProgressCanvas && a.progress.parentElement.removeChild(a.progress)
661 | }, updateDimensions: function (a, b, c) {
662 | var d = Math.round(b / this.params.pixelRatio);
663 | a.waveCtx.canvas.width = b, a.waveCtx.canvas.height = c, this.style(a.waveCtx.canvas, {width: d + "px"}), this.style(this.progressWave, {display: "block"}), this.hasProgressCanvas && (a.progressCtx.canvas.width = b, a.progressCtx.canvas.height = c, this.style(a.progressCtx.canvas, {width: d + "px"}))
664 | }, clearWave: function (a) {
665 | a.waveCtx.clearRect(0, 0, a.waveCtx.canvas.width, a.waveCtx.canvas.height), this.hasProgressCanvas && a.progressCtx.clearRect(0, 0, a.progressCtx.canvas.width, a.progressCtx.canvas.height)
666 | }, drawBars: function (b, c) {
667 | if (b[0] instanceof Array) {
668 | var d = b;
669 | if (this.params.splitChannels)return this.setHeight(d.length * this.params.height * this.params.pixelRatio), void d.forEach(this.drawBars, this);
670 | b = d[0]
671 | }
672 | var e = [].some.call(b, function (a) {
673 | return 0 > a
674 | });
675 | e && (b = [].filter.call(b, function (a, b) {
676 | return b % 2 == 0
677 | }));
678 | var f = this.width, g = this.params.height * this.params.pixelRatio, h = g * c || 0, i = g / 2, j = b.length, k = this.params.barWidth * this.params.pixelRatio, l = Math.max(this.params.pixelRatio, ~~(k / 2)), m = k + l, n = 1;
679 | this.params.normalize && (n = a.util.max(b));
680 | var o = j / f;
681 | this.canvases[0].waveCtx.fillStyle = this.params.waveColor, this.canvases[0].progressCtx && (this.canvases[0].progressCtx.fillStyle = this.params.progressColor);
682 | for (var p = 0; f > p; p += m) {
683 | var q = Math.round(b[Math.floor(p * o)] / n * i);
684 | this.fillRect(p + this.halfPixel, i - q + h, k + this.halfPixel, 2 * q)
685 | }
686 | }, drawWave: function (b, c) {
687 | if (b[0] instanceof Array) {
688 | var d = b;
689 | if (this.params.splitChannels)return this.setHeight(d.length * this.params.height * this.params.pixelRatio), void d.forEach(this.drawWave, this);
690 | b = d[0]
691 | }
692 | var e = [].some.call(b, function (a) {
693 | return 0 > a
694 | });
695 | if (!e) {
696 | for (var f = [], g = 0, h = b.length; h > g; g++)f[2 * g] = b[g], f[2 * g + 1] = -b[g];
697 | b = f
698 | }
699 | var i = this.params.height * this.params.pixelRatio, j = i * c || 0, k = i / 2, l = ~~(b.length / 2), m = 1;
700 | this.params.fillParent && this.width != l && (m = this.width / l);
701 | var n = 1;
702 | if (this.params.normalize) {
703 | var o = a.util.max(b), p = a.util.min(b);
704 | n = -p > o ? -p : o
705 | }
706 | this.drawLine(l, b, n, k, m, j), this.fillRect(0, k + j - this.halfPixel, this.width, this.halfPixel)
707 | }, drawLine: function (a, b, c, d, e, f) {
708 | for (var g in this.canvases) {
709 | var h = this.canvases[g];
710 | this.setFillStyles(h), this.drawLineToContext(h.waveCtx, g, b, c, d, e, f), this.drawLineToContext(h.progressCtx, g, b, c, d, e, f)
711 | }
712 | }, drawLineToContext: function (a, b, c, d, e, f, g) {
713 | if (a) {
714 | var h = b * this.maxCanvasWidth, i = h + a.canvas.width + 1;
715 | a.beginPath(), a.moveTo(this.halfPixel, e + g);
716 | for (var j = h; i > j; j++) {
717 | var k = Math.round(c[2 * j] / d * e);
718 | a.lineTo((j - h) * f + this.halfPixel, e - k + g)
719 | }
720 | for (var j = i - 1; j >= h; j--) {
721 | var k = Math.round(c[2 * j + 1] / d * e);
722 | a.lineTo((j - h) * f + this.halfPixel, e - k + g)
723 | }
724 | a.closePath(), a.fill()
725 | }
726 | }, fillRect: function (a, b, c, d) {
727 | for (var e in this.canvases) {
728 | var f = this.canvases[e], g = e * this.maxCanvasWidth, h = {
729 | x1: Math.max(a, e * this.maxCanvasWidth),
730 | y1: b,
731 | x2: Math.min(a + c, e * this.maxCanvasWidth + f.waveCtx.canvas.width),
732 | y2: b + d
733 | };
734 | h.x1 < h.x2 && (this.setFillStyles(f), this.fillRectToContext(f.waveCtx, h.x1 - g, h.y1, h.x2 - h.x1, h.y2 - h.y1), this.fillRectToContext(f.progressCtx, h.x1 - g, h.y1, h.x2 - h.x1, h.y2 - h.y1))
735 | }
736 | }, fillRectToContext: function (a, b, c, d, e) {
737 | a && a.fillRect(b, c, d, e)
738 | }, setFillStyles: function (a) {
739 | a.waveCtx.fillStyle = this.params.waveColor, this.hasProgressCanvas && (a.progressCtx.fillStyle = this.params.progressColor)
740 | }, updateProgress: function (a) {
741 | var b = Math.round(this.width * a) / this.params.pixelRatio;
742 | this.style(this.progressWave, {width: b + "px"})
743 | }
744 | }), function () {
745 | var b = function () {
746 | var b = document.querySelectorAll("wavesurfer");
747 | Array.prototype.forEach.call(b, function (b) {
748 | var c = a.util.extend({container: b, backend: "MediaElement", mediaControls: !0}, b.dataset);
749 | b.style.display = "block";
750 | var d = a.create(c);
751 | if (b.dataset.peaks)var e = JSON.parse(b.dataset.peaks);
752 | d.load(b.dataset.url, e)
753 | })
754 | };
755 | "complete" === document.readyState ? b() : window.addEventListener("load", b)
756 | }(), a
757 | });
--------------------------------------------------------------------------------
/js/wavesurfer.regions.min.js:
--------------------------------------------------------------------------------
1 | /*! wavesurfer.js 1.1.1 (Mon, 04 Apr 2016 09:49:47 GMT)
2 | * https://github.com/katspaugh/wavesurfer.js
3 | * @license CC-BY-3.0 */
4 | 'use strict';
5 |
6 | /**
7 | * Purpose:
8 | * User can add regions over the wavesurfer audio visualizations to help mark segments of the
9 | * audio clip. Original code can be found here:
10 | * https://github.com/katspaugh/wavesurfer.js/blob/master/plugin/wavesurfer.regions.js
11 | * Modifications made:
12 | * - To fix issue when draging region past the end of the wavesurfer representation
13 | * by saving wavesurfer.drawer.wrapper.scrollWidth to this.width on init since
14 | * wavesurfer.drawer.wrapper.scrollWidth changes as regions are dragged outside
15 | * - Trigger region eventUp when user does a mouseup on anywhere on the document instead of just
16 | * the wrapper element
17 | * - Added this.proximity and this.annotation fields
18 | * - Added X button element to the top right corner of each region that deletes the region
19 | * on click. The X icon uses font awesome styling
20 | * - Pass regionUpdateType when the event 'region-update-end' is fired to show if the start or the end of the
21 | * region was updated, or if the whole region was dragged
22 | * - Give smaller regions higher z-indexs
23 | */
24 |
25 | /* Regions manager */
26 | WaveSurfer.Regions = {
27 | init: function (wavesurfer) {
28 | this.wavesurfer = wavesurfer;
29 | this.wrapper = this.wavesurfer.drawer.wrapper;
30 |
31 | /* Id-based hash of regions. */
32 | this.list = {};
33 | },
34 |
35 | /* Add a region. */
36 | add: function (params) {
37 | var region = Object.create(WaveSurfer.Region);
38 | region.init(params, this.wavesurfer);
39 |
40 | this.list[region.id] = region;
41 |
42 | region.on('remove', (function () {
43 | delete this.list[region.id];
44 | }).bind(this));
45 |
46 | return region;
47 | },
48 |
49 | /* Remove all regions. */
50 | clear: function () {
51 | Object.keys(this.list).forEach(function (id) {
52 | this.list[id].remove();
53 | }, this);
54 | },
55 |
56 | enableDragSelection: function (params) {
57 | var my = this;
58 | var drag;
59 | var start;
60 | var region;
61 |
62 | function eventDown(e) {
63 | drag = true;
64 | if (typeof e.targetTouches !== 'undefined' && e.targetTouches.length === 1) {
65 | e.clientX = e.targetTouches[0].clientX;
66 | }
67 | start = my.wavesurfer.drawer.handleEvent(e);
68 | region = null;
69 | }
70 | this.wrapper.addEventListener('mousedown', eventDown);
71 | this.wrapper.addEventListener('touchstart', eventDown);
72 | this.on('disable-drag-selection', function () {
73 | my.wrapper.removeEventListener('touchstart', eventDown);
74 | my.wrapper.removeEventListener('mousedown', eventDown);
75 | });
76 | function eventUp(e) {
77 | drag = false;
78 |
79 | if (region) {
80 | region.fireEvent('update-end', e);
81 | my.wavesurfer.fireEvent('region-update-end', region, e);
82 | }
83 |
84 | region = null;
85 | }
86 |
87 | document.body.addEventListener('mouseup', eventUp);
88 | this.wrapper.addEventListener('touchend', eventUp);
89 | this.on('disable-drag-selection', function () {
90 | my.wrapper.removeEventListener('touchend', eventUp);
91 | document.body.removeEventListener('mouseup', eventUp);
92 | });
93 | function eventMove(e) {
94 | if (!drag) {
95 | return;
96 | }
97 |
98 | if (!region) {
99 | params['x'] = e.x;
100 | region = my.add(params || {});
101 | }
102 |
103 | var duration = my.wavesurfer.getDuration();
104 | if (typeof e.targetTouches !== 'undefined' && e.targetTouches.length === 1) {
105 | e.clientX = e.targetTouches[0].clientX;
106 | }
107 | var end = my.wavesurfer.drawer.handleEvent(e);
108 | region.update({
109 | start: Math.min(end * duration, start * duration),
110 | end: Math.max(end * duration, start * duration)
111 | });
112 | }
113 |
114 | this.wrapper.addEventListener('mousemove', eventMove);
115 | this.wrapper.addEventListener('touchmove', eventMove);
116 | this.on('disable-drag-selection', function () {
117 | my.wrapper.removeEventListener('touchmove', eventMove);
118 | my.wrapper.removeEventListener('mousemove', eventMove);
119 | });
120 | },
121 |
122 | disableDragSelection: function () {
123 | this.fireEvent('disable-drag-selection');
124 | }
125 | };
126 |
127 | WaveSurfer.util.extend(WaveSurfer.Regions, WaveSurfer.Observer);
128 |
129 | WaveSurfer.Region = {
130 | /* Helper function to assign CSS styles. */
131 | style: WaveSurfer.Drawer.style,
132 |
133 | init: function (params, wavesurfer) {
134 | this.wavesurfer = wavesurfer;
135 | this.wrapper = wavesurfer.drawer.wrapper;
136 | this.width = wavesurfer.drawer.wrapper.scrollWidth;
137 |
138 | this.id = params.id == null ? WaveSurfer.util.getId() : params.id;
139 | this.start = Number(params.start) || 0;
140 | this.end = params.end == null ?
141 | // small marker-like region
142 | this.start + (4 / this.width) * this.wavesurfer.getDuration() :
143 | Number(params.end);
144 | this.resize = params.resize === undefined ? true : Boolean(params.resize);
145 | this.drag = params.drag === undefined ? false : Boolean(params.drag);
146 | this.loop = Boolean(params.loop);
147 | this.color = params.color || 'rgba(252, 252, 252, 0.5)';
148 | this.data = params.data || {};
149 | this.attributes = params.attributes || {};
150 | this.annotation = params.annotation || '';
151 | this.proximity = params.proximity || '';
152 |
153 | this.maxLength = params.maxLength;
154 | this.minLength = params.minLength;
155 | this.x = params.x || 0;
156 |
157 | this.bindInOut();
158 | this.render();
159 | this.wavesurfer.on('zoom', this.updateRender.bind(this));
160 | this.wavesurfer.fireEvent('region-created', this);
161 |
162 | },
163 |
164 | /* Update region params. */
165 | update: function (params) {
166 | if (null != params.start) {
167 | this.start = Number(params.start);
168 | }
169 | if (null != params.end) {
170 | this.end = Number(params.end);
171 | }
172 | if (null != params.loop) {
173 | this.loop = Boolean(params.loop);
174 | }
175 | if (null != params.color) {
176 | this.color = params.color;
177 | }
178 | if (null != params.data) {
179 | this.data = params.data;
180 | }
181 | if (null != params.resize) {
182 | this.resize = Boolean(params.resize);
183 | }
184 | if (null != params.drag) {
185 | this.drag = Boolean(params.drag);
186 | }
187 | if (null != params.maxLength) {
188 | this.maxLength = Number(params.maxLength);
189 | }
190 | if (null != params.minLength) {
191 | this.minLength = Number(params.minLength);
192 | }
193 | if (null != params.attributes) {
194 | this.attributes = params.attributes;
195 | }
196 | if (null != params.annotation) {
197 | this.annotation = params.annotation;
198 | }
199 | if (null != params.proximity) {
200 | this.proximity = params.proximity;
201 | }
202 |
203 | this.updateRender();
204 | this.fireEvent('update');
205 | this.wavesurfer.fireEvent('region-updated', this);
206 | },
207 |
208 | /* Remove a single region. */
209 | remove: function () {
210 | if (this.element) {
211 | this.wrapper.removeChild(this.element);
212 | this.element = null;
213 | this.fireEvent('remove');
214 | this.wavesurfer.un('zoom', this.updateRender.bind(this));
215 | this.wavesurfer.fireEvent('region-removed', this);
216 | }
217 | },
218 |
219 | /* Play the audio region. */
220 | play: function () {
221 | this.wavesurfer.play(this.start, this.end);
222 | this.fireEvent('play');
223 | this.wavesurfer.fireEvent('region-play', this);
224 | },
225 |
226 | /* Play the region in loop. */
227 | playLoop: function () {
228 | this.play();
229 | this.once('out', this.playLoop.bind(this));
230 | },
231 |
232 | /* Render a region as a DOM element. */
233 | render: function () {
234 | var regionEl = document.createElement('region');
235 | regionEl.className = 'wavesurfer-region';
236 | regionEl.title = this.formatTime(this.start, this.end);
237 | regionEl.setAttribute('data-id', this.id);
238 |
239 | for (var attrname in this.attributes) {
240 | regionEl.setAttribute('data-region-' + attrname, this.attributes[attrname]);
241 | }
242 |
243 | this.style(regionEl, {
244 | position: 'absolute',
245 | zIndex: 2,
246 | height: '100%',
247 | top: '0px',
248 | textAlign: 'center',
249 | //border: '2px solid white',
250 | });
251 |
252 | /* Resize handles */
253 | if (this.resize) {
254 | var handleLeft = regionEl.appendChild(document.createElement('handle'));
255 | var handleRight = regionEl.appendChild(document.createElement('handle'));
256 | handleLeft.className = 'wavesurfer-handle wavesurfer-handle-start';
257 | handleRight.className = 'wavesurfer-handle wavesurfer-handle-end';
258 | var css = {
259 | position: 'absolute',
260 | left: '-10px',
261 | top: '0px',
262 | width: '20%',
263 | maxWidth: '4px',
264 | height: '100%',
265 | padding: '5px'
266 | };
267 | this.style(handleLeft, css);
268 | this.style(handleRight, css);
269 | this.style(handleRight, {
270 | left: '100%'
271 | });
272 | }
273 |
274 | /*
275 | this.deleteRegion = regionEl.appendChild(document.createElement('i'));
276 | this.deleteRegion.className = 'fa fa-times-circle'
277 |
278 | this.style(this.deleteRegion, {
279 | position: 'absolute',
280 | right: '0px',
281 | top: '0px',
282 | cursor: 'pointer',
283 | fontSize: '20px',
284 | borderRadius: '50%',
285 | backgroundColor: 'white',
286 | height: '17px',
287 | width: '17px',
288 | 'z-index': '3000'
289 | });
290 | */
291 |
292 | this.element = this.wrapper.appendChild(regionEl);
293 | this.handleLeft = handleLeft;
294 | this.handleRight = handleRight;
295 | this.updateRender();
296 | this.bindEvents(regionEl);
297 | },
298 |
299 | formatTime: function (start, end) {
300 | return (start == end ? [start] : [start, end]).map(function (time) {
301 | return [
302 | Math.floor((time % 3600) / 60), // minutes
303 | ('00' + Math.floor(time % 60)).slice(-2) // seconds
304 | ].join(':');
305 | }).join('-');
306 | },
307 |
308 | /* Update element's position, width, color. */
309 | updateRender: function (pxPerSec) {
310 | var dur = this.wavesurfer.getDuration();
311 | var width;
312 | if (pxPerSec) {
313 | width = Math.round(this.wavesurfer.getDuration() * pxPerSec);
314 | }
315 | else {
316 | width = this.width;
317 | }
318 |
319 | if (this.start < 0) {
320 | this.start = 0;
321 | this.end = this.end - this.start;
322 | }
323 | if (this.end > dur) {
324 | this.end = dur;
325 | this.start = dur - (this.end - this.start);
326 | }
327 |
328 | if (this.minLength != null) {
329 | this.end = Math.max(this.start + this.minLength, this.end);
330 | }
331 |
332 | if (this.maxLength != null) {
333 | this.end = Math.min(this.start + this.maxLength, this.end);
334 | }
335 |
336 | if (this.element != null) {
337 | var regionWidth = ~~((this.end - this.start) / dur * width);
338 | this.style(this.element, {
339 | left: ~~(this.start / dur * width) + 'px',
340 | width: regionWidth + 'px',
341 | backgroundColor: this.color,
342 | cursor: this.drag ? 'move' : 'default',
343 | zIndex: width - regionWidth
344 | });
345 |
346 | for (var attrname in this.attributes) {
347 | this.element.setAttribute('data-region-' + attrname, this.attributes[attrname]);
348 | }
349 |
350 | this.element.title = this.formatTime(this.start, this.end);
351 | }
352 |
353 | if (this.handleLeft != null) {
354 | this.style(this.handleLeft, {
355 | cursor: this.resize ? 'col-resize' : 'default',
356 | });
357 | }
358 |
359 | if (this.handleRight != null) {
360 | this.style(this.handleRight, {
361 | cursor: this.resize ? 'col-resize' : 'default',
362 | });
363 | }
364 | },
365 |
366 | /* Bind audio events. */
367 | bindInOut: function () {
368 | var my = this;
369 |
370 | my.firedIn = false;
371 | my.firedOut = false;
372 |
373 | var onProcess = function (time) {
374 | if (!my.firedOut && my.firedIn && (my.start >= Math.round(time * 100) / 100 || my.end <= Math.round(time * 100) / 100)) {
375 | my.firedOut = true;
376 | my.firedIn = false;
377 | my.fireEvent('out');
378 | my.wavesurfer.fireEvent('region-out', my);
379 | }
380 | if (!my.firedIn && my.start <= time && my.end > time) {
381 | my.firedIn = true;
382 | my.firedOut = false;
383 | my.fireEvent('in');
384 | my.wavesurfer.fireEvent('region-in', my);
385 | }
386 | };
387 |
388 | this.wavesurfer.backend.on('audioprocess', onProcess);
389 |
390 | this.on('remove', function () {
391 | my.wavesurfer.backend.un('audioprocess', onProcess);
392 | });
393 |
394 | /* Loop playback. */
395 | this.on('out', function () {
396 | if (my.loop) {
397 | my.wavesurfer.play(my.start);
398 | }
399 | });
400 | },
401 |
402 | /* Bind DOM events. */
403 | bindEvents: function () {
404 | var my = this;
405 | this.element.addEventListener('mouseenter', function (e) {
406 | my.fireEvent('mouseenter', e);
407 | my.wavesurfer.fireEvent('region-mouseenter', my, e);
408 | });
409 |
410 | this.element.addEventListener('mouseleave', function (e) {
411 | my.fireEvent('mouseleave', e);
412 | my.wavesurfer.fireEvent('region-mouseleave', my, e);
413 | });
414 |
415 | this.element.addEventListener('click', function (e) {
416 | e.preventDefault();
417 | my.fireEvent('click', e);
418 | my.wavesurfer.fireEvent('region-click', my, e);
419 | });
420 | /*
421 | this.deleteRegion.addEventListener('click', function (e) {
422 | e.stopPropagation();
423 | my.remove();
424 | });
425 | */
426 |
427 | this.element.addEventListener('dblclick', function (e) {
428 | e.stopPropagation();
429 | e.preventDefault();
430 | my.fireEvent('dblclick', e);
431 | my.wavesurfer.fireEvent('region-dblclick', my, e);
432 | });
433 |
434 | /* Drag or resize on mousemove. */
435 | (this.drag || this.resize) && (function () {
436 | var duration = my.wavesurfer.getDuration();
437 | var drag;
438 | var resize;
439 | var moved;
440 | var startTime;
441 |
442 | var onDown = function (e) {
443 | if (e.target.classList.contains('fa-times-circle') || e.target.classList.contains('wavesurfer-handle')) {
444 | e.stopPropagation();
445 | if (e.target.classList.contains('fa-times-circle')) {
446 | my.wavesurfer.fireEvent('region-removed', my, e);
447 | }
448 | }
449 | startTime = my.wavesurfer.drawer.handleEvent(e) * duration;
450 |
451 | if (e.target.tagName.toLowerCase() == 'handle') {
452 | if (e.target.classList.contains('wavesurfer-handle-start')) {
453 | resize = 'start';
454 | } else {
455 | resize = 'end';
456 | }
457 | } else {
458 | drag = true;
459 | }
460 | };
461 | var onUp = function (e) {
462 | if (drag || resize) {
463 | var regionUpdateType = resize ? resize : 'drag';
464 | drag = false;
465 | resize = false;
466 | e.stopPropagation();
467 | e.preventDefault();
468 |
469 | if (moved && (my.drag || my.resize)) {
470 | my.fireEvent('update-end', e);
471 | my.wavesurfer.fireEvent('region-update-end', my, e, regionUpdateType);
472 | moved = false;
473 | }
474 | }
475 | };
476 | var onMove = function (e) {
477 | if (drag || resize) {
478 | moved = true;
479 | var time = my.wavesurfer.drawer.handleEvent(e) * duration;
480 | var delta = time - startTime;
481 | startTime = time;
482 |
483 | // Drag
484 | if (my.drag && drag) {
485 | my.onDrag(delta);
486 | }
487 |
488 | // Resize
489 | if (my.resize && resize) {
490 | my.onResize(delta, resize);
491 | }
492 | }
493 | };
494 |
495 | my.element.addEventListener('mousedown', onDown);
496 | my.wrapper.addEventListener('mousemove', onMove);
497 | document.body.addEventListener('mouseup', onUp);
498 |
499 | my.on('remove', function () {
500 | document.body.removeEventListener('mouseup', onUp);
501 | my.wrapper.removeEventListener('mousemove', onMove);
502 | });
503 |
504 | my.wavesurfer.on('destroy', function () {
505 | document.body.removeEventListener('mouseup', onUp);
506 | });
507 | }());
508 | },
509 |
510 | onDrag: function (delta) {
511 | this.update({
512 | start: this.start + delta,
513 | end: this.end + delta
514 | });
515 | },
516 |
517 | onResize: function (delta, direction) {
518 | if (direction == 'start') {
519 | this.update({
520 | start: Math.min(this.start + delta, this.end),
521 | end: Math.max(this.start + delta, this.end)
522 | });
523 | } else {
524 | this.update({
525 | start: Math.min(this.end + delta, this.start),
526 | end: Math.max(this.end + delta, this.start)
527 | });
528 | }
529 | }
530 | };
531 |
532 | WaveSurfer.util.extend(WaveSurfer.Region, WaveSurfer.Observer);
533 |
534 |
535 | /* Augment WaveSurfer with region methods. */
536 | WaveSurfer.initRegions = function () {
537 | if (!this.regions) {
538 | this.regions = Object.create(WaveSurfer.Regions);
539 | this.regions.init(this);
540 | }
541 | };
542 |
543 | WaveSurfer.addRegion = function (options) {
544 | this.initRegions();
545 | return this.regions.add(options);
546 | };
547 |
548 | WaveSurfer.clearRegions = function () {
549 | this.regions && this.regions.clear();
550 | };
551 |
552 | WaveSurfer.enableDragSelection = function (options) {
553 | this.initRegions();
554 | this.regions.enableDragSelection(options);
555 | };
556 |
557 | WaveSurfer.disableDragSelection = function () {
558 | this.regions.disableDragSelection();
559 | };
--------------------------------------------------------------------------------
/js/wavesurfer.spectrogram.min.js:
--------------------------------------------------------------------------------
1 | /*! wavesurfer.js 1.1.1 (Mon, 04 Apr 2016 09:49:47 GMT)
2 | * https://github.com/katspaugh/wavesurfer.js
3 | * @license CC-BY-3.0 */!function(a,b){"function"==typeof define&&define.amd?define(["wavesurfer.min"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("wavesurfer.js")):b(WaveSurfer)}(this,function(a){"use strict";a.Spectrogram={init:function(a){this.params=a;var b=this.wavesurfer=a.wavesurfer;if(!this.wavesurfer)throw Error("No WaveSurfer instance provided");this.frequenciesDataUrl=a.frequenciesDataUrl;var c=this.drawer=this.wavesurfer.drawer;if(this.container="string"==typeof a.container?document.querySelector(a.container):a.container,!this.container)throw Error("No container for WaveSurfer spectrogram");this.width=c.width,this.pixelRatio=this.params.pixelRatio||b.params.pixelRatio,this.fftSamples=this.params.fftSamples||b.params.fftSamples||512,this.height=this.fftSamples/2,this.noverlap=a.noverlap,this.windowFunc=a.windowFunc,this.alpha=a.alpha,this.createWrapper(),this.createCanvas(),this.render(),b.drawer.wrapper.onscroll=this.updateScroll.bind(this),b.on("redraw",this.render.bind(this)),b.on("destroy",this.destroy.bind(this))},destroy:function(){this.unAll(),this.wrapper&&(this.wrapper.parentNode.removeChild(this.wrapper),this.wrapper=null)},createWrapper:function(){var a=this.container.querySelector("spectrogram");a&&this.container.removeChild(a);var b=this.wavesurfer.params;this.wrapper=this.container.appendChild(document.createElement("spectrogram")),this.drawer.style(this.wrapper,{display:"block",position:"relative",userSelect:"none",webkitUserSelect:"none",height:this.height+"px"}),(b.fillParent||b.scrollParent)&&this.drawer.style(this.wrapper,{width:"100%",overflowX:"hidden",overflowY:"hidden"});var c=this;this.wrapper.addEventListener("click",function(a){a.preventDefault();var b="offsetX"in a?a.offsetX:a.layerX;c.fireEvent("click",b/c.scrollWidth||0)})},createCanvas:function(){var a=this.canvas=this.wrapper.appendChild(document.createElement("canvas"));this.spectrCc=a.getContext("2d"),this.wavesurfer.drawer.style(a,{position:"absolute",zIndex:4})},render:function(){this.updateCanvasStyle(),this.frequenciesDataUrl?this.loadFrequenciesData(this.frequenciesDataUrl):this.getFrequencies(this.drawSpectrogram)},updateCanvasStyle:function(){var a=Math.round(this.width/this.pixelRatio)+"px";this.canvas.width=this.width,this.canvas.height=this.height,this.canvas.style.width=a},drawSpectrogram:function(a,b){for(var c=(b.spectrCc,b.wavesurfer.backend.getDuration(),b.height),d=b.resample(a),e=b.buffer?2/b.buffer.numberOfChannels:1,f=0;fp;p++)o[p]=Math.max(-255,45*Math.log10(n[p]));h.push(o),l+=c-i}b(h,this)},loadFrequenciesData:function(b){var c=this,d=a.util.ajax({url:b});return d.on("success",function(a){c.drawSpectrogram(JSON.parse(a),c)}),d.on("error",function(a){c.fireEvent("error","XHR error: "+a.target.statusText)}),d},updateScroll:function(a){this.wrapper.scrollLeft=a.target.scrollLeft},resample:function(a,b){for(var b=this.width,c=[],d=1/a.length,e=1/b,f=0;b>f;f++){for(var g=new Array(a[0].length),h=0;h=j||i>=l?0:Math.min(Math.max(j,k),Math.max(l,i))-Math.max(Math.min(j,k),Math.min(l,i));if(m>0)for(var n=0;ne;e++)this.windowValues[e]=2/(a-1)*((a-1)/2-Math.abs(e-(a-1)/2));break;case"bartlettHann":for(var e=0;a>e;e++)this.windowValues[e]=.62-.48*Math.abs(e/(a-1)-.5)-.38*Math.cos(2*Math.PI*e/(a-1));break;case"blackman":d=d||.16;for(var e=0;a>e;e++)this.windowValues[e]=(1-d)/2-.5*Math.cos(2*Math.PI*e/(a-1))+d/2*Math.cos(4*Math.PI*e/(a-1));break;case"cosine":for(var e=0;a>e;e++)this.windowValues[e]=Math.cos(Math.PI*e/(a-1)-Math.PI/2);break;case"gauss":d=d||.25;for(var e=0;a>e;e++)this.windowValues[e]=Math.pow(Math.E,-.5*Math.pow((e-(a-1)/2)/(d*(a-1)/2),2));break;case"hamming":for(var e=0;a>e;e++)this.windowValues[e]=.54-.46*Math.cos(2*Math.PI*e/(a-1));break;case"hann":case void 0:for(var e=0;a>e;e++)this.windowValues[e]=.5*(1-Math.cos(2*Math.PI*e/(a-1)));break;case"lanczoz":for(var e=0;a>e;e++)this.windowValues[e]=Math.sin(Math.PI*(2*e/(a-1)-1))/(Math.PI*(2*e/(a-1)-1));break;case"rectangular":for(var e=0;a>e;e++)this.windowValues[e]=1;break;case"triangular":for(var e=0;a>e;e++)this.windowValues[e]=2/a*(a/2-Math.abs(e-(a-1)/2));break;default:throw Error("No such window function '"+c+"'")}for(var e,f=1,g=a>>1;a>f;){for(e=0;f>e;e++)this.reverseTable[e+f]=this.reverseTable[e]+g;f<<=1,g>>=1}for(e=0;a>e;e++)this.sinTable[e]=Math.sin(-Math.PI/e),this.cosTable[e]=Math.cos(-Math.PI/e);this.calculateSpectrum=function(a){var b,c,d,e=this.bufferSize,f=this.cosTable,g=this.sinTable,h=this.reverseTable,i=new Float32Array(e),j=new Float32Array(e),k=2/this.bufferSize,l=Math.sqrt,m=new Float32Array(e/2),n=Math.floor(Math.log(e)/Math.LN2);if(Math.pow(2,n)!==e)throw"Invalid buffer size, must be a power of 2.";if(e!==a.length)throw"Supplied buffer is not the same size as defined FFT. FFT Size: "+e+" Buffer Size: "+a.length;for(var o,p,q,r,s,t,u,v,w=1,x=0;e>x;x++)i[x]=a[h[x]]*this.windowValues[h[x]],j[x]=0;for(;e>w;){o=f[w],p=g[w],q=1,r=0;for(var y=0;w>y;y++){for(var x=y;e>x;)s=x+w,t=q*i[s]-r*j[s],u=q*j[s]+r*i[s],i[s]=i[x]-t,j[s]=j[x]-u,i[x]+=t,j[x]+=u,x+=w<<1;v=q,q=v*o-r*p,r=v*p+r*o}w<<=1}for(var x=0,z=e/2;z>x;x++)b=i[x],c=j[x],d=k*l(b*b+c*c),d>this.peak&&(this.peakBand=x,this.peak=d),m[x]=d;return m}},a.util.extend(a.Spectrogram,a.Observer,a.FFT)});
--------------------------------------------------------------------------------
/js/wavesurfer.timeline.min.js:
--------------------------------------------------------------------------------
1 | (function (root, factory) {
2 | if (typeof define === 'function' && define.amd) {
3 | define(['wavesurfer'], factory);
4 | } else {
5 | root.WaveSurfer.Timeline = factory(root.WaveSurfer);
6 | }
7 | }(this, function (WaveSurfer) {
8 | 'use strict';
9 |
10 | WaveSurfer.Timeline = {
11 | init: function (params) {
12 | this.params = params;
13 | var wavesurfer = this.wavesurfer = params.wavesurfer;
14 |
15 | if (!this.wavesurfer) {
16 | throw Error('No WaveSurfer intance provided');
17 | }
18 |
19 | var drawer = this.drawer = this.wavesurfer.drawer;
20 |
21 | this.container = 'string' == typeof params.container ?
22 | document.querySelector(params.container) : params.container;
23 |
24 | if (!this.container) {
25 | throw Error('No container for WaveSurfer timeline');
26 | }
27 |
28 | this.width = drawer.width;
29 | this.height = this.params.height || 20;
30 | this.notchPercentHeight = this.params.notchPercentHeight || 90;
31 | this.primaryColor = this.params.primaryColor || '#000';
32 | this.secondaryColor = this.params.secondaryColor || '#c0c0c0';
33 | this.primaryFontColor = this.params.primaryFontColor || '#000';
34 | this.secondaryFontColor = this.params.secondaryFontColor || '#000';
35 | this.fontFamily = this.params.fontFamily || 'Arial';
36 | this.fontSize = this.params.fontSize || 10;
37 |
38 | this.createWrapper();
39 | this.createCanvas();
40 | this.render();
41 |
42 | wavesurfer.drawer.wrapper.onscroll = this.updateScroll.bind(this);
43 | wavesurfer.on('redraw', this.render.bind(this));
44 | wavesurfer.on('destroy', this.destroy.bind(this));
45 | },
46 |
47 | destroy: function () {
48 | this.unAll();
49 | if (this.wrapper) {
50 | this.wrapper.parentNode.removeChild(this.wrapper);
51 | this.wrapper = null;
52 | }
53 | },
54 |
55 | createWrapper: function () {
56 | var prevTimeline = this.container.querySelector('timeline');
57 | if (prevTimeline) {
58 | this.container.removeChild(prevTimeline);
59 | }
60 |
61 | var wsParams = this.wavesurfer.params;
62 | this.wrapper = this.container.appendChild(
63 | document.createElement('timeline')
64 | );
65 | this.drawer.style(this.wrapper, {
66 | display: 'block',
67 | position: 'relative',
68 | userSelect: 'none',
69 | webkitUserSelect: 'none',
70 | height: this.height + 'px'
71 | });
72 |
73 | if (wsParams.fillParent || wsParams.scrollParent) {
74 | this.drawer.style(this.wrapper, {
75 | width: '100%',
76 | overflowX: 'hidden',
77 | overflowY: 'hidden'
78 | });
79 | }
80 |
81 | var my = this;
82 | this.wrapper.addEventListener('click', function (e) {
83 | e.preventDefault();
84 | var relX = 'offsetX' in e ? e.offsetX : e.layerX;
85 | my.fireEvent('click', (relX / my.wrapper.scrollWidth) || 0);
86 | });
87 | },
88 |
89 | createCanvas: function () {
90 | var canvas = this.canvas = this.wrapper.appendChild(
91 | document.createElement('canvas')
92 | );
93 |
94 | this.timeCc = canvas.getContext('2d');
95 |
96 | this.wavesurfer.drawer.style(canvas, {
97 | position: 'absolute',
98 | zIndex: 4
99 | });
100 | },
101 |
102 | render: function () {
103 | this.updateCanvasStyle();
104 | this.drawTimeCanvas();
105 | },
106 |
107 | updateCanvasStyle: function () {
108 | var width = this.drawer.wrapper.scrollWidth;
109 | this.canvas.width = width * this.wavesurfer.params.pixelRatio;
110 | this.canvas.height = this.height * this.wavesurfer.params.pixelRatio;;
111 | this.canvas.style.width = width + 'px';
112 | this.canvas.style.height = this.height + 'px';
113 | },
114 |
115 | drawTimeCanvas: function() {
116 | var backend = this.wavesurfer.backend,
117 | wsParams = this.wavesurfer.params,
118 | duration = backend.getDuration();
119 |
120 | if (wsParams.fillParent && !wsParams.scrollParent) {
121 | var width = this.drawer.getWidth();
122 | } else {
123 | width = this.drawer.wrapper.scrollWidth * wsParams.pixelRatio;
124 | }
125 | var pixelsPerSecond = width/duration;
126 |
127 | if (duration > 0) {
128 | var curPixel = 0,
129 | curSeconds = 0,
130 | totalSeconds = parseInt(duration, 10) + 1,
131 | formatTime = function(seconds) {
132 | if (seconds/60 > 1) {
133 | var minutes = parseInt(seconds / 60),
134 | seconds = parseInt(seconds % 60);
135 | seconds = (seconds < 10) ? '0' + seconds : seconds;
136 | return '' + minutes + ':' + seconds;
137 | } else {
138 | return seconds;
139 | }
140 | };
141 |
142 | if (pixelsPerSecond * 1 >= 25) {
143 | var timeInterval = 1;
144 | var primaryLabelInterval = 10;
145 | var secondaryLabelInterval = 5;
146 | } else if (pixelsPerSecond * 5 >= 25) {
147 | var timeInterval = 5;
148 | var primaryLabelInterval = 6;
149 | var secondaryLabelInterval = 2;
150 | } else if (pixelsPerSecond * 15 >= 25) {
151 | var timeInterval = 15;
152 | var primaryLabelInterval = 4;
153 | var secondaryLabelInterval = 2;
154 | } else {
155 | var timeInterval = 60;
156 | var primaryLabelInterval = 4;
157 | var secondaryLabelInterval = 2;
158 | }
159 |
160 | var height1 = this.height - 4,
161 | height2 = (this.height * (this.notchPercentHeight / 100.0)) - 4,
162 | fontSize = this.fontSize * wsParams.pixelRatio;
163 |
164 | for (var i = 0; i < totalSeconds/timeInterval; i++) {
165 | if (i % primaryLabelInterval == 0) {
166 | this.timeCc.fillStyle = this.primaryColor;
167 | this.timeCc.fillRect(curPixel, 0, 1, height1);
168 | this.timeCc.font = fontSize + 'px ' + this.fontFamily;
169 | this.timeCc.fillStyle = this.primaryFontColor;
170 | this.timeCc.fillText(formatTime(curSeconds), curPixel + 5, height1);
171 | } else if (i % secondaryLabelInterval == 0) {
172 | this.timeCc.fillStyle = this.secondaryColor;
173 | this.timeCc.fillRect(curPixel, 0, 1, height1);
174 | this.timeCc.font = fontSize + 'px ' + this.fontFamily;
175 | this.timeCc.fillStyle = this.secondaryFontColor;
176 | this.timeCc.fillText(formatTime(curSeconds), curPixel + 5, height1);
177 | } else {
178 | this.timeCc.fillStyle = this.secondaryColor;
179 | this.timeCc.fillRect(curPixel, 0, 1, height2);
180 | }
181 |
182 | curSeconds += timeInterval;
183 | curPixel += pixelsPerSecond * timeInterval;
184 | }
185 | }
186 | },
187 |
188 | updateScroll: function () {
189 | this.wrapper.scrollLeft = this.drawer.wrapper.scrollLeft;
190 | }
191 | };
192 |
193 | WaveSurfer.util.extend(WaveSurfer.Timeline, WaveSurfer.Observer);
194 |
195 | return WaveSurfer.Timeline;
196 | }));
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 |
2 | const { app, BrowserWindow } = require('electron')
3 | let win
4 |
5 | function createWindow () {
6 | // Create the browser window.
7 | win = new BrowserWindow({
8 | width: 1280,
9 | height: 768,
10 | webPreferences: {
11 | nodeIntegration: true
12 | }
13 | })
14 |
15 | // Set icon
16 | win.setIcon(__dirname + '/img/icon/icon.png');
17 |
18 | // Always maximize
19 | win.maximize()
20 |
21 | // Hide nav bar
22 | win.setMenuBarVisibility(false);
23 |
24 | // and load the index.html of the app.
25 | win.loadFile('index.html')
26 |
27 | // Open the DevTools.
28 | //win.webContents.openDevTools()
29 |
30 | // Emitted when the window is closed.
31 | win.on('closed', () => {
32 | win = null
33 | })
34 | }
35 |
36 | // This method will be called when Electron has finished
37 | app.on('ready', createWindow)
38 |
39 | // Quit when all windows are closed.
40 | app.on('window-all-closed', () => {
41 | if (process.platform !== 'darwin') {
42 | app.quit()
43 | }
44 | })
45 |
46 | app.on('activate', () => {
47 | if (win === null) {
48 | createWindow()
49 | }
50 | })
51 |
--------------------------------------------------------------------------------
/model/group1-shard1of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard1of9.bin
--------------------------------------------------------------------------------
/model/group1-shard2of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard2of9.bin
--------------------------------------------------------------------------------
/model/group1-shard3of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard3of9.bin
--------------------------------------------------------------------------------
/model/group1-shard4of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard4of9.bin
--------------------------------------------------------------------------------
/model/group1-shard5of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard5of9.bin
--------------------------------------------------------------------------------
/model/group1-shard6of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard6of9.bin
--------------------------------------------------------------------------------
/model/group1-shard7of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard7of9.bin
--------------------------------------------------------------------------------
/model/group1-shard8of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard8of9.bin
--------------------------------------------------------------------------------
/model/group1-shard9of9.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kahst/BirdNET-Electron/cc331841d434a3dc802c51c8bff4a42adc964e69/model/group1-shard9of9.bin
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "birdnet-electron",
3 | "version": "0.1.0",
4 | "description": "Electron app for sound file analysis with BirdNET.",
5 | "main": "main.js",
6 | "scripts": {
7 | "start": "electron .",
8 | "export": "electron-packager . --out dist --overwrite"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/kahst/BirdNET-Electron.git"
13 | },
14 | "keywords": [
15 | "Bioacoustics",
16 | "Birdsong",
17 | "DeepLearning",
18 | "Tensorflow.js"
19 | ],
20 | "author": "Stefan Kahl",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/kahst/BirdNET-Electron/issues"
24 | },
25 | "homepage": "https://github.com/kahst/BirdNET-Electron#readme",
26 | "devDependencies": {
27 | "electron": "^7.1.7",
28 | "electron-packager": "^14.1.1"
29 | },
30 | "dependencies": {
31 | "@tensorflow/tfjs": "^1.5.1",
32 | "array-normalize": "^1.1.4",
33 | "audio-loader": "^1.0.3",
34 | "audio-resampler": "^1.0.1",
35 | "bootstrap": "^4.4.1",
36 | "colormap": "^2.3.1",
37 | "jquery": "^3.4.1",
38 | "popper.js": "^1.16.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------