diff --git a/src/SoundManager.ts b/src/SoundManager.ts new file mode 100644 index 00000000..1c50cbbc --- /dev/null +++ b/src/SoundManager.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal, ISoundManager } from './Types'; + +// Source: https://freesound.org/people/altemark/sounds/45759/ +// This sound is released under the Creative Commons Attribution 3.0 Unported +// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been +// made, apart from the conversion to base64. +export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; + +export class SoundManager implements ISoundManager { + private _audioContext: AudioContext; + + constructor( + private _terminal: ITerminal + ) { + } + + public playBellSound(): void { + const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; + if (!this._audioContext && audioContextCtor) { + this._audioContext = new audioContextCtor(); + } + + if (this._audioContext) { + const bellAudioSource = this._audioContext.createBufferSource(); + const context = this._audioContext; + this._audioContext.decodeAudioData(this.base64ToArrayBuffer(this.removeMimeType(this._terminal.options.bellSound)), (buffer) => { + bellAudioSource.buffer = buffer; + bellAudioSource.connect(context.destination); + bellAudioSource.start(0); + }); + } else { + console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version'); + } + } + + private base64ToArrayBuffer(base64: string): ArrayBuffer { + const binaryString = window.atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + return bytes.buffer; + } + + private removeMimeType(dataURI: string): string { + // Split the input to get the mime-type and the data itself + const splitUri = dataURI.split(','); + + // Return only the data + return splitUri[1]; + } +} diff --git a/src/Terminal.ts b/src/Terminal.ts index ef2349f2..721996c9 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -42,7 +42,7 @@ import * as Browser from './shared/utils/Browser'; import * as Strings from './Strings'; import { MouseHelper } from './utils/MouseHelper'; import { CHARSETS } from './Charsets'; -import { BELL_SOUND } from './utils/Sounds'; +import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { MouseZoneManager } from './input/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; @@ -100,7 +100,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { termName: 'xterm', cursorBlink: false, cursorStyle: 'block', - bellSound: BELL_SOUND, + bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', enableBold: true, fontFamily: 'courier-new, courier, monospace', @@ -142,7 +142,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private helperContainer: HTMLElement; private compositionView: HTMLElement; private charSizeStyleElement: HTMLStyleElement; - private bellAudioElement: HTMLAudioElement; + private visualBellTimer: number; public browser: IBrowser = Browser; @@ -224,6 +224,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private userScrolling: boolean; private inputHandler: InputHandler; + public soundManager: SoundManager; private parser: Parser; public renderer: IRenderer; public selectionManager: SelectionManager; @@ -328,6 +329,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.selectionManager = this.selectionManager || null; this.linkifier = this.linkifier || new Linkifier(this); this._mouseZoneManager = this._mouseZoneManager || null; + this.soundManager = this.soundManager || new SoundManager(this); // Create the terminal's buffers and set the current buffer this.buffers = new BufferSet(this); @@ -493,8 +495,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; - case 'bellSound': - case 'bellStyle': this.syncBellSound(); break; } // Inform renderer of changes if (this.renderer) { @@ -693,9 +693,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.helperContainer.appendChild(this.charSizeStyleElement); this.charMeasure = new CharMeasure(document, this.helperContainer); - // Preload audio, this relied on helperContainer - this.syncBellSound(); - // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); @@ -1894,7 +1891,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ public bell(): void { this.emit('bell'); - if (this.soundBell()) this.bellAudioElement.play(); + if (this.soundBell()) { + this.soundManager.playBellSound(); + } if (this.visualBell()) { this.element.classList.add('visual-bell-active'); @@ -2215,24 +2214,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // return this.options.bellStyle === 'sound' || // this.options.bellStyle === 'both'; } - - private syncBellSound(): void { - // Don't update anything if the terminal has not been opened yet - if (!this.element) { - return; - } - - if (this.soundBell() && this.bellAudioElement) { - this.bellAudioElement.setAttribute('src', this.options.bellSound); - } else if (this.soundBell()) { - this.bellAudioElement = document.createElement('audio'); - this.bellAudioElement.setAttribute('preload', 'auto'); - this.bellAudioElement.setAttribute('src', this.options.bellSound); - this.helperContainer.appendChild(this.bellAudioElement); - } else if (this.bellAudioElement) { - this.helperContainer.removeChild(this.bellAudioElement); - } - } } /** diff --git a/src/Types.ts b/src/Types.ts index 2906521d..be292dbf 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -349,3 +349,7 @@ export interface IBrowser { isIphone: boolean; isMSWindows: boolean; } + +export interface ISoundManager { + playBellSound(): void; +} diff --git a/src/utils/Sounds.ts b/src/utils/Sounds.ts deleted file mode 100644 index be26d4a5..00000000 --- a/src/utils/Sounds.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -// Source: https://freesound.org/people/altemark/sounds/45759/ -// This sound is released under the Creative Commons Attribution 3.0 Unported -// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been -// made, apart from the conversion to base64. -export const BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';