Mercurial > hg > ugly-duckling
changeset 31:f6ea31a3b1a3
Encapsulate audio playing and decoding logic in a ng2 service, provided by the root module.
author | Lucas Thompson <dev@lucas.im> |
---|---|
date | Wed, 30 Nov 2016 10:21:27 +0000 |
parents | 5bdfcf493646 |
children | 6fb6c04878ec |
files | src/app/app.component.html src/app/app.component.ts src/app/app.module.ts src/app/services/audio-player.service.spec.ts src/app/services/audio-player.service.ts src/app/waveform/waveform.component.ts |
diffstat | 6 files changed, 118 insertions(+), 15 deletions(-) [+] |
line wrap: on
line diff
--- a/src/app/app.component.html Wed Nov 30 10:18:03 2016 +0000 +++ b/src/app/app.component.html Wed Nov 30 10:21:27 2016 +0000 @@ -4,7 +4,7 @@ <span class="app-toolbar-filler"></span> - <app-audio-file-open (audioLoaded)="onAudioLoaded($event)"></app-audio-file-open> + <app-audio-file-open (fileOpened)="onFileOpened($event)"></app-audio-file-open> <!-- menu opens when trigger button is clicked --> <button md-icon-button [md-menu-trigger-for]="menu"> <md-icon>more_vert</md-icon>
--- a/src/app/app.component.ts Wed Nov 30 10:18:03 2016 +0000 +++ b/src/app/app.component.ts Wed Nov 30 10:21:27 2016 +0000 @@ -1,4 +1,5 @@ -import {Component, Inject} from '@angular/core'; +import {Component} from '@angular/core'; +import {AudioPlayerService} from "./services/audio-player.service"; @Component({ selector: 'app-root', @@ -6,15 +7,22 @@ styleUrls: ['./app.component.css'] }) export class AppComponent { - title = 'Ugly'; + audioBuffer: AudioBuffer; // TODO consider revising - audioBuffer: AudioBuffer = undefined; + constructor(private audioService: AudioPlayerService) {} - constructor( - @Inject('piper-server-uri') private serverUri - ) {} - - onAudioLoaded(buffer: AudioBuffer) { - this.audioBuffer = buffer; + onFileOpened(file: File) { + const reader: FileReader = new FileReader(); + const mimeType = file.type; + reader.onload = (event: any) => { + this.audioService.loadAudioFromUrl( + URL.createObjectURL(new Blob([event.target.result], {type: mimeType})) + ); + // TODO use a rxjs/Subject instead? + this.audioService.decodeAudioData(event.target.result).then(audioBuffer => { + this.audioBuffer = audioBuffer; + }); + }; + reader.readAsArrayBuffer(file); } }
--- a/src/app/app.module.ts Wed Nov 30 10:18:03 2016 +0000 +++ b/src/app/app.module.ts Wed Nov 30 10:21:27 2016 +0000 @@ -4,10 +4,18 @@ import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; -import {MaterialModule} from "@angular/material"; +import { MaterialModule } from "@angular/material"; import { WaveformComponent } from './waveform/waveform.component'; import { AudioFileOpenComponent } from './audio-file-open/audio-file-open.component'; import { PlaybackControlComponent } from './playback-control/playback-control.component'; +import { AudioPlayerService } from "./services/audio-player.service"; + +function createAudioContext(): AudioContext { + return new ( + (window as any).AudioContext + || (window as any).webkitAudioContext + )(); +} @NgModule({ declarations: [ @@ -23,7 +31,9 @@ MaterialModule.forRoot() ], providers: [ - {provide: 'piper-server-uri', useValue: 'ws://not/a/real/path'} + {provide: HTMLAudioElement, useValue: new Audio()}, // TODO use something more generic than HTMLAudioElement + {provide: 'AudioContext', useValue: createAudioContext()}, // use a string token, Safari doesn't seem to like AudioContext + AudioPlayerService ], bootstrap: [AppComponent] })
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/services/audio-player.service.spec.ts Wed Nov 30 10:21:27 2016 +0000 @@ -0,0 +1,16 @@ +/* tslint:disable:no-unused-variable */ + +import { TestBed, async, inject } from '@angular/core/testing'; +import { AudioPlayerService } from './audio-player.service'; + +describe('AudioPlayerService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AudioPlayerService] + }); + }); + + it('should ...', inject([AudioPlayerService], (service: AudioPlayerService) => { + expect(service).toBeTruthy(); + })); +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/services/audio-player.service.ts Wed Nov 30 10:21:27 2016 +0000 @@ -0,0 +1,51 @@ +import {Injectable, Inject} from '@angular/core'; + +@Injectable() +export class AudioPlayerService { + + constructor(@Inject(HTMLAudioElement) private audioElement: HTMLAudioElement /* TODO probably shouldn't play audio this way */, + @Inject('AudioContext') private audioContext: AudioContext) { + } + + getCurrentTime(): number { + return this.audioElement.currentTime; + } + + isPlaying(): boolean { + return !this.audioElement.paused; + } + + decodeAudioData(buffer: ArrayBuffer): Promise<AudioBuffer> { + return new Promise((res, rej) => this.audioContext.decodeAudioData(buffer, res, rej)); + } + + loadAudioFromUrl(url: string): void { + this.audioElement.pause(); + this.audioElement.src = url; + } + + togglePlaying(): void { + this.isPlaying() ? this.audioElement.pause() : this.audioElement.play(); + } + + setVolume(value: number): void { + this.audioElement.volume = value; // TODO check bounds? + } + + seekBy(seconds: number): void { + // TODO some kind of error handling? + this.audioElement.currentTime += seconds; + } + + seekToStart(): void { + this.audioElement.currentTime = 0; + } + + seekToEnd(): void { + this.audioElement.currentTime = this.getDuration(); + } + + getDuration(): number { + return this.audioElement.duration; + } +}
--- a/src/app/waveform/waveform.component.ts Wed Nov 30 10:18:03 2016 +0000 +++ b/src/app/waveform/waveform.component.ts Wed Nov 30 10:21:27 2016 +0000 @@ -1,6 +1,7 @@ import { - Component, OnInit, ViewChild, ElementRef, Input, AfterViewInit + Component, OnInit, ViewChild, ElementRef, Input, AfterViewInit, NgZone } from '@angular/core'; +import {AudioPlayerService} from "../services/audio-player.service"; declare var wavesUI: any; // TODO non-global app scope import type Timeline = any; // TODO what type actually is it.. start a .d.ts for waves-ui? @@ -27,7 +28,8 @@ return this._audioBuffer; } - constructor() {} + constructor(private audioService: AudioPlayerService, + public ngZone: NgZone) {} ngOnInit() {} ngAfterViewInit(): void { @@ -50,7 +52,6 @@ }); timeline.addLayer(timeAxis, 'main', 'default', true); - timeline.state = new wavesUI.states.CenteredZoomState(timeline); return timeline; } @@ -63,6 +64,23 @@ color: 'darkblue' }); (timeline as any).addLayer(waveformLayer, 'main'); + + const cursorLayer = new wavesUI.helpers.CursorLayer({ + height: height + }); + timeline.addLayer(cursorLayer, 'main'); + timeline.state = new wavesUI.states.CenteredZoomState(timeline); + this.ngZone.runOutsideAngular(() => { + // listen for time passing... + // TODO this gets the fans going on large files... worth fixing? or waiting to write a better component? + // or, can this be updated in a more efficient manner? + const updateSeekingCursor = () => { + cursorLayer.currentPosition = this.audioService.getCurrentTime(); + cursorLayer.update(); + requestAnimationFrame(updateSeekingCursor); + }; + updateSeekingCursor(); + }); } }