More core audio work

This commit is contained in:
root
2026-02-26 11:23:10 +00:00
parent c8141a07f8
commit 3116bfb681
13 changed files with 291 additions and 38 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm"
"packageManager": "npm",
"analytics": false
},
"newProjectRoot": "projects",
"projects": {
+73
View File
@@ -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];
});
}
}
}
+1
View File
@@ -0,0 +1 @@
export * from './IndexedArray';
@@ -1,4 +1,4 @@
export class File {
export class FileSystemFile {
constructor(
public filename: string,
public filepath: string,
+58
View File
@@ -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;
}
}
+21
View File
@@ -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';
}
}
}
+11
View File
@@ -0,0 +1,11 @@
export class TrackMeta {
constructor(
public title: string = '',
public artist: string = '',
public album: string = '',
public genre: string = '',
public trackNumber: number = 0,
) {
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from './Track';
export * from './TrackMeta';
export * from './PlaylistItem';
export * from './FileSystemFile';
-12
View File
@@ -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,
) {
}
}
-10
View File
@@ -1,10 +0,0 @@
export class TrackMeta {
constructor(
public title: string,
public artist: string,
public album: string,
public genre: string,
public trackNumber: number,
) {
}
}
+16 -1
View File
@@ -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>
}
+33
View File
@@ -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);
}
}
}
+71 -13
View File
@@ -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();
}
}