159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
import {FileSystemFile, Track, TrackMeta, PlaylistItem} from '../models';
|
|
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 = 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;
|
|
public index: Signal<number> = this.playlist.index;
|
|
public progress: Signal<number> = computed(() => {
|
|
const current = this.current();
|
|
if (!current) return 0;
|
|
if (current.duration() === 0) return 0;
|
|
return current.currentTime() / current.duration();
|
|
})
|
|
|
|
constructor() {
|
|
this.addItem(new Track(
|
|
new FileSystemFile('1.mp3', 'audio/1.mp3', 0),
|
|
new TrackMeta(),
|
|
0,
|
|
'mp3',
|
|
128
|
|
));
|
|
this.addItem(new Track(
|
|
new FileSystemFile('2.mp3', 'audio/2.mp3', 0),
|
|
new TrackMeta(),
|
|
0,
|
|
'mp3',
|
|
128
|
|
));
|
|
this.addItem(new Track(
|
|
new FileSystemFile('3.mp3', 'audio/3.mp3', 0),
|
|
new TrackMeta(),
|
|
0,
|
|
'mp3',
|
|
128
|
|
));
|
|
}
|
|
|
|
addItem(track: Track) {
|
|
const item = new PlaylistItem(track, this);
|
|
this.analyser.connectAudio(item);
|
|
this.playlist.add(item);
|
|
}
|
|
|
|
/**
|
|
* 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> {
|
|
// 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();
|
|
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))
|
|
}
|
|
}
|