audio.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. namespace audio {
  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. exit(): void {
  81. this.clearListeners();
  82. }
  83. abstract play(): void;
  84. abstract start(fromTime?: number, duration?: number): void;
  85. abstract pause(): void;
  86. abstract stop(): void;
  87. abstract getState(): PlayState;
  88. abstract getTime(): number;
  89. abstract getDuration(): number;
  90. }
  91. export class FileTrack extends Track {
  92. manager: AudioManager;
  93. buffer: AudioBuffer;
  94. source: AudioBufferSourceNode | null;
  95. playStartTime: number;
  96. resumeTime: number;
  97. state: PlayState;
  98. constructor(manager: AudioManager, buffer: AudioBuffer) {
  99. super();
  100. this.manager = manager;
  101. this.buffer = buffer;
  102. this.source = null;
  103. this.playStartTime = 0;
  104. this.resumeTime = 0;
  105. this.state = PlayState.UNSTARTED;
  106. }
  107. play(): void {
  108. this.source = this.manager.context.createBufferSource();
  109. this.source.buffer = this.buffer;
  110. this.source.connect(this.manager.output);
  111. this.playStartTime = this.manager.getTime();
  112. this.setState(PlayState.PLAYING);
  113. this.source.start();
  114. }
  115. start(fromTime?: number, duration?: number): void {
  116. if (fromTime !== undefined) {
  117. this.resumeTime = fromTime;
  118. }
  119. this.source = this.manager.context.createBufferSource();
  120. this.source.buffer = this.buffer;
  121. this.source.connect(this.manager.output);
  122. this.source.onended = (event) => {
  123. if (this.source == event.target) {
  124. this.resumeTime = this.manager.getTime() - this.playStartTime;
  125. if (this.resumeTime > this.getDuration()) {
  126. this.resumeTime = 0;
  127. this.setState(PlayState.STOPPED);
  128. } else {
  129. this.setState(PlayState.PAUSED);
  130. }
  131. }
  132. }
  133. this.playStartTime = this.manager.getTime() - this.resumeTime;
  134. this.setState(PlayState.PLAYING);
  135. this.source.start(0, this.resumeTime, duration);
  136. }
  137. pause(): void {
  138. if (this.state === PlayState.PAUSED || this.state === PlayState.STOPPED) return;
  139. this.resumeTime = this.manager.getTime() - this.playStartTime;
  140. if (this.source) {
  141. this.source.stop();
  142. }
  143. this.setState(PlayState.PAUSED);
  144. }
  145. stop(): void {
  146. this.resumeTime = 0;
  147. if (this.source) {
  148. this.source.stop();
  149. }
  150. this.setState(PlayState.STOPPED);
  151. }
  152. exit(): void {
  153. super.exit();
  154. this.stop();
  155. }
  156. getState(): PlayState {
  157. return this.state;
  158. }
  159. getTime(): number {
  160. if (this.state === PlayState.UNSTARTED) {
  161. return 0;
  162. }
  163. else if (this.state === PlayState.PAUSED || this.state === PlayState.STOPPED) {
  164. if (this.resumeTime > 0) {
  165. return this.resumeTime;
  166. } else {
  167. return this.getDuration();
  168. }
  169. } else {
  170. return this.manager.getTime() - this.playStartTime;
  171. }
  172. }
  173. getDuration(): number {
  174. return this.buffer.duration;
  175. }
  176. private setState(state: PlayState): void {
  177. this.state = state;
  178. this.emit(state);
  179. }
  180. }
  181. export class YoutubeTrack extends Track {
  182. private timeoutHandle?: number;
  183. constructor(readonly player: YT.Player, readonly id: string) {
  184. super();
  185. }
  186. preload(): Promise<void> {
  187. return new Promise((resolve) => {
  188. let loaded = false;
  189. const onStateChange: YT.PlayerStateChangeListener = ({ data }) => {
  190. if (data === YT.PlayerState.PLAYING) {
  191. if (!loaded) {
  192. loaded = true;
  193. this.player.pauseVideo();
  194. this.player.seekTo(0);
  195. this.player.unMute();
  196. resolve();
  197. }
  198. }
  199. this.emit(this.mapState(data));
  200. };
  201. this.player.addEventListener('onStateChange', onStateChange);
  202. this.player.mute();
  203. this.player.loadVideoById(this.id);
  204. });
  205. }
  206. play(): void {
  207. this.clearTimeout();
  208. this.player.playVideo();
  209. }
  210. start(fromTime?: number, duration?: number): void {
  211. this.clearTimeout();
  212. if (duration) {
  213. this.timeoutHandle = setTimeout(() => {
  214. this.player.pauseVideo();
  215. }, duration * 1000);
  216. }
  217. if (fromTime !== undefined) {
  218. this.player.seekTo(fromTime, true);
  219. }
  220. this.player.playVideo();
  221. }
  222. pause(): void {
  223. this.clearTimeout();
  224. this.player.pauseVideo();
  225. }
  226. stop(): void {
  227. this.clearTimeout();
  228. this.player.stopVideo();
  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. }
  259. }