@met4citizen/headtts
Version:
HeadTTS: A free Javascript text-to-speech with timestamps and visemes.
207 lines (183 loc) • 5.38 kB
JavaScript
/**
* Test if we are in Node.js environment
* @return {boolean} If true, we are in Node.js
*/
export function isNode() {
return typeof import.meta !== 'undefined' &&
typeof import.meta.url === 'string' &&
import.meta.url.startsWith('file://');
}
/**
* Test support for Module Web Workers
* @return {boolean} If true, we support module web workers
*/
export function isModuleWebWorkers() {
let support = false;
try {
const tester = {
get type() { support = true; }
}
const worker = new Worker('blob://', tester);
} finally {
return support;
}
}
/**
* Test support for WebGPU
* @return {boolean} If true, we support WebGPU
*/
export function isWebGPU() {
return !!(globalThis && globalThis.navigator && globalThis.navigator.gpu);
}
/**
* Traceable subsystems
*/
export const traceMask = {
connection: 1,
messages: 2,
events: 4,
g2p: 8,
language: 16
};
/**
* Write console trace.
*
* @param {...any} outputs Output strings.
*/
export function trace( ...outputs ) {
const s = "HeadTTS [" + new Date().toISOString().slice(11, 23) + "] ";
console.log(s,...outputs);
}
/**
* A utility class that creates a Promise along with its associated
* resolve and reject methods, allowing manual control over the Promise.
*/
export class Deferred {
constructor() {
this.status = "pending";
this.promise = new Promise((resolve, reject) => {
this.resolve = (value) => {
this.status = "resolved";
resolve(value);
};
this.reject = (reason) => {
this.status = "rejected";
reject(reason);
};
});
}
}
// Create a lookup table for base64 decoding
const b64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const b64Lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (let i = 0; i < b64Chars.length; i++) b64Lookup[b64Chars.charCodeAt(i)] = i;
/**
* Convert a Base64 MP3 chunk to ArrayBuffer.
* @param {string} chunk Base64 encoded chunk
* @return {ArrayBuffer} ArrayBuffer
*/
export function b64ToArrayBuffer(chunk) {
// Calculate the needed total buffer length
let bufLen = 3 * chunk.length / 4;
if (chunk[chunk.length - 1] === '=') {
bufLen--;
if (chunk[chunk.length - 2] === '=') {
bufLen--;
}
}
// Create the ArrayBuffer
const arrBuf = new ArrayBuffer(bufLen);
const arr = new Uint8Array(arrBuf);
let i, p = 0, c1, c2, c3, c4;
// Populate the buffer
for (i = 0; i < chunk.length; i += 4) {
c1 = b64Lookup[chunk.charCodeAt(i)];
c2 = b64Lookup[chunk.charCodeAt(i+1)];
c3 = b64Lookup[chunk.charCodeAt(i+2)];
c4 = b64Lookup[chunk.charCodeAt(i+3)];
arr[p++] = (c1 << 2) | (c2 >> 4);
arr[p++] = ((c2 & 15) << 4) | (c3 >> 2);
arr[p++] = ((c3 & 3) << 6) | (c4 & 63);
}
return arrBuf;
}
/**
* Encode float32 samples to PCM 16bit LE buffer.
*
* @param {Float32Array} samples Float32 samples
* @param {number} sampleRate Sample rate
* @param {boolean} [header=true] If true, add WAV header
* @return {ArrayBuffer} WAV or raw PCM 16bit LE samples.
*/
export function encodeAudio(samples, sampleRate, header=true) {
const len = samples.length;
let offset = header ? 44 : 0;
const buffer = new ArrayBuffer(offset + len * 2);
const view = new DataView(buffer);
// Write WAV header
if ( header ) {
function writeString(view, off, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(off + i, string.charCodeAt(i));
}
}
writeString(view, 0, "RIFF");
view.setUint32(4, 32 + samples.length * 2, true);
writeString(view, 8, "WAVE");
writeString(view, 12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
writeString(view, 36, "data");
view.setUint32(40, samples.length * 2, true);
}
// Write samples as PCM 16bit LE
for (let i = 0; i < len; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, samples[i]));
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
return buffer;
}
/**
* Concatenate an array of ArrayBuffers.
* @param {ArrayBuffer[]} bufs Array of ArrayBuffers
* @return {ArrayBuffer} Concatenated ArrayBuffer
*/
export function concatArrayBuffers(bufs) {
const len = bufs.length;
if ( len === 1 ) return bufs[0];
let byteLen = 0;
for( let i=0; i<len; i++ ) {
byteLen += bufs[i].byteLength;
}
const buf = new ArrayBuffer(byteLen);
const arr = new Uint8Array(buf);
let p = 0;
for( let i=0; i<len; i++ ) {
arr.set( new Uint8Array(bufs[i]), p);
p += bufs[i].byteLength;
}
return buf;
}
/**
* Convert PCM buffer to AudioBuffer.
* NOTE: Only signed 16bit little endian supported.
*
* @param {ArrayBuffer} buf PCM 16bit LE array buffer
* @return {AudioBuffer} AudioBuffer
*/
export function pcmToAudioBuffer(buf,samplerate,ctx) {
const arr = new Int16Array(buf);
const floats = new Float32Array(arr.length);
const len = arr.length;
for( let i=0; i<len; i++ ) {
floats[i] = (arr[i] >= 0x8000) ? -(0x10000 - arr[i]) / 0x8000 : arr[i] / 0x7FFF;
}
const audio = ctx.createBuffer(1, floats.length, samplerate );
audio.copyToChannel( floats, 0 , 0 );
return audio;
}