
Успех мультимедийной платформы во многом зависит от качества контента. Поэтому существует множество инструментов для его управления — например, ручная или автоматическая модерация на предмет грубости, оскорблений или нарушений авторских прав, а также ручная или автоматическая обработка загруженных материалов. Один из способов автоматического улучшения контента — обрезка тишины.
Задача выделения голоса или звуков — очень сложная. О том, как мы искали пути её решения при разработке одного из наших проектов — голосового чата BlaBlaPlay — и как реализовать это в iOS-приложении, расскажем в этой статье.
[Теория] Когда нужна функция обрезки тишины?
Например, у вас есть аудиофайл с участками, где уровень громкости слишком низкий, чтобы его можно было услышать. В таком случае эти фрагменты стоит удалить. Или, допустим, вы записываете голосовое сообщение и долго молчите в начале, собираясь с мыслями. Чтобы не слушать несколько секунд тишины каждый раз, проще просто их обрезать. Подобных ситуаций можно привести много, но вывод один — удаление тишины помогает сделать контент лучше и удобнее для восприятия.
Реализовать это можно разными способами и с помощью разных инструментов. Вначале можно выделить две основные группы таких инструментов:
- для ручного удаления: например, любой аудиоредактор имеет базовую функцию выделения отрезка, который нужно удалить или оставить
- для автоматического удаления — они используют ряд вспомогательных технологий для достижения результата. Рассмотрим их подробнее.
[Теория] Автоматические способы определения тишины
Их может быть довольно много, и выбор зависит от задачи, стоящей перед разработчиком. Это в первую очередь связано с тем, что одни инструменты позволяют выделять из аудиопотока только голос, а другие — работать с голосом и посторонними звуками.
Определение тишины по уровню звука
Итак, определение тишины по уровню звука — или, точнее, по его значению — самый простой и быстрый способ. Поэтому его можно использовать для потокового аудио и определять тишину в реальном времени. Но при этом это самый неточный и хрупкий метод. Технология проста:
- Задаётся константное значение в децибелах — примерно равное порогу слышимости для человека.
- Далее всё, что ниже этого порога, автоматически считается тишиной и подлежит обрезке.
.png)
Способ подходит только в случае, когда нужно определить абсолютную тишину, а не распознавать голос или посторонние шумы. Абсолютная тишина — явление редкое, поэтому метод малоэффективен. Следовательно, его можно использовать лишь для индикации наличия или отсутствия звука, но не для анализа тишины.
Выделение голоса из аудиопотока
Это подход «от противного» — если есть речь, значит, тишины нет.
Выделение звука из аудиопотока — задача непростая. Её решают, анализируя уровень звука или его спектрограмму — график, показывающий, как меняются частоты во времени. Есть два подхода к анализу: аналитический и нейросетевой. В нашем приложении мы использовали Voice Activity Detector (VAD) — детектор речи, который выделяет голос или речь на фоне шума или тишины. Рассмотрим его работу на примере.
Аналитическая оценка
В рамках работы с речевым сигналом как правило используют частотно-временную область обработки. Среди основных методов:
• анализ с использованием вейвлет-преобразования.
• анализ с использованием линейного предсказания.
• анализ с использованием преобразования Гильберта-Хуанга.
• анализ с использованием преобразования Фурье.
• анализ с использованием корреляционной функции.
Наиболее эффективным для выделения речи является метод, основанный на том, что речевой аппарат человека способен генерировать определённый набор частот. Эти частоты называются «формантами».
В этом методе входные данные — это непрерывная осциллограмма звуковой волны (график колебаний). Чтобы выделить речь, её разбивают на фреймы — короткие фрагменты звука длительностью от 10 до 20 мс со сдвигом 10 мс. Такой размер выбран с учётом темпа человеческой речи: за 3 секунды человек обычно произносит около 3 слов, в каждом — примерно 4 звука, а каждый звук делится на 3 этапа. Каждый фрейм обрабатывается отдельно: сначала трансформируется, затем из него извлекаются акустические признаки

Далее для каждого окна выполняется преобразование Фурье:
- Производится поиск пиковых значений
- На основе анализа акустических признаков принимается решение — есть речевой сигнал или нет. Подробности процесса описаны в работе У.А. Ли, 1983. «Методы автоматического распознавания речи».
[Теория] Нейросетевой подход к оценке
Нейросетевой подход состоит из двух частей.
- Так называемый feature extractor — средство для извлечения признаков и построения low dimensional space. На вход экстрактору подается осциллограмма звуковой волны и, например, с помощью преобразования Фурье, строится её low dimensional space. То есть из большого числа признаков выделяются ключевые и формируется новое пространство.

