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();
+    });
   }
 
 }