audio.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import * as youtube from './youtube';
  2. export class AudioManager {
  3. context: AudioContext;
  4. volume: GainNode;
  5. output: AudioNode;
  6. constructor() {
  7. this.context = new AudioContext();
  8. this.volume = this.context.createGain();
  9. this.volume.connect(this.context.destination);
  10. this.output = this.volume;
  11. }
  12. getTime(): number {
  13. return this.context.currentTime;
  14. }
  15. async loadTrack(url: string): Promise<FileTrack> {
  16. const response = await window.fetch(url);
  17. const buffer = await response.arrayBuffer();
  18. const audioBuffer = await this.context.decodeAudioData(buffer);
  19. return new FileTrack(this, audioBuffer);
  20. }
  21. async loadTrackFromFile(file: File): Promise<FileTrack> {
  22. const promise = new Promise<ArrayBuffer>((resolve, _) => {
  23. const reader = new FileReader();
  24. reader.onload = () => resolve(reader.result as ArrayBuffer);
  25. reader.readAsArrayBuffer(file);
  26. });
  27. const buffer = await promise;
  28. const audioBuffer = await this.context.decodeAudioData(buffer);
  29. return new FileTrack(this, audioBuffer);
  30. }
  31. async loadTrackWithProgress(url: string, listener: (percentage: number) => void): Promise<FileTrack> {
  32. const promise = new Promise<ArrayBuffer>((resolve, reject) => {
  33. let xhr = new XMLHttpRequest();
  34. xhr.open('GET', url);
  35. xhr.responseType = 'arraybuffer';
  36. xhr.onprogress = (event) => {
  37. if (event.lengthComputable) {
  38. // only up to 80 to factor in decoding time
  39. let percentage = event.loaded / event.total * 80;
  40. listener(percentage);
  41. }
  42. };
  43. xhr.onload = () => resolve(xhr.response);
  44. xhr.onerror = () => reject();
  45. xhr.send();
  46. });
  47. const buffer = await promise;
  48. const audioBuffer = await this.context.decodeAudioData(buffer);
  49. return new FileTrack(this, audioBuffer);
  50. }
  51. async loadTrackFromYoutube(id: string, element: HTMLElement, listener: (percentage: number) => void): Promise<YoutubeTrack> {
  52. await youtube.loadYoutubeApi();
  53. listener(30);
  54. const player = await youtube.createPlayer(element);
  55. listener(60);
  56. const track = new YoutubeTrack(player, id);
  57. await track.preload();
  58. listener(90);
  59. return track;
  60. }
  61. }
  62. export enum PlayState {
  63. UNSTARTED ='unstarted',
  64. PLAYING = 'playing',
  65. PAUSED = 'paused',
  66. STOPPED = 'stopped'
  67. }
  68. export type TrackListener = (track: Track, state: PlayState) => void;
  69. export abstract class Track {
  70. private listeners: TrackListener[] = [];
  71. addListener(listener: TrackListener) {
  72. this.listeners.push(listener);
  73. }
  74. clearListeners() {
  75. this.listeners = [];
  76. }
  77. emit(state: PlayState) {
  78. this.listeners.forEach(l => l(this, state));
  79. }
  80. abstract play(): void;
  81. abstract start(fromTime?: number, duration?: number): void;
  82. abstract pause(): void;
  83. abstract stop(): void;
  84. abstract exit(): void;
  85. abstract getState(): PlayState;
  86. abstract getTime(): number;
  87. abstract getDuration(): number;
  88. }
  89. export class FileTrack extends Track {
  90. manager: AudioManager;
  91. buffer: AudioBuffer;
  92. source: AudioBufferSourceNode | null;
  93. playStartTime: number;
  94. resumeTime: number;
  95. state: PlayState;
  96. constructor(manager: AudioManager, buffer: AudioBuffer) {
  97. super();
  98. this.manager = manager;
  99. this.buffer = buffer;
  100. this.source = null;
  101. this.playStartTime = 0;
  102. this.resumeTime = 0;
  103. this.state = PlayState.UNSTARTED;
  104. }
  105. play(): void {
  106. this.source = this.manager.context.createBufferSource();
  107. this.source.buffer = this.buffer;
  108. this.source.connect(this.manager.output);
  109. this.playStartTime = this.manager.getTime();
  110. this.setState(PlayState.PLAYING);
  111. this.source.start();
  112. }
  113. start(fromTime?: number, duration?: number): void {
  114. if (fromTime !== undefined) {
  115. this.resumeTime = fromTime;
  116. }
  117. this.source = this.manager.context.createBufferSource();
  118. this.source.buffer = this.buffer;
  119. this.source.connect(this.manager.output);
  120. this.source.onended = (event) => {
  121. if (this.source == event.target) {
  122. this.resumeTime = this.manager.getTime() - this.playStartTime;
  123. if (this.resumeTime > this.getDuration()) {
  124. this.resumeTime = 0;
  125. this.setState(PlayState.STOPPED);
  126. } else {
  127. this.setState(PlayState.PAUSED);
  128. }
  129. }
  130. }
  131. this.playStartTime = this.manager.getTime() - this.resumeTime;
  132. this.setState(PlayState.PLAYING);
  133. this.source.start(0, this.resumeTime, duration);
  134. }
  135. pause(): void {
  136. if (this.state === PlayState.PAUSED || this.state === PlayState.STOPPED) return;
  137. this.resumeTime = this.manager.getTime() - this.playStartTime;
  138. if (this.source) {
  139. this.source.stop();
  140. }
  141. this.setState(PlayState.PAUSED);
  142. }
  143. stop(): void {
  144. this.resumeTime = 0;
  145. if (this.source) {
  146. this.source.stop();
  147. }
  148. this.setState(PlayState.STOPPED);
  149. }
  150. exit(): void {
  151. this.stop();
  152. }
  153. getState(): PlayState {
  154. return this.state;
  155. }
  156. getTime(): number {
  157. if (this.state === PlayState.UNSTARTED) {
  158. return 0;
  159. }
  160. else if (this.state === PlayState.PAUSED || this.state === PlayState.STOPPED) {
  161. if (this.resumeTime > 0) {
  162. return this.resumeTime;
  163. } else {
  164. return this.getDuration();
  165. }
  166. } else {
  167. return this.manager.getTime() - this.playStartTime;
  168. }
  169. }
  170. getDuration(): number {
  171. return this.buffer.duration;
  172. }
  173. private setState(state: PlayState): void {
  174. this.state = state;
  175. this.emit(state);
  176. }
  177. }
  178. export class YoutubeTrack extends Track {
  179. private timeoutHandle?: number;
  180. constructor(readonly player: YT.Player, readonly id: string) {
  181. super();
  182. }
  183. preload(): Promise<void> {
  184. return new Promise((resolve) => {
  185. let loaded = false;
  186. const onStateChange: YT.PlayerStateChangeListener = ({ data }) => {
  187. if (data === YT.PlayerState.PLAYING) {
  188. if (!loaded) {
  189. loaded = true;
  190. this.player.pauseVideo();
  191. this.player.seekTo(0);
  192. this.player.unMute();
  193. resolve();
  194. }
  195. }
  196. this.emit(this.mapState(data));
  197. };
  198. this.player.addEventListener('onStateChange', onStateChange);
  199. this.player.mute();
  200. this.player.loadVideoById(this.id);
  201. });
  202. }
  203. play(): void {
  204. this.clearTimeout();
  205. this.player.playVideo();
  206. }
  207. start(fromTime?: number, duration?: number): void {
  208. this.clearTimeout();
  209. if (duration) {
  210. this.timeoutHandle = setTimeout(() => {
  211. this.player.pauseVideo();
  212. }, duration * 1000);
  213. }
  214. if (fromTime !== undefined) {
  215. this.player.seekTo(fromTime, true);
  216. }
  217. this.player.playVideo();
  218. }
  219. pause(): void {
  220. this.clearTimeout();
  221. this.player.pauseVideo();
  222. }
  223. stop(): void {
  224. this.clearTimeout();
  225. this.player.stopVideo();
  226. }
  227. exit(): void {
  228. // the element gets removed by the background manager and stops that way
  229. }
  230. getState(): PlayState {
  231. return this.mapState(this.player.getPlayerState());
  232. }
  233. getTime(): number {
  234. return this.player.getCurrentTime();
  235. }
  236. getDuration(): number {
  237. return this.player.getDuration();
  238. }
  239. private clearTimeout(): void {
  240. if (this.timeoutHandle) {
  241. clearTimeout(this.timeoutHandle);
  242. }
  243. }
  244. private mapState(ytState: YT.PlayerState): PlayState {
  245. switch (ytState) {
  246. case YT.PlayerState.PLAYING:
  247. return PlayState.PLAYING;
  248. case YT.PlayerState.ENDED:
  249. return PlayState.STOPPED;
  250. case YT.PlayerState.UNSTARTED:
  251. case YT.PlayerState.CUED:
  252. return PlayState.UNSTARTED;
  253. case YT.PlayerState.BUFFERING:
  254. case YT.PlayerState.PAUSED:
  255. return PlayState.PAUSED;
  256. }
  257. }
  258. }