More core audio work
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import {computed, effect, Signal, signal, WritableSignal} from '@angular/core';
|
||||
|
||||
export class IndexedArray<T> {
|
||||
protected list: WritableSignal<T[]> = signal([]);
|
||||
protected currentIndex: WritableSignal<number> = signal(0);
|
||||
|
||||
public playlist: Signal<T[]> = this.list.asReadonly();
|
||||
public index: Signal<number> = this.currentIndex.asReadonly();
|
||||
public current: Signal<T|null>;
|
||||
|
||||
constructor() {
|
||||
this.current = computed(() : T | null => {
|
||||
if (this.currentIndex() > -1 && this.list().length === 0) return null;
|
||||
return this.list()[this.currentIndex()];
|
||||
});
|
||||
}
|
||||
|
||||
next(): T | null {
|
||||
const length = this.list().length;
|
||||
if (length === 0) return null;
|
||||
if (length === 1) return this.list()[0];
|
||||
|
||||
if (this.currentIndex() === length - 1) {
|
||||
this.currentIndex.set(0);
|
||||
} else {
|
||||
this.currentIndex.set(this.currentIndex() + 1);
|
||||
}
|
||||
|
||||
return this.current();
|
||||
}
|
||||
|
||||
prev(): T | null {
|
||||
const length = this.list().length;
|
||||
if (length === 0) return null;
|
||||
if (length === 1) return this.list()[0];
|
||||
|
||||
if (this.currentIndex() === 0) {
|
||||
this.currentIndex.set(length - 1);
|
||||
} else {
|
||||
this.currentIndex.set(this.currentIndex() - 1);
|
||||
}
|
||||
|
||||
return this.current();
|
||||
}
|
||||
|
||||
add(item: T) {
|
||||
this.list.update((list: T[]) => {
|
||||
return [...list, item];
|
||||
});
|
||||
}
|
||||
|
||||
remove(item: T) {
|
||||
// Check item is actually in the list, and find its index
|
||||
const index = this.list().indexOf(item);
|
||||
if (index > -1) {
|
||||
// If we deleted an item before the current index pointer, then shift the index back one to maintain current item
|
||||
if (index < this.currentIndex()) {
|
||||
this.currentIndex.update((index) => index - 1);
|
||||
}
|
||||
|
||||
// If we are at the end of the list (and the list won't become empty) shift index back to maintain pointer at end of list
|
||||
if (index === this.list().length - 1 && index > 0) {
|
||||
this.currentIndex.update((index) => index - 1);
|
||||
}
|
||||
|
||||
// Remove the item
|
||||
this.list.update((list: T[]) => {
|
||||
list.splice(index, 1);
|
||||
return [...list];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './IndexedArray';
|
||||
@@ -1,4 +1,4 @@
|
||||
export class File {
|
||||
export class FileSystemFile {
|
||||
constructor(
|
||||
public filename: string,
|
||||
public filepath: string,
|
||||
@@ -0,0 +1,58 @@
|
||||
import {Track} from './Track';
|
||||
|
||||
enum STATE {
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped
|
||||
}
|
||||
|
||||
export class PlaylistItem {
|
||||
protected element: HTMLAudioElement;
|
||||
protected state: STATE = STATE.Stopped;
|
||||
|
||||
constructor(
|
||||
public track: Track,
|
||||
protected context: AudioContext
|
||||
) {
|
||||
this.element = this.createElement(this.track.file.filepath, this.track.getMimeType())
|
||||
context.createMediaElementSource(this.element).connect(context.destination);
|
||||
}
|
||||
|
||||
private createElement(filename: string, type: string) {
|
||||
const element = document.createElement('audio');
|
||||
const source = document.createElement('source');
|
||||
source.src = filename;
|
||||
source.type = type;
|
||||
element.appendChild(source);
|
||||
return element;
|
||||
}
|
||||
|
||||
public async play() {
|
||||
if (this.state === STATE.Stopped || this.state === STATE.Paused) {
|
||||
await this.element.play();
|
||||
this.state = STATE.Playing;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
this.element.currentTime = 0;
|
||||
this.element.pause();
|
||||
this.state = STATE.Stopped;
|
||||
}
|
||||
|
||||
public async pause() {
|
||||
if (this.state === STATE.Paused) {
|
||||
return this.play();
|
||||
}
|
||||
if (this.state === STATE.Playing) {
|
||||
this.element.pause();
|
||||
this.state = STATE.Paused;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public isPlaying() {
|
||||
return this.state === STATE.Playing;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {TrackMeta} from './TrackMeta';
|
||||
import {FileSystemFile} from './FileSystemFile';
|
||||
|
||||
export class Track {
|
||||
constructor(
|
||||
public file: FileSystemFile,
|
||||
public meta: TrackMeta,
|
||||
public length: number,
|
||||
public format: string,
|
||||
public bitrate: number,
|
||||
) {
|
||||
}
|
||||
|
||||
getMimeType() {
|
||||
switch (this.format) {
|
||||
case 'mp3':
|
||||
default:
|
||||
return 'audio/mp3';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export class TrackMeta {
|
||||
|
||||
constructor(
|
||||
public title: string = '',
|
||||
public artist: string = '',
|
||||
public album: string = '',
|
||||
public genre: string = '',
|
||||
public trackNumber: number = 0,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './Track';
|
||||
export * from './TrackMeta';
|
||||
export * from './PlaylistItem';
|
||||
export * from './FileSystemFile';
|
||||
@@ -1,12 +0,0 @@
|
||||
import {TrackMeta} from './trackmeta';
|
||||
|
||||
export class Track {
|
||||
constructor(
|
||||
public file: File,
|
||||
public meta: TrackMeta,
|
||||
public length: number,
|
||||
public format: string,
|
||||
public bitrate: number,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export class TrackMeta {
|
||||
constructor(
|
||||
public title: string,
|
||||
public artist: string,
|
||||
public album: string,
|
||||
public genre: string,
|
||||
public trackNumber: number,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
Home
|
||||
|
||||
<button id="play" (click)="play($event)">Play</button>
|
||||
<button (click)="play($event)">Play</button>
|
||||
<button (click)="stop($event)">Stop</button>
|
||||
<button (click)="next($event)">Next</button>
|
||||
<button (click)="prev($event)">Prev</button>
|
||||
<button (click)="pause($event)">Pause/Resume</button>
|
||||
<button (click)="add($event)">Add</button>
|
||||
<button (click)="remove($event)">Remove</button>
|
||||
|
||||
@for (item of audio.list(); track item) {
|
||||
<div>
|
||||
@if ($index === audio.index()) {
|
||||
*
|
||||
}
|
||||
{{ item.track.file.filename }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Component, OnInit, viewChild} from '@angular/core';
|
||||
import {AudioService} from '../../services/audio.service';
|
||||
import {FileSystemFile, Track, TrackMeta} from '../../models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -15,4 +16,36 @@ export class HomePage{
|
||||
play(event: Event): void {
|
||||
this.audio.play();
|
||||
}
|
||||
|
||||
stop(event: Event): void {
|
||||
this.audio.stop();
|
||||
}
|
||||
|
||||
next(event: Event): void {
|
||||
this.audio.next();
|
||||
}
|
||||
prev(event: Event): void {
|
||||
this.audio.prev();
|
||||
}
|
||||
|
||||
pause(event: Event): void {
|
||||
this.audio.pause();
|
||||
}
|
||||
|
||||
add(event: Event): void {
|
||||
this.audio.addItem(new Track(
|
||||
new FileSystemFile('3.mp3', 'audio/3.mp3', 0),
|
||||
new TrackMeta(),
|
||||
0,
|
||||
'mp3',
|
||||
128
|
||||
));
|
||||
}
|
||||
|
||||
remove(event: Event): void {
|
||||
const item = this.audio.current();
|
||||
if (item) {
|
||||
this.audio.removeItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,88 @@
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {FileSystemFile, Track, TrackMeta, PlaylistItem} from '../models';
|
||||
import {Signal} from '@angular/core';
|
||||
import {IndexedArray} from '../lib';
|
||||
|
||||
export class AudioService {
|
||||
protected context: AudioContext;
|
||||
protected track?: MediaElementAudioSourceNode;
|
||||
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;
|
||||
|
||||
constructor() {
|
||||
this.context = new AudioContext();
|
||||
|
||||
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
|
||||
));
|
||||
}
|
||||
|
||||
createElement(filename: string) {
|
||||
const element = document.createElement('audio');
|
||||
const source = document.createElement('source');
|
||||
source.src = filename;
|
||||
source.type = 'audio/mp3';
|
||||
element.appendChild(source);
|
||||
return element;
|
||||
addItem(track: Track) {
|
||||
this.playlist.add(new PlaylistItem(track, this.context));
|
||||
}
|
||||
|
||||
removeItem(item: PlaylistItem) {
|
||||
if (item.isPlaying() && item === this.current()) {
|
||||
this.current()?.stop();
|
||||
this.playlist.remove(item);
|
||||
this.current()?.play();
|
||||
return;
|
||||
}
|
||||
|
||||
this.playlist.remove(item);
|
||||
}
|
||||
|
||||
async play(): Promise<void> {
|
||||
if (this.context.state === 'suspended') {
|
||||
await this.context.resume();
|
||||
}
|
||||
const element = this.createElement('audio/1.mp3');
|
||||
this.track = this.context.createMediaElementSource(element);
|
||||
this.track.connect(this.context.destination);
|
||||
|
||||
await element.play();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
async pause(): Promise<void> {
|
||||
await this.current()?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user