More core audio work
This commit is contained in:
+2
-1
@@ -2,7 +2,8 @@
|
|||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"cli": {
|
"cli": {
|
||||||
"packageManager": "npm"
|
"packageManager": "npm",
|
||||||
|
"analytics": false
|
||||||
},
|
},
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
|
|||||||
@@ -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(
|
constructor(
|
||||||
public filename: string,
|
public filename: string,
|
||||||
public filepath: 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
|
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 {Component, OnInit, viewChild} from '@angular/core';
|
||||||
import {AudioService} from '../../services/audio.service';
|
import {AudioService} from '../../services/audio.service';
|
||||||
|
import {FileSystemFile, Track, TrackMeta} from '../../models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@@ -15,4 +16,36 @@ export class HomePage{
|
|||||||
play(event: Event): void {
|
play(event: Event): void {
|
||||||
this.audio.play();
|
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 {
|
export class AudioService {
|
||||||
protected context: AudioContext;
|
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() {
|
constructor() {
|
||||||
this.context = new AudioContext();
|
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) {
|
addItem(track: Track) {
|
||||||
const element = document.createElement('audio');
|
this.playlist.add(new PlaylistItem(track, this.context));
|
||||||
const source = document.createElement('source');
|
}
|
||||||
source.src = filename;
|
|
||||||
source.type = 'audio/mp3';
|
removeItem(item: PlaylistItem) {
|
||||||
element.appendChild(source);
|
if (item.isPlaying() && item === this.current()) {
|
||||||
return element;
|
this.current()?.stop();
|
||||||
|
this.playlist.remove(item);
|
||||||
|
this.current()?.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playlist.remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
async play(): Promise<void> {
|
async play(): Promise<void> {
|
||||||
if (this.context.state === 'suspended') {
|
if (this.context.state === 'suspended') {
|
||||||
await this.context.resume();
|
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