webaudio.js 3.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. JSMpeg.AudioOutput.WebAudio = (function() { "use strict";
  2. var WebAudioOut = function(options) {
  3. this.context = WebAudioOut.CachedContext =
  4. WebAudioOut.CachedContext ||
  5. new (window.AudioContext || window.webkitAudioContext)();
  6. this.gain = this.context.createGain();
  7. this.destination = this.gain;
  8. // Keep track of the number of connections to this AudioContext, so we
  9. // can safely close() it when we're the only one connected to it.
  10. this.gain.connect(this.context.destination);
  11. this.context._connections = (this.context._connections || 0) + 1;
  12. this.startTime = 0;
  13. this.buffer = null;
  14. this.wallclockStartTime = 0;
  15. this.volume = 1;
  16. this.enabled = true;
  17. this.unlocked = !WebAudioOut.NeedsUnlocking();
  18. Object.defineProperty(this, 'enqueuedTime', {get: this.getEnqueuedTime});
  19. };
  20. WebAudioOut.prototype.destroy = function() {
  21. this.gain.disconnect();
  22. this.context._connections--;
  23. if (this.context._connections === 0) {
  24. this.context.close();
  25. WebAudioOut.CachedContext = null;
  26. }
  27. };
  28. WebAudioOut.prototype.play = function(sampleRate, left, right) {
  29. if (!this.enabled) {
  30. return;
  31. }
  32. // If the context is not unlocked yet, we simply advance the start time
  33. // to "fake" actually playing audio. This will keep the video in sync.
  34. if (!this.unlocked) {
  35. var ts = JSMpeg.Now()
  36. if (this.wallclockStartTime < ts) {
  37. this.wallclockStartTime = ts;
  38. }
  39. this.wallclockStartTime += left.length / sampleRate;
  40. return;
  41. }
  42. this.gain.gain.value = this.volume;
  43. var buffer = this.context.createBuffer(2, left.length, sampleRate);
  44. buffer.getChannelData(0).set(left);
  45. buffer.getChannelData(1).set(right);
  46. var source = this.context.createBufferSource();
  47. source.buffer = buffer;
  48. source.connect(this.destination);
  49. var now = this.context.currentTime;
  50. var duration = buffer.duration;
  51. if (this.startTime < now) {
  52. this.startTime = now;
  53. this.wallclockStartTime = JSMpeg.Now();
  54. }
  55. source.start(this.startTime);
  56. this.startTime += duration;
  57. this.wallclockStartTime += duration;
  58. };
  59. WebAudioOut.prototype.stop = function() {
  60. // Meh; there seems to be no simple way to get a list of currently
  61. // active source nodes from the Audio Context, and maintaining this
  62. // list ourselfs would be a pain, so we just set the gain to 0
  63. // to cut off all enqueued audio instantly.
  64. this.gain.gain.value = 0;
  65. };
  66. WebAudioOut.prototype.getEnqueuedTime = function() {
  67. // The AudioContext.currentTime is only updated every so often, so if we
  68. // want to get exact timing, we need to rely on the system time.
  69. return Math.max(this.wallclockStartTime - JSMpeg.Now(), 0)
  70. };
  71. WebAudioOut.prototype.resetEnqueuedTime = function() {
  72. this.startTime = this.context.currentTime;
  73. this.wallclockStartTime = JSMpeg.Now();
  74. };
  75. WebAudioOut.prototype.unlock = function(callback) {
  76. if (this.unlocked) {
  77. if (callback) {
  78. callback();
  79. }
  80. return;
  81. }
  82. this.unlockCallback = callback;
  83. // Create empty buffer and play it
  84. var buffer = this.context.createBuffer(1, 1, 22050);
  85. var source = this.context.createBufferSource();
  86. source.buffer = buffer;
  87. source.connect(this.destination);
  88. source.start(0);
  89. setTimeout(this.checkIfUnlocked.bind(this, source, 0), 0);
  90. };
  91. WebAudioOut.prototype.checkIfUnlocked = function(source, attempt) {
  92. if (
  93. source.playbackState === source.PLAYING_STATE ||
  94. source.playbackState === source.FINISHED_STATE
  95. ) {
  96. this.unlocked = true;
  97. if (this.unlockCallback) {
  98. this.unlockCallback();
  99. this.unlockCallback = null;
  100. }
  101. }
  102. else if (attempt < 10) {
  103. // Jeez, what a shit show. Thanks iOS!
  104. setTimeout(this.checkIfUnlocked.bind(this, source, attempt+1), 100);
  105. }
  106. };
  107. WebAudioOut.NeedsUnlocking = function() {
  108. return /iPhone|iPad|iPod/i.test(navigator.userAgent);
  109. };
  110. WebAudioOut.IsSupported = function() {
  111. return (window.AudioContext || window.webkitAudioContext);
  112. };
  113. WebAudioOut.CachedContext = null;
  114. return WebAudioOut;
  115. })();