Skip to content

Commit a5e4d58

Browse files
author
NellowTCS
committed
So much moreeeeeee
1 parent 9034992 commit a5e4d58

49 files changed

Lines changed: 12580 additions & 4429 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Build/package-lock.json

Lines changed: 9415 additions & 4270 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Build/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@
7171
"zustand": "^5.0.12"
7272
},
7373
"devDependencies": {
74-
"jest": "^29.7.0",
75-
"ts-jest": "^29.1.2",
7674
"@tauri-apps/cli": "^2.10.1",
7775
"@types/jasmine": "^6.0.0",
7876
"@types/jest": "^30.0.0",
@@ -88,8 +86,10 @@
8886
"cross-env": "^10.1.0",
8987
"eslint": "^9.39.4",
9088
"eslint-plugin-react": "^7.37.5",
89+
"jest": "^29.7.0",
9190
"npm-check-updates": "^19.6.3",
9291
"prettier": "^3.8.1",
92+
"ts-jest": "^29.1.2",
9393
"vite": "^7.3.1",
9494
"vite-plugin-pwa": "^1.2.0",
9595
"vite-plugin-top-level-await": "^1.6.0",

Build/src/core/engine/engine.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,9 @@ import type {
99
} from "./types";
1010
import { KomorebiEvents } from "./events";
1111
import { StateMachine } from "./state";
12-
import { QueueManager } from "./queue";
13-
import { Scheduler, CrossfadeScheduler, GaplessScheduler } from "./scheduler";
14-
15-
export interface IAudioBackend {
16-
load(url: string): Promise<void>;
17-
play(): Promise<void>;
18-
pause(): void;
19-
stop(): void;
20-
seek(time: number): void;
21-
setVolume(volume: number): void;
22-
setPlaybackRate(rate: number): void;
23-
getCurrentTime(): number;
24-
getDuration(): number;
25-
onTimeUpdate(callback: (time: number) => void): void;
26-
onEnded(callback: () => void): void;
27-
onError(callback: (error: Error) => void): void;
28-
dispose(): void;
29-
}
12+
import { QueueManager, type WeightedRandomizer, type ShuffleMode } from "./queue";
13+
import { Scheduler } from "./scheduler";
14+
import type { IAudioBackend } from "../../platform/audio";
3015

3116
export interface IAudioEngineConfig {
3217
crossfade: {
@@ -113,6 +98,22 @@ export class KomorebiEngine {
11398
});
11499
}
115100

101+
setWeightedRandomizer(randomizer: WeightedRandomizer | null): void {
102+
this.queue.setWeightedRandomizer(randomizer);
103+
if (this.settings.smartShuffle) {
104+
this.queue.setShuffleMode("smart");
105+
}
106+
}
107+
108+
setShuffleMode(mode: ShuffleMode): void {
109+
this.queue.setShuffleMode(mode);
110+
this.settings.smartShuffle = mode === "smart";
111+
}
112+
113+
getShuffleMode(): ShuffleMode {
114+
return this.queue.getShuffleMode();
115+
}
116+
116117
load(track: Track, playlist?: Playlist): void {
117118
if (!this.backend) {
118119
this.emitError("NO_BACKEND", "No audio backend configured");
@@ -390,14 +391,14 @@ export class KomorebiEngine {
390391

391392
private async loadAndPlay(track: Track): Promise<void> {
392393
return new Promise((resolve, reject) => {
393-
const onLoad = () => {
394-
this.backend?.off("ended", onLoad);
394+
const onEndedHandler = () => {
395+
this.backend?.offEnded(onEndedHandler);
395396
this.play()
396397
.then(resolve)
397398
.catch(reject);
398399
};
399400

400-
this.backend?.on("ended", onLoad);
401+
this.backend?.onEnded(onEndedHandler);
401402
this.load(track);
402403
});
403404
}
@@ -452,7 +453,7 @@ export class KomorebiEngine {
452453
}
453454
}
454455

455-
private scheduleTransition(track: Track): void {
456+
private scheduleTransition(_track: Track): void {
456457
if (this.scheduledTransitionId !== null) return;
457458

458459
this.stateMachine.transition("transitioning");

Build/src/core/engine/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export * from "./engine";
44
export { StateMachine } from "./state";
55
export { QueueManager } from "./queue";
66
export { Scheduler, CrossfadeScheduler, GaplessScheduler } from "./scheduler";
7-
export type { IAudioBackend } from "./engine";
7+
export type { IAudioBackend } from "../../platform/audio";
88
export type {
99
ScheduledTransition,
1010
TransitionCurve,

Build/src/core/engine/queue/index.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { Track, Playlist, QueueState, PlayHistory } from "../types";
22

3+
export type ShuffleMode = "random" | "smart";
4+
5+
export interface WeightedRandomizer {
6+
getWeightedRandomTrack(trackIds: string[]): string | null;
7+
}
8+
39
export class QueueManager {
410
private state: QueueState = {
511
tracks: [],
@@ -10,6 +16,20 @@ export class QueueManager {
1016

1117
private history: Map<string, PlayHistory> = new Map();
1218
private maxHistorySize = 1000;
19+
private weightedRandomizer: WeightedRandomizer | null = null;
20+
private shuffleMode: ShuffleMode = "random";
21+
22+
setWeightedRandomizer(randomizer: WeightedRandomizer | null): void {
23+
this.weightedRandomizer = randomizer;
24+
}
25+
26+
getShuffleMode(): ShuffleMode {
27+
return this.shuffleMode;
28+
}
29+
30+
setShuffleMode(mode: ShuffleMode): void {
31+
this.shuffleMode = mode;
32+
}
1333

1434
setPlaylist(playlist: Playlist | null, preserveCurrent = true): void {
1535
if (!playlist) {
@@ -59,7 +79,7 @@ export class QueueManager {
5979
return [...this.state.shuffleOrder];
6080
}
6181

62-
getNextIndex(smartShuffle: boolean): number {
82+
getNextIndex(_smartShuffle: boolean): number {
6383
const order = this.getPlayOrder();
6484
const currentPos = order.indexOf(this.state.currentIndex);
6585

@@ -75,7 +95,7 @@ export class QueueManager {
7595
return order[nextPos];
7696
}
7797

78-
getPreviousIndex(smartShuffle: boolean): number {
98+
getPreviousIndex(_smartShuffle: boolean): number {
7999
const order = this.getPlayOrder();
80100
const currentPos = order.indexOf(this.state.currentIndex);
81101

@@ -109,10 +129,53 @@ export class QueueManager {
109129

110130
shuffle(preserveCurrent = true): void {
111131
this.state.shuffled = true;
112-
this.state.shuffleOrder = this.generateShuffleOrder(
113-
this.state.tracks.length,
114-
preserveCurrent ? this.state.currentIndex : -1
115-
);
132+
133+
if (this.shuffleMode === "smart" && this.weightedRandomizer) {
134+
this.state.shuffleOrder = this.generateSmartShuffleOrder(
135+
this.state.tracks.length,
136+
preserveCurrent ? this.state.currentIndex : -1
137+
);
138+
} else {
139+
this.state.shuffleOrder = this.generateShuffleOrder(
140+
this.state.tracks.length,
141+
preserveCurrent ? this.state.currentIndex : -1
142+
);
143+
}
144+
}
145+
146+
private generateSmartShuffleOrder(length: number, preserveIndex: number): number[] {
147+
if (length === 0) return [];
148+
149+
const trackIds = this.state.tracks.map((t) => t.id);
150+
const selectedIds: string[] = [];
151+
const availableIndices = Array.from({ length }, (_, i) => i);
152+
153+
while (availableIndices.length > 0) {
154+
const availableTrackIds = availableIndices.map((i) => trackIds[i]);
155+
const selectedId = this.weightedRandomizer!.getWeightedRandomTrack(availableTrackIds);
156+
157+
if (!selectedId) break;
158+
159+
const selectedIndex = trackIds.indexOf(selectedId);
160+
if (selectedIndex === -1) break;
161+
162+
const actualIndex = availableIndices.indexOf(selectedIndex);
163+
if (actualIndex === -1) break;
164+
165+
selectedIds.push(selectedId);
166+
availableIndices.splice(actualIndex, 1);
167+
}
168+
169+
if (preserveIndex >= 0 && preserveIndex < length) {
170+
const preserveId = trackIds[preserveIndex];
171+
const idxInSelected = selectedIds.indexOf(preserveId);
172+
if (idxInSelected > 0) {
173+
selectedIds.splice(idxInSelected, 1);
174+
selectedIds.unshift(preserveId);
175+
}
176+
}
177+
178+
return selectedIds.map((id) => trackIds.indexOf(id)).filter((i) => i >= 0);
116179
}
117180

118181
unshuffle(): void {

Build/src/core/engine/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface Track {
1212
embeddedLyrics?: EmbeddedLyrics[];
1313
encoding?: EncodingDetails;
1414
gapless?: GaplessInfo;
15+
replayGain?: ReplayGainInfo;
1516
}
1617

1718
export interface EmbeddedLyrics {
@@ -43,6 +44,14 @@ export interface GaplessInfo {
4344
encoderPadding?: number;
4445
}
4546

47+
export interface ReplayGainInfo {
48+
trackGain?: number;
49+
trackPeak?: number;
50+
albumGain?: number;
51+
albumPeak?: number;
52+
referenceLoudness?: number;
53+
}
54+
4655
export interface Playlist {
4756
id: string;
4857
name: string;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { IAudioBackend } from "../../audio/index";
2+
3+
export abstract class BaseAudioBackend implements IAudioBackend {
4+
protected timeUpdateCallbacks: Set<(time: number) => void> = new Set();
5+
protected endedCallbacks: Set<() => void> = new Set();
6+
protected errorCallbacks: Set<(error: Error) => void> = new Set();
7+
8+
abstract load(url: string): Promise<void>;
9+
abstract play(): Promise<void>;
10+
abstract pause(): void;
11+
abstract stop(): void;
12+
abstract seek(time: number): void;
13+
abstract setVolume(volume: number): void;
14+
abstract setPlaybackRate(rate: number): void;
15+
abstract getCurrentTime(): number;
16+
abstract getDuration(): number;
17+
18+
dispose(): void {
19+
this.timeUpdateCallbacks.clear();
20+
this.endedCallbacks.clear();
21+
this.errorCallbacks.clear();
22+
}
23+
24+
onTimeUpdate(callback: (time: number) => void): void {
25+
this.timeUpdateCallbacks.add(callback);
26+
}
27+
28+
offTimeUpdate(callback: (time: number) => void): void {
29+
this.timeUpdateCallbacks.delete(callback);
30+
}
31+
32+
onEnded(callback: () => void): void {
33+
this.endedCallbacks.add(callback);
34+
}
35+
36+
offEnded(callback: () => void): void {
37+
this.endedCallbacks.delete(callback);
38+
}
39+
40+
onError(callback: (error: Error) => void): void {
41+
this.errorCallbacks.add(callback);
42+
}
43+
44+
offError(callback: (error: Error) => void): void {
45+
this.errorCallbacks.delete(callback);
46+
}
47+
48+
protected emitTimeUpdate(time: number): void {
49+
this.timeUpdateCallbacks.forEach((cb) => cb(time));
50+
}
51+
52+
protected emitEnded(): void {
53+
this.endedCallbacks.forEach((cb) => cb());
54+
}
55+
56+
protected emitError(error: Error): void {
57+
this.errorCallbacks.forEach((cb) => cb(error));
58+
}
59+
}

0 commit comments

Comments
 (0)