├── .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 clicking
on File > Open audio file in the top menu.

74 |
75 |
76 |
77 |
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 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
#TimestampCommon nameScientific nameConfidence
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 | --------------------------------------------------------------------------------