├── .gitignore ├── LICENSE ├── README.md ├── data └── gangnamStyleAnalysis.json ├── js ├── algorithm │ ├── InfiniteBeats.js │ ├── LICENSE │ ├── calculateNearestNeighbors.js │ ├── package.json │ ├── random.js │ └── remixTrack.js └── examples │ ├── basic │ ├── main.js │ └── package.json │ └── playerAndVisualizer │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── server.js │ └── web │ ├── index.html │ ├── lib │ ├── AudioQueue.js │ ├── makeVisualizer.js │ └── util.js │ ├── main.js │ ├── styles │ ├── canonizer_styles.css │ └── styles.css │ └── third-party │ ├── jquery-ui.css │ ├── raphael-min.js │ └── three-dots.css └── tools ├── spotifyAudioAnalysisClient ├── CopySongLink.png ├── README.md ├── main.js ├── package-lock.json ├── package.json └── util.js └── spotifyBeatMetronome ├── README.md ├── main.js ├── metronome-tick.wav └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | creds/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 UnderMybrella 4 | Copyright (c) 2023 Adam Comella 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infinite Jukebox Algorithm 2 | 3 | The algorithm for the Infinite Jukebox which generates a never-ending and ever changing version of any song. Extracted from https://github.com/UnderMybrella/EternalJukebox/. 4 | 5 | ## Rationale 6 | The code for the algorithm of the original Infinite Jukebox implementation was coupled with the code that did audio, rendering, etc. The intention of this repo is to provide the code for the algorithm on its own so that it's easier to use in other applications. 7 | 8 | ## Dependency: Spotify's audio analysis web API 9 | The Infinite Jukebox relies on [Spotify's audio analysis web API](https://developer.spotify.com/documentation/web-api/reference/get-audio-analysis). For a song in Spotify's catalog, the API provides information about its structure and musical content including rhythm, pitch, and timbre. The Infinite Jukebox algorithm uses this information to figure out which sections of the song are so similar that it can jump the song from one section to the other without the listener noticing a seam in the music. 10 | 11 | This repo includes [`/data/gangnamStyleAnalysis.json`](./data/gangnamStyleAnalysis.json), a file with Spotify's audio analysis for Gangnam Style, so that you can play with the code in the repo without having to use Spotify's web API. 12 | 13 | This repo also includes [`/tools/spotifyAudioAnalysisClient/`](./tools/spotifyAudioAnalysisClient/), a tool that illustrates how to use Spotify's audio analysis web API. 14 | 15 | ## Repo layout 16 | - [`/js/`](./js/): Files related to the JavaScript implementation of the Infinite Jukebox algorithm. 17 | - [`algorithm/`](./js/algorithm/): The JavaScript implementation of the algorithm. 18 | - [`examples/`](./js/examples/): Example usage of the algorithm's API. 19 | - [`basic/`](./js/examples/basic/): A bare-bones example. 20 | - [`playerAndVisualizer/`](./js/examples/playerAndVisualizer/): A more substantial example that shows how to use the algorithm to play audio and visualize the beat of the song that's currently playing. 21 | - [`/tools/`](./tools/): Tools that come in handy when making use of the algorithm. 22 | - [`spotifyAudioAnalysisClient/`](./tools/spotifyAudioAnalysisClient/): Illustrates how to use Spotify's audio analysis web API. 23 | - [`spotifyBeatMetronome/`](./tools/spotifyBeatMetronome/): Generates a WAV audio file which plays a tick at each beat identified by Spotify's audio analysis. Useful when trying to figure out how to get your copy of the song in sync with Spotify's audio analysis. 24 | - [`/data/gangnamStyleAnalysis.json`](./data/gangnamStyleAnalysis.json): The result of calling Spotify's audio analysis web API on the song Gangnam Style. You can give this file as input to the examples and tools in this repo to see how they operate. 25 | 26 | ## Credits 27 | The original implementation of The Infinite Jukebox is by [Paul Lamere](https://musicmachinery.com/2012/11/12/the-infinite-jukebox/). 28 | 29 | The code in this repo is derived from [The Eternal Jukebox by UnderMybrella](https://github.com/UnderMybrella/EternalJukebox/), a rework of the original project. 30 | 31 | ## License 32 | [MIT](./LICENSE) 33 | -------------------------------------------------------------------------------- /js/algorithm/InfiniteBeats.js: -------------------------------------------------------------------------------- 1 | // This file contains code derived from EternalJukebox (https://github.com/UnderMybrella/EternalJukebox/). 2 | // Copyright 2021 UnderMybrella 3 | // See the LICENSE file for the full MIT license terms. 4 | 5 | import remixTrack from './remixTrack.js'; 6 | import calculateNearestNeighbors from './calculateNearestNeighbors.js'; 7 | 8 | // configs for chances to branch 9 | const randomBranchChanceDelta = .018; 10 | const maxRandomBranchChance = .5 11 | const minRandomBranchChance = .18 12 | 13 | function defaultRandom() { 14 | return Math.random(); 15 | } 16 | 17 | export default class InfiniteBeats { 18 | constructor(spotifyAnalysis, random=defaultRandom) { 19 | let track = { 20 | analysis: { 21 | sections: spotifyAnalysis.sections, 22 | bars: spotifyAnalysis.bars, 23 | beats: spotifyAnalysis.beats, 24 | tatums: spotifyAnalysis.tatums, 25 | segments: spotifyAnalysis.segments, 26 | }, 27 | }; 28 | 29 | // Deep clone track since we're going to modify it. 30 | track = JSON.parse(JSON.stringify(track)); 31 | 32 | this._curRandomBranchChance = minRandomBranchChance 33 | 34 | this._random = random; 35 | 36 | this._beats = track.analysis.beats; 37 | 38 | remixTrack(track); 39 | const { lastBranchPoint } = calculateNearestNeighbors(track); 40 | this._lastBranchPoint = lastBranchPoint; 41 | } 42 | 43 | getNextBeat(curBeat) { 44 | // console.log('next'); 45 | if (!curBeat) { 46 | return this._beats[0]; 47 | } else { 48 | const nextIndex = curBeat.which + 1; 49 | 50 | if (nextIndex < 0) { 51 | return this._beats[0]; 52 | } else if (nextIndex >= this._beats.length) { 53 | return undefined; 54 | } else { 55 | return this._selectRandomNextBeat(this._beats[nextIndex]); 56 | } 57 | } 58 | } 59 | 60 | _selectRandomNextBeat(seed) { 61 | if (seed.neighbors.length === 0) { 62 | return seed; 63 | } else if (this._shouldRandomBranch(seed)) { 64 | var next = seed.neighbors.shift(); 65 | seed.neighbors.push(next); 66 | var beat = next.dest; 67 | return beat; 68 | } else { 69 | return seed; 70 | } 71 | } 72 | 73 | _shouldRandomBranch(q) { 74 | if (q.which == this._lastBranchPoint) { 75 | return true; 76 | } 77 | 78 | // return true; // TEST, remove 79 | 80 | this._curRandomBranchChance += randomBranchChanceDelta; 81 | if (this._curRandomBranchChance > maxRandomBranchChance) { 82 | this._curRandomBranchChance = maxRandomBranchChance; 83 | } 84 | var shouldBranch = this._random() < this._curRandomBranchChance; 85 | if (shouldBranch) { 86 | this._curRandomBranchChance = minRandomBranchChance; 87 | } 88 | return shouldBranch; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /js/algorithm/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 UnderMybrella 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 | -------------------------------------------------------------------------------- /js/algorithm/calculateNearestNeighbors.js: -------------------------------------------------------------------------------- 1 | // This file contains code derived from EternalJukebox (https://github.com/UnderMybrella/EternalJukebox/). 2 | // Copyright 2021 UnderMybrella 3 | // See the LICENSE file for the full MIT license terms. 4 | 5 | export default function calculateNearestNeighbors(track) { 6 | const maxBranches = 4; 7 | const maxBranchThreshold = 80; // max allowed distance threshold 8 | let nextEdgeId = 0; 9 | 10 | function dynamicCalculateNearestNeighbors(type) { 11 | var count = 0; 12 | var targetBranchCount = track.analysis[type].length / 6; 13 | 14 | precalculateNearestNeighbors(type, maxBranches, maxBranchThreshold); 15 | 16 | for (var threshold = 10; threshold < maxBranchThreshold; threshold += 5) { 17 | count = collectNearestNeighbors(type, threshold); 18 | if (count >= targetBranchCount) { 19 | break; 20 | } 21 | } 22 | const lastBranchPoint = postProcessNearestNeighbors(type, threshold); 23 | return { lastBranchPoint } 24 | } 25 | 26 | function precalculateNearestNeighbors(type, maxNeighbors, maxThreshold) { 27 | // skip if this is already done 28 | if ('all_neighbors' in track.analysis[type][0]) { 29 | return; 30 | } 31 | for (var qi = 0; qi < track.analysis[type].length; qi++) { 32 | var q1 = track.analysis[type][qi]; 33 | calculateNearestNeighborsForQuantum(type, maxNeighbors, maxThreshold, q1); 34 | } 35 | } 36 | 37 | function calculateNearestNeighborsForQuantum(type, maxNeighbors, maxThreshold, q1) { 38 | var edges = []; 39 | var id = 0; 40 | for (var i = 0; i < track.analysis[type].length; i++) { 41 | 42 | if (i === q1.which) { 43 | continue; 44 | } 45 | 46 | var q2 = track.analysis[type][i]; 47 | var sum = 0; 48 | for (var j = 0; j < q1.overlappingSegments.length; j++) { 49 | var seg1 = q1.overlappingSegments[j]; 50 | var distance = 100; 51 | if (j < q2.overlappingSegments.length) { 52 | var seg2 = q2.overlappingSegments[j]; 53 | // some segments can overlap many quantums, 54 | // we don't want this self segue, so give them a 55 | // high distance 56 | if (seg1.which === seg2.which) { 57 | distance = 100 58 | } else { 59 | distance = get_seg_distances(seg1, seg2); 60 | } 61 | } 62 | sum += distance; 63 | } 64 | var pdistance = q1.indexInParent == q2.indexInParent ? 0 : 100; 65 | var totalDistance = sum / q1.overlappingSegments.length + pdistance; 66 | if (totalDistance < maxThreshold) { 67 | var edge = { 68 | id: id, 69 | src: q1, 70 | dest: q2, 71 | distance: totalDistance, 72 | }; 73 | edges.push(edge); 74 | id++; 75 | } 76 | } 77 | 78 | edges.sort( 79 | function (a, b) { 80 | if (a.distance > b.distance) { 81 | return 1; 82 | } else if (b.distance > a.distance) { 83 | return -1; 84 | } else { 85 | return 0; 86 | } 87 | } 88 | ); 89 | 90 | q1.all_neighbors = []; 91 | for (i = 0; i < maxNeighbors && i < edges.length; i++) { 92 | var edge = edges[i]; 93 | q1.all_neighbors.push(edge); 94 | 95 | edge.id = nextEdgeId; 96 | ++nextEdgeId; 97 | } 98 | } 99 | 100 | const timbreWeight = 1; 101 | const pitchWeight = 10; 102 | const loudStartWeight = 1; 103 | const loudMaxWeight = 1; 104 | const durationWeight = 100; 105 | const confidenceWeight = 1; 106 | 107 | function get_seg_distances(seg1, seg2) { 108 | var timbre = seg_distance(seg1, seg2, 'timbre', true); 109 | var pitch = seg_distance(seg1, seg2, 'pitches'); 110 | var sloudStart = Math.abs(seg1.loudness_start - seg2.loudness_start); 111 | var sloudMax = Math.abs(seg1.loudness_max - seg2.loudness_max); 112 | var duration = Math.abs(seg1.duration - seg2.duration); 113 | var confidence = Math.abs(seg1.confidence - seg2.confidence); 114 | var distance = timbre * timbreWeight + pitch * pitchWeight + 115 | sloudStart * loudStartWeight + sloudMax * loudMaxWeight + 116 | duration * durationWeight + confidence * confidenceWeight; 117 | return distance; 118 | } 119 | 120 | function seg_distance(seg1, seg2, field, weighted) { 121 | if (weighted) { 122 | return weighted_euclidean_distance(seg1[field], seg2[field]); 123 | } else { 124 | return euclidean_distance(seg1[field], seg2[field]); 125 | } 126 | } 127 | 128 | function weighted_euclidean_distance(v1, v2) { 129 | var sum = 0; 130 | 131 | //for (var i = 0; i < 4; i++) { 132 | for (var i = 0; i < v1.length; i++) { 133 | var delta = v2[i] - v1[i]; 134 | //var weight = 1.0 / ( i + 1.0); 135 | var weight = 1.0; 136 | sum += delta * delta * weight; 137 | } 138 | return Math.sqrt(sum); 139 | } 140 | 141 | function euclidean_distance(v1, v2) { 142 | var sum = 0; 143 | 144 | for (var i = 0; i < v1.length; i++) { 145 | var delta = v2[i] - v1[i]; 146 | sum += delta * delta; 147 | } 148 | return Math.sqrt(sum); 149 | } 150 | 151 | function collectNearestNeighbors(type, maxThreshold) { 152 | var branchingCount = 0; 153 | for (var qi = 0; qi < track.analysis[type].length; qi++) { 154 | var q1 = track.analysis[type][qi]; 155 | q1.neighbors = extractNearestNeighbors(q1, maxThreshold); 156 | if (q1.neighbors.length > 0) { 157 | branchingCount += 1; 158 | } 159 | } 160 | return branchingCount; 161 | } 162 | 163 | function extractNearestNeighbors(q, maxThreshold) { 164 | var neighbors = []; 165 | 166 | for (var i = 0; i < q.all_neighbors.length; i++) { 167 | var neighbor = q.all_neighbors[i]; 168 | 169 | var distance = neighbor.distance; 170 | if (distance <= maxThreshold) { 171 | neighbors.push(neighbor); 172 | } 173 | } 174 | return neighbors; 175 | } 176 | 177 | function postProcessNearestNeighbors(type, threshold) { 178 | if (longestBackwardBranch(type) < 50) { 179 | insertBestBackwardBranch(type, threshold, 65); 180 | } else { 181 | insertBestBackwardBranch(type, threshold, 55); 182 | } 183 | calculateReachability(type); 184 | const lastBranchPoint = findBestLastBeat(type); 185 | filterOutBadBranches(type, lastBranchPoint); 186 | return lastBranchPoint; 187 | } 188 | 189 | // we want to find the best, long backwards branch 190 | // and ensure that it is included in the graph to 191 | // avoid short branching songs like: 192 | // http://labs.echonest.com/Uploader/index.html?trid=TRVHPII13AFF43D495 193 | 194 | function longestBackwardBranch(type) { 195 | var longest = 0 196 | var quanta = track.analysis[type]; 197 | for (var i = 0; i < quanta.length; i++) { 198 | var q = quanta[i]; 199 | for (var j = 0; j < q.neighbors.length; j++) { 200 | var neighbor = q.neighbors[j]; 201 | var which = neighbor.dest.which; 202 | var delta = i - which; 203 | if (delta > longest) { 204 | longest = delta; 205 | } 206 | } 207 | } 208 | var lbb = longest * 100 / quanta.length; 209 | return lbb; 210 | } 211 | 212 | function insertBestBackwardBranch(type, threshold, maxThreshold) { 213 | var branches = []; 214 | var quanta = track.analysis[type]; 215 | for (var i = 0; i < quanta.length; i++) { 216 | var q = quanta[i]; 217 | for (var j = 0; j < q.all_neighbors.length; j++) { 218 | var neighbor = q.all_neighbors[j]; 219 | 220 | var which = neighbor.dest.which; 221 | var thresh = neighbor.distance; 222 | var delta = i - which; 223 | if (delta > 0 && thresh < maxThreshold) { 224 | var percent = delta * 100 / quanta.length; 225 | var edge = [percent, i, which, q, neighbor] 226 | branches.push(edge); 227 | } 228 | } 229 | } 230 | 231 | if (branches.length === 0) { 232 | return; 233 | } 234 | 235 | branches.sort( 236 | function (a, b) { 237 | return a[0] - b[0]; 238 | } 239 | ) 240 | branches.reverse(); 241 | var best = branches[0]; 242 | var bestQ = best[3]; 243 | var bestNeighbor = best[4]; 244 | var bestThreshold = bestNeighbor.distance; 245 | if (bestThreshold > threshold) { 246 | bestQ.neighbors.push(bestNeighbor); 247 | // console.log('added bbb from', bestQ.which, 'to', bestNeighbor.dest.which, 'thresh', bestThreshold); 248 | } else { 249 | // console.log('bbb is already in from', bestQ.which, 'to', bestNeighbor.dest.which, 'thresh', bestThreshold); 250 | } 251 | } 252 | 253 | function calculateReachability(type) { 254 | var maxIter = 1000; 255 | var iter = 0; 256 | var quanta = track.analysis[type]; 257 | 258 | for (var qi = 0; qi < quanta.length; qi++) { 259 | var q = quanta[qi]; 260 | q.reach = quanta.length - q.which; 261 | } 262 | 263 | for (iter = 0; iter < maxIter; iter++) { 264 | var changeCount = 0; 265 | for (qi = 0; qi < quanta.length; qi++) { 266 | var q = quanta[qi]; 267 | var changed = false; 268 | 269 | for (var i = 0; i < q.neighbors.length; i++) { 270 | var q2 = q.neighbors[i].dest; 271 | if (q2.reach > q.reach) { 272 | q.reach = q2.reach; 273 | changed = true; 274 | } 275 | } 276 | 277 | if (qi < quanta.length - 1) { 278 | var q2 = quanta[qi + 1]; 279 | if (q2.reach > q.reach) { 280 | q.reach = q2.reach; 281 | changed = true; 282 | } 283 | } 284 | 285 | if (changed) { 286 | changeCount++; 287 | for (var j = 0; j < q.which; j++) { 288 | var q2 = quanta[j]; 289 | if (q2.reach < q.reach) { 290 | q2.reach = q.reach; 291 | } 292 | } 293 | } 294 | } 295 | if (changeCount == 0) { 296 | break; 297 | } 298 | } 299 | 300 | if (false) { 301 | for (var qi = 0; qi < quanta.length; qi++) { 302 | var q = quanta[qi]; 303 | console.log(q.which, q.reach, Math.round(q.reach * 100 / quanta.length)); 304 | } 305 | } 306 | // console.log('reachability map converged after ' + iter + ' iterations. total ' + quanta.length); 307 | } 308 | 309 | function findBestLastBeat(type) { 310 | var reachThreshold = 50; 311 | var quanta = track.analysis[type]; 312 | var longest = 0; 313 | var longestReach = 0; 314 | for (var i = quanta.length - 1; i >= 0; i--) { 315 | var q = quanta[i]; 316 | //var reach = q.reach * 100 / quanta.length; 317 | var distanceToEnd = quanta.length - i; 318 | 319 | // if q is the last quanta, then we can never go past it 320 | // which limits our reach 321 | 322 | var reach = (q.reach - distanceToEnd) * 100 / quanta.length; 323 | 324 | if (reach > longestReach && q.neighbors.length > 0) { 325 | longestReach = reach; 326 | longest = i; 327 | if (reach >= reachThreshold) { 328 | break; 329 | } 330 | } 331 | } 332 | // console.log('NBest last beat is', longest, 'reach', longestReach, reach); 333 | 334 | return longest 335 | } 336 | 337 | function filterOutBadBranches(type, lastIndex) { 338 | var quanta = track.analysis[type]; 339 | for (var i = 0; i < lastIndex; i++) { 340 | var q = quanta[i]; 341 | var newList = []; 342 | for (var j = 0; j < q.neighbors.length; j++) { 343 | var neighbor = q.neighbors[j]; 344 | if (neighbor.dest.which < lastIndex) { 345 | newList.push(neighbor); 346 | } else { 347 | // console.log('filtered out arc from', q.which, 'to', neighbor.dest.which); 348 | } 349 | } 350 | q.neighbors = newList; 351 | } 352 | } 353 | 354 | if (track) { 355 | return dynamicCalculateNearestNeighbors('beats'); 356 | } else { 357 | throw new Error('track is null'); 358 | } 359 | } -------------------------------------------------------------------------------- /js/algorithm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /js/algorithm/random.js: -------------------------------------------------------------------------------- 1 | const values = [0.06136311543402151, 0.16661870076049023, 0.32287054867866316, 0.4868303558923166, 0.4680633963223022, 0.4723839106114409, 0.3288803524567454, 0.00551810008996112, 0.9854030204502853, 0.03610264699914212, 0.8613920407243563, 0.30405969564984314, 0.9576660862571278, 0.17198927186905566, 0.8602849268739408, 0.965268615239431, 0.4548249688505668, 0.48757374270156295, 0.3465718403570266, 0.42577246231338894, 0.8962996980198856, 0.6012772397980648, 0.23278382748172088, 0.04829939368936742, 0.9246949979318906, 0.5198343274937145, 0.6845007867383828, 0.07666232880839652, 0.800119929160497, 0.17000293216856432, 0.9568228813073782, 0.25786979378012687, 0.8484203143813194, 0.01104588521861749, 0.7832647652350191, 0.3221531622969651, 0.7340558921422387, 0.41849309331449946, 0.9121098785129453, 0.45870065307727215, 0.6165184980364553, 0.377973620073778, 0.6565871634533036, 0.24252831505651828, 0.4491802328220693, 0.5823441566891174, 0.3987734923131161, 0.512747936070012, 0.9141607355913197, 0.4584712871053138, 0.5827513453932827, 0.23324738246270216, 0.8811192880538623, 0.07253536987776643, 0.48544580455735487, 0.14412573454839195, 0.2854098164024488, 0.1083589150960953, 0.29785217294890476, 0.48874654382819505, 0.8133578157982917, 0.44401131409658867, 0.49490927119132566, 0.3866754089936626, 0.25962464428763976, 0.11699846269111647, 0.6488552063786908, 0.6967454001814293, 0.7922689759081885, 0.7719944404354537, 0.9449417584491662, 0.2643998227603137, 0.2920176948136217, 0.8285831052055386, 0.5241707679523353, 0.7933157203817667, 0.36192096094657344, 0.5783891694763728, 0.12375460150644768, 0.10552734915952522, 0.630486803962651, 0.1392395020251076, 0.8880849339101347, 0.6208231887187525, 0.44414008223401846, 0.8304976503248396, 0.04718240035461174, 0.4320058478494391, 0.424780137017702, 0.19780912275396711, 0.31782067532440816, 0.7436121542164085, 0.7696966128629465, 0.6455306848925615, 0.21325735320636796, 0.21607370885912558, 0.9321970862454578, 0.9935013240367006, 0.8657635214345325, 0.7800164093996629, 0.7392227236126996, 0.35191835487418377, 0.8311283830230913, 0.5631938124983638, 0.2879045071851043, 0.9798296854407249, 0.6202721808375851, 0.9363275224735412, 0.2481565843300566, 0.7653515431345974, 0.3358742788305016, 0.3512307181950258, 0.24175479353693197, 0.8219902493978215, 0.1861946114479449, 0.676568426636383, 0.23646339684178153, 0.6808897554688644, 0.0008795945904822577, 0.9498352247162447, 0.2382942205611278, 0.2856245670526021, 0.5200680425577915, 0.12880312500924895, 0.9727617934634649, 0.13127720001803533, 0.6554715034164782, 0.4414064843874419, 0.5800370288522807, 0.47766001486674825, 0.42460686086609, 0.8357977583113498, 0.9335551670391931, 0.17173350648414987, 0.33434707699851973, 0.3327310286924532, 0.06011319898525702, 0.737287390483691, 0.8456036353680556, 0.7027623463358088, 0.15591148122737764, 0.5022181368276439, 0.9137100347147373, 0.11449962305474659, 0.9079222813213341, 0.4582728745652209, 0.3023179547603554, 0.719617068791462, 0.10090397882012758, 0.790560414808162, 0.45756705411902243, 0.1691811774182741, 0.790846535980879, 0.3277917740957135, 0.3621905768050844, 0.6523077086004037, 0.9109587759917583, 0.05005541588151741, 0.9898936862830992, 0.6717796745951636, 0.026704110333094278, 0.2304006329869399, 0.9805345270611674, 0.8055372311647948, 0.9285397345795625, 0.5013484525756629, 0.3532594700874958, 0.7071005623794162, 0.706608218867683, 0.026873234197230422, 0.10274632660844607, 0.1792148581256996, 0.015312457586421857, 0.9309061468907809, 0.5047731905306125, 0.8910011702384808, 0.4741884601611399, 0.7295355586859538, 0.6421566437251955, 0.6670031195500243, 0.7594392061260693, 0.11311596090526499, 0.9525459023531253, 0.5187143589812975, 0.8313743638658648, 0.1399467530666334, 0.8409183175561481, 0.8095383921908212, 0.8537846519214563, 0.30260710085698594, 0.9797325642603172, 0.5062828316075914, 0.6077458712989523, 0.903145311972263, 0.6905470846217254, 0.1680682287362265, 0.7218265696589639, 0.13489564567845536, 0.21862860663056916, 0.8097065511254387, 0.7732112692933055, 0.07037867267640197, 0.8991519444894309, 0.5317522864972126, 0.388262397338889, 0.4358477018941067, 0.27119961828394334, 0.4609744981800665, 0.95976040087038, 0.34939537698130163, 0.37249659914342925, 0.23286679280725164, 0.3836866649794499, 0.29676497837593696, 0.22717667553266785, 0.51698803414525, 0.7400993122835957, 0.7023975304295664, 0.8434669791204346, 0.33210971263218103, 0.19516237970355954, 0.8886708652368076, 0.8507380537927745, 0.6364316457677806, 0.4793297059690118, 0.20187615415169669, 0.04357567533702289, 0.9289036790239373, 0.4784218801993301, 0.5383155487838664, 0.7873689207038732, 0.019592913497297415, 0.7068259084624362, 0.5033933153185528, 0.6238294446952461, 0.768743855706369, 0.1210010375487931, 0.3483007564788716, 0.623849731126823, 0.31105906201869105, 0.4010280029554125, 0.9331742820258213, 0.558367003124228, 0.6845800649993101, 0.46433035630578035, 0.8363970590299628, 0.3078565514001299, 0.8191160305294372, 0.17549356239638048, 0.31298097095583466, 0.7830944901988859, 0.30107210085435265, 0.6884162674389247, 0.19995722832226814, 0.10066623743008507, 0.5490129769190062, 0.06217081595145957, 0.21630247879085562, 0.36188559423567845, 0.6829557144902463, 0.7169630853317901, 0.3452803200208123, 0.25254233893114053, 0.4545046596771374, 0.07139851466446356, 0.8078770231449168, 0.1547861076947652, 0.9061508246395251, 0.7382749899524654, 0.09768477176821322, 0.9213273889072731, 0.9808468864165671, 0.21596934252938094, 0.4194931570428735, 0.6806855138482526, 0.3433788506177904, 0.21879431681794514, 0.6791673611016111, 0.7368400290120616, 0.6342097162996376, 0.27285460901452163, 0.35424741971265994, 0.6062214990513681, 0.7268059829097269, 0.5203444529542622, 0.852230536317448, 0.14142280592008505, 0.24270221602327324, 0.7963479542096068, 0.0211239981378315, 0.09467170691838112, 0.7990759395837959, 0.7048385594325499, 0.24581924362848673, 0.043380439988937036, 0.08143033141912204, 0.6209657793914944, 0.18273754646478735, 0.8937625088687535, 0.15708170935358434, 0.9338234830437908, 0.5285517277493925, 0.5163873343006251, 0.17711176637735515, 0.27143024471820976, 0.6821229290794724, 0.806983445057236, 0.27612687960501825, 0.21527513825679012, 0.6883681721898853, 0.4982072080863609, 0.9566920080029018, 0.1080417115219856, 0.22693036000640165, 0.188787945497072, 0.13096000803294983, 0.20022751510422276, 0.9944169429774563, 0.6727525451749694, 0.4373767661679444, 0.874578250649076, 0.37759707156848665, 0.01870295197209737, 0.9557097632402058, 0.7196925023407543, 0.7714456962020275, 0.8034587730361724, 0.8600070276213607, 0.7885464245195488, 0.6986567560459747, 0.46413958716744674, 0.03319153769859051, 0.7708934228714783, 0.6768888684137588, 0.7890971461668159, 0.45822180156555836, 0.6758183326136515, 0.8845042260997678, 0.7703182149544203, 0.8244141285860214, 0.13708391280803567, 0.45094579617299146, 0.9766552049567996, 0.4760781454810483, 0.6974114409815486, 0.3374095694340662, 0.8307196126617054, 0.2859562393248296, 0.9670485751964291, 0.9375763305602951, 0.53972870751537, 0.778939573687252, 0.8752690168671198, 0.27511219730677805, 0.46389744465302174, 0.22176695261733825, 0.20124715450318043, 0.04367530366128314, 0.5279584822764449, 0.3487391272805591, 0.35692888740118867, 0.6003499755930657, 0.25131589301125534, 0.21936427140935466, 0.3543095750688443, 0.5424022564086999, 0.1764132963868723, 0.3490266793708665, 0.5346575959989637, 0.22234148512079677, 0.10235072825885716, 0.7935717003409044, 0.10402152487475802, 0.4304662643512953, 0.2995907329676497, 0.6226521422591071, 0.626070672445338, 0.6172754322866991, 0.6042848401441454, 0.21634199648832508, 0.7550161390548955, 0.007693167693747949, 0.7815898697130554, 0.8034956475601915, 0.21133031870362085, 0.853710666831716, 0.8279418048459641, 0.6277113751539494, 0.3650372243453359, 0.9494651625045512, 0.44514356534552446, 0.13627727331114792, 0.7849766741927502, 0.8380507513013611, 0.917943710694215, 0.08061053270443885, 0.7197107404394252, 0.8652093940801844, 0.9602548557229216, 0.1864668163784815, 0.15293431155777104, 0.7832957208682685, 0.7931829861752235, 0.7395996744134923, 0.626494745828748, 0.3486197311589827, 0.41000682937846533, 0.36030929440348936, 0.5153500694558408, 0.26464576853229893, 0.9989697038803489, 0.18401200468473422, 0.8403164184301277, 0.6930413588102591, 0.0446682484371288, 0.47770199193968765, 0.3379047101572723, 0.4553683215514399, 0.7018838607424469, 0.619713824985173, 0.8624477320162272, 0.543128941860358, 0.7475286613280501, 0.5960721582164517, 0.4727803910174653, 0.4207249331027194, 0.19491852976105872, 0.22520844822770902, 0.5562161831863337, 0.9932417019847477, 0.3413605872818406, 0.8554397168776597, 0.6581798968314565, 0.09013602637770268, 0.8584612130528035, 0.3732734412651968, 0.9313439625075897, 0.35753809399752545, 0.929688973314692, 0.25519144014815565, 0.32524871644097497, 0.008915670388909946, 0.5821962077203922, 0.08997041000569905, 0.9316249398911745, 0.9613989663357021, 0.922904247483026, 0.013333809192039414, 0.25736363920493277, 0.6759091538898323, 0.46268065022149973, 0.9895566879597979, 0.16823871442533944, 0.38961478602055366, 0.03914871935916775, 0.3119715630983213, 0.70312904652492, 0.24359783786540756, 0.8024235103362951, 0.0033291522078775504, 0.6374102153872301, 0.9928569038692268, 0.062334337714705645, 0.5330799192478111, 0.066306184737148, 0.8368232616719311, 0.2633896496529353, 0.7706030980079499, 0.8792647604068566, 0.7790046796390848, 0.768736348808174, 0.5201742083538428, 0.8260141101569418, 0.8217092175112286, 0.5073027019110976, 0.4240150745198972, 0.6927048396459798, 0.8716942085855948, 0.7999480528434542, 0.9188237156889829, 0.9672593848940694, 0.8676156712359224, 0.43356457701203754, 0.7245874449669247, 0.522139587516206, 0.6144573248919707, 0.7231608190468175, 0.5171532184035026, 0.8712047199995627, 0.8367231550222562, 0.4723711543946121, 0.6379649702282006, 0.35793095832522703, 0.4294110038852945, 0.38502113002341587, 0.7145034938965531, 0.006150345278842728, 0.43502238559766493, 0.07418799129632925, 0.9570670764119078, 0.37634629367660577, 0.36798593934232726, 0.33292405835338035, 0.1732634489092859, 0.0753713148511177, 0.8745422218645929, 0.4524868426310322, 0.30267749787118325, 0.40393046746165795, 0.415466338526449, 0.026377683496851212, 0.20296101244780385, 0.648624488723123, 0.17998489547282914, 0.34304998805856646, 0.8368913754818363, 0.5673559982440253, 0.19924646057425965, 0.057729682406492344, 0.0979042224768214, 0.07107651508163526, 0.8089823444518085, 0.8606649644198194, 0.43077868085395377, 0.8192928301550435, 0.8912717531503862, 0.13810862172296834, 0.5394467475857261, 0.29003141982880676, 0.017221661427490265, 0.777774278678041, 0.36710603467938197, 0.9458392154271535, 0.6395672617415196, 0.8043536676307004, 0.4824601513428495, 0.1982384346611581, 0.07424454122672586, 0.2774142505277244, 0.5762198900449809, 0.5936872081296345, 0.5402326053961846, 0.9926962720166204, 0.44126332020710524, 0.948043642749882, 0.11160786395756483, 0.8464644209329515, 0.9015809460212352, 0.6149688526372785, 0.6506343917851558, 0.05389335293609521, 0.24724466542685763, 0.5855533664113413, 0.6957866068459646, 0.6069899483598236, 0.7095479503484008, 0.048968092681017206, 0.2535399169617143, 0.9803111211428825, 0.5312601561065675, 0.33468653604219933, 0.7103215912211802, 0.09833376463142862, 0.7772803800185857, 0.4360704051607529, 0.6390208056238653, 0.6564635505074643, 0.47426229107513396, 0.5119697239508989, 0.18037555568361752, 0.3484504167158238, 0.1657133537611868, 0.024980003590969302, 0.3945315224285988, 0.8452219641864405, 0.034639085523324065, 0.09437117380492888, 0.029027353784731647, 0.3492069570989753, 0.5412748849978761, 0.6038015230579419, 0.8868364035260823, 0.4911359568034437, 0.9771076909370775, 0.20775421835343133, 0.227394218196862, 0.9619946448885981, 0.7105190128044321, 0.5943877527174612, 0.08135762490307696, 0.05411392502764101, 0.17454745883625544, 0.04489006569331999, 0.9896884982279373, 0.17765520296853388, 0.8638154328902772, 0.3255464597872826, 0.6531038476488655, 0.8718824588378975, 0.5003038607859889, 0.3146687819068179, 0.4984423737119592, 0.16716444765294258, 0.861950104114448, 0.025027386865559498, 0.7758401764402383, 0.20179414160861908, 0.7558331733080417, 0.08205663032890054, 0.1749232973530639, 0.4237163466147853, 0.8557134399584367, 0.8221152299645014, 0.8567911173167946, 0.42053918658528056, 0.048411814776404105, 0.6736793274664008, 0.37721738243187475, 0.06936620043225439, 0.1655532546303684, 0.35735730866792936, 0.8343237212619927, 0.5170350124569072, 0.6690666650202843, 0.529540736498284, 0.8485524992584144, 0.8471887337994695, 0.6085497858330988, 0.05464092587570568, 0.3685540894164001, 0.41554537125280944, 0.37096791472773694, 0.857207529201383, 0.732987194386606, 0.9030407108246188, 0.47635878877583515, 0.675851250001545, 0.633650596604117, 0.30257859932265574, 0.25915332747637265, 0.6763386288777518, 0.060150608458857, 0.45406756852585795, 0.4073302834825936, 0.35274616173152484, 0.5276220688128519, 0.2872726418235221, 0.5400572143732458, 0.9887651497635097, 0.8973865000982801, 0.2077253076892689, 0.06018172204149508, 0.02080157356647816, 0.8475094823174165, 0.36086708389579925, 0.5784548597920665, 0.45753082508700693, 0.12252867148227242, 0.7657544233206499, 0.828433878374434, 0.5284776200242214, 0.615796900810019, 0.9026792040010896, 0.19569805667628914, 0.6335334140529414, 0.5162955616210665, 0.8768532008356005, 0.18791751018204006, 0.02168903776179354, 0.7317419716953197, 0.577146789702309, 0.904468871376171, 0.7238911154644716, 0.6777645290612684, 0.03604735337188458, 0.053129632553768236, 0.8781835904576449, 0.009913317743893257, 0.3452717933010112, 0.8618567615205759, 0.41528093002584354, 0.8729081645648151, 0.04917272631427361, 0.10096189855670268, 0.3102522316085934, 0.8046655185948406, 0.7854715937407122, 0.12212611532879558, 0.7946226732948225, 0.6306226728299764, 0.7868582792508667, 0.2547540856327537, 0.9574797736808864, 0.7300192186524199, 0.120239891678946, 0.4715061022985556, 0.6475518661196966, 0.4427728262536159, 0.9289032487732454, 0.46828269001710887, 0.903671680706895, 0.9231629953182625, 0.6062163786038226, 0.5771340005369545, 0.2786713018303293, 0.9375016861167429, 0.3627639913636256, 0.9344261008825425, 0.13602799979429192, 0.3495708203866488, 0.7071074017640222, 0.7972439678303322, 0.4090259157401537, 0.30048333492943846, 0.5166025907266667, 0.8842129907421046, 0.98382241919196, 0.520301295769021, 0.5731356052283625, 0.640444243558423, 0.9247044166337552, 0.08041541085452741, 0.24056872992311584, 0.5799352812851224, 0.015379269630979575, 0.7442163420455166, 0.9461167228291245, 0.05954519079794918, 0.40913743791983626, 0.8665007028351894, 0.7218542162534956, 0.7555828860283675, 0.5002260178596476, 0.5619221696273349, 0.7952085455292501, 0.49504640463344485, 0.13275999800908056, 0.23116342929002243, 0.8882665604840798, 0.9597692703754708, 0.7163331069597214, 0.23782473718323605, 0.2818416075989485, 0.408323301124335, 0.08532621965927656, 0.7011128431987528, 0.4517431059192427, 0.989764242302259, 0.7663067928091756, 0.826604952486176, 0.09762132880395802, 0.6283461357142575, 0.19240183076112682, 0.9332380950439161, 0.04336293168561611, 0.26402707695946437, 0.507964943781728, 0.8388701786150825, 0.20560424049403392, 0.46008617787820305, 0.6376254420338547, 0.8617857743207342, 0.5776266960783538, 0.017845189117102533, 0.32895121140013406, 0.5330226238941611, 0.21476315461194329, 0.08787437199530124, 0.1180934006098846, 0.19893493999702705, 0.4271486110805178, 0.1658980202857936, 0.8301532956049358, 0.09436630030674054, 0.19274008180128455, 0.2546563634208945, 0.9382196070097282, 0.8573838967783707, 0.5270628546116503, 0.04462639567755233, 0.7976204462787115, 0.9839837833902136, 0.6767417913805367, 0.9888258781196935, 0.706879841592081, 0.005018148857464988, 0.7835319625066537, 0.4898286707327759, 0.3006942239789261, 0.9757390955150229, 0.4955358262731644, 0.9977861011862883, 0.5792694194220165, 0.14277151956311185, 0.9606457078681694, 0.29495592177067675, 0.9802998623825521, 0.2319209026930349, 0.0026834909885504743, 0.8787244386130542, 0.6014893621332704, 0.3365888815127229, 0.8439369312793834, 0.8164275135638086, 0.49011545835870707, 0.905397092780027, 0.38077831072369084, 0.33794758477834463, 0.2369092755818205, 0.1760657165272912, 0.5081001015612643, 0.6646411442884954, 0.3279723505308987, 0.7600446023318912, 0.5718239559881795, 0.044526262018710794, 0.8120700525587565, 0.564798098287077, 0.6176742245302032, 0.1525268748036508, 0.09272678880239771, 0.44627490759009314, 0.7155877140813844, 0.9238758280636279, 0.4979961089836731, 0.2473773568269353, 0.14765410399601642, 0.7283436809302768, 0.1597089429177676, 0.359380818245405, 0.4105349918721022, 0.7234137710930508, 0.38646360615956943, 0.02722655104092686, 0.5572527548051269, 0.423957216015862, 0.6234106410844327, 0.6715119299332042, 0.2644925675307426, 0.06749677337029114, 0.6087751133204189, 0.9236763002874238, 0.5135973639944875, 0.9736853345790994, 0.03438311207904832, 0.8849777071804621, 0.07982945357699212, 0.8208997420812443, 0.5056560371212993, 0.8773334863582545, 0.7310668056838989, 0.9163710703882817, 0.2415321378018611, 0.8509128161663286, 0.3640268711329746, 0.3046401483463128, 0.16195395296723003, 0.4721622599002413, 0.6967208254901838, 0.08542011648678138, 0.5774293189468254, 0.5949088476041793, 0.6547073926839806, 0.6980252924272727, 0.16692116836290793, 0.44999050126408746, 0.9294793254152587, 0.24766965287517895, 0.5821568107299973, 0.10220094783107725, 0.8408164196263554, 0.8980336379964287, 0.1270771111923037, 0.5202286542635071, 0.8553097840928292, 0.12583881613493975, 0.15428839518495474, 0.6081968084822891, 0.6163966809432004, 0.1461936398738155, 0.27844750876982705, 0.21078168725022128, 0.7508228625104008, 0.6949087698700744, 0.6083738703423249, 0.8707245535846491, 0.6680003322464747, 0.7619163649671172, 0.7235356351438265, 0.653770907507182, 0.2244724217219931, 0.9489452153299864, 0.11625061480282839, 0.9971867876217768, 0.6143181919010063, 0.9483570534573633, 0.4547559152972984, 0.34044907313828765, 0.5463571141360108, 0.7497822507438088, 0.1333592040691336, 0.3607990450972489, 0.912303637687754, 0.8750311535909021, 0.7625437726831372, 0.9314795496201997, 0.2062779783988491, 0.49866838989514184, 0.6516504202530984, 0.7584113010041604, 0.2256446318758314, 0.25824364739101213, 0.13261989977903355, 0.24948768140436428, 0.9686043159371192, 0.21092855829751556, 0.7878415638816079, 0.8688008944615695, 0.6339844118529563, 0.14580551975596467, 0.7635822614632617, 0.10831379777598382, 0.8401867225007111, 0.7113518094252629, 0.17454651633028706, 0.49622558324492827, 0.6892858612456461, 0.5590515436557346, 0.34572177003825355, 0.7633391716324363, 0.5659641078823454, 0.4445825975148494, 0.5879074892477989, 0.8197531954103516, 0.5747653043248468, 0.2722872529633844, 0.7917031181171605, 0.5893368841045665, 0.6105462015824057, 0.8386777472362139, 0.5799665295196432, 0.45832588878649827, 0.08342885434024572, 0.71328733423161, 0.9939187009355037, 0.22626913525369274, 0.30496694786980316, 0.21993223563884357, 0.20266023545738254, 0.6262204255382258, 0.7276016668950152, 0.19815494524963007, 0.06272537677582513, 0.15758872539140723, 0.6158657846681612, 0.5724171583850206, 0.3081963368558447, 0.005899771085419392, 0.5630863752469053, 0.3831098022492019, 0.5420107212637053, 0.8243776838504797, 0.44194235322414954, 0.3236269880729792, 0.14621401139501922, 0.3891658876512172, 0.5822216066343555, 0.9847643425271635, 0.25028492930759194, 0.8332283987056321, 0.3001898347969221, 0.17539414271894893, 0.05188756318088439, 0.4562521378443434, 0.1868374749626014, 0.004835289969685075, 0.727471659345527, 0.34047769577342923, 0.06357335919937812, 0.9244165028384312, 0.10209285037563487, 0.13755498346830874, 0.7274999037208536, 0.40784107958401616, 0.8783480164386908, 0.23471435375784133, 0.618505032084399, 0.5876304154203815, 0.3816854253210238, 0.8136018698871581, 0.6112259838391909, 0.28781712317001595, 0.5346354679397227, 0.46984919590583263, 0.5966723450295268, 0.6500976414448689, 0.8042538106397286, 0.17809358523095442, 0.9428011258987927, 0.24366746967361896]; 2 | let index = 0; 3 | 4 | export default function random() { 5 | const value = values[index]; 6 | index = (index + 1) % values.length; 7 | return value; 8 | } 9 | -------------------------------------------------------------------------------- /js/algorithm/remixTrack.js: -------------------------------------------------------------------------------- 1 | // This file contains code derived from EternalJukebox (https://github.com/UnderMybrella/EternalJukebox/). 2 | // Copyright 2021 UnderMybrella 3 | // See the LICENSE file for the full MIT license terms. 4 | 5 | export default function remixTrack(track) { 6 | preprocessTrack(track); 7 | } 8 | 9 | function preprocessTrack(track) { 10 | const trackAnalysis = track.analysis; 11 | 12 | // trace('preprocessTrack'); 13 | var types = ['sections', 'bars', 'beats', 'tatums', 'segments']; 14 | for (const type of types) { 15 | // trace('preprocessTrack ' + type); 16 | const qlist = trackAnalysis[type]; 17 | for (let [index, q] of qlist.entries()) { 18 | q.track = track; 19 | q.which = index; 20 | if (index > 0) { 21 | q.prev = qlist[index - 1]; 22 | } else { 23 | q.prev = null 24 | } 25 | 26 | if (index < qlist.length - 1) { 27 | q.next = qlist[index + 1]; 28 | } else { 29 | q.next = null 30 | } 31 | } 32 | } 33 | 34 | connectQuanta(trackAnalysis, 'sections', 'bars'); 35 | connectQuanta(trackAnalysis, 'bars', 'beats'); 36 | connectQuanta(trackAnalysis, 'beats', 'tatums'); 37 | connectQuanta(trackAnalysis, 'tatums', 'segments'); 38 | 39 | connectFirstOverlappingSegment(trackAnalysis, 'bars'); 40 | connectFirstOverlappingSegment(trackAnalysis, 'beats'); 41 | connectFirstOverlappingSegment(trackAnalysis, 'tatums'); 42 | 43 | connectAllOverlappingSegments(trackAnalysis, 'bars'); 44 | connectAllOverlappingSegments(trackAnalysis, 'beats'); 45 | connectAllOverlappingSegments(trackAnalysis, 'tatums'); 46 | 47 | 48 | filterSegments(trackAnalysis); 49 | } 50 | 51 | function filterSegments(trackAnalysis) { 52 | var threshold = .3; 53 | var fsegs = []; 54 | fsegs.push(trackAnalysis.segments[0]); 55 | for (var i = 1; i < trackAnalysis.segments.length; i++) { 56 | var seg = trackAnalysis.segments[i]; 57 | var last = fsegs[fsegs.length - 1]; 58 | if (isSimilar(seg, last) && seg.confidence < threshold) { 59 | fsegs[fsegs.length - 1].duration += seg.duration; 60 | } else { 61 | fsegs.push(seg); 62 | } 63 | } 64 | trackAnalysis.fsegments = fsegs; 65 | } 66 | 67 | function isSimilar(seg1, seg2) { 68 | var threshold = 1; 69 | var distance = timbral_distance(seg1, seg2); 70 | return (distance < threshold); 71 | } 72 | 73 | function timbral_distance(s1, s2) { 74 | return euclidean_distance(s1.timbre, s2.timbre); 75 | } 76 | 77 | function euclidean_distance(v1, v2) { 78 | var sum = 0; 79 | for (var i = 0; i < 3; i++) { 80 | var delta = v2[i] - v1[i]; 81 | sum += delta * delta; 82 | } 83 | return Math.sqrt(sum); 84 | } 85 | 86 | function connectQuanta(trackAnalysis, parent, child) { 87 | var last = 0; 88 | var qparents = trackAnalysis[parent]; 89 | var qchildren = trackAnalysis[child]; 90 | 91 | for (const qparent of qparents) { 92 | qparent.children = []; 93 | 94 | for (var j = last; j < qchildren.length; j++) { 95 | var qchild = qchildren[j]; 96 | if (qchild.start >= qparent.start 97 | && qchild.start < qparent.start + qparent.duration) { 98 | qchild.parent = qparent; 99 | qchild.indexInParent = qparent.children.length; 100 | qparent.children.push(qchild); 101 | last = j; 102 | } else if (qchild.start > qparent.start) { 103 | break; 104 | } 105 | } 106 | } 107 | } 108 | 109 | // connects a quanta with the first overlapping segment 110 | function connectFirstOverlappingSegment(trackAnalysis, quanta_name) { 111 | var last = 0; 112 | var quanta = trackAnalysis[quanta_name]; 113 | var segs = trackAnalysis.segments; 114 | 115 | for (const q of quanta) { 116 | for (var j = last; j < segs.length; j++) { 117 | var qseg = segs[j]; 118 | if (qseg.start >= q.start) { 119 | q.oseg = qseg; 120 | last = j; 121 | break 122 | } 123 | } 124 | } 125 | } 126 | 127 | function connectAllOverlappingSegments(trackAnalysis, quanta_name) { 128 | var last = 0; 129 | var quanta = trackAnalysis[quanta_name]; 130 | var segs = trackAnalysis.segments; 131 | 132 | for (const q of quanta) { 133 | q.overlappingSegments = []; 134 | 135 | for (var j = last; j < segs.length; j++) { 136 | var qseg = segs[j]; 137 | // seg starts before quantum so no 138 | if ((qseg.start + qseg.duration) < q.start) { 139 | continue; 140 | } 141 | // seg starts after quantum so no 142 | if (qseg.start > (q.start + q.duration)) { 143 | break; 144 | } 145 | last = j; 146 | q.overlappingSegments.push(qseg); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /js/examples/basic/main.js: -------------------------------------------------------------------------------- 1 | // Uses the InfiniteBeats class to generate the first 200 beats to be played 2 | // of an infinite variation of the song Gangnam Style. 3 | // 4 | 5 | import InfiniteBeats from '../../algorithm/InfiniteBeats.js'; 6 | import fs from 'fs/promises'; 7 | 8 | async function main() { 9 | // gangnamStyleAnalysis.json came from Spotify's audio analysis API: 10 | // https://api.spotify.com/v1/audio-analysis/03UrZgTINDqvnUMbbIMhql 11 | // Docs: https://developer.spotify.com/documentation/web-api/reference/get-audio-analysis 12 | const track = JSON.parse(await fs.readFile('../../../data/gangnamStyleAnalysis.json', { encoding: 'utf8' })); 13 | 14 | const infiniteBeats = new InfiniteBeats(track); 15 | 16 | // Process the first 200 beats. 17 | let prevBeat = undefined; 18 | for (let i = 0; i < 200; ++i) { 19 | const curBeat = infiniteBeats.getNextBeat(prevBeat); 20 | 21 | if (!prevBeat) { 22 | console.log('Start playing at ' + curBeat.start + ' seconds'); 23 | } else if (!curBeat) { 24 | console.log('Stop'); 25 | return; 26 | } else if (prevBeat.which + 1 === curBeat.which) { 27 | // The beats are adjacent so there's nothing to do. If we were playing 28 | // audio, we'd allow it to continue playing as it is. 29 | } else { 30 | // The beats are not adjacent so we need to seek. 31 | const prevBeatEnd = prevBeat.start + prevBeat.duration; 32 | console.log('Seek: ' + prevBeatEnd + ' seconds -> ' + curBeat.start + ' seconds'); 33 | } 34 | 35 | prevBeat = curBeat; 36 | } 37 | } 38 | 39 | main(); 40 | -------------------------------------------------------------------------------- /js/examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/README.md: -------------------------------------------------------------------------------- 1 | # Player & Visualizer Example 2 | 3 | This example illustrates how to connect the Infinite Jukebox algorithm to audio and a visualizer. 4 | 5 | Given a spotify audio analysis file and an audio file, plays the audio for the song while visually showing you which beat of the song is playing. Also visually shows when the algorithm chooses to seek the song to another beat rather than allowing it to continue sequentially. A seek is represented by a green line in the visualizer. 6 | 7 | ## Setup 8 | - Run `npm install` to install the example's dependencies. 9 | 10 | ## Usage 11 | `node server.js ` 12 | 13 | - `spotify-analysis.json`: Path to a file from [Spotify's audio analysis web API](https://developer.spotify.com/documentation/web-api/reference/get-audio-analysis) for your song of interest. Can be a local file path or an HTTP URL. For sample code for interacting with the Spotify web API, see [`/tools/spotifyAudioAnalysisClient/`](../../../tools/spotifyAudioAnalysisClient/). 14 | - `song.wav`: Path to the audio file for your song of interest. Can be a local file path or an HTTP URL. 15 | 16 | Here's an example invocation for using this tool with the song Gangnam Style: 17 | 18 | `node server.js ../../../data/gangnamStyleAnalysis.json "https://www.eternalboxmirror.xyz/api/audio/jukebox/03UrZgTINDqvnUMbbIMhql"` 19 | 20 | ## Code layout 21 | - [`./server.js`](./server.js): Launches a local HTTP server that serves this web app at http://localhost:2012/. 22 | - [`./web/`](./web): Contains the HTML, JavaScript, and CSS for this web app. 23 | - [`main.js`](./web/main.js): Entry-point for the code that runs in the browser. This is likely the file you're most interested in as it illustrates how to connect the Infinite Jukebox algorithm to the browser's audio API and to the visualizer. The result is you hear an infinitely long variation of the song while seeing a visualization of the song's current beat. 24 | - [`lib/`](./web/lib/): This app's custom JavaScript utility code. 25 | - [`styles/`](./web/styles/): This app's custom CSS styles. 26 | - [`third-party/`](./web/third-party/): JavaScript and CSS styles from third-party libraries. 27 | 28 | ## Third-party licenses 29 | All code in the [`./web/third-party/`](./web/third-party/) folder uses the MIT license: 30 | 31 | - [`jquery-ui.css`](./web/third-party/jquery-ui.css) 32 | - [`raphael-min.js`](./web/third-party/raphael-min.js) 33 | - [`three-dots.css`](./web/third-party/three-dots.css) 34 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playerAndVisualizer", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "express": "^4.18.2", 9 | "node-fetch": "^3.3.2" 10 | } 11 | }, 12 | "node_modules/accepts": { 13 | "version": "1.3.8", 14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 15 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 16 | "dependencies": { 17 | "mime-types": "~2.1.34", 18 | "negotiator": "0.6.3" 19 | }, 20 | "engines": { 21 | "node": ">= 0.6" 22 | } 23 | }, 24 | "node_modules/array-flatten": { 25 | "version": "1.1.1", 26 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 27 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 28 | }, 29 | "node_modules/body-parser": { 30 | "version": "1.20.1", 31 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 32 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 33 | "dependencies": { 34 | "bytes": "3.1.2", 35 | "content-type": "~1.0.4", 36 | "debug": "2.6.9", 37 | "depd": "2.0.0", 38 | "destroy": "1.2.0", 39 | "http-errors": "2.0.0", 40 | "iconv-lite": "0.4.24", 41 | "on-finished": "2.4.1", 42 | "qs": "6.11.0", 43 | "raw-body": "2.5.1", 44 | "type-is": "~1.6.18", 45 | "unpipe": "1.0.0" 46 | }, 47 | "engines": { 48 | "node": ">= 0.8", 49 | "npm": "1.2.8000 || >= 1.4.16" 50 | } 51 | }, 52 | "node_modules/bytes": { 53 | "version": "3.1.2", 54 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 55 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 56 | "engines": { 57 | "node": ">= 0.8" 58 | } 59 | }, 60 | "node_modules/call-bind": { 61 | "version": "1.0.2", 62 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 63 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 64 | "dependencies": { 65 | "function-bind": "^1.1.1", 66 | "get-intrinsic": "^1.0.2" 67 | }, 68 | "funding": { 69 | "url": "https://github.com/sponsors/ljharb" 70 | } 71 | }, 72 | "node_modules/content-disposition": { 73 | "version": "0.5.4", 74 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 75 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 76 | "dependencies": { 77 | "safe-buffer": "5.2.1" 78 | }, 79 | "engines": { 80 | "node": ">= 0.6" 81 | } 82 | }, 83 | "node_modules/content-type": { 84 | "version": "1.0.5", 85 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 86 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 87 | "engines": { 88 | "node": ">= 0.6" 89 | } 90 | }, 91 | "node_modules/cookie": { 92 | "version": "0.5.0", 93 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 94 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 95 | "engines": { 96 | "node": ">= 0.6" 97 | } 98 | }, 99 | "node_modules/cookie-signature": { 100 | "version": "1.0.6", 101 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 102 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 103 | }, 104 | "node_modules/data-uri-to-buffer": { 105 | "version": "4.0.1", 106 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 107 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 108 | "engines": { 109 | "node": ">= 12" 110 | } 111 | }, 112 | "node_modules/debug": { 113 | "version": "2.6.9", 114 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 115 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 116 | "dependencies": { 117 | "ms": "2.0.0" 118 | } 119 | }, 120 | "node_modules/depd": { 121 | "version": "2.0.0", 122 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 123 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 124 | "engines": { 125 | "node": ">= 0.8" 126 | } 127 | }, 128 | "node_modules/destroy": { 129 | "version": "1.2.0", 130 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 131 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 132 | "engines": { 133 | "node": ">= 0.8", 134 | "npm": "1.2.8000 || >= 1.4.16" 135 | } 136 | }, 137 | "node_modules/ee-first": { 138 | "version": "1.1.1", 139 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 140 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 141 | }, 142 | "node_modules/encodeurl": { 143 | "version": "1.0.2", 144 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 145 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 146 | "engines": { 147 | "node": ">= 0.8" 148 | } 149 | }, 150 | "node_modules/escape-html": { 151 | "version": "1.0.3", 152 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 153 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 154 | }, 155 | "node_modules/etag": { 156 | "version": "1.8.1", 157 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 158 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 159 | "engines": { 160 | "node": ">= 0.6" 161 | } 162 | }, 163 | "node_modules/express": { 164 | "version": "4.18.2", 165 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 166 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 167 | "dependencies": { 168 | "accepts": "~1.3.8", 169 | "array-flatten": "1.1.1", 170 | "body-parser": "1.20.1", 171 | "content-disposition": "0.5.4", 172 | "content-type": "~1.0.4", 173 | "cookie": "0.5.0", 174 | "cookie-signature": "1.0.6", 175 | "debug": "2.6.9", 176 | "depd": "2.0.0", 177 | "encodeurl": "~1.0.2", 178 | "escape-html": "~1.0.3", 179 | "etag": "~1.8.1", 180 | "finalhandler": "1.2.0", 181 | "fresh": "0.5.2", 182 | "http-errors": "2.0.0", 183 | "merge-descriptors": "1.0.1", 184 | "methods": "~1.1.2", 185 | "on-finished": "2.4.1", 186 | "parseurl": "~1.3.3", 187 | "path-to-regexp": "0.1.7", 188 | "proxy-addr": "~2.0.7", 189 | "qs": "6.11.0", 190 | "range-parser": "~1.2.1", 191 | "safe-buffer": "5.2.1", 192 | "send": "0.18.0", 193 | "serve-static": "1.15.0", 194 | "setprototypeof": "1.2.0", 195 | "statuses": "2.0.1", 196 | "type-is": "~1.6.18", 197 | "utils-merge": "1.0.1", 198 | "vary": "~1.1.2" 199 | }, 200 | "engines": { 201 | "node": ">= 0.10.0" 202 | } 203 | }, 204 | "node_modules/fetch-blob": { 205 | "version": "3.2.0", 206 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 207 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 208 | "funding": [ 209 | { 210 | "type": "github", 211 | "url": "https://github.com/sponsors/jimmywarting" 212 | }, 213 | { 214 | "type": "paypal", 215 | "url": "https://paypal.me/jimmywarting" 216 | } 217 | ], 218 | "dependencies": { 219 | "node-domexception": "^1.0.0", 220 | "web-streams-polyfill": "^3.0.3" 221 | }, 222 | "engines": { 223 | "node": "^12.20 || >= 14.13" 224 | } 225 | }, 226 | "node_modules/finalhandler": { 227 | "version": "1.2.0", 228 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 229 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 230 | "dependencies": { 231 | "debug": "2.6.9", 232 | "encodeurl": "~1.0.2", 233 | "escape-html": "~1.0.3", 234 | "on-finished": "2.4.1", 235 | "parseurl": "~1.3.3", 236 | "statuses": "2.0.1", 237 | "unpipe": "~1.0.0" 238 | }, 239 | "engines": { 240 | "node": ">= 0.8" 241 | } 242 | }, 243 | "node_modules/formdata-polyfill": { 244 | "version": "4.0.10", 245 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 246 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 247 | "dependencies": { 248 | "fetch-blob": "^3.1.2" 249 | }, 250 | "engines": { 251 | "node": ">=12.20.0" 252 | } 253 | }, 254 | "node_modules/forwarded": { 255 | "version": "0.2.0", 256 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 257 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 258 | "engines": { 259 | "node": ">= 0.6" 260 | } 261 | }, 262 | "node_modules/fresh": { 263 | "version": "0.5.2", 264 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 265 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 266 | "engines": { 267 | "node": ">= 0.6" 268 | } 269 | }, 270 | "node_modules/function-bind": { 271 | "version": "1.1.1", 272 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 273 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 274 | }, 275 | "node_modules/get-intrinsic": { 276 | "version": "1.2.1", 277 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 278 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 279 | "dependencies": { 280 | "function-bind": "^1.1.1", 281 | "has": "^1.0.3", 282 | "has-proto": "^1.0.1", 283 | "has-symbols": "^1.0.3" 284 | }, 285 | "funding": { 286 | "url": "https://github.com/sponsors/ljharb" 287 | } 288 | }, 289 | "node_modules/has": { 290 | "version": "1.0.3", 291 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 292 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 293 | "dependencies": { 294 | "function-bind": "^1.1.1" 295 | }, 296 | "engines": { 297 | "node": ">= 0.4.0" 298 | } 299 | }, 300 | "node_modules/has-proto": { 301 | "version": "1.0.1", 302 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 303 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 304 | "engines": { 305 | "node": ">= 0.4" 306 | }, 307 | "funding": { 308 | "url": "https://github.com/sponsors/ljharb" 309 | } 310 | }, 311 | "node_modules/has-symbols": { 312 | "version": "1.0.3", 313 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 314 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 315 | "engines": { 316 | "node": ">= 0.4" 317 | }, 318 | "funding": { 319 | "url": "https://github.com/sponsors/ljharb" 320 | } 321 | }, 322 | "node_modules/http-errors": { 323 | "version": "2.0.0", 324 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 325 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 326 | "dependencies": { 327 | "depd": "2.0.0", 328 | "inherits": "2.0.4", 329 | "setprototypeof": "1.2.0", 330 | "statuses": "2.0.1", 331 | "toidentifier": "1.0.1" 332 | }, 333 | "engines": { 334 | "node": ">= 0.8" 335 | } 336 | }, 337 | "node_modules/iconv-lite": { 338 | "version": "0.4.24", 339 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 340 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 341 | "dependencies": { 342 | "safer-buffer": ">= 2.1.2 < 3" 343 | }, 344 | "engines": { 345 | "node": ">=0.10.0" 346 | } 347 | }, 348 | "node_modules/inherits": { 349 | "version": "2.0.4", 350 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 351 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 352 | }, 353 | "node_modules/ipaddr.js": { 354 | "version": "1.9.1", 355 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 356 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 357 | "engines": { 358 | "node": ">= 0.10" 359 | } 360 | }, 361 | "node_modules/media-typer": { 362 | "version": "0.3.0", 363 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 364 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 365 | "engines": { 366 | "node": ">= 0.6" 367 | } 368 | }, 369 | "node_modules/merge-descriptors": { 370 | "version": "1.0.1", 371 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 372 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 373 | }, 374 | "node_modules/methods": { 375 | "version": "1.1.2", 376 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 377 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 378 | "engines": { 379 | "node": ">= 0.6" 380 | } 381 | }, 382 | "node_modules/mime": { 383 | "version": "1.6.0", 384 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 385 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 386 | "bin": { 387 | "mime": "cli.js" 388 | }, 389 | "engines": { 390 | "node": ">=4" 391 | } 392 | }, 393 | "node_modules/mime-db": { 394 | "version": "1.52.0", 395 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 396 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 397 | "engines": { 398 | "node": ">= 0.6" 399 | } 400 | }, 401 | "node_modules/mime-types": { 402 | "version": "2.1.35", 403 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 404 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 405 | "dependencies": { 406 | "mime-db": "1.52.0" 407 | }, 408 | "engines": { 409 | "node": ">= 0.6" 410 | } 411 | }, 412 | "node_modules/ms": { 413 | "version": "2.0.0", 414 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 415 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 416 | }, 417 | "node_modules/negotiator": { 418 | "version": "0.6.3", 419 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 420 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 421 | "engines": { 422 | "node": ">= 0.6" 423 | } 424 | }, 425 | "node_modules/node-domexception": { 426 | "version": "1.0.0", 427 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 428 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 429 | "funding": [ 430 | { 431 | "type": "github", 432 | "url": "https://github.com/sponsors/jimmywarting" 433 | }, 434 | { 435 | "type": "github", 436 | "url": "https://paypal.me/jimmywarting" 437 | } 438 | ], 439 | "engines": { 440 | "node": ">=10.5.0" 441 | } 442 | }, 443 | "node_modules/node-fetch": { 444 | "version": "3.3.2", 445 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 446 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 447 | "dependencies": { 448 | "data-uri-to-buffer": "^4.0.0", 449 | "fetch-blob": "^3.1.4", 450 | "formdata-polyfill": "^4.0.10" 451 | }, 452 | "engines": { 453 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 454 | }, 455 | "funding": { 456 | "type": "opencollective", 457 | "url": "https://opencollective.com/node-fetch" 458 | } 459 | }, 460 | "node_modules/object-inspect": { 461 | "version": "1.12.3", 462 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 463 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 464 | "funding": { 465 | "url": "https://github.com/sponsors/ljharb" 466 | } 467 | }, 468 | "node_modules/on-finished": { 469 | "version": "2.4.1", 470 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 471 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 472 | "dependencies": { 473 | "ee-first": "1.1.1" 474 | }, 475 | "engines": { 476 | "node": ">= 0.8" 477 | } 478 | }, 479 | "node_modules/parseurl": { 480 | "version": "1.3.3", 481 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 482 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 483 | "engines": { 484 | "node": ">= 0.8" 485 | } 486 | }, 487 | "node_modules/path-to-regexp": { 488 | "version": "0.1.7", 489 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 490 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 491 | }, 492 | "node_modules/proxy-addr": { 493 | "version": "2.0.7", 494 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 495 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 496 | "dependencies": { 497 | "forwarded": "0.2.0", 498 | "ipaddr.js": "1.9.1" 499 | }, 500 | "engines": { 501 | "node": ">= 0.10" 502 | } 503 | }, 504 | "node_modules/qs": { 505 | "version": "6.11.0", 506 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 507 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 508 | "dependencies": { 509 | "side-channel": "^1.0.4" 510 | }, 511 | "engines": { 512 | "node": ">=0.6" 513 | }, 514 | "funding": { 515 | "url": "https://github.com/sponsors/ljharb" 516 | } 517 | }, 518 | "node_modules/range-parser": { 519 | "version": "1.2.1", 520 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 521 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 522 | "engines": { 523 | "node": ">= 0.6" 524 | } 525 | }, 526 | "node_modules/raw-body": { 527 | "version": "2.5.1", 528 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 529 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 530 | "dependencies": { 531 | "bytes": "3.1.2", 532 | "http-errors": "2.0.0", 533 | "iconv-lite": "0.4.24", 534 | "unpipe": "1.0.0" 535 | }, 536 | "engines": { 537 | "node": ">= 0.8" 538 | } 539 | }, 540 | "node_modules/safe-buffer": { 541 | "version": "5.2.1", 542 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 543 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 544 | "funding": [ 545 | { 546 | "type": "github", 547 | "url": "https://github.com/sponsors/feross" 548 | }, 549 | { 550 | "type": "patreon", 551 | "url": "https://www.patreon.com/feross" 552 | }, 553 | { 554 | "type": "consulting", 555 | "url": "https://feross.org/support" 556 | } 557 | ] 558 | }, 559 | "node_modules/safer-buffer": { 560 | "version": "2.1.2", 561 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 562 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 563 | }, 564 | "node_modules/send": { 565 | "version": "0.18.0", 566 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 567 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 568 | "dependencies": { 569 | "debug": "2.6.9", 570 | "depd": "2.0.0", 571 | "destroy": "1.2.0", 572 | "encodeurl": "~1.0.2", 573 | "escape-html": "~1.0.3", 574 | "etag": "~1.8.1", 575 | "fresh": "0.5.2", 576 | "http-errors": "2.0.0", 577 | "mime": "1.6.0", 578 | "ms": "2.1.3", 579 | "on-finished": "2.4.1", 580 | "range-parser": "~1.2.1", 581 | "statuses": "2.0.1" 582 | }, 583 | "engines": { 584 | "node": ">= 0.8.0" 585 | } 586 | }, 587 | "node_modules/send/node_modules/ms": { 588 | "version": "2.1.3", 589 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 590 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 591 | }, 592 | "node_modules/serve-static": { 593 | "version": "1.15.0", 594 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 595 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 596 | "dependencies": { 597 | "encodeurl": "~1.0.2", 598 | "escape-html": "~1.0.3", 599 | "parseurl": "~1.3.3", 600 | "send": "0.18.0" 601 | }, 602 | "engines": { 603 | "node": ">= 0.8.0" 604 | } 605 | }, 606 | "node_modules/setprototypeof": { 607 | "version": "1.2.0", 608 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 609 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 610 | }, 611 | "node_modules/side-channel": { 612 | "version": "1.0.4", 613 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 614 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 615 | "dependencies": { 616 | "call-bind": "^1.0.0", 617 | "get-intrinsic": "^1.0.2", 618 | "object-inspect": "^1.9.0" 619 | }, 620 | "funding": { 621 | "url": "https://github.com/sponsors/ljharb" 622 | } 623 | }, 624 | "node_modules/statuses": { 625 | "version": "2.0.1", 626 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 627 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 628 | "engines": { 629 | "node": ">= 0.8" 630 | } 631 | }, 632 | "node_modules/toidentifier": { 633 | "version": "1.0.1", 634 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 635 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 636 | "engines": { 637 | "node": ">=0.6" 638 | } 639 | }, 640 | "node_modules/type-is": { 641 | "version": "1.6.18", 642 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 643 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 644 | "dependencies": { 645 | "media-typer": "0.3.0", 646 | "mime-types": "~2.1.24" 647 | }, 648 | "engines": { 649 | "node": ">= 0.6" 650 | } 651 | }, 652 | "node_modules/unpipe": { 653 | "version": "1.0.0", 654 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 655 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 656 | "engines": { 657 | "node": ">= 0.8" 658 | } 659 | }, 660 | "node_modules/utils-merge": { 661 | "version": "1.0.1", 662 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 663 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 664 | "engines": { 665 | "node": ">= 0.4.0" 666 | } 667 | }, 668 | "node_modules/vary": { 669 | "version": "1.1.2", 670 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 671 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 672 | "engines": { 673 | "node": ">= 0.8" 674 | } 675 | }, 676 | "node_modules/web-streams-polyfill": { 677 | "version": "3.2.1", 678 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 679 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", 680 | "engines": { 681 | "node": ">= 8" 682 | } 683 | } 684 | }, 685 | "dependencies": { 686 | "accepts": { 687 | "version": "1.3.8", 688 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 689 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 690 | "requires": { 691 | "mime-types": "~2.1.34", 692 | "negotiator": "0.6.3" 693 | } 694 | }, 695 | "array-flatten": { 696 | "version": "1.1.1", 697 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 698 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 699 | }, 700 | "body-parser": { 701 | "version": "1.20.1", 702 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 703 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 704 | "requires": { 705 | "bytes": "3.1.2", 706 | "content-type": "~1.0.4", 707 | "debug": "2.6.9", 708 | "depd": "2.0.0", 709 | "destroy": "1.2.0", 710 | "http-errors": "2.0.0", 711 | "iconv-lite": "0.4.24", 712 | "on-finished": "2.4.1", 713 | "qs": "6.11.0", 714 | "raw-body": "2.5.1", 715 | "type-is": "~1.6.18", 716 | "unpipe": "1.0.0" 717 | } 718 | }, 719 | "bytes": { 720 | "version": "3.1.2", 721 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 722 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 723 | }, 724 | "call-bind": { 725 | "version": "1.0.2", 726 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 727 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 728 | "requires": { 729 | "function-bind": "^1.1.1", 730 | "get-intrinsic": "^1.0.2" 731 | } 732 | }, 733 | "content-disposition": { 734 | "version": "0.5.4", 735 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 736 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 737 | "requires": { 738 | "safe-buffer": "5.2.1" 739 | } 740 | }, 741 | "content-type": { 742 | "version": "1.0.5", 743 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 744 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" 745 | }, 746 | "cookie": { 747 | "version": "0.5.0", 748 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 749 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" 750 | }, 751 | "cookie-signature": { 752 | "version": "1.0.6", 753 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 754 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 755 | }, 756 | "data-uri-to-buffer": { 757 | "version": "4.0.1", 758 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 759 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" 760 | }, 761 | "debug": { 762 | "version": "2.6.9", 763 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 764 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 765 | "requires": { 766 | "ms": "2.0.0" 767 | } 768 | }, 769 | "depd": { 770 | "version": "2.0.0", 771 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 772 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 773 | }, 774 | "destroy": { 775 | "version": "1.2.0", 776 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 777 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 778 | }, 779 | "ee-first": { 780 | "version": "1.1.1", 781 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 782 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 783 | }, 784 | "encodeurl": { 785 | "version": "1.0.2", 786 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 787 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 788 | }, 789 | "escape-html": { 790 | "version": "1.0.3", 791 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 792 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 793 | }, 794 | "etag": { 795 | "version": "1.8.1", 796 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 797 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 798 | }, 799 | "express": { 800 | "version": "4.18.2", 801 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 802 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 803 | "requires": { 804 | "accepts": "~1.3.8", 805 | "array-flatten": "1.1.1", 806 | "body-parser": "1.20.1", 807 | "content-disposition": "0.5.4", 808 | "content-type": "~1.0.4", 809 | "cookie": "0.5.0", 810 | "cookie-signature": "1.0.6", 811 | "debug": "2.6.9", 812 | "depd": "2.0.0", 813 | "encodeurl": "~1.0.2", 814 | "escape-html": "~1.0.3", 815 | "etag": "~1.8.1", 816 | "finalhandler": "1.2.0", 817 | "fresh": "0.5.2", 818 | "http-errors": "2.0.0", 819 | "merge-descriptors": "1.0.1", 820 | "methods": "~1.1.2", 821 | "on-finished": "2.4.1", 822 | "parseurl": "~1.3.3", 823 | "path-to-regexp": "0.1.7", 824 | "proxy-addr": "~2.0.7", 825 | "qs": "6.11.0", 826 | "range-parser": "~1.2.1", 827 | "safe-buffer": "5.2.1", 828 | "send": "0.18.0", 829 | "serve-static": "1.15.0", 830 | "setprototypeof": "1.2.0", 831 | "statuses": "2.0.1", 832 | "type-is": "~1.6.18", 833 | "utils-merge": "1.0.1", 834 | "vary": "~1.1.2" 835 | } 836 | }, 837 | "fetch-blob": { 838 | "version": "3.2.0", 839 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 840 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 841 | "requires": { 842 | "node-domexception": "^1.0.0", 843 | "web-streams-polyfill": "^3.0.3" 844 | } 845 | }, 846 | "finalhandler": { 847 | "version": "1.2.0", 848 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 849 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 850 | "requires": { 851 | "debug": "2.6.9", 852 | "encodeurl": "~1.0.2", 853 | "escape-html": "~1.0.3", 854 | "on-finished": "2.4.1", 855 | "parseurl": "~1.3.3", 856 | "statuses": "2.0.1", 857 | "unpipe": "~1.0.0" 858 | } 859 | }, 860 | "formdata-polyfill": { 861 | "version": "4.0.10", 862 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 863 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 864 | "requires": { 865 | "fetch-blob": "^3.1.2" 866 | } 867 | }, 868 | "forwarded": { 869 | "version": "0.2.0", 870 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 871 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 872 | }, 873 | "fresh": { 874 | "version": "0.5.2", 875 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 876 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" 877 | }, 878 | "function-bind": { 879 | "version": "1.1.1", 880 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 881 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 882 | }, 883 | "get-intrinsic": { 884 | "version": "1.2.1", 885 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 886 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 887 | "requires": { 888 | "function-bind": "^1.1.1", 889 | "has": "^1.0.3", 890 | "has-proto": "^1.0.1", 891 | "has-symbols": "^1.0.3" 892 | } 893 | }, 894 | "has": { 895 | "version": "1.0.3", 896 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 897 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 898 | "requires": { 899 | "function-bind": "^1.1.1" 900 | } 901 | }, 902 | "has-proto": { 903 | "version": "1.0.1", 904 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 905 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" 906 | }, 907 | "has-symbols": { 908 | "version": "1.0.3", 909 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 910 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 911 | }, 912 | "http-errors": { 913 | "version": "2.0.0", 914 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 915 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 916 | "requires": { 917 | "depd": "2.0.0", 918 | "inherits": "2.0.4", 919 | "setprototypeof": "1.2.0", 920 | "statuses": "2.0.1", 921 | "toidentifier": "1.0.1" 922 | } 923 | }, 924 | "iconv-lite": { 925 | "version": "0.4.24", 926 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 927 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 928 | "requires": { 929 | "safer-buffer": ">= 2.1.2 < 3" 930 | } 931 | }, 932 | "inherits": { 933 | "version": "2.0.4", 934 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 935 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 936 | }, 937 | "ipaddr.js": { 938 | "version": "1.9.1", 939 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 940 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 941 | }, 942 | "media-typer": { 943 | "version": "0.3.0", 944 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 945 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 946 | }, 947 | "merge-descriptors": { 948 | "version": "1.0.1", 949 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 950 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 951 | }, 952 | "methods": { 953 | "version": "1.1.2", 954 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 955 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" 956 | }, 957 | "mime": { 958 | "version": "1.6.0", 959 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 960 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 961 | }, 962 | "mime-db": { 963 | "version": "1.52.0", 964 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 965 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 966 | }, 967 | "mime-types": { 968 | "version": "2.1.35", 969 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 970 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 971 | "requires": { 972 | "mime-db": "1.52.0" 973 | } 974 | }, 975 | "ms": { 976 | "version": "2.0.0", 977 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 978 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 979 | }, 980 | "negotiator": { 981 | "version": "0.6.3", 982 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 983 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 984 | }, 985 | "node-domexception": { 986 | "version": "1.0.0", 987 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 988 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" 989 | }, 990 | "node-fetch": { 991 | "version": "3.3.2", 992 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 993 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 994 | "requires": { 995 | "data-uri-to-buffer": "^4.0.0", 996 | "fetch-blob": "^3.1.4", 997 | "formdata-polyfill": "^4.0.10" 998 | } 999 | }, 1000 | "object-inspect": { 1001 | "version": "1.12.3", 1002 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 1003 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" 1004 | }, 1005 | "on-finished": { 1006 | "version": "2.4.1", 1007 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1008 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1009 | "requires": { 1010 | "ee-first": "1.1.1" 1011 | } 1012 | }, 1013 | "parseurl": { 1014 | "version": "1.3.3", 1015 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1016 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1017 | }, 1018 | "path-to-regexp": { 1019 | "version": "0.1.7", 1020 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1021 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 1022 | }, 1023 | "proxy-addr": { 1024 | "version": "2.0.7", 1025 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1026 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1027 | "requires": { 1028 | "forwarded": "0.2.0", 1029 | "ipaddr.js": "1.9.1" 1030 | } 1031 | }, 1032 | "qs": { 1033 | "version": "6.11.0", 1034 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 1035 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 1036 | "requires": { 1037 | "side-channel": "^1.0.4" 1038 | } 1039 | }, 1040 | "range-parser": { 1041 | "version": "1.2.1", 1042 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1043 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1044 | }, 1045 | "raw-body": { 1046 | "version": "2.5.1", 1047 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 1048 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 1049 | "requires": { 1050 | "bytes": "3.1.2", 1051 | "http-errors": "2.0.0", 1052 | "iconv-lite": "0.4.24", 1053 | "unpipe": "1.0.0" 1054 | } 1055 | }, 1056 | "safe-buffer": { 1057 | "version": "5.2.1", 1058 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1059 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1060 | }, 1061 | "safer-buffer": { 1062 | "version": "2.1.2", 1063 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1064 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1065 | }, 1066 | "send": { 1067 | "version": "0.18.0", 1068 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 1069 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 1070 | "requires": { 1071 | "debug": "2.6.9", 1072 | "depd": "2.0.0", 1073 | "destroy": "1.2.0", 1074 | "encodeurl": "~1.0.2", 1075 | "escape-html": "~1.0.3", 1076 | "etag": "~1.8.1", 1077 | "fresh": "0.5.2", 1078 | "http-errors": "2.0.0", 1079 | "mime": "1.6.0", 1080 | "ms": "2.1.3", 1081 | "on-finished": "2.4.1", 1082 | "range-parser": "~1.2.1", 1083 | "statuses": "2.0.1" 1084 | }, 1085 | "dependencies": { 1086 | "ms": { 1087 | "version": "2.1.3", 1088 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1089 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1090 | } 1091 | } 1092 | }, 1093 | "serve-static": { 1094 | "version": "1.15.0", 1095 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 1096 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 1097 | "requires": { 1098 | "encodeurl": "~1.0.2", 1099 | "escape-html": "~1.0.3", 1100 | "parseurl": "~1.3.3", 1101 | "send": "0.18.0" 1102 | } 1103 | }, 1104 | "setprototypeof": { 1105 | "version": "1.2.0", 1106 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1107 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1108 | }, 1109 | "side-channel": { 1110 | "version": "1.0.4", 1111 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 1112 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 1113 | "requires": { 1114 | "call-bind": "^1.0.0", 1115 | "get-intrinsic": "^1.0.2", 1116 | "object-inspect": "^1.9.0" 1117 | } 1118 | }, 1119 | "statuses": { 1120 | "version": "2.0.1", 1121 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1122 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 1123 | }, 1124 | "toidentifier": { 1125 | "version": "1.0.1", 1126 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1127 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 1128 | }, 1129 | "type-is": { 1130 | "version": "1.6.18", 1131 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1132 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1133 | "requires": { 1134 | "media-typer": "0.3.0", 1135 | "mime-types": "~2.1.24" 1136 | } 1137 | }, 1138 | "unpipe": { 1139 | "version": "1.0.0", 1140 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1141 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 1142 | }, 1143 | "utils-merge": { 1144 | "version": "1.0.1", 1145 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1146 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" 1147 | }, 1148 | "vary": { 1149 | "version": "1.1.2", 1150 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1151 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 1152 | }, 1153 | "web-streams-polyfill": { 1154 | "version": "3.2.1", 1155 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 1156 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" 1157 | } 1158 | } 1159 | } 1160 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "server.js", 3 | "type": "module", 4 | "dependencies": { 5 | "express": "^4.18.2", 6 | "node-fetch": "^3.3.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/server.js: -------------------------------------------------------------------------------- 1 | // Illustrates how to connect the Infinite Jukebox algorithm to audio and a 2 | // visualizer. See ./README.md for details. 3 | // 4 | 5 | import express from 'express'; 6 | import fetch from 'node-fetch'; 7 | import path, { dirname } from 'path'; 8 | import { fileURLToPath } from 'url'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | const port = 2012; 14 | const repoRoot = path.join(__dirname, '../../../'); 15 | const algorithmDir = path.join(repoRoot, 'js', 'algorithm'); 16 | 17 | function serveLocalOrRemotePath(app, sourcePath, targetPath) { 18 | if (targetPath.startsWith('http://') || targetPath.startsWith('https://')) { 19 | app.get(sourcePath, async (req, resp) => { 20 | try { 21 | const fetchResult = await fetch(targetPath); 22 | if (fetchResult.ok) { 23 | resp.set('Content-Type', fetchResult.headers.get('Content-Type')); 24 | fetchResult.body.pipe(resp); 25 | } else { 26 | resp.status(fetchResult.status).send(); 27 | } 28 | } catch (error) { 29 | console.error('Error fetching data:', error); 30 | resp.status(500).send(); 31 | } 32 | }); 33 | } else { 34 | app.use(sourcePath, express.static(targetPath)); 35 | } 36 | } 37 | 38 | function main(args) { 39 | if (args.length !== 2) { 40 | console.log('Usage: node server.js '); 41 | return; 42 | } 43 | 44 | const [spotifyAnalysisPath, songPath] = args; 45 | 46 | const app = express(); 47 | 48 | serveLocalOrRemotePath(app, '/data/analysis.json', spotifyAnalysisPath); 49 | serveLocalOrRemotePath(app, '/data/song.wav', songPath); 50 | app.use('/algorithm', express.static(algorithmDir)); 51 | app.use('/', express.static('./web')); 52 | 53 | app.listen(port, () => { 54 | console.log(`Server is running on http://localhost:${port}`); 55 | }); 56 | } 57 | 58 | main(process.argv.slice(2)); 59 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Infinite Jukebox Player & Visualizer 25 | 26 | 27 | 38 |
39 |
40 |
41 |
42 |
43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/lib/AudioQueue.js: -------------------------------------------------------------------------------- 1 | // This file contains code derived from EternalJukebox (https://github.com/UnderMybrella/EternalJukebox/). 2 | // Copyright 2021 UnderMybrella 3 | // See the LICENSE file for the full MIT license terms. 4 | 5 | function assert(pred, msg) { 6 | if (!pred) { 7 | throw new Error('Assert failed: ' + msg); 8 | } 9 | } 10 | 11 | function pp(x) { 12 | return JSON.stringify(x, undefined, 2); 13 | } 14 | 15 | async function fetchArrayBuffer(url) { 16 | const resp = await fetch(url); 17 | assert(resp.ok, 'fetchArrayBuffer failed: ' + pp({ 18 | url, 19 | httpStatus: resp.status + ' ' + resp.statusText 20 | })); 21 | return await resp.arrayBuffer(); 22 | } 23 | 24 | async function fetchAudioContext(url, onProgress) { 25 | const arrayBuffer = await fetchArrayBuffer(url); 26 | const audioContext = new AudioContext(); 27 | const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); 28 | return { 29 | audioContext, 30 | audioBuffer, 31 | }; 32 | } 33 | 34 | export default class AudioQueue { 35 | static async fromUrl(url) { 36 | const result = await fetchAudioContext(url); 37 | return new AudioQueue(result.audioContext, result.audioBuffer); 38 | } 39 | 40 | constructor(audioContext, audioBuffer) { 41 | this._audioContext = audioContext; 42 | this._audioBuffer = audioBuffer; 43 | this._endOfLastQueuedBeat = 0; 44 | 45 | this._lastQueuedBeat = undefined; 46 | this._lastQueuedAudioSource = undefined; 47 | } 48 | 49 | get endOfLastQueuedBeat() { 50 | return this._endOfLastQueuedBeat; 51 | } 52 | 53 | get currentTime() { 54 | return this._audioContext.currentTime; 55 | } 56 | 57 | queue(beat) { 58 | const beatsAreAdjacent = this._lastQueuedBeat && this._lastQueuedBeat.which + 1 === beat.which; 59 | if (!beatsAreAdjacent) { 60 | if (!this._lastQueuedBeat) { 61 | console.log('Play: ' + beat.start); 62 | this._endOfLastQueuedBeat = this._audioContext.currentTime; 63 | } else { 64 | const prevBeatEnd = this._lastQueuedBeat.start + this._lastQueuedBeat.duration; 65 | console.log('Seek: ' + prevBeatEnd + ' -> ' + beat.start); 66 | } 67 | 68 | const audioSource = this._audioContext.createBufferSource(); 69 | audioSource.buffer = this._audioBuffer; 70 | audioSource.connect(this._audioContext.destination); 71 | audioSource.start(this._endOfLastQueuedBeat, beat.start); 72 | 73 | this._lastQueuedAudioSource?.stop(this._endOfLastQueuedBeat); 74 | 75 | this._lastQueuedAudioSource = audioSource; 76 | } 77 | 78 | this._endOfLastQueuedBeat += beat.duration; 79 | this._lastQueuedBeat = beat; 80 | } 81 | 82 | stop() { 83 | this._lastQueuedAudioSource?.stop(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/lib/makeVisualizer.js: -------------------------------------------------------------------------------- 1 | // This file contains code derived from EternalJukebox (https://github.com/UnderMybrella/EternalJukebox/). 2 | // Copyright 2021 UnderMybrella 3 | // See the LICENSE file for the full MIT license terms. 4 | 5 | import remixTrack from '/algorithm/remixTrack.js'; 6 | import calculateNearestNeighbors from '/algorithm/calculateNearestNeighbors.js'; 7 | 8 | export default function makeVisualizer(spotifyAnalysis, driver) { 9 | let track = { 10 | analysis: { 11 | sections: spotifyAnalysis.sections, 12 | bars: spotifyAnalysis.bars, 13 | beats: spotifyAnalysis.beats, 14 | tatums: spotifyAnalysis.tatums, 15 | segments: spotifyAnalysis.segments, 16 | }, 17 | }; 18 | 19 | // Deep clone track since we're going to modify it. 20 | track = JSON.parse(JSON.stringify(track)); 21 | 22 | remixTrack(track); 23 | calculateNearestNeighbors(track); 24 | 25 | const cmin = [100, 100, 100]; 26 | const cmax = [-100, -100, -100]; 27 | 28 | const W = 900; 29 | const H = 680; 30 | const paper = Raphael("tiles", W, H); 31 | 32 | const highlightColor = "#0000ff"; 33 | const jumpHighlightColor = "#00ff22"; 34 | const selectColor = "#ff0000"; 35 | const debugMode = true; 36 | 37 | const shifted = false; 38 | 39 | const minTileWidth = 10; 40 | const maxTileWidth = 90; 41 | const growthPerPlay = 10; 42 | const curGrowFactor = 1; 43 | 44 | const maxBranches = 4; // max branches allowed per beat 45 | 46 | const jukeboxData = { 47 | tiles: [], 48 | }; 49 | 50 | function info(msg) { 51 | } 52 | 53 | function readyToPlay(t) { 54 | setDisplayMode(true); 55 | normalizeColor(); 56 | trackReady(t); 57 | drawVisualization(); 58 | } 59 | 60 | function setDisplayMode(playMode) { 61 | if (playMode) { 62 | $("#song-div").hide(); 63 | $("#select-track").hide(); 64 | $("#running").show(); 65 | $(".rotate").hide(); 66 | } else { 67 | $("#song-div").show(); 68 | $("#select-track").show(); 69 | $("#running").hide(); 70 | $(".rotate").show(); 71 | } 72 | info(""); 73 | } 74 | 75 | function normalizeColor() { 76 | var qlist = track.analysis.segments; 77 | for (var i = 0; i < qlist.length; i++) { 78 | for (var j = 0; j < 3; j++) { 79 | var t = qlist[i].timbre[j + 1]; 80 | 81 | if (t < cmin[j]) { 82 | cmin[j] = t; 83 | } 84 | if (t > cmax[j]) { 85 | cmax[j] = t; 86 | } 87 | } 88 | } 89 | } 90 | 91 | function trackReady(t) { 92 | // t.fixedTitle = getTitle(t.info.title, t.info.artist, t.info.url); 93 | // document.title = 'Eternal Jukebox for ' + t.fixedTitle; 94 | // $("#song-title").text(t.fixedTitle); 95 | // $("#song-url").attr("href", "https://open.spotify.com/track/" + t.info.id); 96 | } 97 | 98 | function drawVisualization() { 99 | createTilePanel('beats'); 100 | } 101 | 102 | function createTilePanel(which) { 103 | removeAllTiles(); 104 | jukeboxData.tiles = createTiles(which); 105 | } 106 | 107 | function removeAllTiles() { 108 | for (var i = 0; i < jukeboxData.tiles.length; i++) { 109 | jukeboxData.tiles[i].rect.remove(); 110 | } 111 | jukeboxData.tiles = []; 112 | } 113 | 114 | function createTiles(qtype) { 115 | return createTileCircle(qtype, 250); 116 | } 117 | 118 | function createTileCircle(qtype, radius) { 119 | // var start = now(); 120 | var y_padding = 90; 121 | var x_padding = 200; 122 | var maxWidth = 90; 123 | var tiles = []; 124 | var qlist = track.analysis[qtype]; 125 | var n = qlist.length; 126 | var R = radius; 127 | var alpha = Math.PI * 2 / n; 128 | var perimeter = 2 * n * R * Math.sin(alpha / 2); 129 | var a = perimeter / n; 130 | var width = a * 20; 131 | var angleOffset = -Math.PI / 2; 132 | // var angleOffset = 0; 133 | 134 | if (width > maxWidth) { 135 | width = maxWidth; 136 | } 137 | 138 | width = minTileWidth; 139 | 140 | paper.clear(); 141 | 142 | var angle = angleOffset; 143 | for (var i = 0; i < qlist.length; i++) { 144 | var tile = createNewTile(i, qlist[i], a, width); 145 | var y = y_padding + R + R * Math.sin(angle); 146 | var x = x_padding + R + R * Math.cos(angle); 147 | tile.move(x, y); 148 | tile.rotate(angle); 149 | tiles.push(tile); 150 | angle += alpha; 151 | } 152 | 153 | // now connect every tile to its neighbors 154 | 155 | // a horrible hack until I figure out 156 | // geometry 157 | var roffset = width / 2; 158 | var yoffset = width * .52; 159 | var xoffset = width * 1; 160 | var center = ' S 450 350 '; 161 | var branchCount = 0; 162 | R -= roffset; 163 | for (var i = 0; i < tiles.length; i++) { 164 | var startAngle = alpha * i + angleOffset; 165 | var tile = tiles[i]; 166 | var y1 = y_padding + R + R * Math.sin(startAngle) + yoffset; 167 | var x1 = x_padding + R + R * Math.cos(startAngle) + xoffset; 168 | 169 | for (var j = 0; j < tile.q.neighbors.length; j++) { 170 | var destAngle = alpha * tile.q.neighbors[j].dest.which + angleOffset; 171 | var y2 = y_padding + R + R * Math.sin(destAngle) + yoffset; 172 | var x2 = x_padding + R + R * Math.cos(destAngle) + xoffset; 173 | 174 | var path = 'M' + x1 + ' ' + y1 + center + x2 + ' ' + y2; 175 | var curve = paper.path(path); 176 | curve.edge = tile.q.neighbors[j]; 177 | addCurveClickHandler(curve); 178 | highlightCurve(curve, false, false); 179 | tile.q.neighbors[j].curve = curve; 180 | branchCount++; 181 | } 182 | } 183 | jukeboxData.branchCount = branchCount; 184 | return tiles; 185 | } 186 | 187 | function createNewTile(which, q, height, width) { 188 | var padding = 0; 189 | var tile = Object.create(tilePrototype); 190 | tile.which = which; 191 | tile.width = width; 192 | tile.height = height; 193 | tile.branchColor = getBranchColor(q); 194 | tile.quantumColor = getQuantumColor(q); 195 | tile.normalColor = tile.quantumColor; 196 | tile.isPlaying = false; 197 | tile.isScaled = false; 198 | tile.playCount = 0; 199 | 200 | tile.rect = paper.rect(0, 0, tile.width, tile.height); 201 | tile.rect.attr("stroke", tile.normalColor); 202 | tile.rect.attr('stroke-width', 0); 203 | tile.q = q; 204 | tile.init(); 205 | q.tile = tile; 206 | tile.normal(); 207 | return tile; 208 | } 209 | 210 | var tilePrototype = { 211 | normalColor: "#5f9", 212 | 213 | move: function (x, y) { 214 | this.rect.attr({ x: x, y: y }); 215 | if (this.label) { 216 | this.label.attr({ x: x + 2, y: y + 8 }); 217 | } 218 | }, 219 | 220 | rotate: function (angle) { 221 | var dangle = 360 * (angle / (Math.PI * 2)); 222 | this.rect.transform('r' + dangle); 223 | }, 224 | 225 | play: function (force) { 226 | if (force || shifted) { 227 | this.playStyle(true); 228 | player.play(0, this.q); 229 | } else { 230 | this.selectStyle(); 231 | } 232 | if (force) { 233 | info("Selected tile " + this.q.which); 234 | jukeboxData.selectedTile = this; 235 | } 236 | }, 237 | 238 | 239 | selectStyle: function () { 240 | this.rect.attr("fill", "#C9a"); 241 | }, 242 | 243 | queueStyle: function () { 244 | this.rect.attr("fill", "#aFF"); 245 | }, 246 | 247 | pauseStyle: function () { 248 | this.rect.attr("fill", "#F8F"); 249 | }, 250 | 251 | playStyle: function (didJump) { 252 | if (!this.isPlaying) { 253 | this.isPlaying = true; 254 | if (!this.isScaled) { 255 | this.isScaled = true; 256 | this.rect.attr('width', maxTileWidth); 257 | } 258 | this.rect.toFront(); 259 | this.rect.attr("fill", highlightColor); 260 | highlightCurves(this, true, didJump); 261 | } 262 | }, 263 | 264 | 265 | normal: function () { 266 | this.rect.attr("fill", this.normalColor); 267 | if (this.isScaled) { 268 | this.isScaled = false; 269 | //this.rect.scale(1/1.5, 1/1.5); 270 | var newWidth = Math.round((minTileWidth + this.playCount * growthPerPlay) * curGrowFactor); 271 | if (newWidth < 1) { 272 | newWidth = 1; 273 | } 274 | if (newWidth > 90) { 275 | curGrowFactor /= 2; 276 | redrawTiles(); 277 | } else { 278 | this.rect.attr('width', newWidth); 279 | } 280 | } 281 | highlightCurves(this, false, false); 282 | this.isPlaying = false; 283 | }, 284 | 285 | init: function () { 286 | var that = this; 287 | 288 | this.rect.mouseover(function (event) { 289 | that.playStyle(false); 290 | if (debugMode) { 291 | if (that.q.which > jukeboxData.lastBranchPoint) { 292 | $("#beats").text(that.q.which + ' ' + that.q.reach + '*'); 293 | } else { 294 | var qlength = track.analysis.beats.length; 295 | var distanceToEnd = qlength - that.q.which; 296 | $("#beats").text(that.q.which + ' ' + that.q.reach 297 | + ' ' + Math.floor((that.q.reach - distanceToEnd) * 100 / qlength)); 298 | } 299 | } 300 | event.preventDefault(); 301 | }); 302 | 303 | this.rect.mouseout(function (event) { 304 | that.normal(); 305 | event.preventDefault(); 306 | }); 307 | 308 | this.rect.mousedown(function (event) { 309 | event.preventDefault(); 310 | driver.setNextTile(that); 311 | if (!driver.isRunning()) { 312 | driver.start(); 313 | } 314 | }); 315 | } 316 | }; 317 | 318 | function highlightCurves(tile, enable, didJump) { 319 | for (var i = 0; i < tile.q.neighbors.length; i++) { 320 | var curve = tile.q.neighbors[i].curve; 321 | highlightCurve(curve, enable, didJump); 322 | if (driver.isRunning()) { 323 | break; // just highlight the first one 324 | } 325 | } 326 | } 327 | 328 | function redrawTiles() { 329 | jukeboxData.tiles.forEach(function (tile) { 330 | var newWidth = Math.round((minTileWidth + tile.playCount * growthPerPlay) * curGrowFactor); 331 | if (newWidth < 1) { 332 | newWidth = 1; 333 | } 334 | tile.rect.attr('width', newWidth); 335 | }); 336 | } 337 | 338 | function getBranchColor(q) { 339 | if (q.neighbors.length === 0) { 340 | return to_rgb(0, 0, 0); 341 | } else { 342 | var red = q.neighbors.length / maxBranches; 343 | return to_rgb(red, 0, (1. - red)); 344 | } 345 | } 346 | 347 | function getQuantumColor(q) { 348 | if (isSegment(q)) { 349 | return getSegmentColor(q); 350 | } else { 351 | q = getQuantumSegment(q); 352 | if (q != null) { 353 | return getSegmentColor(q); 354 | } else { 355 | return "#000"; 356 | } 357 | } 358 | } 359 | 360 | function getSegmentColor(seg) { 361 | var results = []; 362 | for (var i = 0; i < 3; i++) { 363 | var t = seg.timbre[i + 1]; 364 | var norm = (t - cmin[i]) / (cmax[i] - cmin[i]); 365 | results[i] = norm * 255; 366 | results[i] = norm; 367 | } 368 | return to_rgb(results[1], results[2], results[0]); 369 | //return to_rgb(results[0], results[1], results[2]); 370 | } 371 | 372 | function to_rgb(r, g, b) { 373 | return "#" + convert(r * 255) + convert(g * 255) + convert(b * 255); 374 | } 375 | 376 | function convert(value) { 377 | var integer = Math.round(value); 378 | var str = Number(integer).toString(16); 379 | return str.length === 1 ? "0" + str : str; 380 | }; 381 | 382 | function isSegment(q) { 383 | return 'timbre' in q; 384 | } 385 | 386 | function getQuantumSegment(q) { 387 | return q.oseg; 388 | } 389 | 390 | function addCurveClickHandler(curve) { 391 | curve.click( 392 | function () { 393 | if (jukeboxData.selectedCurve) { 394 | highlightCurve(jukeboxData.selectedCurve, false, false); 395 | } 396 | selectCurve(curve, true); 397 | jukeboxData.selectedCurve = curve; 398 | }); 399 | 400 | curve.mouseover( 401 | function () { 402 | highlightCurve(curve, true, false); 403 | } 404 | ); 405 | 406 | curve.mouseout( 407 | function () { 408 | if (curve != jukeboxData.selectedCurve) { 409 | highlightCurve(curve, false, false); 410 | } 411 | } 412 | ); 413 | } 414 | 415 | function highlightCurve(curve, enable, jump) { 416 | if (curve) { 417 | if (enable) { 418 | var color = jump ? jumpHighlightColor : highlightColor; 419 | curve.attr('stroke-width', 4); 420 | curve.attr('stroke', color); 421 | curve.attr('stroke-opacity', 1.0); 422 | curve.toFront(); 423 | } else { 424 | if (curve.edge) { 425 | curve.attr('stroke-width', 3); 426 | curve.attr('stroke', curve.edge.src.tile.quantumColor); 427 | curve.attr('stroke-opacity', .7); 428 | } 429 | } 430 | } 431 | } 432 | 433 | function selectCurve(curve) { 434 | curve.attr('stroke-width', 6); 435 | curve.attr('stroke', selectColor); 436 | curve.attr('stroke-opacity', 1.0); 437 | curve.toFront(); 438 | } 439 | 440 | let curBeat = undefined; 441 | function setBeatIndex(beatIndex) { 442 | const didJump = curBeat && curBeat.which + 1 !== beatIndex; 443 | 444 | const prevBeat = curBeat; 445 | curBeat = jukeboxData.tiles[beatIndex]; 446 | 447 | prevBeat?.normal(); 448 | curBeat.playStyle(didJump); 449 | } 450 | 451 | readyToPlay(track); 452 | 453 | return { 454 | setBeatIndex, 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/lib/util.js: -------------------------------------------------------------------------------- 1 | function assert(pred, msg) { 2 | if (!pred) { 3 | throw new Error('Assert failed: ' + msg); 4 | } 5 | } 6 | 7 | // Returns pretty-printed JSON. 8 | function pp(x) { 9 | return JSON.stringify(x, undefined, 2); 10 | } 11 | 12 | export function timeout(ms) { 13 | return new Promise(resolve => { 14 | if (ms < 0) { 15 | resolve(); 16 | } else { 17 | setTimeout(() => { 18 | resolve(); 19 | }, ms); 20 | } 21 | }); 22 | } 23 | 24 | export async function fetchJson(url) { 25 | const resp = await fetch(url); 26 | assert(resp.ok, 'fetchJson failed: ' + pp({ 27 | url, 28 | httpStatus: resp.status + ' ' + resp.statusText 29 | })); 30 | return await resp.json(); 31 | } 32 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/main.js: -------------------------------------------------------------------------------- 1 | import InfiniteBeats from '/algorithm/InfiniteBeats.js'; 2 | import makeVisualizer from './lib/makeVisualizer.js'; 3 | import AudioQueue from './lib/AudioQueue.js'; 4 | import { fetchJson, timeout } from './lib/util.js'; 5 | 6 | let isPlaying = false; 7 | async function onPlay(track, audioQueue) { 8 | if (isPlaying) return; 9 | isPlaying = true; 10 | 11 | const infiniteBeats = new InfiniteBeats(track); 12 | 13 | let userRequestedBeat = undefined; 14 | const visualizer = makeVisualizer(track, { 15 | isRunning: () => { 16 | return isPlaying; 17 | }, 18 | setNextTile: (tile) => { 19 | // The user clicked a beat to play in the visualization. 20 | userRequestedBeat = tile.q; 21 | }, 22 | start: () => { 23 | // The user clicked a beat to play in the visualization and we're not 24 | // currently playing. Not providing an implementation since we're always 25 | // playing. 26 | }, 27 | }); 28 | 29 | let prevBeat = undefined; 30 | while (true) { 31 | const curBeat = ( 32 | userRequestedBeat ? userRequestedBeat : 33 | infiniteBeats.getNextBeat(prevBeat) 34 | ); 35 | userRequestedBeat = undefined; 36 | 37 | audioQueue.queue(curBeat); 38 | visualizer.setBeatIndex(curBeat.which); 39 | 40 | // Sleep until just before the last queued beat finishes playing. 41 | const delaySeconds = audioQueue.endOfLastQueuedBeat - audioQueue.currentTime; 42 | const delayMs = delaySeconds * 1000; 43 | await timeout(delayMs - 10); 44 | 45 | prevBeat = curBeat; 46 | } 47 | 48 | isPlaying = false; 49 | audioQueue.stop(); 50 | } 51 | 52 | function setHeaderState(state) { 53 | const headerEl = document.getElementById('header'); 54 | headerEl.classList.remove('loading', 'ready', 'error'); 55 | headerEl.classList.add(state); 56 | } 57 | 58 | async function main() { 59 | try { 60 | const track = await fetchJson('data/analysis.json'); 61 | const audioQueue = await AudioQueue.fromUrl('data/song.wav'); 62 | 63 | setHeaderState('ready'); 64 | 65 | document.getElementById('play-button').addEventListener('click', event => { 66 | onPlay(track, audioQueue); 67 | }); 68 | } catch (error) { 69 | console.log('Error:', error); 70 | setHeaderState('error'); 71 | document.getElementById('error-message').textContent = error; 72 | } 73 | } 74 | 75 | main(); 76 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/styles/canonizer_styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains code derived from EternalJukebox (https://github.com/UnderMybrella/EternalJukebox/). 3 | * Copyright 2021 UnderMybrella 4 | * See the LICENSE file for the full MIT license terms. 5 | */ 6 | 7 | li a, .dropbtn { 8 | display: inline-block; 9 | color: white; 10 | text-align: center; 11 | padding: 14px 16px; 12 | text-decoration: none; 13 | } 14 | 15 | li.dropdown { 16 | display: inline-block; 17 | } 18 | 19 | .dropdown-content { 20 | display: none; 21 | position: absolute; 22 | background-color: #f9f9f9; 23 | min-width: 160px; 24 | box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); 25 | z-index: 1; 26 | } 27 | 28 | .dropdown-content a { 29 | color: #7a8288; 30 | padding: 12px 16px; 31 | text-decoration: none; 32 | display: block; 33 | text-align: left; 34 | background-image: linear-gradient(280deg, #202328, #1c1f23) 35 | } 36 | 37 | .dropdown-content a:hover {background-image: linear-gradient(280deg, #202328, #272b30)} 38 | 39 | .dropdown:hover .dropdown-content { 40 | display: block; 41 | } 42 | 43 | #tiles { 44 | padding-left:20px; 45 | } 46 | 47 | #footer { 48 | margin-top:60px; 49 | } 50 | 51 | 52 | #file { 53 | margin-top:10px; 54 | width:auto; 55 | } 56 | 57 | #select-track { 58 | margin-top:20px; 59 | margin-bottom:20px; 60 | } 61 | 62 | #numbers { 63 | margin-left:30px; 64 | width:300px; 65 | height:20px; 66 | } 67 | 68 | #stats { 69 | margin-top:30px; 70 | margin-left:40px; 71 | } 72 | 73 | #play { 74 | width:80px; 75 | } 76 | 77 | 78 | #info-div { 79 | margin-left:40px; 80 | width: 90%; 81 | margin-bottom:10px; 82 | } 83 | 84 | #info { 85 | margin-right: 20px; 86 | font-size:28px; 87 | line-height:38px; 88 | width: 90%; 89 | } 90 | 91 | #error { 92 | margin-left:10px; 93 | margin-bottom:8px; 94 | margin-top: 10px; 95 | 96 | font-size:22px; 97 | height:60px; 98 | margin-bottom:10px; 99 | color:red; 100 | } 101 | 102 | 103 | 104 | .nval { 105 | margin-right:20px; 106 | width:60px; 107 | } 108 | 109 | 110 | #tweet-span { 111 | position:relative; 112 | top:2px; 113 | margin-left:20px; 114 | float:right; 115 | } 116 | 117 | #song-list { 118 | cursor:pointer; 119 | } 120 | 121 | .ui-dialog { 122 | top: 75px; 123 | left: 20px; 124 | } 125 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains code derived from EternalJukebox (https://github.com/UnderMybrella/EternalJukebox/). 3 | * Copyright 2021 UnderMybrella 4 | * See the LICENSE file for the full MIT license terms. 5 | */ 6 | 7 | body { 8 | background: #594f4f; 9 | background: #000000; 10 | width:900px; 11 | color: #45ada8; 12 | font-family: 'Questrial', sans-serif; 13 | margin:0 auto; 14 | } 15 | 16 | #header { 17 | margin-top: 30px; 18 | margin-left: 30px; 19 | } 20 | 21 | #header .loading-ui { 22 | font-size: 24px; 23 | } 24 | 25 | #header .loading-ui .dot-pulse { 26 | display: inline-block; 27 | margin-left: 20px; 28 | } 29 | 30 | #header .error-ui { 31 | font-size: 24px; 32 | } 33 | 34 | #header .loading-ui { display: none; } 35 | #header .ready-ui { display: none; } 36 | #header .error-ui { display: none; } 37 | #header.loading .loading-ui { display: initial; } 38 | #header.ready .ready-ui { display: initial; } 39 | #header.error .error-ui { display: initial; } 40 | 41 | h2 { 42 | margin-left:auto; 43 | margin-right:auto; 44 | font-size:28px; 45 | width:900px; 46 | } 47 | 48 | #faq { 49 | margin-top:20px; 50 | margin-left:auto; 51 | margin-right:auto; 52 | width:600px; 53 | color:#aaa; 54 | } 55 | 56 | #song-title { 57 | font-size:20px; 58 | overflow:hidden; 59 | width:600px; 60 | height:12px; 61 | } 62 | 63 | #faq h1 { 64 | text-align:center; 65 | } 66 | 67 | hr { 68 | width: 90%; 69 | } 70 | 71 | #new { 72 | } 73 | 74 | #go { 75 | } 76 | 77 | #load { 78 | text-align:center; 79 | margin-left:32px; 80 | width:320px; 81 | height:30px; 82 |     -moz-border-radius:15px; 83 | -webkit-border-radius:10px; 84 | border: 1px solid #ccc; 85 | background: #45ada8; 86 | border:1px solid #2a7ecd; 87 | padding:3px 10px; 88 | font-size: 18px; 89 | margin-bottom: 10px; 90 | color: #594f4f; 91 | } 92 | 93 | #details { 94 | margin-bottom:10px; 95 | margin-right:10px; 96 | margin-left:10px; 97 | } 98 | 99 | #stats { 100 | width: 900px; 101 | margin-bottom:4px; 102 | } 103 | 104 | #sbeats { 105 | position:relative; 106 | wdith: 150px; 107 | left:10px; 108 | } 109 | 110 | 111 | #stime { 112 | position:relative; 113 | float:right; 114 | 115 | } 116 | 117 | #file { 118 | width:200px; 119 | } 120 | 121 | 122 | 123 | #info { 124 | margin-left: 20px; 125 | margin-top: 10px; 126 | margin-bottom:10px; 127 | min-height:28px; 128 | font-size:24px; 129 | } 130 | 131 | #info2 { 132 | margin-left: 20px; 133 | margin-top: 10px; 134 | margin-bottom:10px; 135 | height:20px; 136 | font-size:24px; 137 | } 138 | 139 | #error { 140 | margin-left: auto; 141 | margin-right: auto; 142 | margin-top: 10px; 143 | 144 | font-size:24px; 145 | height:60px; 146 | margin-bottom:10px; 147 | width: 700px; 148 | text-align:center; 149 | color:red; 150 | } 151 | 152 | 153 | 154 | #title { 155 | width:900px; 156 | margin-top:20px; 157 | margin-left: auto; 158 | margin-right: auto; 159 | text-align:center; 160 | margin-bottom: 10px; 161 | font-size: 32px; 162 | font-weight: bold; 163 | } 164 | 165 | #title a {} 166 | 167 | #select-track { 168 | margin-top:20px; 169 | font-size:20px; 170 | } 171 | 172 | #file { 173 | margin-left:20px; 174 | } 175 | 176 | #main { 177 | width:900px; 178 | margin-left: auto; 179 | margin-right: auto; 180 | margin-bottom: 10px; 181 | text-align:center; 182 | } 183 | 184 | #song-div { 185 | width:500px; 186 | text-align:center; 187 | margin-left: auto; 188 | margin-right: auto; 189 | } 190 | 191 | #song-list { 192 | width:500px; 193 | text-align:left; 194 | } 195 | 196 | .song-link:hover { 197 | cursor:pointer; 198 | } 199 | 200 | a { 201 | color: #9de0ad; 202 | font-weight:bold; 203 | } 204 | 205 | a:link {text-decoration: none; } 206 | 207 | #footer { 208 | width:900px; 209 | margin-top:6px; 210 | margin-left:10px; 211 | margin-bottom:20px; 212 | text-align:center; 213 | font-size:12px; 214 | border-top:ridge; 215 | border-color: #45ada8; 216 | padding-top:6px; 217 | } 218 | 219 | #footer-list { 220 | text-align:left; 221 | margin-left:auto; 222 | margin-right:auto; 223 | width:450px; 224 | } 225 | 226 | .cbut { 227 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf) ); 228 | background-color:#ededed; 229 | -webkit-border-radius:6px; 230 | border-radius:6px; 231 | border:1px solid #dcdcdc; 232 | display:inline-block; 233 | font-size:10px; 234 | font-weight:bold; 235 | padding:3px 13px; 236 | text-decoration:none; 237 | } 238 | 239 | .cbut:hover { 240 | background-color:#dfdfdf; 241 | } 242 | 243 | .cbut:active { 244 | position:relative; 245 | top:1px; 246 | } 247 | 248 | #button-panel { 249 | margin-left:10px; 250 | top:6px; 251 | float:left; 252 | } 253 | 254 | #control-instructions { 255 | font-style:italic; 256 | font-size:12px; 257 | margin:20px; 258 | text-align:left; 259 | } 260 | 261 | #tweet-span { 262 | top:6px; 263 | float:right; 264 | } 265 | 266 | #open-img-left { 267 | width:200px; 268 | float:left; 269 | margin-bottom:20px; 270 | } 271 | 272 | #open-img-right { 273 | width:200px; 274 | float:right; 275 | margin-bottom:20px; 276 | 277 | -webkit-animation-name: rotate; 278 | -webkit-animation-duration: 0.5s; 279 | -webkit-animation-iteration-count: infinite; 280 | -webkit-animation-timing-function: linear; 281 | } 282 | 283 | 284 | /* gratuitous eye-candy */ 285 | .rotate{ 286 | -webkit-transition-duration: 0.8s; 287 | -moz-transition-duration: 0.8s; 288 | -o-transition-duration: 0.8s; 289 | transition-duration: 0.8s; 290 | 291 | -webkit-transition-property: -webkit-transform; 292 | -moz-transition-property: -moz-transform; 293 | -o-transition-property: -o-transform; 294 | transition-property: transform; 295 | overflow:hidden; 296 | } 297 | 298 | .sel-list { 299 | font-weight:bold; 300 | color: #9de0ad; 301 | } 302 | 303 | #sel-text { 304 | text-align:left; 305 | margin-left:20px; 306 | } 307 | 308 | .sel-list:hover { 309 | cursor:pointer; 310 | } 311 | 312 | .activated { 313 | text-decoration:underline; 314 | } 315 | 316 | #controls { 317 | display:none; 318 | font-size:12px; 319 | text-align:center; 320 | } 321 | 322 | #l-counts { 323 | margin-left:auto; 324 | margin-right:auto; 325 | height:18px; 326 | } 327 | 328 | #tune-info { 329 | margin-top:10px; 330 | 331 | width: 170px; 332 | 333 | margin-left:auto; 334 | margin-right:auto; 335 | text-align:left; 336 | } 337 | 338 | 339 | #l-last-branch { 340 | margin-top:10px; 341 | font-weight: bold; 342 | text-align: center; 343 | } 344 | 345 | #l-reverse-branch { 346 | font-weight: bold; 347 | text-align: center; 348 | } 349 | 350 | #l-long-branch { 351 | font-weight: bold; 352 | text-align: center; 353 | } 354 | 355 | #l-sequential-branch { 356 | font-weight: bold; 357 | text-align: center; 358 | margin-bottom:10px; 359 | } 360 | 361 | .ti-val { 362 | font-weight:bold; 363 | float:right; 364 | } 365 | 366 | #sthreshold { 367 | text-align:left; 368 | font-weight:bold; 369 | margin-bottom:10px; 370 | } 371 | 372 | #svolume { 373 | text-align:left; 374 | font-weight:bold; 375 | margin-bottom:10px; 376 | } 377 | 378 | #probability-div { 379 | margin-top: 20px; 380 | } 381 | 382 | #sprobability { 383 | text-align:left; 384 | font-weight:bold; 385 | margin-bottom:10px; 386 | } 387 | 388 | 389 | .slider { 390 | width:300px; 391 | float:left; 392 | margin-right:20px; 393 | margin-bottom:4px; 394 | } 395 | 396 | #slider-labels { 397 | font-size:12px; 398 | width:300px; 399 | margin-left:0px; 400 | margin-right:0px; 401 | margin-bottom:25px; 402 | } 403 | 404 | #reset-edges { 405 | margin-left:auto; 406 | margin-right:auto; 407 | width:80%; 408 | text-align:center; 409 | margin-bottom:20px; 410 | } 411 | 412 | #submit-edges { 413 | margin-top:18px; 414 | margin-left:auto; 415 | margin-right:auto; 416 | width:80%; 417 | text-align:center; 418 | margin-bottom:2px; 419 | } 420 | 421 | .left-label { 422 | position:relative; 423 | float:left; 424 | font-size:10px; 425 | } 426 | 427 | .right-label { 428 | position:relative; 429 | float:right; 430 | margin-right:0px; 431 | font-size:10px; 432 | text-align:right; 433 | } 434 | 435 | #faq ul li { 436 | margin-top:20px; 437 | } 438 | 439 | #vote { 440 | } 441 | 442 | #search-form { 443 | margin-bottom:30px; 444 | } 445 | 446 | /* 447 | .rotate:hover 448 | { 449 | -webkit-transform:rotate(360deg); 450 | -moz-transform:rotate(360deg); 451 | -o-transform:rotate(360deg); 452 | } 453 | */ 454 | 455 | .ui-dialog { 456 | top: 75px; 457 | left: 20px; 458 | } 459 | -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/third-party/jquery-ui.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.9.0 - 2012-10-05 2 | * http://jqueryui.com 3 | * Includes: jquery.ui.core.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css 4 | * Copyright 2012 jQuery Foundation and other contributors; Licensed MIT */ 5 | 6 | /* Layout helpers 7 | ----------------------------------*/ 8 | .ui-helper-hidden { display: none; } 9 | .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } 10 | .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } 11 | .ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; } 12 | .ui-helper-clearfix:after { clear: both; } 13 | .ui-helper-clearfix { zoom: 1; } 14 | .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } 15 | 16 | 17 | /* Interaction Cues 18 | ----------------------------------*/ 19 | .ui-state-disabled { cursor: default !important; } 20 | 21 | 22 | /* Icons 23 | ----------------------------------*/ 24 | 25 | /* states and images */ 26 | .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } 27 | 28 | 29 | /* Misc visuals 30 | ----------------------------------*/ 31 | 32 | /* Overlays */ 33 | .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } 34 | 35 | .ui-accordion .ui-accordion-header { display: block; cursor: pointer; position: relative; margin-top: 2px; padding: .5em .5em .5em .7em; zoom: 1; } 36 | .ui-accordion .ui-accordion-icons { padding-left: 2.2em; } 37 | .ui-accordion .ui-accordion-noicons { padding-left: .7em; } 38 | .ui-accordion .ui-accordion-icons .ui-accordion-icons { padding-left: 2.2em; } 39 | .ui-accordion .ui-accordion-header .ui-accordion-header-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } 40 | .ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; overflow: auto; zoom: 1; } 41 | 42 | .ui-autocomplete { position: absolute; cursor: default; } 43 | 44 | /* workarounds */ 45 | * html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ 46 | 47 | .ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ 48 | .ui-button, .ui-button:link, .ui-button:visited, .ui-button:hover, .ui-button:active { text-decoration: none; } 49 | .ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ 50 | button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ 51 | .ui-button-icons-only { width: 3.4em; } 52 | button.ui-button-icons-only { width: 3.7em; } 53 | 54 | /*button text element */ 55 | .ui-button .ui-button-text { display: block; line-height: 1.4; } 56 | .ui-button-text-only .ui-button-text { padding: .4em 1em; } 57 | .ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } 58 | .ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } 59 | .ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } 60 | .ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } 61 | /* no icon support for input elements, provide padding by default */ 62 | input.ui-button { padding: .4em 1em; } 63 | 64 | /*button icon element(s) */ 65 | .ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } 66 | .ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } 67 | .ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } 68 | .ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 69 | .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 70 | 71 | /*button sets*/ 72 | .ui-buttonset { margin-right: 7px; } 73 | .ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } 74 | 75 | /* workarounds */ 76 | button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ 77 | 78 | .ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; } 79 | .ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; } 80 | .ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } 81 | .ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; } 82 | .ui-datepicker .ui-datepicker-prev { left:2px; } 83 | .ui-datepicker .ui-datepicker-next { right:2px; } 84 | .ui-datepicker .ui-datepicker-prev-hover { left:1px; } 85 | .ui-datepicker .ui-datepicker-next-hover { right:1px; } 86 | .ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } 87 | .ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } 88 | .ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } 89 | .ui-datepicker select.ui-datepicker-month-year {width: 100%;} 90 | .ui-datepicker select.ui-datepicker-month, 91 | .ui-datepicker select.ui-datepicker-year { width: 49%;} 92 | .ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } 93 | .ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } 94 | .ui-datepicker td { border: 0; padding: 1px; } 95 | .ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } 96 | .ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } 97 | .ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } 98 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } 99 | 100 | /* with multiple calendars */ 101 | .ui-datepicker.ui-datepicker-multi { width:auto; } 102 | .ui-datepicker-multi .ui-datepicker-group { float:left; } 103 | .ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } 104 | .ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } 105 | .ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } 106 | .ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } 107 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } 108 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } 109 | .ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } 110 | .ui-datepicker-row-break { clear:both; width:100%; font-size:0em; } 111 | 112 | /* RTL support */ 113 | .ui-datepicker-rtl { direction: rtl; } 114 | .ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } 115 | .ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } 116 | .ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } 117 | .ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } 118 | .ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } 119 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } 120 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } 121 | .ui-datepicker-rtl .ui-datepicker-group { float:right; } 122 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 123 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 124 | 125 | /* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ 126 | .ui-datepicker-cover { 127 | position: absolute; /*must have*/ 128 | z-index: -1; /*must have*/ 129 | filter: mask(); /*must have*/ 130 | top: -4px; /*must have*/ 131 | left: -4px; /*must have*/ 132 | width: 200px; /*must have*/ 133 | height: 200px; /*must have*/ 134 | } 135 | .ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } 136 | .ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } 137 | .ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } 138 | .ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } 139 | .ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } 140 | .ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } 141 | .ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } 142 | .ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } 143 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } 144 | .ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } 145 | .ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } 146 | .ui-draggable .ui-dialog-titlebar { cursor: move; } 147 | 148 | .ui-menu { list-style:none; padding: 2px; margin: 0; display:block; outline: none; } 149 | .ui-menu .ui-menu { margin-top: -3px; position: absolute; } 150 | .ui-menu .ui-menu-item { margin: 0; padding: 0; zoom: 1; width: 100%; } 151 | .ui-menu .ui-menu-divider { margin: 5px -2px 5px -2px; height: 0; font-size: 0; line-height: 0; border-width: 1px 0 0 0; } 152 | .ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.5; zoom: 1; font-weight: normal; } 153 | .ui-menu .ui-menu-item a.ui-state-focus, 154 | .ui-menu .ui-menu-item a.ui-state-active { font-weight: normal; margin: -1px; } 155 | 156 | .ui-menu .ui-state-disabled { font-weight: normal; margin: .4em 0 .2em; line-height: 1.5; } 157 | .ui-menu .ui-state-disabled a { cursor: default; } 158 | 159 | /* icon support */ 160 | .ui-menu-icons { position: relative; } 161 | .ui-menu-icons .ui-menu-item a { position: relative; padding-left: 2em; } 162 | 163 | /* left-aligned */ 164 | .ui-menu .ui-icon { position: absolute; top: .2em; left: .2em; } 165 | 166 | /* right-aligned */ 167 | .ui-menu .ui-menu-icon { position: static; float: right; } 168 | 169 | .ui-progressbar { height:2em; text-align: left; overflow: hidden; } 170 | .ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } 171 | .ui-resizable { position: relative;} 172 | .ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; } 173 | .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } 174 | .ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } 175 | .ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } 176 | .ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } 177 | .ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } 178 | .ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } 179 | .ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } 180 | .ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } 181 | .ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;} 182 | .ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } 183 | 184 | .ui-slider { position: relative; text-align: left; } 185 | .ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; } 186 | .ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } 187 | 188 | .ui-slider-horizontal { height: .8em; } 189 | .ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } 190 | .ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } 191 | .ui-slider-horizontal .ui-slider-range-min { left: 0; } 192 | .ui-slider-horizontal .ui-slider-range-max { right: 0; } 193 | 194 | .ui-slider-vertical { width: .8em; height: 100px; } 195 | .ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; } 196 | .ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } 197 | .ui-slider-vertical .ui-slider-range-min { bottom: 0; } 198 | .ui-slider-vertical .ui-slider-range-max { top: 0; } 199 | .ui-spinner { position:relative; display: inline-block; overflow: hidden; padding: 0; vertical-align: middle; } 200 | .ui-spinner-input { border: none; background: none; padding: 0; margin: .2em 0; vertical-align: middle; margin-left: .4em; margin-right: 22px; } 201 | .ui-spinner-button { width: 16px; height: 50%; font-size: .5em; padding: 0; margin: 0; z-index: 100; text-align: center; position: absolute; cursor: default; display: block; overflow: hidden; right: 0; } 202 | .ui-spinner a.ui-spinner-button { border-top: none; border-bottom: none; border-right: none; } /* more specificity required here to overide default borders */ 203 | .ui-spinner .ui-icon { position: absolute; margin-top: -8px; top: 50%; left: 0; } /* vertical centre icon */ 204 | .ui-spinner-up { top: 0; } 205 | .ui-spinner-down { bottom: 0; } 206 | 207 | /* TR overrides */ 208 | span.ui-spinner { background: none; } 209 | .ui-spinner .ui-icon-triangle-1-s { 210 | /* need to fix icons sprite */ 211 | background-position:-65px -16px; 212 | } 213 | 214 | .ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ 215 | .ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } 216 | .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 0; margin: 1px .2em 0 0; border-bottom: 0; padding: 0; white-space: nowrap; } 217 | .ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } 218 | .ui-tabs .ui-tabs-nav li.ui-tabs-active { margin-bottom: -1px; padding-bottom: 1px; } 219 | .ui-tabs .ui-tabs-nav li.ui-tabs-active a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-tabs-loading a { cursor: text; } 220 | .ui-tabs .ui-tabs-nav li a, .ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ 221 | .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } 222 | 223 | .ui-tooltip { 224 | padding:8px; 225 | position:absolute; 226 | z-index:9999; 227 | -o-box-shadow: 0 0 5px #aaa; 228 | -moz-box-shadow: 0 0 5px #aaa; 229 | -webkit-box-shadow: 0 0 5px #aaa; 230 | box-shadow: 0 0 5px #aaa; 231 | } 232 | /* Fades and background-images don't work well together in IE6, drop the image */ 233 | * html .ui-tooltip { 234 | background-image: none; 235 | } 236 | body .ui-tooltip { border-width:2px; } 237 | 238 | /* Component containers 239 | ----------------------------------*/ 240 | .ui-widget { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1.1em/*{fsDefault}*/; } 241 | .ui-widget .ui-widget { font-size: 1em; } 242 | .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1em; } 243 | .ui-widget-content { border: 1px solid #aaaaaa/*{borderColorContent}*/; background: #ffffff/*{bgColorContent}*/ /*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/; color: #222222/*{fcContent}*/; } 244 | .ui-widget-content a { color: #222222/*{fcContent}*/; } 245 | .ui-widget-header { border: 1px solid #aaaaaa/*{borderColorHeader}*/; background: #cccccc/*{bgColorHeader}*/ /*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/; color: #222222/*{fcHeader}*/; font-weight: bold; } 246 | .ui-widget-header a { color: #222222/*{fcHeader}*/; } 247 | 248 | /* Interaction states 249 | ----------------------------------*/ 250 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ /*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; } 251 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555/*{fcDefault}*/; text-decoration: none; } 252 | .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999/*{borderColorHover}*/; background: #dadada/*{bgColorHover}*/ /*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcHover}*/; } 253 | .ui-state-hover a, .ui-state-hover a:hover { color: #212121/*{fcHover}*/; text-decoration: none; } 254 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa/*{borderColorActive}*/; background: #ffffff/*{bgColorActive}*/ /*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcActive}*/; } 255 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121/*{fcActive}*/; text-decoration: none; } 256 | 257 | /* Interaction Cues 258 | ----------------------------------*/ 259 | .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fcefa1/*{borderColorHighlight}*/; background: #fbf9ee/*{bgColorHighlight}*/ /*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/; color: #363636/*{fcHighlight}*/; } 260 | .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636/*{fcHighlight}*/; } 261 | .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a/*{borderColorError}*/; background: #fef1ec/*{bgColorError}*/ /*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/; color: #cd0a0a/*{fcError}*/; } 262 | .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a/*{fcError}*/; } 263 | .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a/*{fcError}*/; } 264 | .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } 265 | .ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } 266 | .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } 267 | 268 | /* Icons 269 | ----------------------------------*/ 270 | 271 | /* positioning */ 272 | .ui-icon-carat-1-n { background-position: 0 0; } 273 | .ui-icon-carat-1-ne { background-position: -16px 0; } 274 | .ui-icon-carat-1-e { background-position: -32px 0; } 275 | .ui-icon-carat-1-se { background-position: -48px 0; } 276 | .ui-icon-carat-1-s { background-position: -64px 0; } 277 | .ui-icon-carat-1-sw { background-position: -80px 0; } 278 | .ui-icon-carat-1-w { background-position: -96px 0; } 279 | .ui-icon-carat-1-nw { background-position: -112px 0; } 280 | .ui-icon-carat-2-n-s { background-position: -128px 0; } 281 | .ui-icon-carat-2-e-w { background-position: -144px 0; } 282 | .ui-icon-triangle-1-n { background-position: 0 -16px; } 283 | .ui-icon-triangle-1-ne { background-position: -16px -16px; } 284 | .ui-icon-triangle-1-e { background-position: -32px -16px; } 285 | .ui-icon-triangle-1-se { background-position: -48px -16px; } 286 | .ui-icon-triangle-1-s { background-position: -64px -16px; } 287 | .ui-icon-triangle-1-sw { background-position: -80px -16px; } 288 | .ui-icon-triangle-1-w { background-position: -96px -16px; } 289 | .ui-icon-triangle-1-nw { background-position: -112px -16px; } 290 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; } 291 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; } 292 | .ui-icon-arrow-1-n { background-position: 0 -32px; } 293 | .ui-icon-arrow-1-ne { background-position: -16px -32px; } 294 | .ui-icon-arrow-1-e { background-position: -32px -32px; } 295 | .ui-icon-arrow-1-se { background-position: -48px -32px; } 296 | .ui-icon-arrow-1-s { background-position: -64px -32px; } 297 | .ui-icon-arrow-1-sw { background-position: -80px -32px; } 298 | .ui-icon-arrow-1-w { background-position: -96px -32px; } 299 | .ui-icon-arrow-1-nw { background-position: -112px -32px; } 300 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; } 301 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } 302 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; } 303 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; } 304 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; } 305 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; } 306 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; } 307 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; } 308 | .ui-icon-arrowthick-1-n { background-position: 0 -48px; } 309 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } 310 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; } 311 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; } 312 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; } 313 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; } 314 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; } 315 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; } 316 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } 317 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } 318 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } 319 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } 320 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } 321 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } 322 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } 323 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } 324 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } 325 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } 326 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } 327 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } 328 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; } 329 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; } 330 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; } 331 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; } 332 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } 333 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } 334 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } 335 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } 336 | .ui-icon-arrow-4 { background-position: 0 -80px; } 337 | .ui-icon-arrow-4-diag { background-position: -16px -80px; } 338 | .ui-icon-extlink { background-position: -32px -80px; } 339 | .ui-icon-newwin { background-position: -48px -80px; } 340 | .ui-icon-refresh { background-position: -64px -80px; } 341 | .ui-icon-shuffle { background-position: -80px -80px; } 342 | .ui-icon-transfer-e-w { background-position: -96px -80px; } 343 | .ui-icon-transferthick-e-w { background-position: -112px -80px; } 344 | .ui-icon-folder-collapsed { background-position: 0 -96px; } 345 | .ui-icon-folder-open { background-position: -16px -96px; } 346 | .ui-icon-document { background-position: -32px -96px; } 347 | .ui-icon-document-b { background-position: -48px -96px; } 348 | .ui-icon-note { background-position: -64px -96px; } 349 | .ui-icon-mail-closed { background-position: -80px -96px; } 350 | .ui-icon-mail-open { background-position: -96px -96px; } 351 | .ui-icon-suitcase { background-position: -112px -96px; } 352 | .ui-icon-comment { background-position: -128px -96px; } 353 | .ui-icon-person { background-position: -144px -96px; } 354 | .ui-icon-print { background-position: -160px -96px; } 355 | .ui-icon-trash { background-position: -176px -96px; } 356 | .ui-icon-locked { background-position: -192px -96px; } 357 | .ui-icon-unlocked { background-position: -208px -96px; } 358 | .ui-icon-bookmark { background-position: -224px -96px; } 359 | .ui-icon-tag { background-position: -240px -96px; } 360 | .ui-icon-home { background-position: 0 -112px; } 361 | .ui-icon-flag { background-position: -16px -112px; } 362 | .ui-icon-calendar { background-position: -32px -112px; } 363 | .ui-icon-cart { background-position: -48px -112px; } 364 | .ui-icon-pencil { background-position: -64px -112px; } 365 | .ui-icon-clock { background-position: -80px -112px; } 366 | .ui-icon-disk { background-position: -96px -112px; } 367 | .ui-icon-calculator { background-position: -112px -112px; } 368 | .ui-icon-zoomin { background-position: -128px -112px; } 369 | .ui-icon-zoomout { background-position: -144px -112px; } 370 | .ui-icon-search { background-position: -160px -112px; } 371 | .ui-icon-wrench { background-position: -176px -112px; } 372 | .ui-icon-gear { background-position: -192px -112px; } 373 | .ui-icon-heart { background-position: -208px -112px; } 374 | .ui-icon-star { background-position: -224px -112px; } 375 | .ui-icon-link { background-position: -240px -112px; } 376 | .ui-icon-cancel { background-position: 0 -128px; } 377 | .ui-icon-plus { background-position: -16px -128px; } 378 | .ui-icon-plusthick { background-position: -32px -128px; } 379 | .ui-icon-minus { background-position: -48px -128px; } 380 | .ui-icon-minusthick { background-position: -64px -128px; } 381 | .ui-icon-close { background-position: -80px -128px; } 382 | .ui-icon-closethick { background-position: -96px -128px; } 383 | .ui-icon-key { background-position: -112px -128px; } 384 | .ui-icon-lightbulb { background-position: -128px -128px; } 385 | .ui-icon-scissors { background-position: -144px -128px; } 386 | .ui-icon-clipboard { background-position: -160px -128px; } 387 | .ui-icon-copy { background-position: -176px -128px; } 388 | .ui-icon-contact { background-position: -192px -128px; } 389 | .ui-icon-image { background-position: -208px -128px; } 390 | .ui-icon-video { background-position: -224px -128px; } 391 | .ui-icon-script { background-position: -240px -128px; } 392 | .ui-icon-alert { background-position: 0 -144px; } 393 | .ui-icon-info { background-position: -16px -144px; } 394 | .ui-icon-notice { background-position: -32px -144px; } 395 | .ui-icon-help { background-position: -48px -144px; } 396 | .ui-icon-check { background-position: -64px -144px; } 397 | .ui-icon-bullet { background-position: -80px -144px; } 398 | .ui-icon-radio-on { background-position: -96px -144px; } 399 | .ui-icon-radio-off { background-position: -112px -144px; } 400 | .ui-icon-pin-w { background-position: -128px -144px; } 401 | .ui-icon-pin-s { background-position: -144px -144px; } 402 | .ui-icon-play { background-position: 0 -160px; } 403 | .ui-icon-pause { background-position: -16px -160px; } 404 | .ui-icon-seek-next { background-position: -32px -160px; } 405 | .ui-icon-seek-prev { background-position: -48px -160px; } 406 | .ui-icon-seek-end { background-position: -64px -160px; } 407 | .ui-icon-seek-start { background-position: -80px -160px; } 408 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ 409 | .ui-icon-seek-first { background-position: -80px -160px; } 410 | .ui-icon-stop { background-position: -96px -160px; } 411 | .ui-icon-eject { background-position: -112px -160px; } 412 | .ui-icon-volume-off { background-position: -128px -160px; } 413 | .ui-icon-volume-on { background-position: -144px -160px; } 414 | .ui-icon-power { background-position: 0 -176px; } 415 | .ui-icon-signal-diag { background-position: -16px -176px; } 416 | .ui-icon-signal { background-position: -32px -176px; } 417 | .ui-icon-battery-0 { background-position: -48px -176px; } 418 | .ui-icon-battery-1 { background-position: -64px -176px; } 419 | .ui-icon-battery-2 { background-position: -80px -176px; } 420 | .ui-icon-battery-3 { background-position: -96px -176px; } 421 | .ui-icon-circle-plus { background-position: 0 -192px; } 422 | .ui-icon-circle-minus { background-position: -16px -192px; } 423 | .ui-icon-circle-close { background-position: -32px -192px; } 424 | .ui-icon-circle-triangle-e { background-position: -48px -192px; } 425 | .ui-icon-circle-triangle-s { background-position: -64px -192px; } 426 | .ui-icon-circle-triangle-w { background-position: -80px -192px; } 427 | .ui-icon-circle-triangle-n { background-position: -96px -192px; } 428 | .ui-icon-circle-arrow-e { background-position: -112px -192px; } 429 | .ui-icon-circle-arrow-s { background-position: -128px -192px; } 430 | .ui-icon-circle-arrow-w { background-position: -144px -192px; } 431 | .ui-icon-circle-arrow-n { background-position: -160px -192px; } 432 | .ui-icon-circle-zoomin { background-position: -176px -192px; } 433 | .ui-icon-circle-zoomout { background-position: -192px -192px; } 434 | .ui-icon-circle-check { background-position: -208px -192px; } 435 | .ui-icon-circlesmall-plus { background-position: 0 -208px; } 436 | .ui-icon-circlesmall-minus { background-position: -16px -208px; } 437 | .ui-icon-circlesmall-close { background-position: -32px -208px; } 438 | .ui-icon-squaresmall-plus { background-position: -48px -208px; } 439 | .ui-icon-squaresmall-minus { background-position: -64px -208px; } 440 | .ui-icon-squaresmall-close { background-position: -80px -208px; } 441 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; } 442 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } 443 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; } 444 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; } 445 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } 446 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; } 447 | 448 | 449 | /* Misc visuals 450 | ----------------------------------*/ 451 | 452 | /* Corner radius */ 453 | .ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px/*{cornerRadius}*/; -webkit-border-top-left-radius: 4px/*{cornerRadius}*/; -khtml-border-top-left-radius: 4px/*{cornerRadius}*/; border-top-left-radius: 4px/*{cornerRadius}*/; } 454 | .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px/*{cornerRadius}*/; -webkit-border-top-right-radius: 4px/*{cornerRadius}*/; -khtml-border-top-right-radius: 4px/*{cornerRadius}*/; border-top-right-radius: 4px/*{cornerRadius}*/; } 455 | .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px/*{cornerRadius}*/; -webkit-border-bottom-left-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-left-radius: 4px/*{cornerRadius}*/; border-bottom-left-radius: 4px/*{cornerRadius}*/; } 456 | .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-right-radius: 4px/*{cornerRadius}*/; border-bottom-right-radius: 4px/*{cornerRadius}*/; } 457 | 458 | /* Overlays */ 459 | .ui-widget-overlay { background: #aaaaaa/*{bgColorOverlay}*/ /*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityOverlay}*/; } 460 | .ui-widget-shadow { margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/; padding: 8px/*{thicknessShadow}*/; background: #aaaaaa/*{bgColorShadow}*/ /*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityShadow}*/; -moz-border-radius: 8px/*{cornerRadiusShadow}*/; -khtml-border-radius: 8px/*{cornerRadiusShadow}*/; -webkit-border-radius: 8px/*{cornerRadiusShadow}*/; border-radius: 8px/*{cornerRadiusShadow}*/; } -------------------------------------------------------------------------------- /js/examples/playerAndVisualizer/web/third-party/three-dots.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * three-dots - v0.3.2 3 | * CSS loading animations made with single element 4 | * https://nzbin.github.io/three-dots/ 5 | * 6 | * Copyright (c) 2018 nzbin 7 | * Released under MIT License 8 | */ 9 | 10 | /** 11 | * ============================================== 12 | * Dot Pulse 13 | * ============================================== 14 | */ 15 | .dot-pulse { 16 | position: relative; 17 | left: -9999px; 18 | width: 10px; 19 | height: 10px; 20 | border-radius: 5px; 21 | background-color: white; 22 | color: white; 23 | box-shadow: 9999px 0 0 -5px; 24 | animation: dot-pulse 1.5s infinite linear; 25 | animation-delay: 0.25s; 26 | } 27 | .dot-pulse::before, .dot-pulse::after { 28 | content: ""; 29 | display: inline-block; 30 | position: absolute; 31 | top: 0; 32 | width: 10px; 33 | height: 10px; 34 | border-radius: 5px; 35 | background-color: white; 36 | color: white; 37 | } 38 | .dot-pulse::before { 39 | box-shadow: 9984px 0 0 -5px; 40 | animation: dot-pulse-before 1.5s infinite linear; 41 | animation-delay: 0s; 42 | } 43 | .dot-pulse::after { 44 | box-shadow: 10014px 0 0 -5px; 45 | animation: dot-pulse-after 1.5s infinite linear; 46 | animation-delay: 0.5s; 47 | } 48 | 49 | @keyframes dot-pulse-before { 50 | 0% { 51 | box-shadow: 9984px 0 0 -5px; 52 | } 53 | 30% { 54 | box-shadow: 9984px 0 0 2px; 55 | } 56 | 60%, 100% { 57 | box-shadow: 9984px 0 0 -5px; 58 | } 59 | } 60 | @keyframes dot-pulse { 61 | 0% { 62 | box-shadow: 9999px 0 0 -5px; 63 | } 64 | 30% { 65 | box-shadow: 9999px 0 0 2px; 66 | } 67 | 60%, 100% { 68 | box-shadow: 9999px 0 0 -5px; 69 | } 70 | } 71 | @keyframes dot-pulse-after { 72 | 0% { 73 | box-shadow: 10014px 0 0 -5px; 74 | } 75 | 30% { 76 | box-shadow: 10014px 0 0 2px; 77 | } 78 | 60%, 100% { 79 | box-shadow: 10014px 0 0 -5px; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tools/spotifyAudioAnalysisClient/CopySongLink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rigdern/InfiniteJukeboxAlgorithm/0ce82c9b52142272a7bca3f62695ae15d2ba341f/tools/spotifyAudioAnalysisClient/CopySongLink.png -------------------------------------------------------------------------------- /tools/spotifyAudioAnalysisClient/README.md: -------------------------------------------------------------------------------- 1 | # Spotify Audio Analysis Client 2 | Illustrates how to use [Spotify's audio analysis web API](https://developer.spotify.com/documentation/web-api/reference/get-audio-analysis) to fetch the audio analysis for a Spotify track. 3 | 4 | ## Setup 5 | 1. Run `npm install` to install the tool's dependencies. 6 | 2. Create the configuration file for authenticating with Spotify: 7 | 1. Get a "client ID" and a "client secret" by following [Spotify's "Create an app"](https://developer.spotify.com/documentation/web-api/tutorials/getting-started#create-an-app) instructions. 8 | 2. Create a file called [`./creds/spotify.js`](./creds/spotify.js) and populate it with your "client ID" and "client secret". The file should be formatted like this: 9 | ``` 10 | export const clientId = 'YOUR_ID_HERE'; 11 | export const clientSecret = 'YOUR_SECRET_HERE'; 12 | ``` 13 | 14 | ## Usage 15 | `node main.js ` 16 | 17 | For example, the Spotify track ID for Gangnam Style is `03UrZgTINDqvnUMbbIMhql` so you can fetch Spotify's audio analysis for it and store it in `gangnamStyleAnalysis.json` with this command: 18 | 19 | `node main.js 03UrZgTINDqvnUMbbIMhql gangnamStyleAnalysis.json` 20 | 21 | ## Looking up a Spotify track ID 22 | 1. Find the song of interest in the [Spotify web app](https://open.spotify.com/). 23 | 2. Click the "..." to the right of the song title. ((1) in the screenshot) 24 | 3. In the context menu that appears: 25 | 1. Choose "Share" 26 | 2. Choose "Copy Song Link" ((2) in the screenshot) 27 | 4. The track's URL should now be on your clipboard. For example, for Gangnam Style my clipboard contained `https://open.spotify.com/track/03UrZgTINDqvnUMbbIMhql?si=e148916771984610` 28 | 5. The last path component of the URL is the track's ID. For example, for Gangnam Style it is `03UrZgTINDqvnUMbbIMhql`. 29 | 30 | Here's a screenshot illustrating the steps within the Spotify web app: 31 | ![Screenshot illustrating the steps within the Spotify web app](./CopySongLink.png) 32 | -------------------------------------------------------------------------------- /tools/spotifyAudioAnalysisClient/main.js: -------------------------------------------------------------------------------- 1 | // Illustrates how to use Spotify's audio analysis web API to fetch the audio 2 | // analysis for a Spotify track. See ./README.md for details. 3 | // 4 | 5 | import fsPromises from 'fs/promises'; 6 | import fetch from 'node-fetch'; 7 | import { pp, readFileIfExists } from './util.js'; 8 | import path from 'path'; 9 | import { fileURLToPath } from 'url'; 10 | 11 | // For help in getting these, see https://developer.spotify.com/documentation/web-api/tutorials/getting-started 12 | import { clientId, clientSecret } from './creds/spotify.js'; 13 | 14 | const __filename = fileURLToPath(import.meta.url); 15 | const __dirname = path.dirname(__filename); 16 | 17 | class SpotifyError extends Error { 18 | constructor(message, details) { 19 | super(message); 20 | this.details = details; 21 | } 22 | } 23 | 24 | const spotifyAccessTokenPath = path.join(__dirname, './creds/spotifyAccessToken.json'); 25 | 26 | async function getSpotifyAccessToken() { 27 | const content = await readFileIfExists(spotifyAccessTokenPath, { encoding: 'utf8' }); 28 | if (content) { 29 | return JSON.parse(content).access_token; 30 | } 31 | 32 | const tokenObj = await fetchAccessToken(); 33 | await fsPromises.writeFile(spotifyAccessTokenPath, pp(tokenObj), { encoding: 'utf8' }); 34 | return tokenObj.access_token; 35 | } 36 | 37 | async function clearSpotifyAccessToken() { 38 | await fsPromises.rm(spotifyAccessTokenPath, { force: true }); 39 | } 40 | 41 | async function fetchSpotifyJson(url, init=undefined) { 42 | const response = await fetch(url, init); 43 | const json = await response.json(); 44 | 45 | if (json.error) { 46 | throw new SpotifyError('fetchSpotifyJson error: ' + pp(json), json.error); 47 | } else { 48 | return json; 49 | } 50 | } 51 | 52 | function fetchAccessToken() { 53 | return fetchSpotifyJson('https://accounts.spotify.com/api/token', { 54 | method: 'POST', 55 | headers: { 56 | 'Content-Type': 'application/x-www-form-urlencoded', 57 | }, 58 | body: [ 59 | 'grant_type=client_credentials', 60 | 'client_id=' + clientId, 61 | 'client_secret=' + clientSecret, 62 | ].join('&'), 63 | }); 64 | } 65 | 66 | function fetchAudioAnalysis(spotifyAccessToken, spotifyTrackId) { 67 | return fetchSpotifyJson('https://api.spotify.com/v1/audio-analysis/' + encodeURIComponent(spotifyTrackId), { 68 | headers: { 69 | 'Authorization': 'Bearer ' + spotifyAccessToken, 70 | } 71 | }); 72 | } 73 | 74 | async function main(args) { 75 | if (args.length !== 2) { 76 | console.log('Usage: node main.js '); 77 | return; 78 | } 79 | 80 | const [spotifyTrackId, spotifyAnalysisPath] = args; 81 | 82 | const spotifyAccessToken = await getSpotifyAccessToken(); 83 | 84 | try { 85 | const audioAnalysis = await fetchAudioAnalysis(spotifyAccessToken, spotifyTrackId); 86 | await fsPromises.writeFile(spotifyAnalysisPath, pp(audioAnalysis), { encoding: 'utf8' }); 87 | } catch (error) { 88 | if (error instanceof SpotifyError && error.details?.status === 401) { 89 | // 401 Unauthorized error. Likely access token expired. Clear the old one. 90 | await clearSpotifyAccessToken(); 91 | console.error('It looks like the Spotify access token expired. Try running the program again. A new one will be fetched.'); 92 | } else { 93 | throw error; 94 | } 95 | } 96 | } 97 | 98 | main(process.argv.slice(2)); 99 | -------------------------------------------------------------------------------- /tools/spotifyAudioAnalysisClient/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotifyAudioAnalysisClient", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "node-fetch": "^3.3.2" 9 | } 10 | }, 11 | "node_modules/data-uri-to-buffer": { 12 | "version": "4.0.1", 13 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 14 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 15 | "engines": { 16 | "node": ">= 12" 17 | } 18 | }, 19 | "node_modules/fetch-blob": { 20 | "version": "3.2.0", 21 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 22 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 23 | "funding": [ 24 | { 25 | "type": "github", 26 | "url": "https://github.com/sponsors/jimmywarting" 27 | }, 28 | { 29 | "type": "paypal", 30 | "url": "https://paypal.me/jimmywarting" 31 | } 32 | ], 33 | "dependencies": { 34 | "node-domexception": "^1.0.0", 35 | "web-streams-polyfill": "^3.0.3" 36 | }, 37 | "engines": { 38 | "node": "^12.20 || >= 14.13" 39 | } 40 | }, 41 | "node_modules/formdata-polyfill": { 42 | "version": "4.0.10", 43 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 44 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 45 | "dependencies": { 46 | "fetch-blob": "^3.1.2" 47 | }, 48 | "engines": { 49 | "node": ">=12.20.0" 50 | } 51 | }, 52 | "node_modules/node-domexception": { 53 | "version": "1.0.0", 54 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 55 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 56 | "funding": [ 57 | { 58 | "type": "github", 59 | "url": "https://github.com/sponsors/jimmywarting" 60 | }, 61 | { 62 | "type": "github", 63 | "url": "https://paypal.me/jimmywarting" 64 | } 65 | ], 66 | "engines": { 67 | "node": ">=10.5.0" 68 | } 69 | }, 70 | "node_modules/node-fetch": { 71 | "version": "3.3.2", 72 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 73 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 74 | "dependencies": { 75 | "data-uri-to-buffer": "^4.0.0", 76 | "fetch-blob": "^3.1.4", 77 | "formdata-polyfill": "^4.0.10" 78 | }, 79 | "engines": { 80 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 81 | }, 82 | "funding": { 83 | "type": "opencollective", 84 | "url": "https://opencollective.com/node-fetch" 85 | } 86 | }, 87 | "node_modules/web-streams-polyfill": { 88 | "version": "3.2.1", 89 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 90 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", 91 | "engines": { 92 | "node": ">= 8" 93 | } 94 | } 95 | }, 96 | "dependencies": { 97 | "data-uri-to-buffer": { 98 | "version": "4.0.1", 99 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 100 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" 101 | }, 102 | "fetch-blob": { 103 | "version": "3.2.0", 104 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 105 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 106 | "requires": { 107 | "node-domexception": "^1.0.0", 108 | "web-streams-polyfill": "^3.0.3" 109 | } 110 | }, 111 | "formdata-polyfill": { 112 | "version": "4.0.10", 113 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 114 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 115 | "requires": { 116 | "fetch-blob": "^3.1.2" 117 | } 118 | }, 119 | "node-domexception": { 120 | "version": "1.0.0", 121 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 122 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" 123 | }, 124 | "node-fetch": { 125 | "version": "3.3.2", 126 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 127 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 128 | "requires": { 129 | "data-uri-to-buffer": "^4.0.0", 130 | "fetch-blob": "^3.1.4", 131 | "formdata-polyfill": "^4.0.10" 132 | } 133 | }, 134 | "web-streams-polyfill": { 135 | "version": "3.2.1", 136 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 137 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tools/spotifyAudioAnalysisClient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "main.js", 3 | "type": "module", 4 | "dependencies": { 5 | "node-fetch": "^3.3.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tools/spotifyAudioAnalysisClient/util.js: -------------------------------------------------------------------------------- 1 | import fsPromises from 'fs/promises'; 2 | 3 | // Pretty-print JSON. 4 | export function pp(json) { 5 | return JSON.stringify(json, undefined, 2); 6 | } 7 | 8 | export async function readFileIfExists(filePath, options) { 9 | try { 10 | return await fsPromises.readFile(filePath, options); 11 | } catch (error) { 12 | if (error.code === 'ENOENT') { 13 | return undefined; 14 | } else { 15 | throw error; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tools/spotifyBeatMetronome/README.md: -------------------------------------------------------------------------------- 1 | # Spotify Beat Metronome 2 | Generates a WAV audio file that consists solely of a tick at each beat where the timestamp of each beat is given by a Spotify audio analysis file. 3 | 4 | ## Rationale 5 | A challenge with [Spotify's audio analysis web API](https://developer.spotify.com/documentation/web-api/reference/get-audio-analysis) is that the analysis is for the copy of the song that Spotify has which may not be identical to the copy of the song that you have. For example, maybe the music begins immediately in Spotify's copy of the track whereas your copy of the track begins with a couple of seconds of silence. To remedy this, you need to figure out by how many seconds you need to shift Spotify's analysis so that it aligns exactly with your copy of the song. 6 | 7 | This tool is intended to help with this process. It helps you to understand Spotify's audio analysis file by generating a WAV audio file which plays a sound at each beat identified by Spotify's audio analysis. You can import your copy of the song along with this beat WAV file into an audio editor like [Audacity](https://www.audacityteam.org/) and then utilize your auditory and visual senses to make an assessment: 8 | - Auditory. Listen to the song and the beat WAV play together to see whether the Spotify-identified beats play on the beat of your copy of the song. If not, the timestamps in Spotify's analysis file may need to be shifted. 9 | - Visual. If you identify the song's tempo/BPM, you can (1) generate a metronome audio track that plays a tick at that tempo and (2) align that metronome audio track to the beat of the song. Then you can visually compare the waveforms of the metronome audio track with those of the beat WAV audio file. The distance between the beats in these waveforms is a clue about how much you need to shift the timestamps in Spotify's audio analysis file. Some helpful resources: 10 | - [How to find a song's tempo/BPM with ArrowVortex](https://youtu.be/Z49UKFefu5c) (video) 11 | - [How to add a metronome track in Audacity](https://bsmg.wiki/mapping/basic-audio.html#add-a-click-track) (the relevant section is titled "Add a Click Track") 12 | 13 | ## Usage 14 | `node main.js ` 15 | 16 | For example, here's how to create a beat WAV audio file for the beats identified by Spotify's audio analysis of Gangnam Style: 17 | 18 | `node main.js ../../data/gangnamStyleAnalysis.json gangnamStyleBeat.wav` 19 | 20 | If you play `gangnamStyleBeat.wav` in an audio player, you'll hear a tick at each beat identified by Spotify's audio analysis and silence elsewhere. 21 | -------------------------------------------------------------------------------- /tools/spotifyBeatMetronome/main.js: -------------------------------------------------------------------------------- 1 | // Generates a WAV audio file that consists solely of a tick at each beat where 2 | // the timestamp of each beat is given by a Spotify audio analysis file. See 3 | // ./README.md for details. 4 | // 5 | 6 | import fsPromises from 'fs/promises'; 7 | import path from 'path'; 8 | import { fileURLToPath } from 'url'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | const metronomeTickPath = path.join(__dirname, './metronome-tick.wav'); 14 | 15 | function assert(pred, msg) { 16 | if (!pred) { 17 | throw new Error('Assert failed: ' + msg); 18 | } 19 | } 20 | 21 | // WAV file parser & serializer 22 | // 23 | 24 | function ensureString(buffer, value, index) { 25 | const actual = buffer.toString('utf8', index, index + value.length); 26 | assert(value === actual, 'Unexpected value: ' + JSON.stringify({ 27 | expected: value, 28 | actual, 29 | })); 30 | return actual; 31 | } 32 | 33 | function ensureUInt16LE(buffer, value, index) { 34 | const actual = buffer.readUInt16LE(index); 35 | assert(value === actual, 'Unexpected value: ' + JSON.stringify({ 36 | expected: value, 37 | actual, 38 | })); 39 | return actual; 40 | } 41 | 42 | function ensureUInt32LE(buffer, value, index) { 43 | const actual = buffer.readUInt32LE(index); 44 | assert(value === actual, 'Unexpected value: ' + JSON.stringify({ 45 | expected: value, 46 | actual, 47 | })); 48 | return actual; 49 | } 50 | 51 | function parseWav(buffer) { 52 | const chunkId = ensureString(buffer, 'RIFF', 0); 53 | const chunkSize = buffer.readUInt32LE(4); 54 | const format = ensureString(buffer, 'WAVE', 8); 55 | const subchunk1Id = ensureString(buffer, 'fmt ', 12); 56 | const subchunk1Size = ensureUInt32LE(buffer, 16, 16); // 16 for PCM 57 | const audioFormat = ensureUInt16LE(buffer, 1, 20); // 1 for PCM 58 | const numChannels = buffer.readUInt16LE(22); 59 | const sampleRate = buffer.readUInt32LE(24); 60 | const byteRate = buffer.readUInt32LE(28); 61 | const blockAlign = buffer.readUInt16LE(32); 62 | const bitsPerSample = buffer.readUInt16LE(34); 63 | const subchunk2Id = ensureString(buffer, 'data', 36); 64 | const subchunk2Size = buffer.readUInt32LE(40); 65 | const data = buffer.slice(44); 66 | 67 | return { 68 | chunkId, 69 | chunkSize, 70 | format, 71 | subchunk1Id, 72 | subchunk1Size, 73 | audioFormat, 74 | numChannels, 75 | sampleRate, 76 | byteRate, 77 | blockAlign, 78 | bitsPerSample, 79 | subchunk2Id, 80 | subchunk2Size, 81 | data, 82 | }; 83 | } 84 | 85 | function serializeWav({ 86 | numChannels, 87 | sampleRate, 88 | byteRate, 89 | blockAlign, 90 | bitsPerSample, 91 | data, 92 | }) { 93 | const fileSize = 44 + data.length; 94 | 95 | const buffer = new Buffer.alloc(fileSize); 96 | buffer.write('RIFF', 0); 97 | buffer.writeUInt32LE(fileSize - 8, 4); 98 | buffer.write('WAVE', 8); 99 | buffer.write('fmt ', 12); 100 | buffer.writeUInt32LE(16, 16); // 16 for PCM 101 | buffer.writeUInt16LE(1, 20); // 1 for PCM 102 | buffer.writeUInt16LE(numChannels, 22); 103 | buffer.writeUInt32LE(sampleRate, 24); 104 | buffer.writeUInt32LE(byteRate, 28); 105 | buffer.writeUInt16LE(blockAlign, 32); 106 | buffer.writeUInt16LE(bitsPerSample, 34); 107 | buffer.write('data', 36); 108 | buffer.writeUInt32LE(data.length, 40); 109 | data.copy(buffer, 44, 0); 110 | 111 | return buffer; 112 | } 113 | 114 | // Utility for generating a WAV file that plays a sound at specific timestamps. 115 | // 116 | 117 | class WavTickBuilder { 118 | constructor(wav, tick) { 119 | this._wav = wav; 120 | this._tick = tick; 121 | this._pieces = []; 122 | this._lengthBytes = 0; 123 | 124 | this._bytesPerSample = this._wav.blockAlign; 125 | this._samplesPerSecond = this._wav.sampleRate; 126 | } 127 | 128 | get _lengthSamples() { 129 | return this._lengthBytes / this._bytesPerSample; 130 | } 131 | 132 | _makeSilence(lengthSamples) { 133 | return Buffer.alloc(lengthSamples * this._bytesPerSample); 134 | } 135 | 136 | addTickAt(seconds) { 137 | const sampleOffset = (seconds * this._samplesPerSecond) | 0; 138 | const gapSamples = sampleOffset - this._lengthSamples; 139 | assert(gapSamples >= 0, 'Cannot add a tick in the past'); 140 | 141 | const silence = this._makeSilence(gapSamples); 142 | this._pieces.push(silence); 143 | this._pieces.push(this._tick); 144 | this._lengthBytes += silence.length + this._tick.length; 145 | } 146 | 147 | getWav() { 148 | return serializeWav({ 149 | ...this._wav, 150 | data: Buffer.concat(this._pieces), 151 | }); 152 | } 153 | } 154 | 155 | // main 156 | // 157 | 158 | async function main(args) { 159 | if (args.length !== 2) { 160 | console.log('Usage: node main.js '); 161 | return; 162 | } 163 | 164 | const [spotifyAnalysisPath, beatOutputPath] = args; 165 | 166 | const content = await fsPromises.readFile(spotifyAnalysisPath, { encoding: 'utf8' }); 167 | const spotifyAnalysis = JSON.parse(content); 168 | const beats = spotifyAnalysis.beats; 169 | 170 | const tickBuffer = await fsPromises.readFile(metronomeTickPath); 171 | const tickWav = parseWav(tickBuffer); 172 | 173 | const builder = new WavTickBuilder(tickWav, tickWav.data); 174 | for (const beat of beats) { 175 | builder.addTickAt(beat.start); 176 | } 177 | 178 | await fsPromises.writeFile(beatOutputPath, builder.getWav()); 179 | 180 | console.log('Wrote ' + beats.length + ' beats to ' + beatOutputPath); 181 | } 182 | 183 | main(process.argv.slice(2)); 184 | -------------------------------------------------------------------------------- /tools/spotifyBeatMetronome/metronome-tick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rigdern/InfiniteJukeboxAlgorithm/0ce82c9b52142272a7bca3f62695ae15d2ba341f/tools/spotifyBeatMetronome/metronome-tick.wav -------------------------------------------------------------------------------- /tools/spotifyBeatMetronome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | --------------------------------------------------------------------------------