From 4e3a0140405c7a3fef0512d7545278a004fbe1f5 Mon Sep 17 00:00:00 2001 From: SrIzan10 <66965250+SrIzan10@users.noreply.github.com> Date: Thu, 22 May 2025 07:30:56 +0000 Subject: [PATCH 1/8] feat: initial class impl --- src/lib/mersenne.ts | 220 +++++++++++++++++++++ src/lib/stations/sleep.ts | 28 +++ src/lib/types.ts | 24 ++- src/lib/utils.ts | 6 +- src/routes/api/live/[stationId]/+server.ts | 8 +- 5 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 src/lib/mersenne.ts create mode 100644 src/lib/stations/sleep.ts diff --git a/src/lib/mersenne.ts b/src/lib/mersenne.ts new file mode 100644 index 0000000..245ba98 --- /dev/null +++ b/src/lib/mersenne.ts @@ -0,0 +1,220 @@ +/* + * TypeScript port by Thilo Planz + * + * https://gist.github.com/thiloplanz/6abf04f957197e9e3912 + */ + +/* + I've wrapped Makoto Matsumoto and Takuji Nishimura's code in a namespace + so it's better encapsulated. Now you can have multiple random number generators + and they won't stomp all over eachother's state. + + If you want to use this as a substitute for Math.random(), use the random() + method like so: + + var m = new MersenneTwister(); + var randomNumber = m.random(); + + You can also call the other genrand_{foo}() methods on the instance. + + If you want to use a specific seed in order to get a repeatable random + sequence, pass an integer into the constructor: + + var m = new MersenneTwister(123); + + and that will always produce the same random sequence. + + Sean McCullough (banksean@gmail.com) +*/ + +/* + A C-program for MT19937, with initialization improved 2002/1/26. + Coded by Takuji Nishimura and Makoto Matsumoto. + + Before using, initialize the state by using init_genrand(seed) + or init_by_array(init_key, key_length). + + Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. The names of its contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + Any feedback is very welcome. + http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html + email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) +*/ + +class MersenneTwister { + /* Period parameters */ + private N = 624; + private M = 397; + private MATRIX_A = 0x9908b0df; /* constant vector a */ + private UPPER_MASK = 0x80000000; /* most significant w-r bits */ + private LOWER_MASK = 0x7fffffff; /* least significant r bits */ + + private mt = new Array(this.N); /* the array for the state vector */ + private mti = this.N + 1; /* mti==N+1 means mt[N] is not initialized */ + + constructor(seed?: number) { + if (seed == undefined) { + seed = new Date().getTime(); + } + this.init_genrand(seed); + } + + /* initializes mt[N] with a seed */ + private init_genrand(s: number) { + this.mt[0] = s >>> 0; + for (this.mti = 1; this.mti < this.N; this.mti++) { + s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30); + this.mt[this.mti] = + ((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253 + this.mti; + /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ + /* In the previous versions, MSBs of the seed affect */ + /* only MSBs of the array mt[]. */ + /* 2002/01/09 modified by Makoto Matsumoto */ + this.mt[this.mti] >>>= 0; + /* for >32 bit machines */ + } + } + + /* initialize by an array with array-length */ + /* init_key is the array for initializing keys */ + /* key_length is its length */ + /* slight change for C++, 2004/2/26 */ + init_by_array(init_key: number[], key_length: number): void { + var i: number, j: number, k: number; + this.init_genrand(19650218); + i = 1; + j = 0; + k = this.N > key_length ? this.N : key_length; + for (; k; k--) { + var s: number = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); + this.mt[i] = + (this.mt[i] ^ + (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + (s & 0x0000ffff) * 1664525)) + + init_key[j] + + j; /* non linear */ + this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */ + i++; + j++; + if (i >= this.N) { + this.mt[0] = this.mt[this.N - 1]; + i = 1; + } + if (j >= key_length) j = 0; + } + for (k = this.N - 1; k; k--) { + var s: number = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); + this.mt[i] = + (this.mt[i] ^ + (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941)) - + i; /* non linear */ + this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */ + i++; + if (i >= this.N) { + this.mt[0] = this.mt[this.N - 1]; + i = 1; + } + } + + this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */ + } + + /* generates a random number on [0,0xffffffff]-interval */ + genrand_int32() { + var y; + var mag01 = new Array(0x0, this.MATRIX_A); + /* mag01[x] = x * MATRIX_A for x=0,1 */ + + if (this.mti >= this.N) { + /* generate N words at one time */ + var kk; + + if (this.mti == this.N + 1) + /* if init_genrand() has not been called, */ + this.init_genrand(5489); /* a default initial seed is used */ + + for (kk = 0; kk < this.N - this.M; kk++) { + y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK); + this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + for (; kk < this.N - 1; kk++) { + y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK); + this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1]; + } + y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK); + this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1]; + + this.mti = 0; + } + + y = this.mt[this.mti++]; + + /* Tempering */ + y ^= y >>> 11; + y ^= (y << 7) & 0x9d2c5680; + y ^= (y << 15) & 0xefc60000; + y ^= y >>> 18; + + return y >>> 0; + } + + /* generates a random number on [0,0x7fffffff]-interval */ + genrand_int31() { + return this.genrand_int32() >>> 1; + } + + /* generates a random number on [0,1]-real-interval */ + genrand_real1() { + return this.genrand_int32() * (1.0 / 4294967295.0); + /* divided by 2^32-1 */ + } + + /* generates a random number on [0,1)-real-interval */ + random() { + return this.genrand_int32() * (1.0 / 4294967296.0); + /* divided by 2^32 */ + } + + /* generates a random number on (0,1)-real-interval */ + genrand_real3() { + return (this.genrand_int32() + 0.5) * (1.0 / 4294967296.0); + /* divided by 2^32 */ + } + + /* generates a random number on [0,1) with 53-bit resolution*/ + genrand_res53() { + var a = this.genrand_int32() >>> 5, + b = this.genrand_int32() >>> 6; + return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0); + } + + /* These real versions are due to Isaku Wada, 2002/01/09 added */ +} diff --git a/src/lib/stations/sleep.ts b/src/lib/stations/sleep.ts new file mode 100644 index 0000000..2272bd5 --- /dev/null +++ b/src/lib/stations/sleep.ts @@ -0,0 +1,28 @@ +import type { Song, StationClass } from "@/types"; + +export class SleepStation implements StationClass { + stationId = 50000; + private m = new MersenneTwister(); + async getFiles(): Promise { + const res = await fetch('https://lofi-cdn.srizan.dev/sleep/list.json'); + const data = await res.json(); + return data; + } + + async finalData(): Promise { + const files = await this.getFiles(); + const mapped = files.map(file => { + const [artist, title] = file.replace('.opus', '').split(' - '); + return { + id: parseInt((this.m.random() * 1000000).toFixed(0)), + fileId: file.replace('.opus', ''), + endpoint: `https://lofi-cdn.srizan.dev/sleep/${file}`, + artists: artist, + title: title, + image: `https://lofi-cdn.srizan.dev/sleep/thumbs/${file.replace('.opus', '')}.webp`, + }; + }) as Song[]; + const shuffled = mapped.sort(() => 0.5 - this.m.random()).slice(0, 5); + return shuffled; + } +} \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index ee339c0..78325ad 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,16 +5,16 @@ export interface Song { artists: string; title: string; image: string; - likes: number; + likes?: number; featured?: string; - releaseDate: string; - releaseDateText: string; - duration: number; - isrc: string; - label: string; - spotifyId: string; - startTime: string; - endTime: string; + releaseDate?: string; + releaseDateText?: string; + duration?: number; + isrc?: string; + label?: string; + spotifyId?: string; + startTime?: string; + endTime?: string; } export interface Preset { @@ -84,4 +84,10 @@ export interface BRStation { order: string; Name: string; Source: string; +} + +export interface StationClass { + stationId: number; + getFiles(): Promise; + finalData(): Promise; } \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 44cde2b..f53d603 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -26,9 +26,13 @@ export async function getStationSongs(stationId: number) { } export function setSongTime() { + if (!state.currentSong!.startTime) { + state.currentTime = 0; + return; + } const currentTime = new Date().getTime() / 1000; const startTime = new Date(state.currentSong!.startTime).getTime() / 1000; - const endTime = new Date(state.currentSong!.endTime).getTime() / 1000; + const endTime = new Date(state.currentSong!.endTime!).getTime() / 1000; const duration = endTime - startTime; const elapsed = currentTime - startTime; if (elapsed > 0 && elapsed < duration) { diff --git a/src/routes/api/live/[stationId]/+server.ts b/src/routes/api/live/[stationId]/+server.ts index 567226a..c61fcd9 100644 --- a/src/routes/api/live/[stationId]/+server.ts +++ b/src/routes/api/live/[stationId]/+server.ts @@ -1,3 +1,5 @@ +import type { Song } from '@/types.js'; + export async function GET(event) { const { stationId } = event.params; const stationIdInt = parseInt(stationId); @@ -14,12 +16,12 @@ export async function GET(event) { const [artist, title] = noOpus.split(' - '); return { id: parseInt((Math.random() * 1000000).toFixed(0)), - endpoint: `https://lofi-cdn.srizan.dev/sleep/${song}`, fileId: noOpus, - image: `https://lofi-cdn.srizan.dev/sleep/thumbs/${noOpus}.webp`, + endpoint: `https://lofi-cdn.srizan.dev/sleep/${song}`, artists: artist, title: title, - } + image: `https://lofi-cdn.srizan.dev/sleep/thumbs/${noOpus}.webp`, + } as Song; }); return new Response(JSON.stringify(randomData), { headers: { From d6f884a83d78dac4b98325d891ccaf72447e0362 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Thu, 29 May 2025 16:48:04 +0200 Subject: [PATCH 2/8] feat: better station list --- src/lib/state.svelte.ts | 2 +- src/lib/stations/chillhop.ts | 7 ++++++ src/lib/stations/index.ts | 17 +++++++++++++ src/lib/stations/sleep.ts | 49 ++++++++++++++++-------------------- src/lib/types.ts | 6 ----- 5 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 src/lib/stations/chillhop.ts create mode 100644 src/lib/stations/index.ts diff --git a/src/lib/state.svelte.ts b/src/lib/state.svelte.ts index b239225..1676588 100644 --- a/src/lib/state.svelte.ts +++ b/src/lib/state.svelte.ts @@ -14,7 +14,7 @@ export const state = $state({ isLoading: true, error: null as string | null, currentTime: 0, - duration: 0, + duration: 0 as number | undefined, presets: [] as Preset[], stations: [] as Station[], diff --git a/src/lib/stations/chillhop.ts b/src/lib/stations/chillhop.ts new file mode 100644 index 0000000..2564fdb --- /dev/null +++ b/src/lib/stations/chillhop.ts @@ -0,0 +1,7 @@ +import type { Song } from "@/types"; + +export async function getChillhopStation(id: number): Promise { + const res = await fetch(`https://stream.chillhop.com/live/${id}`); + const data = await res.json() as Song[]; + return data; +} diff --git a/src/lib/stations/index.ts b/src/lib/stations/index.ts new file mode 100644 index 0000000..8fa9888 --- /dev/null +++ b/src/lib/stations/index.ts @@ -0,0 +1,17 @@ +import { getChillhopStation } from './chillhop'; +import { getSleepStationSongs } from './sleep'; + +const customStations = { + 50000: getSleepStationSongs, +}; +const chillhopStations = Object.fromEntries( + Array.from({ length: 100 }, (_, i) => { + const stationId = 10000 + i; + return [stationId, getChillhopStation.bind(null, stationId)]; + }) +); + +export const stations = { + ...customStations, + ...chillhopStations, +}; diff --git a/src/lib/stations/sleep.ts b/src/lib/stations/sleep.ts index 2272bd5..5b33ce6 100644 --- a/src/lib/stations/sleep.ts +++ b/src/lib/stations/sleep.ts @@ -1,28 +1,23 @@ -import type { Song, StationClass } from "@/types"; +import type { Song } from "@/types"; -export class SleepStation implements StationClass { - stationId = 50000; - private m = new MersenneTwister(); - async getFiles(): Promise { - const res = await fetch('https://lofi-cdn.srizan.dev/sleep/list.json'); - const data = await res.json(); - return data; - } - - async finalData(): Promise { - const files = await this.getFiles(); - const mapped = files.map(file => { - const [artist, title] = file.replace('.opus', '').split(' - '); - return { - id: parseInt((this.m.random() * 1000000).toFixed(0)), - fileId: file.replace('.opus', ''), - endpoint: `https://lofi-cdn.srizan.dev/sleep/${file}`, - artists: artist, - title: title, - image: `https://lofi-cdn.srizan.dev/sleep/thumbs/${file.replace('.opus', '')}.webp`, - }; - }) as Song[]; - const shuffled = mapped.sort(() => 0.5 - this.m.random()).slice(0, 5); - return shuffled; - } -} \ No newline at end of file +export async function getSleepStationSongs(): Promise { + const m = new MersenneTwister(); + + const res = await fetch('https://lofi-cdn.srizan.dev/sleep/list.json'); + const files = await res.json() as string[]; + + const mapped = files.map(file => { + const [artist, title] = file.replace('.opus', '').split(' - '); + return { + id: parseInt((m.random() * 1000000).toFixed(0)), + fileId: file.replace('.opus', ''), + endpoint: `https://lofi-cdn.srizan.dev/sleep/${file}`, + artists: artist, + title: title, + image: `https://lofi-cdn.srizan.dev/sleep/thumbs/${file.replace('.opus', '')}.webp`, + }; + }) as Song[]; + + const shuffled = mapped.sort(() => 0.5 - m.random()).slice(0, 5); + return shuffled; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 78325ad..7bec215 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -85,9 +85,3 @@ export interface BRStation { Name: string; Source: string; } - -export interface StationClass { - stationId: number; - getFiles(): Promise; - finalData(): Promise; -} \ No newline at end of file From 3ef50464aad752fd914a7c2801711ff666b16081 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Thu, 29 May 2025 16:55:01 +0200 Subject: [PATCH 3/8] chore: use sfmt and hardcoded id in sleep --- src/lib/mersenne.ts | 281 ++++++++++++++++++++++++-------------- src/lib/stations/sleep.ts | 6 +- 2 files changed, 182 insertions(+), 105 deletions(-) diff --git a/src/lib/mersenne.ts b/src/lib/mersenne.ts index 245ba98..5b816a1 100644 --- a/src/lib/mersenne.ts +++ b/src/lib/mersenne.ts @@ -1,7 +1,8 @@ /* - * TypeScript port by Thilo Planz - * + * Original API from TypeScript port by Thilo Planz. * https://gist.github.com/thiloplanz/6abf04f957197e9e3912 + * + * SFMT implementation by Claude Sonnet 4. */ /* @@ -12,7 +13,7 @@ If you want to use this as a substitute for Math.random(), use the random() method like so: - var m = new MersenneTwister(); + var m = new SIMDMersenneTwister(); var randomNumber = m.random(); You can also call the other genrand_{foo}() methods on the instance. @@ -20,13 +21,21 @@ If you want to use a specific seed in order to get a repeatable random sequence, pass an integer into the constructor: - var m = new MersenneTwister(123); + var m = new SIMDMersenneTwister(123); and that will always produce the same random sequence. Sean McCullough (banksean@gmail.com) */ +/* + * SIMD-oriented Fast Mersenne Twister (SFMT) TypeScript implementation + * Based on the SFMT algorithm by Mutsuo Saito and Makoto Matsumoto + * + * This implementation provides better performance through SIMD-like operations + * while maintaining the same API as the standard Mersenne Twister. + */ + /* A C-program for MT19937, with initialization improved 2002/1/26. Coded by Takuji Nishimura and Makoto Matsumoto. @@ -70,151 +79,219 @@ email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) */ -class MersenneTwister { - /* Period parameters */ - private N = 624; - private M = 397; - private MATRIX_A = 0x9908b0df; /* constant vector a */ - private UPPER_MASK = 0x80000000; /* most significant w-r bits */ - private LOWER_MASK = 0x7fffffff; /* least significant r bits */ +class SIMDMersenneTwister { + /* SFMT period parameters for SFMT19937 */ + private readonly MEXP = 19937; + private readonly N = Math.floor(this.MEXP / 128) + 1; // 156 + private readonly N32 = this.N * 4; // 624 + private readonly POS1 = 122; + private readonly SL1 = 18; + private readonly SL2 = 1; + private readonly SR1 = 11; + private readonly SR2 = 1; + private readonly MSK1 = 0xdfffffef; + private readonly MSK2 = 0xddfecb7f; + private readonly MSK3 = 0xbffaffff; + private readonly MSK4 = 0xbffffff6; + private readonly PARITY1 = 0x00000001; + private readonly PARITY2 = 0x00000000; + private readonly PARITY3 = 0x00000000; + private readonly PARITY4 = 0x13c9e684; - private mt = new Array(this.N); /* the array for the state vector */ - private mti = this.N + 1; /* mti==N+1 means mt[N] is not initialized */ + /* 128-bit state array represented as 32-bit chunks */ + private state = new Uint32Array(this.N32); + private idx = this.N32; /* index counter */ constructor(seed?: number) { - if (seed == undefined) { + if (seed === undefined) { seed = new Date().getTime(); } this.init_genrand(seed); } - /* initializes mt[N] with a seed */ - private init_genrand(s: number) { - this.mt[0] = s >>> 0; - for (this.mti = 1; this.mti < this.N; this.mti++) { - s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30); - this.mt[this.mti] = - ((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253 + this.mti; - /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ - /* In the previous versions, MSBs of the seed affect */ - /* only MSBs of the array mt[]. */ - /* 2002/01/09 modified by Makoto Matsumoto */ - this.mt[this.mti] >>>= 0; - /* for >32 bit machines */ + /* Initialize generator state with seed */ + private init_genrand(seed: number): void { + this.state[0] = seed >>> 0; + for (let i = 1; i < this.N32; i++) { + this.state[i] = (1812433253 * (this.state[i - 1] ^ (this.state[i - 1] >>> 30)) + i) >>> 0; + } + this.idx = this.N32; + this.period_certification(); + } + + /* Period certification */ + private period_certification(): void { + const PARITY = [this.PARITY1, this.PARITY2, this.PARITY3, this.PARITY4]; + let inner = 0; + + for (let i = 0; i < 4; i++) { + inner ^= this.state[i] & PARITY[i]; + } + + for (let i = 16; i > 0; i >>= 1) { + inner ^= inner >> i; + } + inner &= 1; + + if (inner === 1) return; + + /* Period certification failed, fix it */ + for (let i = 0; i < 4; i++) { + let work = 1; + for (let j = 0; j < 32; j++) { + if ((work & PARITY[i]) !== 0) { + this.state[i] ^= work; + return; + } + work <<= 1; + } } } - /* initialize by an array with array-length */ - /* init_key is the array for initializing keys */ - /* key_length is its length */ - /* slight change for C++, 2004/2/26 */ + /* Initialize by an array with array-length */ init_by_array(init_key: number[], key_length: number): void { - var i: number, j: number, k: number; + let i = 1, j = 0; + let count = this.N32 > key_length ? this.N32 : key_length; + this.init_genrand(19650218); - i = 1; - j = 0; - k = this.N > key_length ? this.N : key_length; - for (; k; k--) { - var s: number = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); - this.mt[i] = - (this.mt[i] ^ - (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + (s & 0x0000ffff) * 1664525)) + - init_key[j] + - j; /* non linear */ - this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */ + + for (; count > 0; count--) { + this.state[i] = ((this.state[i] ^ ((this.state[i - 1] ^ (this.state[i - 1] >>> 30)) * 1664525)) + init_key[j] + j) >>> 0; i++; j++; - if (i >= this.N) { - this.mt[0] = this.mt[this.N - 1]; + if (i >= this.N32) { + this.state[0] = this.state[this.N32 - 1]; i = 1; } if (j >= key_length) j = 0; } - for (k = this.N - 1; k; k--) { - var s: number = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); - this.mt[i] = - (this.mt[i] ^ - (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941)) - - i; /* non linear */ - this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */ + + for (count = this.N32 - 1; count > 0; count--) { + this.state[i] = ((this.state[i] ^ ((this.state[i - 1] ^ (this.state[i - 1] >>> 30)) * 1566083941)) - i) >>> 0; i++; - if (i >= this.N) { - this.mt[0] = this.mt[this.N - 1]; + if (i >= this.N32) { + this.state[0] = this.state[this.N32 - 1]; i = 1; } } + + this.state[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */ + this.idx = this.N32; + this.period_certification(); + } - this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */ + /* SIMD 128-bit recursion */ + private do_recursion(a: Uint32Array, b: Uint32Array, c: Uint32Array, d: Uint32Array): void { + const lshift128 = (a: Uint32Array, out: Uint32Array, shift: number): void => { + const th = shift >>> 5; + const tl = shift & 31; + const tr = 32 - tl; + + if (th >= 4) { + out[0] = out[1] = out[2] = out[3] = 0; + return; + } + + for (let i = 0; i < 4 - th; i++) { + out[i] = (a[i + th] << tl) | (i + th + 1 < 4 ? a[i + th + 1] >>> tr : 0); + } + for (let i = 4 - th; i < 4; i++) { + out[i] = 0; + } + }; + + const rshift128 = (a: Uint32Array, out: Uint32Array, shift: number): void => { + const th = shift >>> 5; + const tl = shift & 31; + const tr = 32 - tl; + + if (th >= 4) { + out[0] = out[1] = out[2] = out[3] = 0; + return; + } + + for (let i = 3; i >= th; i--) { + out[i] = (a[i - th] >>> tl) | (i - th - 1 >= 0 ? a[i - th - 1] << tr : 0); + } + for (let i = th - 1; i >= 0; i--) { + out[i] = 0; + } + }; + + const x = new Uint32Array(4); + const y = new Uint32Array(4); + + lshift128(a, x, this.SL2); + rshift128(c, y, this.SR2); + + a[0] = a[0] ^ x[0] ^ ((b[0] >>> this.SR1) & this.MSK1) ^ y[0] ^ (d[0] << this.SL1); + a[1] = a[1] ^ x[1] ^ ((b[1] >>> this.SR1) & this.MSK2) ^ y[1] ^ (d[1] << this.SL1); + a[2] = a[2] ^ x[2] ^ ((b[2] >>> this.SR1) & this.MSK3) ^ y[2] ^ (d[2] << this.SL1); + a[3] = a[3] ^ x[3] ^ ((b[3] >>> this.SR1) & this.MSK4) ^ y[3] ^ (d[3] << this.SL1); + } + + /* Generate the next N 128-bit blocks */ + private gen_rand_all(): void { + let r1 = this.N - 2; + let r2 = this.N - 1; + + for (let i = 0; i < this.N - this.POS1; i++) { + this.do_recursion( + this.state.subarray(i * 4, (i + 1) * 4), + this.state.subarray((i + this.POS1) * 4, (i + this.POS1 + 1) * 4), + this.state.subarray(r1 * 4, (r1 + 1) * 4), + this.state.subarray(r2 * 4, (r2 + 1) * 4) + ); + r1 = r2; + r2 = i; + } + + for (let i = this.N - this.POS1; i < this.N; i++) { + this.do_recursion( + this.state.subarray(i * 4, (i + 1) * 4), + this.state.subarray((i + this.POS1 - this.N) * 4, (i + this.POS1 - this.N + 1) * 4), + this.state.subarray(r1 * 4, (r1 + 1) * 4), + this.state.subarray(r2 * 4, (r2 + 1) * 4) + ); + r1 = r2; + r2 = i; + } + + this.idx = 0; } /* generates a random number on [0,0xffffffff]-interval */ - genrand_int32() { - var y; - var mag01 = new Array(0x0, this.MATRIX_A); - /* mag01[x] = x * MATRIX_A for x=0,1 */ - - if (this.mti >= this.N) { - /* generate N words at one time */ - var kk; - - if (this.mti == this.N + 1) - /* if init_genrand() has not been called, */ - this.init_genrand(5489); /* a default initial seed is used */ - - for (kk = 0; kk < this.N - this.M; kk++) { - y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK); - this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1]; - } - for (; kk < this.N - 1; kk++) { - y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK); - this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1]; - } - y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK); - this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1]; - - this.mti = 0; + genrand_int32(): number { + if (this.idx >= this.N32) { + this.gen_rand_all(); } - - y = this.mt[this.mti++]; - - /* Tempering */ - y ^= y >>> 11; - y ^= (y << 7) & 0x9d2c5680; - y ^= (y << 15) & 0xefc60000; - y ^= y >>> 18; - - return y >>> 0; + return this.state[this.idx++]; } /* generates a random number on [0,0x7fffffff]-interval */ - genrand_int31() { + genrand_int31(): number { return this.genrand_int32() >>> 1; } /* generates a random number on [0,1]-real-interval */ - genrand_real1() { + genrand_real1(): number { return this.genrand_int32() * (1.0 / 4294967295.0); - /* divided by 2^32-1 */ } /* generates a random number on [0,1)-real-interval */ - random() { + random(): number { return this.genrand_int32() * (1.0 / 4294967296.0); - /* divided by 2^32 */ } /* generates a random number on (0,1)-real-interval */ - genrand_real3() { + genrand_real3(): number { return (this.genrand_int32() + 0.5) * (1.0 / 4294967296.0); - /* divided by 2^32 */ } /* generates a random number on [0,1) with 53-bit resolution*/ - genrand_res53() { - var a = this.genrand_int32() >>> 5, - b = this.genrand_int32() >>> 6; + genrand_res53(): number { + const a = this.genrand_int32() >>> 5; + const b = this.genrand_int32() >>> 6; return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0); } - - /* These real versions are due to Isaku Wada, 2002/01/09 added */ } diff --git a/src/lib/stations/sleep.ts b/src/lib/stations/sleep.ts index 5b33ce6..3f22b8f 100644 --- a/src/lib/stations/sleep.ts +++ b/src/lib/stations/sleep.ts @@ -1,7 +1,7 @@ import type { Song } from "@/types"; export async function getSleepStationSongs(): Promise { - const m = new MersenneTwister(); + const m = new SIMDMersenneTwister(); const res = await fetch('https://lofi-cdn.srizan.dev/sleep/list.json'); const files = await res.json() as string[]; @@ -9,7 +9,7 @@ export async function getSleepStationSongs(): Promise { const mapped = files.map(file => { const [artist, title] = file.replace('.opus', '').split(' - '); return { - id: parseInt((m.random() * 1000000).toFixed(0)), + id: 727, fileId: file.replace('.opus', ''), endpoint: `https://lofi-cdn.srizan.dev/sleep/${file}`, artists: artist, @@ -17,7 +17,7 @@ export async function getSleepStationSongs(): Promise { image: `https://lofi-cdn.srizan.dev/sleep/thumbs/${file.replace('.opus', '')}.webp`, }; }) as Song[]; - + const shuffled = mapped.sort(() => 0.5 - m.random()).slice(0, 5); return shuffled; } From 8ef1e62e76d56c80dfc713b02388e4b90d638a6d Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Thu, 29 May 2025 17:17:11 +0200 Subject: [PATCH 4/8] feat: optimize and use new object --- src/lib/mersenne.ts | 2 +- src/lib/stations/index.ts | 33 +++++++++++------ src/lib/stations/sleep.ts | 1 + src/routes/api/live/[stationId]/+server.ts | 43 ++++++---------------- 4 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/lib/mersenne.ts b/src/lib/mersenne.ts index 5b816a1..2a468e3 100644 --- a/src/lib/mersenne.ts +++ b/src/lib/mersenne.ts @@ -79,7 +79,7 @@ email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) */ -class SIMDMersenneTwister { +export class SIMDMersenneTwister { /* SFMT period parameters for SFMT19937 */ private readonly MEXP = 19937; private readonly N = Math.floor(this.MEXP / 128) + 1; // 156 diff --git a/src/lib/stations/index.ts b/src/lib/stations/index.ts index 8fa9888..1503179 100644 --- a/src/lib/stations/index.ts +++ b/src/lib/stations/index.ts @@ -1,17 +1,28 @@ +import type { Song } from '@/types'; import { getChillhopStation } from './chillhop'; import { getSleepStationSongs } from './sleep'; -const customStations = { +const customStations: Record Promise> = { 50000: getSleepStationSongs, }; -const chillhopStations = Object.fromEntries( - Array.from({ length: 100 }, (_, i) => { - const stationId = 10000 + i; - return [stationId, getChillhopStation.bind(null, stationId)]; - }) -); -export const stations = { - ...customStations, - ...chillhopStations, -}; +export const stations: Record Promise> = new Proxy(customStations, { + get(target, prop) { + const stationId = Number(prop); + + if (target[stationId]) { + return target[stationId]; + } + + if (stationId >= 10000 && stationId < 20000) { + return () => getChillhopStation(stationId); + } + + return undefined; + }, + + has(target, prop) { + const stationId = Number(prop); + return target[stationId] !== undefined || (stationId >= 10000 && stationId < 20000); + } +}); diff --git a/src/lib/stations/sleep.ts b/src/lib/stations/sleep.ts index 3f22b8f..156e254 100644 --- a/src/lib/stations/sleep.ts +++ b/src/lib/stations/sleep.ts @@ -1,4 +1,5 @@ import type { Song } from "@/types"; +import { SIMDMersenneTwister } from "@/mersenne"; export async function getSleepStationSongs(): Promise { const m = new SIMDMersenneTwister(); diff --git a/src/routes/api/live/[stationId]/+server.ts b/src/routes/api/live/[stationId]/+server.ts index c61fcd9..c3d101e 100644 --- a/src/routes/api/live/[stationId]/+server.ts +++ b/src/routes/api/live/[stationId]/+server.ts @@ -1,4 +1,4 @@ -import type { Song } from '@/types.js'; +import { stations } from '@/stations/index.js'; export async function GET(event) { const { stationId } = event.params; @@ -7,36 +7,15 @@ export async function GET(event) { return new Response('Invalid station ID', { status: 400 }); } - if (stationId.startsWith('50')) { - // custom stations - if (stationIdInt === 50000) { - const data = await fetch('https://lofi-cdn.srizan.dev/sleep/list.json').then(async (res) => await res.json() as string[]); - const randomData = data.sort(() => 0.5 - Math.random()).slice(0, 5).map(song => { - const noOpus = song.replace('.opus', ''); - const [artist, title] = noOpus.split(' - '); - return { - id: parseInt((Math.random() * 1000000).toFixed(0)), - fileId: noOpus, - endpoint: `https://lofi-cdn.srizan.dev/sleep/${song}`, - artists: artist, - title: title, - image: `https://lofi-cdn.srizan.dev/sleep/thumbs/${noOpus}.webp`, - } as Song; - }); - return new Response(JSON.stringify(randomData), { - headers: { - 'Content-Type': 'application/json' - } - }); - } - } else { - // chillhop stations - const res = await fetch(`https://stream.chillhop.com/live/${stationId}`); - const data = await res.json(); - return new Response(JSON.stringify(data), { - headers: { - 'Content-Type': 'application/json' - } - }); + const stationFunction = stations[stationIdInt]; + if (!stationFunction) { + return new Response('Station not found', { status: 404 }); } + + const songs = await stationFunction(); + return new Response(JSON.stringify(songs), { + headers: { + 'Content-Type': 'application/json' + } + }); } \ No newline at end of file From 8aeba1e87c7c0716b5756763be39fd10f0d30856 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 31 May 2025 17:06:16 +0200 Subject: [PATCH 5/8] chore: adhoc setup to remove database --- src/app.d.ts | 4 +- src/lib/stations/index.ts | 6 +++ .../api/dev/populateStations/+server.ts | 48 ------------------- src/routes/api/live/[stationId]/+server.ts | 2 +- src/routes/api/stations/+server.ts | 32 ++++++------- 5 files changed, 23 insertions(+), 69 deletions(-) delete mode 100644 src/routes/api/dev/populateStations/+server.ts diff --git a/src/app.d.ts b/src/app.d.ts index b992e0d..dab9943 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -7,8 +7,8 @@ declare global { // interface PageData {} // interface PageState {} interface Platform { - caches: CacheStorage & { default: Cache }; - } + caches: CacheStorage & { default: Cache } + } } } diff --git a/src/lib/stations/index.ts b/src/lib/stations/index.ts index 1503179..3a0c8d5 100644 --- a/src/lib/stations/index.ts +++ b/src/lib/stations/index.ts @@ -1,6 +1,7 @@ import type { Song } from '@/types'; import { getChillhopStation } from './chillhop'; import { getSleepStationSongs } from './sleep'; +import { getChillhopData } from '@/utils'; const customStations: Record Promise> = { 50000: getSleepStationSongs, @@ -26,3 +27,8 @@ export const stations: Record Promise> = new Proxy(customS return target[stationId] !== undefined || (stationId >= 10000 && stationId < 20000); } }); + +export const stationMetadata = Promise.resolve([ + { id: 50000, name: 'Lofi Sleep', description: 'Custom station with relaxing lofi beats' }, + ...(await getChillhopData()).stations +]) \ No newline at end of file diff --git a/src/routes/api/dev/populateStations/+server.ts b/src/routes/api/dev/populateStations/+server.ts deleted file mode 100644 index 1bbca1f..0000000 --- a/src/routes/api/dev/populateStations/+server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { getChillhopData } from '@/utils'; -import type { RequestHandler } from './$types'; -import { BASEROW_ENDPOINT, BASEROW_TOKEN } from '$env/static/private' -import { dev } from '$app/environment'; -import type { BRStation } from '@/types'; - -export const GET: RequestHandler = async () => { - if (!dev) { - return new Response('Not allowed', { - status: 403 - }); - } - const generalData = await getChillhopData(); - const existingRows = await fetch(`${BASEROW_ENDPOINT}/api/database/rows/table/685/?user_field_names=true`, { - headers: { - Authorization: `Token ${BASEROW_TOKEN}` - } - }).then(async (res) => await res.json()).then(d => d.results as BRStation[]); - const chillhopStations = existingRows.filter(r => r.Source === 'chillhop'); - await fetch(`${BASEROW_ENDPOINT}/api/database/rows/table/685/batch-delete/ `, { - method: 'POST', - headers: { - Authorization: `Token ${BASEROW_TOKEN}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - items: chillhopStations.map(r => r.id) - }) - }) - - const stations = generalData.stations.map((station) => ({ - ID: station.id, - Name: station.name, - Source: 'chillhop' - })); - await fetch(`${BASEROW_ENDPOINT}/api/database/rows/table/685/batch/?user_field_names=true`, { - method: 'POST', - headers: { - Authorization: `Token ${BASEROW_TOKEN}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - items: stations - }) - }); - - return new Response('done'); -} diff --git a/src/routes/api/live/[stationId]/+server.ts b/src/routes/api/live/[stationId]/+server.ts index c3d101e..27bb558 100644 --- a/src/routes/api/live/[stationId]/+server.ts +++ b/src/routes/api/live/[stationId]/+server.ts @@ -7,7 +7,7 @@ export async function GET(event) { return new Response('Invalid station ID', { status: 400 }); } - const stationFunction = stations[stationIdInt]; + const stationFunction = stations[stationIdInt]; if (!stationFunction) { return new Response('Station not found', { status: 404 }); } diff --git a/src/routes/api/stations/+server.ts b/src/routes/api/stations/+server.ts index 23f3c7d..a89f26a 100644 --- a/src/routes/api/stations/+server.ts +++ b/src/routes/api/stations/+server.ts @@ -1,27 +1,23 @@ -import { BASEROW_ENDPOINT, BASEROW_TOKEN } from "$env/static/private" -import type { BRStation } from "@/types"; -import { getChillhopData } from "@/utils"; -import type { RequestHandler } from "@sveltejs/kit"; +import { stationMetadata } from '@/stations'; +import { getChillhopData } from '@/utils'; +import type { RequestHandler } from '@sveltejs/kit'; export const GET: RequestHandler = async () => { const responses = await Promise.all([ - fetch(`${BASEROW_ENDPOINT}/api/database/rows/table/685/?user_field_names=true`, { - headers: { - Authorization: `Token ${BASEROW_TOKEN}` - } - }).then(async (res) => await res.json()).then(d => d.results as BRStation[]).then(stations => stations.map(stations => { - return { - id: stations.ID, - name: stations.Name, - } - })), + stationMetadata, getChillhopData(), ]); - return new Response(JSON.stringify({ stations: responses[0], backgrounds: responses[1].backgrounds, atmospheres: responses[1].atmospheres }), { + const responseJson = JSON.stringify({ + stations: responses[0], + backgrounds: responses[1].backgrounds, + atmospheres: responses[1].atmospheres, + }); + + return new Response(responseJson, { status: 200, headers: { - "Content-Type": "application/json" - } + 'Content-Type': 'application/json', + }, }); -}; \ No newline at end of file +}; From 3c9ddd466db7c7a8c696c0274d384624e20bc1d1 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 31 May 2025 17:24:12 +0200 Subject: [PATCH 6/8] feat: api reworks --- src/lib/components/app/daemon.svelte | 2 +- src/lib/stations/chillhop.ts | 17 ++++++++++++++--- src/lib/stations/sleep.ts | 2 +- src/lib/types.ts | 10 ++++++++++ src/lib/utils.ts | 16 ---------------- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/lib/components/app/daemon.svelte b/src/lib/components/app/daemon.svelte index e6e50e1..45e76f3 100644 --- a/src/lib/components/app/daemon.svelte +++ b/src/lib/components/app/daemon.svelte @@ -218,7 +218,7 @@ {#if !appState.isLoading}