352 строки
12 KiB
TypeScript
352 строки
12 KiB
TypeScript
///<reference path='refs.ts'/>
|
|
|
|
module TDev.RT {
|
|
//? A sound effect
|
|
//@ stem("snd") icon("fa-headphones") ctx(general,gckey,walltap)
|
|
export class Sound
|
|
extends RTValue
|
|
{
|
|
private _pan : number = 0;
|
|
private _pitch : number = 0;
|
|
private _volume : number = 1;
|
|
private _url: string = undefined;
|
|
private _urlToken: SoundUrlTokenDomain = SoundUrlTokenDomain.None;
|
|
private _originalUrl: string = undefined;
|
|
private _buffer : AudioBuffer = undefined; // buffer of data used with browsers supporting web audio api
|
|
private _audio: HTMLAudioElement = undefined; // default HTML5 impl
|
|
|
|
constructor() {
|
|
super()
|
|
}
|
|
//public jsonFields() { return ["_pan", "_pitch", "_volume", "_url", "_urlToken", "_originalUrl"]; }
|
|
|
|
static mk(
|
|
url: string,
|
|
urlToken : SoundUrlTokenDomain = SoundUrlTokenDomain.None,
|
|
originalUrl : string = null) : Sound
|
|
{
|
|
var s = new Sound();
|
|
s._url = url;
|
|
s._urlToken = urlToken;
|
|
s._originalUrl = originalUrl;
|
|
return s;
|
|
}
|
|
|
|
static fromDataUrl(dataUrl: string, originalUrl : string) : Promise
|
|
{
|
|
if (!dataUrl) return Promise.as(null);
|
|
|
|
var m = dataUrl.match(/^data:audio\/(wav|mp4|mp3);base64,/i);
|
|
if (!m) return Promise.as(null);
|
|
|
|
var p = Sound.mk(dataUrl, SoundUrlTokenDomain.None, originalUrl);
|
|
return p.initAsync()
|
|
}
|
|
|
|
static dataUriMimeType(url: string) : string {
|
|
var m = /^data:(audio\/(wav|mp4|mp3));base64,/.exec(url);
|
|
return m ? m[1] : undefined;
|
|
}
|
|
|
|
// specialized in various platforms
|
|
static patchLocalArtUrl(url : string) : string {
|
|
return url;
|
|
}
|
|
|
|
// no caching
|
|
static fromUrl(url : string) : Promise
|
|
{
|
|
// do not test for CORS with data urls
|
|
if (Sound.dataUriMimeType(url))
|
|
return Sound.fromDataUrl(url, null);
|
|
|
|
var s = Sound.mk(url, SoundUrlTokenDomain.None, url);
|
|
return s.initAsync();
|
|
}
|
|
|
|
static fromArtId(id:string) : Promise
|
|
{
|
|
return Sound.fromArtUrl('https://az31353.vo.msecnd.net/pub/' + id);
|
|
}
|
|
|
|
static fromArtUrl(url:string) : Promise
|
|
{
|
|
// do not test for CORS with data urls
|
|
if (Sound.dataUriMimeType(url))
|
|
return Sound.fromDataUrl(url, null);
|
|
|
|
if (/^\.\/art\//.test(url)) {
|
|
url = Sound.patchLocalArtUrl(url);
|
|
}
|
|
if (!Browser.audioWav && ArtCache.isArtResource(url)) {
|
|
url = HTML.patchWavToMp4Url(url);
|
|
Util.log('fixed art sound: ' + url);
|
|
}
|
|
|
|
url = HTML.proxyResource(url);
|
|
|
|
function streamed() : Promise {
|
|
var s = Sound.mk(url, SoundUrlTokenDomain.None, url);
|
|
return s.initAsync();
|
|
}
|
|
|
|
if (Browser.audioDataUrls || AudioContextManager.isSupported()) {
|
|
return ArtCache.getArtAsync(url, "audio/*")
|
|
.then(dataUrl => {
|
|
// art caching might fail
|
|
if (dataUrl) return Sound.fromDataUrl(dataUrl, url);
|
|
else return streamed();
|
|
});
|
|
}
|
|
|
|
return streamed();
|
|
}
|
|
|
|
public toWabRequestAsync(): Promise {
|
|
return this.createUrlAsync()
|
|
.then(url => {
|
|
return {
|
|
uri : url,
|
|
pan : this._pan,
|
|
pitch : this._pitch,
|
|
volume : this._volume
|
|
};
|
|
});
|
|
}
|
|
|
|
public initAsync() : Promise
|
|
{
|
|
if (this._buffer || this._audio) return Promise.as(this);
|
|
|
|
// if Web Audio supported, simply load the sound data
|
|
if (AudioContextManager.isSupported() &&
|
|
Sound.dataUriMimeType(this._url)) {
|
|
var array = Util.decodeDataURL(this._url);
|
|
if (array)
|
|
return AudioContextManager
|
|
.loadAsync(array.buffer)
|
|
.then(b => {
|
|
this._buffer = b;
|
|
return this;
|
|
});
|
|
}
|
|
|
|
// HTML5 way, using an audio tag
|
|
return this.createAudioAsync()
|
|
.then(audio => HTML.audioLoadAsync(audio))
|
|
.then(audio => {
|
|
this._audio = audio;
|
|
return this;
|
|
});
|
|
}
|
|
|
|
public getDataUri() : string {
|
|
if(this._url && Sound.dataUriMimeType(this._url))
|
|
return this._url;
|
|
return undefined;
|
|
}
|
|
|
|
public createUrlAsync(): Promise // string
|
|
{
|
|
var url = this._url;
|
|
switch (this._urlToken) {
|
|
case SoundUrlTokenDomain.TouchDevelop:
|
|
url = Cloud.getPrivateApiUrl(url);
|
|
break;
|
|
case SoundUrlTokenDomain.MicrosoftTranslator:
|
|
return AzureMarketplace.requestAccessTokenAsync(ApiManager.microsoftTranslatorClientId,
|
|
ApiManager.microsoftTranslatorClientSecret, "http://api.microsofttranslator.com", "client_credentials")
|
|
.then(accessToken => {
|
|
return url + "&appId=" + encodeURIComponent("BEARER " + accessToken);
|
|
});
|
|
break;
|
|
}
|
|
// wav extension? let's download the sound
|
|
if (/^https?:\/\/.*\.wav$/i.test(url))
|
|
{
|
|
Util.log('sound createurl: loading online wav file');
|
|
var wr = WebRequest.mk(url, null);
|
|
wr.set_accept('audio/wav');
|
|
return wr.sendAsync()
|
|
.then((response: WebResponse) => {
|
|
var bytes = response.contentAsArraybuffer();
|
|
if (bytes) {
|
|
var dataUri = 'data:audio/wav;base64,' + Util.base64EncodeBytes(<number[]><any>bytes);
|
|
Util.log('sound createurl: loaded online wav file');
|
|
this._url = dataUri;
|
|
} else {
|
|
Util.log('sound createurl: failed loading online wav file');
|
|
}
|
|
return dataUri;
|
|
});
|
|
}
|
|
return Promise.as(url);
|
|
}
|
|
|
|
private createAudioAsync(): Promise // HTMLAudioElement
|
|
{
|
|
return this.createUrlAsync()
|
|
.then(url => {
|
|
var audio = HTML.mkAudio(url);
|
|
return audio;
|
|
});
|
|
}
|
|
|
|
private syncAudioProperties(audio : HTMLAudioElement)
|
|
{
|
|
try {
|
|
audio.volume = this._volume;
|
|
audio.playbackRate = 1 + this._pitch / 2;
|
|
} catch (e) { }
|
|
}
|
|
|
|
//? Gets the panning, ranging from -1.0 (full left) to 1.0 (full right).
|
|
//@ readsMutable
|
|
public pan() : number { return this._pan; }
|
|
|
|
//? Sets the panning, ranging from -1.0 (full left) to 1.0 (full right).
|
|
//@ writesMutable
|
|
public set_pan(pan:number) : void { this._pan = pan; }
|
|
|
|
//? Gets the pitch adjustment, ranging from -1 (down one octave) to 1 (up one octave).
|
|
//@ readsMutable
|
|
public pitch() : number
|
|
{
|
|
return this._pitch;
|
|
}
|
|
|
|
//? Sets the pitch adjustment, ranging from -1 (down one octave) to 1 (up one octave).
|
|
//@ writesMutable
|
|
public set_pitch(pitch:number) : void {
|
|
this._pitch = pitch;
|
|
}
|
|
|
|
//? Gets the volume from 0 (silent) to 1 (full volume)
|
|
//@ readsMutable
|
|
public volume() : number { return this._volume; }
|
|
|
|
//? Sets the volume from 0 (silent) to 1 (full volume).
|
|
//@ writesMutable
|
|
public set_volume(v:number) : void {
|
|
this._volume = Math_.normalize(v);
|
|
if (this._audio)
|
|
this.syncAudioProperties(this._audio);
|
|
}
|
|
|
|
//? Gets the duration in seconds.
|
|
//@ returns(number) cachedAsync
|
|
//@ readsMutable
|
|
public duration(r : ResumeCtx) // : number
|
|
{
|
|
this.initAsync()
|
|
.then(() => {
|
|
var d = this._audio ? this._audio.duration : 0;
|
|
r.resumeVal(isNaN(d) ? 0 : d);
|
|
})
|
|
.done();
|
|
}
|
|
|
|
//? Not supported anymore
|
|
//@ obsolete
|
|
//@ writesMutable
|
|
public pause() : void {}
|
|
|
|
// resets the sound position
|
|
public resetAsync() : Promise {
|
|
try {
|
|
if (this._audio) {
|
|
this.syncAudioProperties(this._audio);
|
|
if (this._audio.currentTime != 0) {
|
|
this._audio.currentTime = 0;
|
|
// some streams don't support reseting the position, we have no choice but to reload
|
|
if (this._audio.currentTime != 0) {
|
|
this._audio = null;
|
|
return this.initAsync();
|
|
}
|
|
}
|
|
}
|
|
} catch(e) {
|
|
Time.log('failed to reset sound position - ' + e);
|
|
}
|
|
return Promise.as();
|
|
}
|
|
|
|
public playAsync(): Promise {
|
|
return this.playCoreAsync();
|
|
}
|
|
|
|
public playCoreAsync(): Promise {
|
|
if (!RuntimeSettings.sounds()) {
|
|
return Promise.as(undefined);
|
|
}
|
|
|
|
return this.initAsync()
|
|
.then(() => this.resetAsync())
|
|
.then(() => {
|
|
try {
|
|
if (this._buffer && AudioContextManager.isSupported())
|
|
AudioContextManager.play(this._buffer, this._volume);
|
|
else if (this._audio)
|
|
this._audio.play();
|
|
}
|
|
catch (e) {
|
|
Time.log('failed to play sound - ' + e);
|
|
}
|
|
});
|
|
}
|
|
|
|
//? Plays the sound effect
|
|
//@ cap(musicandsounds) quickAsync
|
|
//@ readsMutable
|
|
//@ import("cordova", "org.apache.cordova.media")
|
|
public play(r : ResumeCtx) : void
|
|
{
|
|
this.playAsync()
|
|
.done(() => r.resumeVal(undefined));
|
|
}
|
|
|
|
//? Plays the song with different volume (0 to 1), pitch (-1 to 1) and pan (-1 to 1).
|
|
//@ cap(musicandsounds) quickAsync
|
|
//@ [volume].defl(1)
|
|
//@ import("cordova", "org.apache.cordova.media")
|
|
public play_special(volume:number, pitch:number, pan:number, r : ResumeCtx) : void
|
|
{
|
|
this.set_volume(volume);
|
|
this.set_pitch(pitch);
|
|
this.set_pan(pan);
|
|
this.play(r);
|
|
}
|
|
|
|
//? Displays a player on the wall
|
|
//@ readsMutable
|
|
public post_to_wall(s:IStackFrame) : void
|
|
{
|
|
s.rt.postBoxedHtml(HTML.mkButton('play', () => {
|
|
this.playAsync().done();
|
|
}), s.pc);
|
|
}
|
|
|
|
//? Not supported anymore
|
|
//@ obsolete
|
|
//@ writesMutable
|
|
public resume() : void { }
|
|
|
|
//? Not supported anymore
|
|
//@ obsolete
|
|
//@ readsMutable
|
|
public state() : string { return undefined; }
|
|
|
|
//? Not supported anymore
|
|
//@ obsolete
|
|
//@ writesMutable
|
|
public stop() : void {}
|
|
}
|
|
|
|
export enum SoundUrlTokenDomain
|
|
{
|
|
None,
|
|
TouchDevelop,
|
|
MicrosoftTranslator
|
|
}
|
|
}
|