- Дальше экстрактор размещает звуки в пространстве так, чтобы похожие оказались рядом. Например, звуки речи группируются вместе, но при этом находятся далеко от звуков барабанов и автомобилей.

Далее модель классификации берёт выходные данные экстрактора признаков и вычисляет вероятность наличия речи в этих данных.
Что использовать?
Процесс выделения звуков или речи сложный и требует много вычислительных ресурсов для быстрой работы. Давайте разберёмся, когда и какой метод стоит применять.

Итак, как видим, определение по уровню сигнала не подойдёт, если нужна точность.
С аналитическим и нейросетевым подходами есть нюансы. Оба требуют большой вычислительной мощности — это ограничивает их применение для потокового аудио. Но в случае аналитического подхода эту проблему можно решить, выбрав более простые реализации, даже если это повлияет на точность. Например, WebRTC_VAD — он не очень точен, но быстро работает с потоковым аудио даже на слабых устройствах.
А если вычислительная мощность есть и вы хотите распознавать не только речь, но и звуки птиц, гитары или что угодно ещё, нейросеть справится с задачей с высокой точностью и за разумное время.
[Практика] Пример обнаружения и обрезки тишины в начале аудиозаписи на iOS
Все iPhone достаточно мощные, а фреймворки Apple хорошо оптимизированы под устройства. Поэтому можно смело использовать нейросети для определения тишины в потоковом аудио, применяя простой подход: там, где нет звука, — тишина. Для анализа будем использовать фреймворк Sound Analysis, а для записи и обрезки аудио — AVFoundation.
Получение и отправка аудиобуферов во время записи
Для выборки буферов из потока аудиозаписи нужен объект AVAudioEngine. А для доставки полученных буферов нужно добавить наблюдателя на выход к подключенной аудионоде.
private var audioEngine: AVAudioEngine?
public func start(withSoundDetection: Bool) {
guard let settings = setupAudioSession() else { return }
do {
let configuration = try configureAudioEngine()
audioEngine = configuration.0
audioRecordingEvents.onNext(.createsAudioFromat(audioFormat: configuration.1))
} catch {
audioRecordingEvents.onError(AudioRecordError.creatingAudioEngineError)
print("Can not start audio engine")
}
configureAudioRecord(settings: settings)
}
private func configureAudioEngine() throws -> (AVAudioEngine, AVAudioFormat) {
let audioEngine = AVAudioEngine()
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 4096, format: recordingFormat) { [weak self] buffer, time in
self?.audioRecordingEvents.onNext(.audioBuffer(buffer, time))
}
audioEngine.prepare()
try audioEngine.start()
return (audioEngine, recordingFormat)
}
Обработка буфера в нейросети и получение результата
Фреймворк Sound Analysis из коробки распознаёт 300 звуков — этого вполне достаточно для нашей задачи. Создадим класс классификатора и правильно настроим объект SNClassifySoundRequest.
final class AudioClassifire
private var analyzer: SNAudioStreamAnalyzer?
private var request: SNClassifySoundRequest?
init() {
request = try? SNClassifySoundRequest(classifierIdentifier: .version1)
request?.windowDuration = CMTimeMakeWithSeconds(1.3, preferredTimescale: 44_100)
request?.overlapFactor = 0.9
}
}
При создании SNClassifySoundRequest c использованием константного значения для окна windowDuration крайне важно использовать overlapFactor отличный от нуля. Он описывает, насколько окна перекрывают друг друга при анализе. Тем самым создает непрерывный контекст и связность между окнами.
Дальше нужен класс-наблюдатель, соответствующий протоколу SNResultsObserving. В него будут поступать все результаты классификации.
enum AudioClassificationEvent {
case result(SNClassificationResult)
case complete
case failure(Error)
}
protocol AudioClassifireObserver: SNResultsObserving {
var audioClassificationEvent: PublishSubject { get }
}
final class AudioClassifireObserverImpl: NSObject, AudioClassifireObserver {
private(set) var audioClassificationEvent = PublishSubject()
}
extension AudioClassifireObserverImpl: SNResultsObserving {
func request(_ request: SNRequest, didProduce result: SNResult) {
guard let result = result as? SNClassificationResult else { return }
audioClassificationEvent.onNext(.result(result))
}
func requestDidComplete(_ request: SNRequest) {
audioClassificationEvent.onNext(.complete)
}
func request(_ request: SNRequest, didFailWithError error: Error) {
audioClassificationEvent.onNext(.failure(error))
}
}
Наблюдатель создан, теперь можно создать сам поток для анализа поступающих аудиобуферов — SNAudioStreamAnalyzer.
func configureClassification(audioFormat: AVAudioFormat) -> AudioClassifireObserver? {
guard let request else { return nil}
let observer = AudioClassifireObserverImpl()
analyzer = SNAudioStreamAnalyzer(format: audioFormat)
try? analyzer?.add(request, withObserver: observer)
self.observer = observer
return observer
}
Теперь всё готово. Можно принимать аудиобуферы и отправлять их в нейросеть на анализ. Сделать это очень просто:
func addSample(_ sample: AVAudioPCMBuffer, when: AVAudioTime) {
analysisQueue.async {
self.analyzer?.analyze(sample, atAudioFramePosition: when.sampleTime)
}
}
После того как AVAudioPCMBuffer будет успешно распознан, в AudioClassifireObserver.audioClassificationEvent придёт событие AudioClassificationEvent.result(SNClassificationResult). Оно будет содержать все распознанные звуки и уровни их достоверности (confidence). Соответственно, если звуков нет или их confidence < 0.75, можно считать, что звук не был распознан — таким результатом можно пренебречь. Определить это можно так:
func detectSounds(_ result: SNClassificationResult) -> [SNClassification] {
return result.classifications.filter { $0.confidence > 0.75 }
}
Обрезка аудиофайла на основе работы детектора звуков
После того, как запись началась и первые аудиобуферы начали поступать в нейросеть для анализа, нужно запустить таймер. Он будет отсчитывать время до появления первых ненулевых результатов. Тут стоит помнить, что первые результаты будут получены не раньше, чем через request?.windowDuration = CMTimeMakeWithSeconds(1.3, preferredTimescale: 44_100). Поэтому стартовые значения таймера должны это учитывать.
private var recordSilenceTime: Double = 0.6
private var silenceTimer: DispatchSourceTimer?
private func processAudioClassifireResult(_ result: SNClassificationResult) {
let results = audioClassifire.detectSounds(result)
guard !results.isEmpty && !currentState.classificationWasStopped && silenceTimer == nil else {
startSilenceTimer()
return
}
results.forEach {
print("Classification result is \($0.description) with confidence: \($0.confidence)")
}
stopObservingClassifire()
}
При первых ненулевых результатах останавливаем таймер и на основании recordSilenceTime можем отрезать кусочек от начала аудиозаписи.
private func processRecordedAudio(fileName: String, filesPath: URL) {
if recordSilenceTime > 0.6,
let trimmedFile = fileName.components(separatedBy: ".").first {
let trimmer = AudioTrimmerImpl()
trimmer.trimAsset(AVURLAsset(url: url), fileName: "\(trimmedFile)", trimTo: recordSilenceTime) { [weak self] url in
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
let record = AVURLAsset(url: url)
}
}
}
}
Обрезка файла выполняется с помощью AVAssetExportSession.
func trimAsset(_ asset: AVURLAsset, fileName: String, trimTo: Double, completion: @escaping (String) -> Void) {
let trimmedSoundFileURL = documentsDirectory.appendingPathComponent("\(fileName)-trimmed.mp4")
do {
if FileManager.default.fileExists(atPath: trimmedSoundFileURL.absoluteString) {
try deleteFile(path: trimmedSoundFileURL)
}
} catch {
print("could not remove \(trimmedSoundFileURL)")
}
print("Export to \(trimmedSoundFileURL)")
if let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) {
exporter.outputFileType = AVFileType.mp4
exporter.outputURL = trimmedSoundFileURL
exporter.metadata = asset.metadata
let timescale = asset.duration.timescale
let startTime = CMTime(seconds: trimTo, preferredTimescale: timescale)
let stopTime = CMTime(seconds: asset.duration.seconds, preferredTimescale: timescale)
exporter.timeRange = CMTimeRangeFromTimeToTime(start: startTime, end: stopTime)
exporter.exportAsynchronously(completionHandler: {
switch exporter.status {
case AVAssetExportSession.Status.failed:
if let error = exporter.error {
print("export failed \(error)")
}
case AVAssetExportSession.Status.cancelled:
print("export cancelled \(String(describing: exporter.error))")
default:
print("export complete")
completion(fileName)
}
})
} else {
print("cannot create AVAssetExportSession for asset \(asset)")
}
}
Результаты и вывод
Обнаружение тишины или звуков в аудиозаписи становится проще для приложений, которые не ориентированы на профессиональную обработку, не теряя при этом точности и эффективности.
У Apple уже есть готовые инструменты для этого, поэтому разрабатывать такой функционал с нуля и тратить на это год не нужно. Люблю нейронные сети. Посмотрите, как это работает в нашем BlaBlaPlay или напишите нам, чтобы реализовать функцию обрезки тишины в вашем iOS-приложении.
