Big progress on core audio/playlist service

This commit is contained in:
2026-03-05 22:40:35 +00:00
parent 1e7e8dfb91
commit c315927cb6
8 changed files with 322 additions and 85 deletions
+104 -40
View File
@@ -1,9 +1,14 @@
import {FileSystemFile, Track, TrackMeta, PlaylistItem} from '../models';
import {computed, Signal} from '@angular/core';
import {computed, Injectable, Signal} from '@angular/core';
import {IndexedArray} from '../lib';
import {AnalyserService} from './analyser.service';
@Injectable({providedIn: 'root'})
export class AudioService {
protected context: AudioContext;
protected context: AudioContext = new AudioContext({
sampleRate: 44100
});
public analyser: AnalyserService = new AnalyserService(this.context);
protected playlist: IndexedArray<PlaylistItem> = new IndexedArray();
public list: Signal<PlaylistItem[]> = this.playlist.playlist;
public current: Signal<PlaylistItem|null> = this.playlist.current;
@@ -16,8 +21,6 @@ export class AudioService {
})
constructor() {
this.context = new AudioContext();
this.addItem(new Track(
new FileSystemFile('1.mp3', 'audio/1.mp3', 0),
new TrackMeta(),
@@ -42,53 +45,114 @@ export class AudioService {
}
addItem(track: Track) {
this.playlist.add(new PlaylistItem(track, this.context));
const item = new PlaylistItem(track, this);
this.analyser.connectAudio(item);
this.playlist.add(item);
}
removeItem(item: PlaylistItem) {
if (item.isPlaying() && item === this.current()) {
this.current()?.stop();
this.playlist.remove(item);
this.current()?.play();
return;
}
/**
* Remove a track from the current playlist.
* If we remove the current track, then, if it was playing, start playing the newly "current" item
* @param item
*/
async removeItem(item: PlaylistItem) {
this.playlist.remove(item);
// If the item was not the currently selected track, then we're done.
if (item !== this.current()) return;
const wasPlaying = item.isPlaying();
await item.stop();
item.removeMedia(); // Remove the media so that if it is still being downloaded, we stop that request and unblock loading any other tracks
// If we were playing the removed item, start playing the next item in the playlist (if there is one)
if (wasPlaying) await this.current()?.play();
}
async play(): Promise<void> {
if (this.context.state === 'suspended') {
await this.context.resume();
}
// Make sure the audio context isn't blocked by the browser (disabled autoplay etc)
if (this.context.state === 'suspended') await this.context.resume();
await this.current()?.play();
}
async stop(): Promise<void> {
await this.current()?.stop();
}
async next(): Promise<void> {
if (this.current()?.isPlaying()) {
await this.current()?.stop();
this.playlist.next();
await this.current()?.play();
} else {
this.playlist.next();
}
}
async prev(): Promise<void> {
if (this.current()?.isPlaying()) {
await this.current()?.stop();
this.playlist.prev();
await this.current()?.play();
} else {
this.playlist.prev();
}
this.analyser.start();
}
async pause(): Promise<void> {
await this.current()?.pause();
}
async stop(): Promise<void> {
this.analyser.stop();
await this.current()?.stop();
}
async next(): Promise<void> {
const current = this.current();
if (!current) return;
const wasPlaying = current.isPlaying();
await current.stop();
const next = this.playlist.next(true);
// If the track we have just moved on from was still doing the initial download, then reset the <audio> element
// for that track (which will abort its request) so that the next track request (<audio> element) is not blocked.
// This can be very important for slower connections (or if transcoding is slow).
if (current.isLoading()) current.removeMedia();
// If we pressed next while playing a track, autoplay the next track
if (wasPlaying) await next?.play();
}
async prev(): Promise<void> {
const current = this.current();
if (!current) return;
const wasPlaying = current.isPlaying();
await current.stop();
const prev = this.playlist.prev();
// If the track we have just moved on from was still doing the initial load, then abort the loading so that the next track request is not blocked.
if (current.isLoading()) current.removeMedia();
// If we pressed prev while playing a track, autoplay the next track
if (wasPlaying) await prev?.play();
}
/**
* Move to the next track and start playing it. This is mostly used for auto-advancing to the next track
* when the current track has ended.
*/
async playNext(): Promise<void> {
const current = this.current();
if (!current) return;
if (current.isPlaying()) await current.stop(); // Track should have ended anyway, but call stop to handle some edge-cases.
const next = this.playlist.next();
if (next) await next.play();
}
/**
* Start playing the track at position "index" in the playlist.
* Deliberately take no action if the track referenced is already the current track.
* @param index
*/
async playIndex(index: number): Promise<void> {
const current = this.current();
const target = this.playlist.getIndex(index);
if (!target || current === target) return;
await current?.stop();
this.playlist.setIndex(index);
await target.play();
}
/**
* Seek to position in the current track
* @param percentage Value between 0 and 100. Values outside this range will be clamped.
*/
async seekPercentage(percentage: number): Promise<void> {
const clampedPercentage = Math.min(Math.max(percentage, 0), 100);
const current = this.current();
if (current) current.seek(current.duration() * (clampedPercentage / 100))
}
}