UNPKG

@salutejs/client

Version:

Модуль взаимодействия с виртуальным ассистентом

460 lines (452 loc) 17.9 kB
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 };