@salutejs/client
Version:
Модуль взаимодействия с виртуальным ассистентом
460 lines (452 loc) • 17.9 kB
JavaScript
import { d as createNanoEvents } from './common-ba25e019.js';
var IS_APPLE_MOBILE = typeof window !== 'undefined'
? (/iPad|iPhone|iPod/.test(navigator.platform) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
!window.MSStream
: false;
function createSilentAudioFile(sampleRate) {
var arrayBuffer = new ArrayBuffer(10);
var dataView = new DataView(arrayBuffer);
dataView.setUint32(0, sampleRate, true);
dataView.setUint32(4, sampleRate, true);
dataView.setUint16(8, 1, true);
var missingCharacters = window
.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)))
.slice(0, 13);
return "data:audio/wav;base64,UklGRisAAABXQVZFZm10IBAAAAABAAEA" + missingCharacters + "AgAZGF0YQcAAACAgICAgICAAAA=";
}
/**
* Создает объект, который позволяет воспроизводить аудио с активным silent mode.
* Перед использование нужно вызвать initialize()
* Создает <audio> и воспроизводит тишину, управляется turnOn/turnOff.
* @returns object
*/
var createIosSilentModePatch = function () {
var audio = null;
var destroy = function () {
if (audio === null) {
return;
}
audio.src = 'about:blank';
audio.load();
audio = null;
};
/**
* Инициализирует патч,
* вызывать по событию взаимодействия пользователя со страницей (click)
*/
var initialize = function () {
if (!IS_APPLE_MOBILE) {
return;
}
destroy();
audio = new Audio();
audio.setAttribute('x-webkit-airplay', 'deny');
audio.preload = 'auto';
audio.loop = true;
audio.src = createSilentAudioFile(16000);
audio.load();
};
var turnOff = function () {
audio === null || audio === void 0 ? void 0 : audio.pause();
};
return {
destroy: destroy,
initialize: initialize,
turnOff: turnOff,
turnOn: function () {
audio === null || audio === void 0 ? void 0 : audio.play().catch(function (e) {
console.error('ios audio patch excepted', e);
});
},
get isActive() {
return (audio === null || audio === void 0 ? void 0 : audio.paused) === false;
},
};
};
var iosSilentModePatch = createIosSilentModePatch();
/** Создает коллекцию треков */
var createTrackCollection = function () {
var trackIds;
var trackMap;
var clear = function () {
trackIds = new Array();
trackMap = new Map();
};
var push = function (id, track) {
if (trackMap.has(id)) {
throw new Error('Track already exists');
}
trackMap.set(id, track);
trackIds.push(id);
};
var has = function (id) { return trackMap.has(id); };
var getById = function (id) {
var track = trackMap.get(id);
if (track === undefined) {
throw new Error('Unknown track id');
}
return track;
};
var getByIndex = function (index) {
if (index < 0 || index >= trackIds.length) {
throw new Error('Index out of bounds');
}
var track = trackMap.get(trackIds[index]);
if (track == null) {
throw new Error('Something wrong...');
}
return track;
};
var some = function (predicate) { return trackIds.some(function (id) { return predicate(getById(id)); }); };
clear();
return {
clear: clear,
has: has,
get: getById,
getByIndex: getByIndex,
push: push,
some: some,
get length() {
return trackIds.length;
},
};
};
/** Создает структуру для хранения загружаемых и воспроизводимых частей трека */
var createChunkQueue = function () {
var chunks = []; // очередь воспроизведения
/** Добавить чанк в очередь воспроизведения */
var push = function (chunk) {
chunks.push(chunk);
};
/** Удалить чанк из очереди воспроизведения */
var remove = function (chunk) {
chunks.splice(chunks.indexOf(chunk), 1);
};
return {
get chunks() {
return chunks;
},
remove: remove,
push: push,
get length() {
return chunks.length;
},
get ended() {
// считаем трек законченным, когда все воспроизведено
return chunks.length === 0;
},
};
};
/** Парсит WAV заголовок и возвращает информацию о файле */
var parseWavHeader = function (data) {
if (data.length < 12)
return null;
// Проверка RIFF сигнатуры
var riffSignature = new TextDecoder().decode(data.slice(0, 4));
var waveSignature = new TextDecoder().decode(data.slice(8, 12));
if (riffSignature !== 'RIFF' || waveSignature !== 'WAVE') {
return null;
}
// Минимальный размер для получения основной информации
if (data.length < 44)
return null;
try {
// Извлечение параметров из заголовка
var dataView = new DataView(data.buffer, data.byteOffset);
var sampleRate = dataView.getUint32(24, true);
var channels = dataView.getUint16(22, true);
// Поиск data chunk для определения точного размера заголовка
var headerSize = 44; // минимальный размер
var offset = 12;
while (offset < data.length - 8) {
var chunkId = new TextDecoder().decode(data.slice(offset, offset + 4));
var chunkSize = dataView.getUint32(offset + 4, true);
if (chunkId === 'data') {
headerSize = offset + 8;
break;
}
offset += 8 + chunkSize;
}
return { sampleRate: sampleRate, channels: channels, headerSize: headerSize };
}
catch (error) {
console.warn('Ошибка при парсинге WAV заголовка:', error);
return null;
}
};
var HZ_BYTES_COUNT = 2;
var from16BitToFloat32 = function (incomingData) {
var l = incomingData.length;
var outputData = new Float32Array(l);
for (var i = 0; i < l; i += 1) {
var sample = incomingData[i] / 32768.0;
// Ограничиваем значения диапазоном [-1.0, 1.0]
sample = Math.max(-1.0, Math.min(1.0, sample));
// Защита от NaN и бесконечности
if (!isFinite(sample)) {
sample = 0.0;
}
outputData[i] = sample;
}
return outputData;
};
/** Возвращает потоковый подгружаемый трек, который умеет себя проигрывать */
var createTrackStream = function (ctx, _a) {
var _b = _a === void 0 ? {} : _a, _c = _b.sampleRate, sampleRate = _c === void 0 ? 24000 : _c, _d = _b.numberOfChannels, numberOfChannels = _d === void 0 ? 1 : _d, _e = _b.delay, delay = _e === void 0 ? 0 : _e, onPlay = _b.onPlay, onEnd = _b.onEnd, onStop = _b.onStop, trackStatus = _b.trackStatus;
// очередь загруженных чанков (кусочков) трека
var queue = createChunkQueue();
var buffer = new ArrayBuffer(0);
var extraByte = null;
var status = trackStatus || 'stop';
var lastChunkOffset = 0;
var startTime = 0;
var firstChunk = true;
var loaded = false;
var end = function () {
// останавливаем воспроизведение чанков из очереди воспроизведения
queue.chunks.forEach(function (chunk) {
chunk.stop();
});
status = 'end';
onEnd && onEnd();
startTime = 0;
lastChunkOffset = 0;
};
var stop = function () {
onStop === null || onStop === void 0 ? void 0 : onStop();
end();
};
var play = function () {
var _a;
if (status === 'end') {
return;
}
var isPlaying = status === 'play';
if (!isPlaying) {
status = 'play';
onPlay && onPlay();
}
var bytesPerSecond = sampleRate * HZ_BYTES_COUNT * numberOfChannels;
var bufferDurationSeconds = buffer.byteLength / bytesPerSecond;
// воспроизводим трек, если источник уже проигрывается или поток полностью загрузился или длина загруженного
// больше задержки
if (isPlaying || loaded || bufferDurationSeconds >= delay) {
if (buffer.byteLength > 0) {
var chunk = getChunkFromBuffer();
startTime = queue.length === 0 ? ctx.currentTime : startTime;
queue.push(chunk);
chunk.start(startTime + lastChunkOffset);
lastChunkOffset += ((_a = chunk.buffer) === null || _a === void 0 ? void 0 : _a.duration) || 0;
}
}
if (loaded && queue.ended) {
end();
return;
}
};
/** Удаляет или добавляет байт для четности */
var getExtraBytes = function (data, bytesArraysSizes) {
if (extraByte == null && bytesArraysSizes.incomingMessageVoiceDataLength % 2) {
extraByte = data[bytesArraysSizes.incomingMessageVoiceDataLength - 1];
bytesArraysSizes.incomingMessageVoiceDataLength -= 1;
bytesArraysSizes.sourceLen -= 1;
}
else if (extraByte != null) {
bytesArraysSizes.prepend = extraByte;
bytesArraysSizes.start = 1;
if (bytesArraysSizes.incomingMessageVoiceDataLength % 2) {
bytesArraysSizes.incomingMessageVoiceDataLength += 1;
extraByte = null;
}
else {
extraByte = data[bytesArraysSizes.incomingMessageVoiceDataLength - 1];
bytesArraysSizes.sourceLen -= 1;
}
}
};
var createChunk = function (chunk) {
var audioBuffer = ctx.createBuffer(numberOfChannels, chunk.length / numberOfChannels, sampleRate);
for (var i = 0; i < numberOfChannels; i++) {
var channelChunk = new Float32Array(chunk.length / numberOfChannels);
var index = 0;
for (var j = i; j < chunk.length; j += numberOfChannels) {
channelChunk[index++] = chunk[j];
}
audioBuffer.getChannelData(i).set(channelChunk);
}
var source = ctx.createBufferSource();
source.buffer = audioBuffer;
source.connect(ctx.destination);
source.onended = function () {
queue.remove(source);
if (queue.ended && status !== 'end' && loaded) {
status = 'end';
onEnd && onEnd();
}
};
return source;
};
/** Получить чанк из буфера */
var getChunkFromBuffer = function () {
var tmp = buffer;
buffer = new ArrayBuffer(0);
var data = new Uint8Array(tmp);
var bytesArraysSizes = {
incomingMessageVoiceDataLength: data.length,
sourceLen: data.length,
start: 0,
prepend: null,
};
// выравние по два байта
getExtraBytes(data, bytesArraysSizes);
var dataBuffer = new ArrayBuffer(bytesArraysSizes.incomingMessageVoiceDataLength);
var bufferUi8 = new Uint8Array(dataBuffer);
var bufferI16 = new Int16Array(dataBuffer);
bufferUi8.set(data.slice(0, bytesArraysSizes.sourceLen), bytesArraysSizes.start);
if (bytesArraysSizes.prepend != null) {
bufferUi8[0] = bytesArraysSizes.prepend;
}
return createChunk(from16BitToFloat32(bufferI16));
};
/** добавляет чанк в очередь на воспроизведение */
var write = function (data) {
var slicePoint = 0;
if (firstChunk) {
// Проверяем наличие WAV заголовка
var wavHeader = parseWavHeader(data);
if (wavHeader) {
// Найден валидный WAV заголовок - пропускаем его
slicePoint = wavHeader.headerSize;
// actualSampleRate = wavHeader.sampleRate;
// actualChannels = wavHeader.channels;
// Предупреждение о несоответствии параметров
if (wavHeader.sampleRate !== sampleRate) {
console.warn("WAV \u0444\u0430\u0439\u043B \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 sampleRate " + wavHeader.sampleRate + ", \u043D\u043E \u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F " + sampleRate);
}
if (wavHeader.channels !== numberOfChannels) {
console.warn("WAV \u0444\u0430\u0439\u043B \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 " + wavHeader.channels + " \u043A\u0430\u043D\u0430\u043B\u043E\u0432, \u043D\u043E \u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F " + numberOfChannels);
}
}
else {
slicePoint = 0;
}
firstChunk = false;
}
if (slicePoint >= data.length) {
return;
}
var tmp = new Uint8Array(buffer.byteLength + data.length - slicePoint);
tmp.set(new Uint8Array(buffer), 0);
tmp.set(slicePoint ? data.slice(slicePoint) : data, buffer.byteLength);
buffer = tmp;
if (status === 'play') {
play();
}
};
return {
get loaded() {
return loaded;
},
setLoaded: function () {
loaded = true;
if (status === 'play') {
play();
}
},
write: write,
get status() {
return status;
},
play: play,
stop: stop,
};
};
var createVoicePlayer = function (actx, _a) {
var _b = _a === void 0 ? {} : _a, _c = _b.startVoiceDelay, startVoiceDelay = _c === void 0 ? 0.2 : _c, sampleRate = _b.sampleRate, numberOfChannels = _b.numberOfChannels;
var _d = createNanoEvents(), on = _d.on, emit = _d.emit;
var tracks = createTrackCollection();
// true - воспроизводим все треки в очереди (новые в том числе), false - скипаем всю очередь (новые в т.ч.)
var active = true;
// индекс текущего трека в tracks
var cursor = 0;
var play = function () {
if (cursor >= tracks.length) {
if (tracks.some(function (track) { return !track.loaded; })) {
return;
}
iosSilentModePatch.turnOff();
// очищаем коллекцию, если все треки были воспроизведены
cursor = 0;
tracks.clear();
return;
}
// хак для silent mode ios
if (!iosSilentModePatch.isActive) {
iosSilentModePatch.turnOn();
}
// рекурсивно последовательно включаем треки из очереди
var current = tracks.getByIndex(cursor);
if (current.status === 'end') {
if (cursor < tracks.length) {
cursor++;
play();
}
}
else {
current.play();
}
};
var append = function (data, trackId, last) {
if (last === void 0) { last = false; }
var current = tracks.has(trackId) ? tracks.get(trackId) : undefined;
if (current == null) {
/// если trackId нет в коллекции - создаем трек
/// по окончании проигрывания - запускаем следующий трек, вызывая play
current = createTrackStream(actx, {
sampleRate: sampleRate,
numberOfChannels: numberOfChannels,
delay: startVoiceDelay,
onPlay: function () { return emit('play', trackId); },
onStop: function () { return emit('stop', trackId); },
onEnd: function () {
emit('end', trackId);
play();
},
trackStatus: active ? 'stop' : 'end',
});
tracks.push(trackId, current);
}
if (current.status !== 'end' && data.length) {
current.write(data);
}
if (last) {
// все чанки трека загружены
current.setLoaded();
}
play();
};
var stop = function () {
while (cursor < tracks.length) {
var cur = cursor;
cursor++;
tracks.getByIndex(cur).stop();
}
iosSilentModePatch.turnOff();
};
return {
append: append,
setActive: function (value) {
active = value;
if (value) {
play();
}
else {
stop();
}
},
on: on,
stop: stop,
};
};
export { createVoicePlayer as c, iosSilentModePatch as i };