166 строки
6.7 KiB
TypeScript
166 строки
6.7 KiB
TypeScript
///<reference path='refs.ts'/>
|
|
//declare class AudioContext {}
|
|
declare class AudioBuffer {}
|
|
|
|
module TDev.RT {
|
|
export module AudioContextManager {
|
|
var _context : any;
|
|
function context() {
|
|
if (!_context) _context = freshContext();
|
|
return _context;
|
|
}
|
|
function freshContext() {
|
|
(<any>window).AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
|
|
if ((<any>window).AudioContext) {
|
|
try {
|
|
// this call my crash.
|
|
// SyntaxError: audio resources unavailable for AudioContext construction
|
|
return new (<any>window).AudioContext();
|
|
} catch(e) {}
|
|
}
|
|
return undefined;
|
|
}
|
|
export function isSupported() { return !!context(); }
|
|
export function loadAsync(buffer : ArrayBuffer) : Promise { // AudioBuffer
|
|
var ctx = context();
|
|
return new Promise((onSuccess, onError, onProgress) => {
|
|
ctx.decodeAudioData(buffer,
|
|
b => onSuccess(b),
|
|
e => onSuccess(undefined)
|
|
);
|
|
});
|
|
}
|
|
|
|
export function play(buffer : AudioBuffer, volume : number) {
|
|
var ctx = context();
|
|
if (ctx) {
|
|
var source = ctx.createBufferSource();
|
|
source.buffer = buffer;
|
|
var gain = ctx.createGain();
|
|
gain.gain.value = volume;
|
|
source.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
|
|
source.start(0);
|
|
}
|
|
}
|
|
|
|
function createNode(ctx : any) {
|
|
if(!ctx.createScriptProcessor)
|
|
return ctx.createJavaScriptNode(4096, 1, 1);
|
|
else
|
|
return ctx.createScriptProcessor(4096, 1, 1);
|
|
}
|
|
|
|
export function isMicrophoneSupported() { return isSupported() && UserMediaManager.isSupported(); }
|
|
|
|
export function recordMicrophoneAsync() : Promise { // wav
|
|
if(!isSupported()) return Promise.as(undefined);
|
|
return UserMediaManager.getMicrophoneStreamAsync()
|
|
.then(stream => {
|
|
if (!stream) return Promise.as(undefined);
|
|
return new Promise((onSuccess, onError, onProgress)=>{
|
|
var ctx = freshContext();
|
|
if (!ctx) {
|
|
App.log("failed to acquire AudioContext");
|
|
onSuccess(undefined);
|
|
return;
|
|
}
|
|
var source = ctx.createMediaStreamSource( stream );
|
|
var node = createNode(ctx);
|
|
var buffers : Float32Array[] = [];
|
|
var buffersLength = 0;
|
|
node.onaudioprocess = (e) => {
|
|
if (!buffers) return;
|
|
var b = e.inputBuffer.getChannelData(0);
|
|
var clone = new Float32Array(b.length);
|
|
clone.set(b);
|
|
buffers.push(clone);
|
|
buffersLength += clone.length;
|
|
};
|
|
source.connect(node);
|
|
// if the script node is not connected to an output the "onaudioprocess" event
|
|
// is not triggered in chrome.
|
|
node.connect(ctx.destination);
|
|
|
|
var wav = undefined;
|
|
var m = new ModalDialog();
|
|
m.add(div('wall-dialog-header', 'recording microphone...'));
|
|
m.add(div('wall-dialog-buttons', HTML.mkButton('done', () => {
|
|
source.disconnect();
|
|
source = null;
|
|
node.disconnect();
|
|
node.onaudioprocess = null;
|
|
node = null;
|
|
var wavBytes = encodeToWav(buffers, buffersLength, ctx.sampleRate);
|
|
wav = 'data:audio/wav;base64,' + Util.base64EncodeBytes(<number[]><any>wavBytes);
|
|
m.dismiss();
|
|
}))
|
|
);
|
|
m.onDismiss = () => {
|
|
if (source) {
|
|
source.disconnect();
|
|
source = null;
|
|
}
|
|
if (node) {
|
|
node.disconnect();
|
|
node.onaudioprocess = null;
|
|
node = null;
|
|
}
|
|
ctx = null;
|
|
onSuccess(wav);
|
|
};
|
|
m.show();
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
function encodeToWav(buffers : Float32Array[], buffersLength : number, sampleRate : number) : Uint8Array {
|
|
var buffer = new ArrayBuffer(44 + buffersLength * 2);
|
|
var view = new DataView(buffer);
|
|
var offset = 0;
|
|
|
|
function writeString(s : string){
|
|
for (var i = 0; i < s.length; i++, offset++){
|
|
view.setUint8(offset, s.charCodeAt(i));
|
|
}
|
|
}
|
|
function writeUint32(u : number) {
|
|
view.setUint32(offset, u, true);
|
|
offset += 4;
|
|
}
|
|
function writeUint16(u : number) {
|
|
view.setUint16(offset, u, true);
|
|
offset += 2;
|
|
}
|
|
function writePCM(buffer : Float32Array){
|
|
for (var i = 0; i < buffer.length; i++, offset+=2){
|
|
var s = Math.max(-1, Math.min(1, buffer[i]));
|
|
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
|
}
|
|
}
|
|
|
|
var numChannels = 1; // mono
|
|
var bytesPerSample = 2; // 16 bits
|
|
// WAV header
|
|
writeString('RIFF');
|
|
writeUint32(36 + buffersLength * bytesPerSample);
|
|
writeString('WAVE');
|
|
writeString('fmt ');
|
|
writeUint32(16); // 16 for PCM
|
|
writeUint16(1); // PCM
|
|
writeUint16(numChannels);
|
|
writeUint32(sampleRate);
|
|
writeUint32(sampleRate * bytesPerSample * numChannels);
|
|
writeUint16(numChannels * bytesPerSample);
|
|
writeUint16(bytesPerSample * 8);
|
|
writeString('data');
|
|
writeUint32(buffersLength * numChannels * bytesPerSample);
|
|
// PCM
|
|
for(var j = 0; j<buffers.length;++j)
|
|
writePCM(buffers[j]);
|
|
return new Uint8Array(buffer, 0, buffer.byteLength);
|
|
}
|
|
}
|
|
} |