[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

fix lint errors

Kasper (Oct 2, 2025, 11:46 PM +0200) 091d7c36 109a0dac

+294 -192
+5 -5
src/lib/player.ts
··· 18 18 import { tracks_page_item_ids } from '$components/TrackList.svelte' 19 19 20 20 const audio = new Audio() 21 - export const audioContext = new AudioContext() 22 - export const mediaElementSource = audioContext.createMediaElementSource(audio) 23 - const gain_node = audioContext.createGain() 24 - mediaElementSource.connect(gain_node) 25 - gain_node.connect(audioContext.destination) 21 + export const audio_context = new AudioContext() 22 + export const media_element_source = audio_context.createMediaElementSource(audio) 23 + const gain_node = audio_context.createGain() 24 + media_element_source.connect(gain_node) 25 + gain_node.connect(audio_context.destination) 26 26 27 27 let is_stopped = true 28 28 export const stopped = (() => {
+209 -118
src/lib/visualizer/ShaderToyLite.ts
··· 2 2 3 3 import { create_singular_request_animation_frame } from '$lib/helpers' 4 4 5 - export function ShaderToyLite(canvas) { 6 - var hdr = `#version 300 es 5 + type Key = 'A' | 'B' | 'C' | 'D' | 'Image' 6 + 7 + const possible_keys = ['A', 'B', 'C', 'D', 'Image'] as const 8 + 9 + type Locations = { 10 + iResolution: WebGLUniformLocation | null 11 + iTime: WebGLUniformLocation | null 12 + iTimeDelta: WebGLUniformLocation | null 13 + iFrameRate: WebGLUniformLocation | null 14 + iFrame: WebGLUniformLocation | null 15 + iChannelTime: WebGLUniformLocation | null 16 + iChannelResolution: WebGLUniformLocation | null 17 + iChannel0: WebGLUniformLocation | null 18 + iChannel1: WebGLUniformLocation | null 19 + iChannel2: WebGLUniformLocation | null 20 + iChannel3: WebGLUniformLocation | null 21 + iMouse: WebGLUniformLocation | null 22 + iDate: WebGLUniformLocation | null 23 + iSampleRate: WebGLUniformLocation | null 24 + iStream: WebGLUniformLocation | null 25 + iVolume: WebGLUniformLocation | null 26 + vertexInPosition: number 27 + } 28 + 29 + export type ShaderToyLite = NonNullable<ReturnType<typeof shader_toy_lite>['data']> 30 + 31 + export function shader_toy_lite(canvas: HTMLCanvasElement) { 32 + const hdr = `#version 300 es 7 33 #ifdef GL_ES 8 34 precision highp float; 9 35 precision highp int; ··· 52 78 -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 53 79 ]) 54 80 55 - var opts = { 81 + const opts = { 56 82 alpha: false, 57 83 depth: false, 58 84 stencil: false, ··· 62 88 power_preference: 'high-performance', 63 89 } 64 90 65 - var gl = canvas.getContext('webgl2', opts) 91 + const retrieved_gl = canvas.getContext('webgl2', opts) 92 + if (!(retrieved_gl instanceof WebGL2RenderingContext)) { 93 + return { data: null, error: 'WebGL2 is not supported' } 94 + } 95 + const gl = retrieved_gl 66 96 67 97 // timing 68 - var is_playing = false 69 - var first_draw_time = 0 70 - var prev_draw_time = 0 98 + let is_playing = false 99 + let first_draw_time = 0 100 + let prev_draw_time = 0 71 101 72 102 // callback 73 - var on_draw_callback 103 + let on_draw_callback: (() => void) | undefined 74 104 75 105 // uniforms 76 - var iframe = 0 77 - var imouse = { x: 0, y: 0, clickX: 0, clickY: 0 } 78 - var istream = 0 79 - var ivolume = 0 106 + let iframe = 0 107 + const imouse = { x: 0, y: 0, clickX: 0, clickY: 0 } 108 + let istream = 0 109 + let ivolume = 0 80 110 81 111 // shader common source 82 - var common = '' 112 + let common = '' 83 113 84 114 // render passes variables. valid keys: 85 115 // 'A', 'B', 'C', 'D', 'Image' 86 - var sourcecode = {} // fragment shader code 87 - var ichannels = {} // texture inputs 88 - var atexture = {} // front texture (input/output) 89 - var btexture = {} // back texture (input/output) 90 - var aframebuf = {} // front buffer (output) 91 - var bframebuf = {} // back buffer (output) 92 - var program = {} // webgl program 93 - var fragment_shaders = {} 94 - var vertex_shaders = {} 95 - var location = {} // uniform location 96 - var flip = {} // a b flip 97 - var quad_buffer // two full screen triangles 116 + const sourcecode: Partial<Record<Key, string>> = {} // fragment shader code 117 + const ichannels: Partial<Record<Key, Record<number, Key>>> = {} // texture inputs 118 + const atexture: Partial<Record<Key, WebGLTexture | null>> = {} // front texture (input/output) 119 + const btexture: Partial<Record<Key, WebGLTexture | null>> = {} // back texture (input/output) 120 + const aframebuf: Partial<Record<Key, WebGLFramebuffer | null>> = {} // front buffer (output) 121 + const bframebuf: Partial<Record<Key, WebGLFramebuffer | null>> = {} // back buffer (output) 122 + const program: Partial<Record<Key, WebGLProgram | null>> = {} // webgl program 123 + const fragment_shaders: Partial<Record<Key, WebGLShader | null>> = {} 124 + const vertex_shaders: Partial<Record<Key, WebGLShader | null>> = {} 125 + const location: Partial<Record<Key, Partial<Locations>>> = {} // uniform location 126 + const flip: Partial<Record<Key, boolean>> = {} // a b flip 127 + let quad_buffer: WebGLBuffer | null // two full screen triangles 98 128 99 - var setup = () => { 129 + const setup = () => { 100 130 gl.getExtension('OES_texture_float_linear') 101 131 gl.getExtension('OES_texture_half_float_linear') 102 132 gl.getExtension('EXT_color_buffer_float') 103 133 gl.getExtension('WEBGL_debug_shaders') 104 - ;['A', 'B', 'C', 'D', 'Image'].forEach((key) => { 134 + possible_keys.forEach((key) => { 105 135 sourcecode[key] = '' 106 136 ichannels[key] = {} 107 137 program[key] = null 108 138 location[key] = {} 109 - if (key != 'Image') { 139 + if (key !== 'Image') { 110 140 atexture[key] = create_texture() 111 141 btexture[key] = create_texture() 112 142 aframebuf[key] = create_frame_buffer(atexture[key]) ··· 145 175 }) 146 176 } 147 177 148 - var create_texture = () => { 149 - var texture = gl.createTexture() 178 + const create_texture = () => { 179 + const texture = gl.createTexture() 150 180 gl.bindTexture(gl.TEXTURE_2D, texture) 151 181 gl.texImage2D( 152 182 gl.TEXTURE_2D, ··· 166 196 return texture 167 197 } 168 198 169 - var create_frame_buffer = (texture) => { 170 - var framebuffer = gl.createFramebuffer() 199 + const create_frame_buffer = (texture: WebGLTexture) => { 200 + const framebuffer = gl.createFramebuffer() 171 201 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) 172 202 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0) 173 203 gl.bindTexture(gl.TEXTURE_2D, null) ··· 175 205 return framebuffer 176 206 } 177 207 178 - var compile_program = (key) => { 208 + const compile_program = (key: Key) => { 179 209 // Delete previous program + shaders if they exist 180 210 if (program[key]) { 181 211 gl.deleteProgram(program[key]) ··· 190 220 fragment_shaders[key] = null 191 221 } 192 222 193 - var vert = gl.createShader(gl.VERTEX_SHADER) 223 + const vert = gl.createShader(gl.VERTEX_SHADER) 224 + if (!vert) { 225 + return { data: null, error: 'Failed to create vertex shader' } 226 + } 194 227 gl.shaderSource(vert, basic_vertex_shader) 195 228 gl.compileShader(vert) 196 229 197 230 if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) { 198 - console.error('Vertex Shader compilation failed: ' + gl.getShaderInfoLog(vert)) 199 231 gl.deleteShader(vert) 200 - return null 232 + return { data: null, error: 'Vertex Shader compilation failed: ' + gl.getShaderInfoLog(vert) } 201 233 } 202 234 203 - var frag_source = hdr + common + sourcecode[key] 204 - var frag = gl.createShader(gl.FRAGMENT_SHADER) 235 + const frag_source = hdr + common + sourcecode[key] 236 + const frag = gl.createShader(gl.FRAGMENT_SHADER) 237 + if (!frag) { 238 + return { error: 'Failed to create fragment shader' } 239 + } 205 240 gl.shaderSource(frag, frag_source) 206 241 gl.compileShader(frag) 207 242 ··· 212 247 return null 213 248 } 214 249 215 - var new_program = gl.createProgram() 250 + const new_program = gl.createProgram() 216 251 gl.attachShader(new_program, vert) 217 252 gl.attachShader(new_program, frag) 218 253 gl.linkProgram(new_program) ··· 221 256 gl.deleteShader(frag) 222 257 223 258 if (!gl.getProgramParameter(new_program, gl.LINK_STATUS)) { 224 - console.error('Program initialization failed: ' + gl.getProgramInfoLog(new_program)) 225 - return null 259 + return { 260 + data: null, 261 + error: 'Program initialization failed: ' + gl.getProgramInfoLog(new_program), 262 + } 263 + } 264 + 265 + if (!location[key]) { 266 + return { data: null, error: 'Unexpected missing location[key]' } 226 267 } 227 268 228 269 // uniform locations ··· 252 293 return new_program 253 294 } 254 295 255 - var repeat = (times, arr) => { 256 - let result = [] 296 + const repeat = (times: number, arr: number[]) => { 297 + let result: number[] = [] 257 298 for (let i = 0; i < times; i++) { 258 299 result = [...result, ...arr] 259 300 } 260 301 return result 261 302 } 262 303 263 - var set_shader = (config, key) => { 304 + type ShaderConfig = { 305 + source?: string 306 + iChannel0?: Key 307 + iChannel1?: Key 308 + iChannel2?: Key 309 + iChannel3?: Key 310 + } 311 + 312 + const set_shader = (config: ShaderConfig, key: Key) => { 264 313 // Unbind before deleting 265 314 gl.bindTexture(gl.TEXTURE_2D, null) 266 315 gl.bindFramebuffer(gl.FRAMEBUFFER, null) ··· 292 341 flip[key] = false 293 342 } 294 343 295 - for (let i = 0; i < 4; i++) { 296 - var s = config[`iChannel${i}`] 297 - if (s == 'A' || s == 'B' || s == 'C' || s == 'D') { 344 + for (const i of [0, 1, 2, 3] as const) { 345 + const s = config[`iChannel${i}`] 346 + if (s === 'A' || s === 'B' || s === 'C' || s === 'D') { 347 + if (!ichannels[key]) { 348 + return { error: 'Unexpected missing ichannels[key]' } 349 + } 298 350 ichannels[key][i] = s 299 351 } 300 352 } ··· 304 356 } 305 357 } 306 358 307 - var draw = () => { 359 + const draw = () => { 308 360 // current time 309 - var now = is_playing ? Date.now() : prev_draw_time 310 - var date = new Date(now) 361 + const now = is_playing ? Date.now() : prev_draw_time 362 + const date = new Date(now) 311 363 312 364 // first draw? 313 - if (first_draw_time == 0) { 365 + if (first_draw_time === 0) { 314 366 first_draw_time = now 315 367 } 316 368 ··· 320 372 } 321 373 322 374 // time difference between frames in seconds 323 - var itime_delta = (now - prev_draw_time) * 0.001 375 + const itime_delta = (now - prev_draw_time) * 0.001 324 376 325 377 // time in seconds 326 - var itime = (now - first_draw_time) * 0.001 327 - var idate = [date.getFullYear(), date.getMonth(), date.getDate(), date.getTime() * 0.001] 378 + const itime = (now - first_draw_time) * 0.001 379 + const idate = [date.getFullYear(), date.getMonth(), date.getDate(), date.getTime() * 0.001] 328 380 329 381 // channel uniforms 330 - var ichannel_times = new Float32Array(repeat(4, [itime])) 331 - var ichannel_resolutions = new Float32Array(repeat(4, [gl.canvas.width, gl.canvas.height, 0])) 382 + const ichannel_times = new Float32Array(repeat(4, [itime])) 383 + const ichannel_resolutions = new Float32Array(repeat(4, [gl.canvas.width, gl.canvas.height, 0])) 332 384 333 - ;['A', 'B', 'C', 'D', 'Image'].forEach((key) => { 385 + for (const key of possible_keys) { 334 386 if (program[key]) { 335 387 // framebuffer 336 388 if (key === 'Image') { 337 389 gl.bindFramebuffer(gl.FRAMEBUFFER, null) 338 390 } else { 339 - var output = flip[key] ? bframebuf[key] : aframebuf[key] 340 - gl.bindFramebuffer(gl.FRAMEBUFFER, output) 391 + const output = flip[key] ? bframebuf[key] : aframebuf[key] 392 + gl.bindFramebuffer(gl.FRAMEBUFFER, output ?? null) 341 393 } 342 394 343 395 // textures 344 396 for (let i = 0; i < 4; i++) { 345 - var chkey = ichannels[key][i] 397 + if (!ichannels[key]) { 398 + return { error: 'Unexpected missing ichannels[key]' } 399 + } 400 + const chkey = ichannels[key][i] 346 401 if (chkey) { 347 - var input = flip[chkey] ? atexture[chkey] : btexture[chkey] 348 - gl.activeTexture(gl[`TEXTURE${i}`]) 349 - gl.bindTexture(gl.TEXTURE_2D, input) 402 + const input = flip[chkey] ? atexture[chkey] : btexture[chkey] 403 + if (!gl[`TEXTURE${i}` as 'TEXTURE0']) { 404 + return { error: `Unexpected missing gl.TEXTURE${i}` } 405 + } 406 + gl.activeTexture(gl[`TEXTURE${i}` as 'TEXTURE0']) 407 + gl.bindTexture(gl.TEXTURE_2D, input ?? null) 350 408 } 351 409 } 352 410 353 411 // program 354 412 gl.useProgram(program[key]) 355 413 414 + if (!location[key]) { 415 + return { error: 'Unexpected missing location[key]' } 416 + } 417 + 356 418 // uniforms 357 - gl.uniform3f(location[key]['iResolution'], gl.canvas.width, gl.canvas.height, 1.0) 358 - gl.uniform1f(location[key]['iTime'], itime) 359 - gl.uniform1f(location[key]['iTimeDelta'], itime_delta) 360 - gl.uniform1f(location[key]['iFrameRate'], 60) 361 - gl.uniform1i(location[key]['iFrame'], iframe) 362 - gl.uniform1fv(location[key]['iChannelTime'], ichannel_times) 363 - gl.uniform3fv(location[key]['iChannelResolution'], ichannel_resolutions) 364 - gl.uniform1i(location[key]['iChannel0'], 0) 365 - gl.uniform1i(location[key]['iChannel1'], 1) 366 - gl.uniform1i(location[key]['iChannel2'], 2) 367 - gl.uniform1i(location[key]['iChannel3'], 3) 368 - gl.uniform4f(location[key]['iMouse'], imouse.x, imouse.y, imouse.clickX, imouse.clickY) 369 - gl.uniform4f(location[key]['iDate'], idate[0], idate[1], idate[2], idate[3]) 370 - gl.uniform1f(location[key]['iSampleRate'], 44100) 371 - gl.uniform1f(location[key]['iStream'], istream) 372 - gl.uniform1f(location[key]['iVolume'], ivolume) 419 + gl.uniform3f(location[key]['iResolution'] ?? null, gl.canvas.width, gl.canvas.height, 1.0) 420 + gl.uniform1f(location[key]['iTime'] ?? null, itime) 421 + gl.uniform1f(location[key]['iTimeDelta'] ?? null, itime_delta) 422 + gl.uniform1f(location[key]['iFrameRate'] ?? null, 60) 423 + gl.uniform1i(location[key]['iFrame'] ?? null, iframe) 424 + gl.uniform1fv(location[key]['iChannelTime'] ?? null, ichannel_times) 425 + gl.uniform3fv(location[key]['iChannelResolution'] ?? null, ichannel_resolutions) 426 + gl.uniform1i(location[key]['iChannel0'] ?? null, 0) 427 + gl.uniform1i(location[key]['iChannel1'] ?? null, 1) 428 + gl.uniform1i(location[key]['iChannel2'] ?? null, 2) 429 + gl.uniform1i(location[key]['iChannel3'] ?? null, 3) 430 + gl.uniform4f( 431 + location[key]['iMouse'] ?? null, 432 + imouse.x, 433 + imouse.y, 434 + imouse.clickX, 435 + imouse.clickY, 436 + ) 437 + gl.uniform4f(location[key]['iDate'] ?? null, idate[0], idate[1], idate[2], idate[3]) 438 + gl.uniform1f(location[key]['iSampleRate'] ?? null, 44100) 439 + gl.uniform1f(location[key]['iStream'] ?? null, istream) 440 + gl.uniform1f(location[key]['iVolume'] ?? null, ivolume) 373 441 374 442 // viewport 375 443 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) 376 444 377 445 // vertexs 378 446 gl.bindBuffer(gl.ARRAY_BUFFER, quad_buffer) 447 + if (location[key]['vertexInPosition'] === undefined) { 448 + return { error: 'Unexpected missing location[key].vertexInPosition' } 449 + } 379 450 gl.vertexAttribPointer(location[key]['vertexInPosition'], 2, gl.FLOAT, false, 0, 0) 380 451 gl.enableVertexAttribArray(location[key]['vertexInPosition']) 381 452 ··· 384 455 385 456 flip[key] = !flip[key] 386 457 } 387 - }) 458 + } 388 459 389 460 // time of last draw 390 461 prev_draw_time = now ··· 396 467 const raf = create_singular_request_animation_frame() 397 468 398 469 // Animation loop 399 - var animate = () => { 470 + const animate = () => { 400 471 if (is_playing) { 401 472 draw() 402 473 raf(animate) 403 474 } 404 475 } 405 476 406 - this.set_common = (source) => { 477 + const set_common = (source: string) => { 407 478 if (source === undefined) { 408 479 source = '' 409 480 } ··· 411 482 source = '' 412 483 } 413 484 common = source 414 - ;['A', 'B', 'C', 'D', 'Image'].forEach((key) => { 485 + possible_keys.forEach((key) => { 415 486 if (program[key]) { 416 487 program[key] = compile_program(key) 417 488 } 418 489 }) 419 490 } 420 491 421 - this.set_buffer_a = (config) => { 492 + const set_buffer_a = (config: ShaderConfig) => { 422 493 set_shader(config, 'A') 423 494 } 424 495 425 - this.set_buffer_b = (config) => { 496 + const set_buffer_b = (config: ShaderConfig) => { 426 497 set_shader(config, 'B') 427 498 } 428 499 429 - this.set_buffer_c = (config) => { 500 + const set_buffer_c = (config: ShaderConfig) => { 430 501 set_shader(config, 'C') 431 502 } 432 503 433 - this.set_buffer_d = (config) => { 504 + const set_buffer_d = (config: ShaderConfig) => { 434 505 set_shader(config, 'D') 435 506 } 436 507 437 - this.set_image = (config) => { 508 + const set_image = (config: ShaderConfig) => { 438 509 set_shader(config, 'Image') 439 510 } 440 511 441 - this.set_on_draw = (callback) => { 512 + const set_on_draw = (callback: (() => void) | undefined) => { 442 513 on_draw_callback = callback 443 514 } 444 515 445 - this.set_stream = (value) => { 516 + const set_stream = (value: number) => { 446 517 istream = value 447 518 } 448 519 449 - this.set_volume = (value) => { 520 + const set_volume = (value: number) => { 450 521 ivolume = value 451 522 } 452 523 453 - this.add_texture = (texture, key) => { 524 + const add_texture = (texture: WebGLTexture, key: Key) => { 454 525 if (atexture[key]) { 455 526 gl.deleteTexture(atexture[key]) 456 527 atexture[key] = null ··· 472 543 flip[key] = false 473 544 } 474 545 475 - this.time = () => { 546 + const time = () => { 476 547 return (prev_draw_time - first_draw_time) * 0.001 477 548 } 478 549 479 - this.is_playing = () => is_playing 480 - 481 - this.reset = () => { 482 - var now = new Date() 550 + const reset = () => { 551 + const now = Date.now() 483 552 first_draw_time = now 484 553 prev_draw_time = now 485 554 iframe = 0 486 555 draw() 487 556 } 488 557 489 - this.pause = () => { 558 + const pause = () => { 490 559 is_playing = false 491 560 } 492 561 493 - this.play = () => { 562 + const play = () => { 494 563 if (!is_playing) { 495 564 is_playing = true 496 - var now = Date.now() 497 - var elapsed = prev_draw_time - first_draw_time 565 + const now = Date.now() 566 + const elapsed = prev_draw_time - first_draw_time 498 567 first_draw_time = now - elapsed 499 568 prev_draw_time = now 500 569 animate() 501 570 } 502 571 } 503 572 504 - var recreate_textures = () => { 505 - ;['A', 'B', 'C', 'D'].forEach((key) => { 573 + const recreate_textures = () => { 574 + for (const key of ['A', 'B', 'C', 'D'] as const) { 506 575 if (atexture[key]) { 507 576 // Delete old textures 508 577 gl.deleteTexture(atexture[key]) 509 - gl.deleteTexture(btexture[key]) 510 - gl.deleteFramebuffer(aframebuf[key]) 511 - gl.deleteFramebuffer(bframebuf[key]) 578 + gl.deleteTexture(btexture[key] ?? null) 579 + gl.deleteFramebuffer(aframebuf[key] ?? null) 580 + gl.deleteFramebuffer(bframebuf[key] ?? null) 512 581 513 582 // Create new textures with updated canvas size 514 583 atexture[key] = create_texture() ··· 517 586 bframebuf[key] = create_frame_buffer(btexture[key]) 518 587 flip[key] = false 519 588 } 520 - }) 589 + } 521 590 } 522 591 523 - this.resize = () => { 592 + const resize = () => { 524 593 // Update WebGL viewport 525 594 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) 526 595 ··· 528 597 recreate_textures() 529 598 530 599 // Reset timing to avoid glitches 531 - var now = Date.now() 532 - var elapsed = prev_draw_time - first_draw_time 600 + const now = Date.now() 601 + const elapsed = prev_draw_time - first_draw_time 533 602 first_draw_time = now - elapsed 534 603 prev_draw_time = now 535 604 ··· 539 608 } 540 609 } 541 610 542 - this.destroy = () => { 611 + const destroy = () => { 543 612 // Stop animation loop 544 613 is_playing = false 545 614 546 615 // Delete all programs and shaders 547 - ;['A', 'B', 'C', 'D', 'Image'].forEach((key) => { 616 + possible_keys.forEach((key) => { 548 617 if (program[key]) { 549 618 gl.deleteProgram(program[key]) 550 619 program[key] = null ··· 560 629 }) 561 630 562 631 // Delete all textures and framebuffers 563 - ;['A', 'B', 'C', 'D'].forEach((key) => { 632 + for (const key of ['A', 'B', 'C', 'D'] as const) { 564 633 if (atexture[key]) { 565 634 gl.deleteTexture(atexture[key]) 566 635 atexture[key] = null ··· 577 646 gl.deleteFramebuffer(bframebuf[key]) 578 647 bframebuf[key] = null 579 648 } 580 - }) 649 + } 581 650 582 651 // Delete quad buffer 583 652 if (quad_buffer) { ··· 596 665 } 597 666 598 667 // Clear callback 599 - on_draw_callback = null 600 - 601 - // Nullify gl reference 602 - gl = null 668 + on_draw_callback = undefined 603 669 } 604 670 605 671 setup() 672 + 673 + return { 674 + data: { 675 + set_common, 676 + set_buffer_a, 677 + set_buffer_b, 678 + set_buffer_c, 679 + set_buffer_d, 680 + set_image, 681 + set_on_draw, 682 + set_stream, 683 + set_volume, 684 + add_texture, 685 + time, 686 + reset, 687 + pause, 688 + play, 689 + resize, 690 + destroy, 691 + is_playing() { 692 + return is_playing 693 + }, 694 + }, 695 + error: null, 696 + } 606 697 }
+55 -44
src/lib/visualizer/Visualizer.svelte
··· 1 1 <script lang="ts"> 2 2 import { start_visualizer } from './visualizer' 3 - import { ShaderToyLite } from './ShaderToyLite.js' 3 + import { shader_toy_lite, type ShaderToyLite } from './ShaderToyLite.js' 4 4 import { onDestroy } from 'svelte' 5 - import { audioContext, mediaElementSource } from '$lib/player' 5 + import { audio_context, media_element_source } from '$lib/player' 6 6 import Player from '$components/Player.svelte' 7 7 import { fly } from 'svelte/transition' 8 8 import { check_modifiers } from '$lib/helpers' 9 + import { error_popup } from '$lib/error' 9 10 10 11 let { on_close }: { on_close: () => void } = $props() 11 12 ··· 97 98 }, 98 99 ] 99 100 100 - const imageShader = ` 101 + const image_shader = ` 101 102 void mainImage( out vec4 fragColor, in vec2 fragCoord ) { 102 103 vec2 uv = fragCoord.xy / iResolution.xy; 103 104 vec4 col = texture(iChannel0, uv); 104 105 fragColor = vec4(col.rgb, 1.); 105 106 } 106 107 ` 107 - function create_toy(canvas: HTMLCanvasElement) { 108 - const toy = new ShaderToyLite(canvas) 109 - toy.set_common('') 110 - toy.set_image({ source: imageShader, iChannel0: 'A' }) 111 - return toy 112 - } 113 108 114 109 // main visualizer, and a transition visualizer 115 110 type Vis = { ··· 131 126 let main_vis = visualisers[0] 132 127 let next_vis = visualisers[1] 133 128 134 - let currentShaderIndex = 0 135 - let isTransitioning = false 136 - let autoTransitionTimeout: ReturnType<typeof setTimeout> | undefined 129 + let current_shader_index = 0 130 + let is_transitioning = false 131 + let auto_transition_timeout: ReturnType<typeof setTimeout> | undefined 137 132 138 - const visualizer = start_visualizer(audioContext, mediaElementSource, (info) => { 133 + const visualizer = start_visualizer(audio_context, media_element_source, (info) => { 139 134 main_vis.toy?.set_stream(info.stream) 140 135 main_vis.toy?.set_volume(info.volume) 141 136 // Also update transition toy if it exists ··· 143 138 next_vis.toy?.set_volume(info.volume) 144 139 }) 145 140 146 - let pendingTransition: number | null = null 141 + let pending_transition: number | null = null 147 142 148 - async function transitionToShader(newShaderIndex: number, duration = 2000) { 149 - if (isTransitioning) { 150 - pendingTransition = newShaderIndex 143 + async function transition_to_shader(new_shader_index: number, duration = 2000) { 144 + if (is_transitioning) { 145 + pending_transition = new_shader_index 151 146 return 152 147 } 153 - if (currentShaderIndex === newShaderIndex) return 148 + if (current_shader_index === new_shader_index) return 154 149 155 150 if (!main_vis.canvas || !next_vis.canvas) { 156 151 return console.error('Missing canvas') 157 152 } 158 153 159 - console.log('Starting transition to:', newShaderIndex) 160 - isTransitioning = true 161 - clearTimeout(autoTransitionTimeout) 154 + console.log('Starting transition to:', new_shader_index) 155 + is_transitioning = true 156 + clearTimeout(auto_transition_timeout) 162 157 163 158 // Start transition visualizer 164 159 if (!next_vis.toy) { 165 - next_vis.toy = create_toy(next_vis.canvas) 160 + const toy_result = shader_toy_lite(next_vis.canvas) 161 + if (!toy_result.data) { 162 + error_popup(toy_result.error) 163 + on_close() 164 + return 165 + } 166 + next_vis.toy = toy_result.data 167 + next_vis.toy.set_common('') 168 + next_vis.toy.set_image({ source: image_shader, iChannel0: 'A' }) 166 169 } 167 170 if (next_vis.should_resize) { 168 171 next_vis.should_resize = false 169 172 schedule_resize(next_vis) 170 173 } 171 - next_vis.toy.set_buffer_a({ source: shaders[newShaderIndex].shader }) 174 + next_vis.toy.set_buffer_a({ source: shaders[new_shader_index].shader }) 172 175 next_vis.toy.play() 173 176 174 177 const animation_new = next_vis.canvas.animate( ··· 199 202 200 203 next_vis.toy?.pause() // Keep the instance for later use 201 204 202 - currentShaderIndex = newShaderIndex 203 - isTransitioning = false 205 + current_shader_index = new_shader_index 206 + is_transitioning = false 204 207 205 - scheduleAutoTransition() 208 + schedule_auto_transition() 206 209 207 - if (pendingTransition !== null) { 208 - const nextIndex = pendingTransition 209 - pendingTransition = null 210 - transitionToShader(nextIndex) 210 + if (pending_transition !== null) { 211 + const next_index = pending_transition 212 + pending_transition = null 213 + transition_to_shader(next_index) 211 214 } 212 215 } 213 216 214 - function scheduleAutoTransition() { 215 - clearTimeout(autoTransitionTimeout) 216 - autoTransitionTimeout = setTimeout(() => { 217 - if (!isTransitioning) { 218 - const nextIndex = (currentShaderIndex + 1) % shaders.length 219 - transitionToShader(nextIndex) 217 + function schedule_auto_transition() { 218 + clearTimeout(auto_transition_timeout) 219 + auto_transition_timeout = setTimeout(() => { 220 + if (!is_transitioning) { 221 + const next_index = (current_shader_index + 1) % shaders.length 222 + transition_to_shader(next_index) 220 223 } 221 224 }, 10000) 222 225 } 223 226 224 - scheduleAutoTransition() 227 + schedule_auto_transition() 225 228 226 229 let show_player = $state(false) 227 230 let hide_player_timeout: ReturnType<typeof setTimeout> | undefined ··· 234 237 } 235 238 236 239 function schedule_resize(vis: Vis) { 237 - if (!isTransitioning && !vis.is_main) { 240 + if (!is_transitioning && !vis.is_main) { 238 241 vis.should_resize = true 239 242 return 240 243 } ··· 246 249 } 247 250 248 251 onDestroy(() => { 249 - clearTimeout(autoTransitionTimeout) 252 + clearTimeout(auto_transition_timeout) 250 253 visualizer.destroy() 251 254 main_vis.toy?.destroy() 252 255 next_vis.toy?.destroy() ··· 272 275 onkeydown={(e) => { 273 276 if (check_modifiers(e, {})) { 274 277 if (e.key === 'ArrowLeft') { 275 - transitionToShader((currentShaderIndex - 1 + shaders.length) % shaders.length, 500) 278 + transition_to_shader((current_shader_index - 1 + shaders.length) % shaders.length, 500) 276 279 } else if (e.key === 'ArrowRight') { 277 - transitionToShader((currentShaderIndex + 1) % shaders.length, 500) 280 + transition_to_shader((current_shader_index + 1) % shaders.length, 500) 278 281 } else if (/^[0-9]$/.test(e.key)) { 279 - transitionToShader(Math.min(currentShaderIndex + 1, shaders.length - 1), 500) 282 + transition_to_shader(Math.min(current_shader_index + 1, shaders.length - 1), 500) 280 283 } else { 281 284 show_player_temporarily() 282 285 } ··· 284 287 }} 285 288 > 286 289 <div class="h-screen w-screen"> 287 - {#each visualisers as vis, i (vis)} 290 + {#each visualisers as vis} 288 291 <canvas 289 292 bind:this={vis.canvas} 290 293 class="fixed inset-0" ··· 296 299 {@attach (canvas) => { 297 300 vis.canvas = canvas 298 301 if (vis.is_main && !vis.toy) { 299 - const toy = create_toy(canvas) 302 + const toy_result = shader_toy_lite(canvas) 303 + if (!toy_result.data) { 304 + error_popup(toy_result.error) 305 + on_close() 306 + return 307 + } 308 + const toy = toy_result.data 309 + toy.set_common('') 310 + toy.set_image({ source: image_shader, iChannel0: 'A' }) 300 311 toy.set_buffer_a({ source: shaders[0].shader }) 301 312 toy.play() 302 313 vis.toy = toy
+25 -25
src/lib/visualizer/visualizer.ts
··· 17 17 const FILTER_Q = 0.75 18 18 19 19 export function start_visualizer( 20 - audioContext: AudioContext, 21 - mediaElementSource: MediaElementAudioSourceNode, 20 + audio_context: AudioContext, 21 + media_element_source: MediaElementAudioSourceNode, 22 22 on_update: (info: { stream: number; volume: number }) => void, 23 23 ) { 24 - const analyser = audioContext.createAnalyser() 25 - const filter = audioContext.createBiquadFilter() 26 - const timeBuffer = new Float32Array(BIT_DEPTH) 24 + const analyser = audio_context.createAnalyser() 25 + const filter = audio_context.createBiquadFilter() 26 + const time_buffer = new Float32Array(BIT_DEPTH) 27 27 const raf = create_singular_request_animation_frame() 28 28 29 - mediaElementSource.connect(filter) 29 + media_element_source.connect(filter) 30 30 filter.connect(analyser) 31 31 analyser.smoothingTimeConstant = 0 32 32 analyser.fftSize = BIT_DEPTH ··· 38 38 let volume = 0 39 39 40 40 const def = visualizer_settings?.def || [2.5, 0.09] 41 - const volumeBuffer: number[] = [] 41 + const volume_buffer: number[] = [] 42 42 43 43 // Pre-fill buffer with some baseline values to avoid initial instability 44 44 for (let i = 0; i < 60; i++) { 45 - volumeBuffer.push(0.1) // Small baseline value 45 + volume_buffer.push(0.1) // Small baseline value 46 46 } 47 47 48 - function sampleVolume(totalSamples: number) { 48 + function sample_volume(total_samples: number) { 49 49 let value = 0 50 - const start = Math.max(volumeBuffer.length - 1, 0) 51 - const end = Math.max(start - totalSamples, 0) 50 + const start = Math.max(volume_buffer.length - 1, 0) 51 + const end = Math.max(start - total_samples, 0) 52 52 let min = Infinity 53 53 for (let i = start; i >= end; i--) { 54 - value += volumeBuffer[i] 55 - if (volumeBuffer[i] < min) min = volumeBuffer[i] 54 + value += volume_buffer[i] 55 + if (volume_buffer[i] < min) min = volume_buffer[i] 56 56 } 57 - return [value / totalSamples, min] 57 + return [value / total_samples, min] 58 58 } 59 59 60 - function measure_volume(frameRate: number) { 61 - const rawVolume = getRawVolume() 62 - volumeBuffer.push(rawVolume) 60 + function measure_volume(frame_rate: number) { 61 + const raw_volume = get_raw_volume() 62 + volume_buffer.push(raw_volume) 63 63 64 64 // Need minimum samples before doing complex calculations 65 - const minSamplesNeeded = Math.max(2, (def[1] * 1000) / (1000 / frameRate)) 66 - if (volumeBuffer.length < minSamplesNeeded) { 65 + const min_samples_needed = Math.max(2, (def[1] * 1000) / (1000 / frame_rate)) 66 + if (volume_buffer.length < min_samples_needed) { 67 67 return 0.01 // Small default value while buffer builds up 68 68 } 69 69 70 - const [ref, min] = sampleVolume((def[0] * 1000) / (1000 / frameRate)) 71 - const [sample] = sampleVolume((def[1] * 1000) / (1000 / frameRate)) 70 + const [ref, min] = sample_volume((def[0] * 1000) / (1000 / frame_rate)) 71 + const [sample] = sample_volume((def[1] * 1000) / (1000 / frame_rate)) 72 72 73 73 // Avoid division by zero or very small differences 74 74 const range = ref - min ··· 81 81 return isNaN(raw) ? 0.1 : Math.max(0, raw / 2) // Ensure non-negative 82 82 } 83 83 84 - function getRawVolume() { 85 - analyser.getFloatTimeDomainData(timeBuffer) 86 - return (Meyda.extract('rms', timeBuffer) as number) || 0 84 + function get_raw_volume() { 85 + analyser.getFloatTimeDomainData(time_buffer) 86 + return (Meyda.extract('rms', time_buffer) as number) || 0 87 87 } 88 88 89 89 let destroyed = false ··· 98 98 volume = vol 99 99 } 100 100 101 - const playing = audioContext.state === 'running' 101 + const playing = audio_context.state === 'running' 102 102 103 103 stream = stream + Math.pow(vol, 0.75) / (playing ? 10 : 100) 104 104