Реалізація отправки голосових сповіщень у чаті мобільного додатку
Голосові сповіщення — технічно найвимогливіша фича серед медіа в чаті. Запис, кодування, завантаження, воспроізведення з візуалізацією форми хвилі, прискорене воспроізведення — кожен етап вимагає точної роботи з аудіо API платформи.
Запис аудіо
На iOS використовуємо AVAudioRecorder з налаштуваннями кодека. Оптимальні параметри для голосових сповіщень:
AVFormatIDKey: kAudioFormatMPEG4AAC
AVSampleRateKey: 16000 // достатньо для мови
AVNumberOfChannelsKey: 1 // моно
AVEncoderAudioQualityKey: AVAudioQuality.medium
AAC у моно 16 кГц дає ~20–30 КБ в хвилину — компактно та зрозуміло декодується на Android та в браузері. Формат M4A (контейнер для AAC) підтримується нативно на обох платформах.
Дозвіл на мікрофон запитуємо заздалегідь через AVAudioSession.requestRecordPermission(), не у момент натиску кнопки запису. Якщо користувач відмовить — на Info.plist повинен бути NSMicrophoneUsageDescription з вятним поясненням.
Важливий момент з AVAudioSession: перед початком запису активуємо сессію з категорією .record або .playAndRecord з опцією .defaultToSpeaker. Якщо не зробити цей переключення явно — запис може конфліктувати з воспроізведенням музики через AirPods.
На Android — MediaRecorder з AudioSource.MIC, OutputFormat.MPEG_4, AudioEncoder.AAC. З Android 10+ потрібен дозвіл RECORD_AUDIO через ActivityResultContracts.RequestPermission(). MediaRecorder вимагає точного порядку вызовів: setAudioSource → setOutputFormat → setAudioEncoder → prepare → start — перепутати порядок означає IllegalStateException у рантаймі, не у compile time.
Візуалізація форми хвилі
Це те, що відрізняє хорошу реалізацію від посередньої. Рисувати хвилю у реальному часі під час запису — складніше, ніж здається.
На iOS отримуємо амплітуду через AVAudioRecorder.averagePower(forChannel: 0) з вызовами updateMeters() за таймером кожні 50–100 мс. Значення у дБ від -160 до 0, потрібно нормалізувати в 0..1: pow(10, power / 20). Рисуємо через CAShapeLayer або SwiftUI Canvas — останній простіший анімувати без setNeedsDisplay.
На Android — MediaRecorder.getMaxAmplitude() повертає значення 0–32767. Збираємо в масив за таймером через Handler.postDelayed(), рисуємо через Canvas.drawRect() у кастомному View або через Compose Canvas.
При воспроізведенні форму хвилі показуємо як статичну гістограму з позицією playhead. Позицію оновлюємо через AVPlayer.addPeriodicTimeObserver (iOS) або ExoPlayer.addListener з onPlaybackPositionChanged (Android) — не через UI-таймер.
Завантаження та воспроізведення
Голосове сповіщення зазвичай 5–60 секунд — це 2–200 КБ у AAC. Завантажуємо як звичайний файл, але з однією тонкістю: на iOS при воспроізведенні з URL потрібно переключити AVAudioSession назад у категорію .playback або .playAndRecord, інакше звук піде в earpiece (трубку), а не в динамік.
Прискорене воспроізведення (1.5×, 2×) — через AVPlayer.rate = 1.5 на iOS та ExoPlayer.setPlaybackParameters(PlaybackParameters(1.5f)) на Android. Обидва API працюють без артефактів на мові завдяки pitch correction.
Кешування на клієнті — обов'язково. Повторний запит до сервера при кожному воспроізведенні — поганий UX. Зберігаємо в Library/Caches (iOS) або getCacheDir() (Android) з обмеженням на загальний розмір кеша.
Типічні помилки
Не завершити AVAudioRecorder.stop() перед спробою завантажити файл — файл буде неповним або пошкодженим. На Android — MediaRecorder.stop() + release() перед відкриттям файлу на читання.
Використовувати AudioRecord замість MediaRecorder на Android без необхідності — AudioRecord дає PCM без стиску, файли огромні, потрібен ручний AAC-енкодер через MediaCodec.
Терміни
Запис, кодування AAC, upload, воспроізведення з прогресом — 2–3 дні. Форма хвилі у реальному часі + при воспроізведенні — ще 1–2 дні. Вартість рассчитується індивідуально.







