С расширением бизнеса продуктов компании, в этом году функция жесткого зачистки браузера. Я столкнулся с множеством замечательных проблем и некоторыми расширениями ~ Вот, вы можете поделиться им, у вас есть все проблемы после проблемы.
Анализ данных PCM Base64 для воспроизведения
Эта сцена существует до сих пор. Может не быть проблем со взаимодействием между веб-сокетом и сервером. Однако, если это взаимодействие между собственными приложениями, для обеспечения согласованности данных его необходимо использовать, когда передаются только строки.
-
Разобрать base64 в arrayBuffer.
function base642ArrayBuffer() { const binary_string = window.atob(base64); // 解析base64 const len = binary_string.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } // 如果不`.buffer`则返回的是Unit8Array、各有各的用处吧 // Unit8Array可以用来做fill(0)静音操作,而buffer不行 return bytes.buffer; } -
Так как браузер не может поддерживать воспроизведение данных pcm, если фоновый сервер "неудобен", добавьте его вам
wavЗаголовок запроса Затем нам нужно создать заголовок wav (то есть 44 байта)function buildWaveHeader(opts) { const numFrames = opts.numFrames; const numChannels = opts.numChannels || 1; const sampleRate = opts.sampleRate || 16000; // 采样率16000 const bytesPerSample = opts.bytesPerSample || 2; // 位深2个字节 const blockAlign = numChannels * bytesPerSample; const byteRate = sampleRate * blockAlign; const dataSize = numFrames * blockAlign; const buffer = new ArrayBuffer(44); const dv = new DataView(buffer); let p = 0; p = this.writeString('RIFF', dv, p); // ChunkID p = this.writeUint32(dataSize + 36, dv, p); // ChunkSize p = this.writeString('WAVE', dv, p); // Format p = this.writeString('fmt ', dv, p); // Subchunk1ID p = this.writeUint32(16, dv, p); // Subchunk1Size p = this.writeUint16(1, dv, p); // AudioFormat p = this.writeUint16(numChannels, dv, p); // NumChannels p = this.writeUint32(sampleRate, dv, p); // SampleRate p = this.writeUint32(byteRate, dv, p); // ByteRate p = this.writeUint16(blockAlign, dv, p); // BlockAlign p = this.writeUint16(bytesPerSample * 8, dv, p); // BitsPerSample p = this.writeString('data', dv, p); // Subchunk2ID p = this.writeUint32(dataSize, dv, p); // Subchunk2Size return buffer; } function writeString(s, dv, p) { for (let i = 0; i < s.length; i++) { dv.setUint8(p + i, s.charCodeAt(i)); } p += s.length; return p; } function writeUint32(d, dv, p) { dv.setUint32(p, d, true); p += 4; return p; } function writeUint16(d, dv, p) { dv.setUint16(p, d, true); p += 2; return p; } -
Соберите голову и PCM один раз
concatenate(header, pcmTTS); function concatenate(buffer1, buffer2) { const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); tmp.set(new Uint8Array(buffer1), 0); tmp.set(new Uint8Array(buffer2), buffer1.byteLength); return tmp.buffer; } -
Преобразуйте его в воспроизводимый буферный поток, который можно использовать для получения времени.Если это многосегментный поток данных PCM, его также можно собрать и соединить.
audioCtx.decodeAudioData(TTS, (buffer) => { 存储起来准备播放 }); // buffer.duration 可以用来判断播放时长 // buffer -
играть
const source = audioCtx.createBufferSource(); const gainNode = audioCtx.createGain(); source.buffer = buffer; gainNode.gain.setTargetAtTime(0.1, audioCtx.currentTime + 2, 5); source.connect(gainNode); gainNode.connect(context.destination); source.start('需要播音的时长 简单的可以用buffer.duration或者自己计算拼接后的长度逻辑' + this.context.currentTime);// 这里必须要加上currentTime
После записи выясняется, что шум и эхо являются серьезными в случае публичного выпуска мобильного телефона.
Среди бизнес-требований есть относительно жалкое требование. Сценарий нашего продукта заключается в моделировании процесса коммуникативного диалога между роботом и пользователем. Середина включает в себя воспроизведение звука роботом и запись речи пользователя (из-за функциональных требований робот должен все еще записывать, когда он вещает, чтобы обеспечить существование логики захвата слов). Первоначально это решение все еще может хорошо работать в сценарии ношения наушников, но когда вышеперечисленные функции должны быть реализованы в соответствии с индивидуальными требованиями.Звук мобильного телефона общедоступен, наушники не допускаются.. Потом мы грохнулись и потратили много времени на изучение реализации (на самом деле фронтенда для этого нет, но я могу разобраться со своим когницией).
- **VAD выполняет логику шумоподавления. **vad — обнаружение звуковой активности, определяющее, есть ли звук. Шумоподавление и шумоподавление - разные вещи. Однако добавление модуля алгоритма Вада может сыграть определенную роль в шумоподавлении.Метод заключается в том, чтобы грубо по умолчанию сделать человеческий голос громче окружающего звука и удалить небольшой источник звука. Но это не стандартное шумоподавление.
- Как добиться шумоподавления в целом. Поскольку шум не сильно отличается от человеческого голоса на акустическом уровне, трудно достичь шумоподавления с помощью чисто программных алгоритмов. Поэтому обычно он фильтруется аппаратно один раз, а затем уже на уровне алгоритма, фильтровать после того, как шум собран, сложно. Таким образом, шумоподавление в основном зависит от аппаратного оборудования (микрофонного массива), но после тестирования различные мобильные телефоны не соответствуют аппаратному оборудованию, и, согласно логике публичного выпуска, Huawei mate20pro/iphoneX и т. д. по-прежнему будут иметь очевидные эхо, поэтому Позже мы были вынуждены сократить спрос на проблему опыта (только когда настала очередь пользователя записывать) — мы можем продолжить расследование в будущем.Стороннее решение, с которым мы связались, не было успешно внедрено для проверки, поэтому мы не можем быть уверены Это направление вообще невыполнимо.
- Есть ли какой-то специальный алгоритм шумоподавления?. Должны быть специальные алгоритмы шумоподавления.Если сцена нативна для устройств Android и IOS, можно напрямую реализовать шумоподавление или даже эхоподавление локально, вызвав базовый API, что эквивалентно установке модуля алгоритма напрямую в виде sdk к приложению. Но если он есть на стороне веба, нет возможности передать его в облако для алгоритмического шумоподавления.
- лучшая практика. В случае ограниченных возможностей алгоритма все же необходимо научить клиентов носить микрофоны. Потому что, что касается сцены, качество ASR очень высокое, чтобы добиться лучшего эффекта диалога и извлечения информации в этой сцене. Шумоподавление с помощью физических устройств может значительно снизить нагрузку на алгоритм. В конце концов, физическое устройство также может обеспечить активное шумоподавление (те наушники с активным шумоподавлением, которые представлены на рынке).
использовать веб-просмотр
По разным причинам мы начали отлаживать, как взаимодействовать с веб-страницей нативно (нативное приложение отвечает за сбор аудио, а pcm-поток аудио предоставляется на веб-страницу для последующей обработки через обратные вызовы методов). Это моя первая стыковка с нативными приложениями, и нашей компании не нужны разработчики в этой области, так что, возможно, я наступил на какие-то проблемы со здравым смыслом. Так же немного навести порядок:
Взаимодействие между родным приложением и веб-просмотром
В прошлом я всегда думал, что интерактивная форма была с причудливыми операциями, такими как функции обратного вызова. После стыковки я знаю, что вызовы на обоих концах можно вызывать и передавать параметры только простым способом. Это приводит к проблеме, наше взаимодействие с нативным приложением должно привязывать метод вwindowвниз, будучи связаннымwindowСледующий метод не имеет vuethisконтексте, поэтому, чтобы открыть поток данных PCM собственного приложения и отправить его в экземпляр vue для логической обработки, я написал простой事件订阅者模式, которая реализована в виде подписки и уведомления.
Как читать журналы консоли
После встраивания веб-просмотра самая большая проблема, вероятно, заключается в том, как мы просматриваем журналы Chrome. Может быть текущий способ все же довольно болезненная реализация.Я скачал средства разработки IOS и средства разработки Android соответственно, а потом попросил их помочь мне настроить среду, а дальше настройка уже мое личное дело. Преимущество этого метода в том, что если я сталкиваюсь с небольшими проблемами (включая собственные изменения), я могу проверить и изменить небольшую логику самостоятельно, не полагаясь на других, и повысить определенную эффективность.
Здесь еще есть ямка, но после того, как Android включил режим отладки хрома, при открытии консоли появится ошибка 404. На самом деле, это требует от вас использования магии для нормального доступа в Интернет. В противном случае, как бы вы ни старались, у вас ничего не получится.
Примечания о разрешениях
После того, как веб-просмотр встроен в нативные приложения, возникает множество проблем с разрешениями, например, разрешать ли локальное хранилище, разрешать ли незаконные сертификаты безопасности (локальная разработка будет подделывать сертификаты для имитации https), разрешать ли разрешения на запись, разрешает ли https загружать http ресурсы, и даже подробные трансляции и так далее. Мой способ столкнуться с этой проблемой — как можно яснее описать моему партнеру, какие операции будет выполнять моя страница, а затем позволить ему решить, какие разрешения вам предоставить.
IOS запутанность любви-ненависти
В IOS слишком много ям, надеюсь я смогу допилить эти ямы для всех.
Веб-запись не поддерживается в wkbview
Это на самом деле немного боли в заднице. Вначале я отправил фрагмент кода для проверки совместимости браузера и попросил наших партнеров (которые пишут нативные приложения для встраивания нашего веб-представления) помочь нам просто проверить, есть ли какие-либо проблемы с совместимостью, прежде чем завершить наш план. В результате считается, что коммуникация была плохой.Перед запуском проекта, когда мы попытались встроить нашу страницу, мы обнаружили, что оригинал не поддерживает эту функцию. Это серьезный шаг на яме, и нет другого пути, кроме как выбрать план временной замены, использовать нативную запись в веб-просмотре, встроенном в IOS, и продолжать использовать веб-запись для другой логики окружения.
**Подводя итог, Safari версии IOS12 (последняя версия на данном этапе) может поддерживать веб-запись, но эта функция не поддерживается в сценарии использования wkwebview (собственное приложение, встроенное в веб-просмотр). ** Я видел, что кто-то на github сказал, что IOS12 будет поддерживать эту функцию в IOS11. Для нас такое малосовместимое решение нужно безжалостно выбросить.
Сообщается об ошибке после нескольких вызовов audioCtx.xxx в Safari.null is not an object
В сафари мы создадимaudioContext, сколько бы операций ни выполнялось под хромом, проблем нет. Но после переключения на сафари обнаруживается, что страница не может быть обработана не более 5 раз, и будет выдаваться сообщение об ошибке до тех пор, пока операция выполняется в 5-й раз. Само собой разумеется, что каждое отношение должно быть самостоятельным.Убедившись в явлении, найдите эту статьюaudiocontext Samplerate returning null after being read 8 times. Это, вероятно, означает, что причина сбоя вызова заключается в том, что audioCtx не может быть создан более 6, иначе он вернет null. Совместив наши 5 раз (это значение может иметь определенное отклонение), можно интуитивно судить, что проблема должна заключаться здесь — наш аудиопример нормально не уничтожился. То есть в кодеaudioCtx = null;Он не попадает в сборку мусора. Также с помощью документации MDN я нашел этот метод.
AudioContext.close();
Закрывает звуковую среду, освобождая любой звук, который использует системные ресурсы.
Так отрезатьaudioContext = nullизменился наaudioContext.close()Идеальное решение.
Аудиотег под сафари не может получить продолжительность, он отображается как бесконечность
В Safari аудиофайл, извлеченный с удаленного конца, помещается вaudioПосле маркировки общее время выборки отображается какInfinityНо под хромом такой проблемы нет, поэтому начал искать проблему. Сначала посмотрите эту статьюaudio.duration returns Infinity on Safari when mp3 is served from PHP, извлеченная из ключевой информации в статье, эта проблема, скорее всего, вызвана проблемой настройки заголовка запроса. Поэтому я попытался извлечь файлы удаленной записи и поместить их в каталог статических файлов, предоставленный egg, и получить к ним доступ в виде статических файлов (я планирую посмотреть, как следует изменить заголовок запроса), и я был приятно удивлен, обнаружил, что в середине обработки статических файлов предоставляется яйцо. Программное обеспечение отлично работает в сафари. Это в основном подтверждает, что банк является удаленной службой, которая не обработала заголовок запроса должным образом. В то же время см. введение в документ MDN дляdutaionВведение .Так что можно судить, что браузер делает за вас обработку под хромом (получается предустановленная длина), а под сафари нужно делать самому.
A double. If the media data is available but the length is unknown, this value is
NaN. If the media is streamed and has no predefined length, the value isInf.
Конечно см.lengthЯ думал, что это былоcontentLength, оказывается, что в ответе ниже есть еще одно предложение:
Причина, по которой Safari возвращает продолжительность как бесконечность, довольно интересна: кажется, что Safari дважды запрашивает сервер для воспроизведения файлов.Сначала он отправляет запрос диапазона на сервер с заголовком диапазона, подобным этому: (байты: 0-1).If сервер не возвращает ответ как частичное содержимое, и если он возвращает весь поток, браузер Safari не будет устанавливать тег audio.duration, что приведет к воспроизведению файла только один раз, и его нельзя будет воспроизвести снова.
Общий смысл в том, что получение аудиоресурсов под сафари отправит как минимум два запроса, причем первый запрос будет в виде (bytes: 0-1), если сервер не вернет соответствующий байтовый контент по этому запросу, то Safari не поможет вам разобрать полный объем аудиоданных, возвращаемых следующим запросом, потеряв функциональные характеристики ряда аудиотегов. Итак, для запроса мы можем решить его примерно так:
const { ctx } = this;
const file = fs.readFileSync('./record.mp3');
ctx.set('Content-Type', 'audio/mpeg');
if (ctx.headers.range === 'bytes=0-1') {
ctx.set('Content-Range', `bytes 0-1/${file.length}`);
ctx.body = file.slice(0, 1);
} else {
ctx.body = file;
}
Конечно, эта обработка является очень грубым методом обработки.Я оглянулся на реализацию промежуточного программного обеспечения koa.static-cacheОн отлично работает в сафари, но без приведенного выше кода. Поэтому я думаю, что приведенный выше код является частичной реализацией взлома. Конечно, правильного решения проблемы пока не найдено.
/deep/ селектор не поддерживается
В настоящее время нет адаптивного решения этой проблемы. Вы можете извлечь только те стили, которые необходимо изменить, в подкомпоненты безscopeтег стиля, чтобы сделать это. На данный момент более плавного совместимого пути не найдено.
Вызов ios останавливает собственную запись, в результате чего wkwebview переходит в состояние приостановки (невозможно использовать переход по маршруту и запрос и т. д.).
На самом деле это проблема собственной записи, но поскольку я сначала подумал, что это проблема внешнего интерфейса, мне потребовалось много времени, чтобы найти проблему. Запишите это здесь, чтобы другие друзья не наступили на яму.
В коде проекта сеанс, который завершается один раз, будет выполнять различные операции сохранения и операции перехода маршрутизации. Однако после доступа к функции записи ios я обнаружил, что хотя запрос страницы отображался, он не был получен в фоновом режиме. —— Наконец-то обнаружена проблема, вызванная вызовом остановки записи ios, вероятно, потому, что страница зависает при выполнении некоторых операций, связанных с очередью задач (если только console.log не будет)
Вот немного решения для ios
// 停止录音队列和移除缓冲区,以及关闭session,这里无需考虑成功与否
AudioQueueStop(_audioQueue, false);
// 移除缓冲区,true代表立即结束录制,false代表将缓冲区处理完再结束
AudioQueueDispose(_audioQueue, false);
Вызовите context.createBufferSouce.stop(), чтобы сообщить об ошибке
После встраивания веб-просмотра, когда страница прерывается, необходимо прервать воспроизводимый в данный момент звук. Выполнение этого метода под ios сообщит об ошибке (есть несколько причин, по которым его необходимо повторить). Для этого вида ошибки выберите самый простойtry {} catch{}Вживую, потому что других ситуаций нет, и после тестирования нескольких ситуаций других проблем возникнуть не должно.
Послесловие, на самом деле, я много чего сделал за это время. Вроде какой web-rtc, но не успел разобраться, если интересно~ могу потом разобраться