audio_waveform_kit 1.8.0
audio_waveform_kit: ^1.8.0 copied to clipboard
Audio recording with waveform and spectrum visualization
1.8.0 #
Features #
AudioPlayerEvent$SetSpeed— скорость воспроизведения: событие ставит рейт и на уже играющий звук, и на все следующие запуски этого блока. Скорость видна в состоянии — новое полеspeedуAudioPlayerState$PlayingиAudioPlayerState$Paused(по умолчанию1.0, существующие конструкторы иswitch-ветки не ломаются), чтобы UI перерисовывал подпись сам.AudioPlayerкэширует рейт в своём поле, но не переприменяет его ни наplay(), ни наsetSource()— поэтому блок ставит его сам после каждого запуска источника, иначе скорость молча сбрасывалась бы на 1.0 при холодном старте и послеCompleted. Отказ платформы сменить рейт проглатывается: звук продолжает играть как играл. Верхнюю границу диапазона задаёт вызывающий —audioplayersдаёт гарантию до 2× (iOS/macOS), Android требует API 23+
1.7.0 #
Features #
AudioRecordButton.defaultBuilder— стандартный круглый вид кнопки доступен отдельно от фабрикиdefaultStyle: возвращаетAudioRecordButtonBuilder, который можно вызвать из собственногоbuilderс подменённымonTap. Нужно приложению, которому перед стартом записи требуется свой шаг (спросить разрешение, показать диалог), — раньше для этого приходилось копировать отрисовку кнопки.defaultStyleтеперь просто зовётdefaultBuilder, поведение не изменилось
1.6.1 #
Fixes #
AudioPlayerBloc._onSeek—restartable()(1.6.0) отменяет толькоemit()устаревшего обработчика, не сам уже запущенный вызов: пока источник ещё не загружен, каждое событиеSeekотonHorizontalDragUpdateвидело состояниеIdle(эмит предыдущего ещё не случился) и заново вызывало_player.setSource(...)— несколько параллельныхsetSource()на одномAudioPlayerоставляли его в нерабочем состоянии, поэтому перемотка отрабатывала один раз и дальше переставала реагировать. ТеперьsetSource()вызывается не больше одного раза (флаг_sourceLoaded), а после каждогоawaitобработчик проверяетemit.isDoneи не выполняет лишнюю работу для уже отменённого событияMessengerWaveformPainter— оттенок уже сыгранной части бара получался затемнениемbarColorк чёрному на фиксированные 25%: для тёмныхbarColor(тёмная тема) разница почти не читалась. Теперь сыгранная часть сдвигается по светлоте (HSLColor.lightness) в сторону, противоположную текущей яркости цвета — контраст виден независимо от того, светлый исходный цвет или тёмныйAudioPlayerBloc._onSeek— ветка для ужеPlaying/Pausedтолько звала_player.seek(...)и полагалась наonPositionChanged, а тот стрим_onPositionChangedнамеренно игнорирует, пока состояние неPlaying. Поэтому перемотка на паузе (обычный случай — тронуть волну доPlay) не обновляла позицию в state после самого первого раза: первыйSeekизIdleэмитилPausedсам, а все следующие уходили в_player.seek()в никуда, ничего не эмитя, — визуально «перемотка перестаёт слушаться» ровно после первого движения пальцем, независимо от фиксаsetSource()выше. Теперь веткиPlaying/Pausedэмитят обновлённую позицию сами, без ожидания стрима
1.6.0 #
Features #
MessengerWaveformPlayer— новый виджет: волна сама служит полосой перемотки. ТребуетAudioPlayerBloc, уже поднятый выше черезBlocProvider(не создаёт свой, в отличие отAudioWaveformPlayer) — можно держать в дереве рядом со списком сегментов или другим UI, читающим тот же инстанс. Позиция — не линия поверх волны, а более тёмный оттенок уже сыгранных баров; управляется через новыеplayedProgress/playedColorуMessengerWaveformPainter(оба по умолчаниюnull— старое поведениеStaticMessengerWaveformDisplay/MessengerWaveformDisplayне меняется)
Fixes #
AudioPlayerBloc.Seekобрабатывался без concurrency-transformer (дефолтbloc— последовательно, не конкурентно): при перетаскивании волны десятки событийSeekвставали в очередь, и второй жест перемотки добавлял свои события в хвост уже забитой очереди первого — визуально перемотка «переставала работать» на много секунд.transformer: restartable()— обрабатывается только последняя позиция, устаревшие в процессе перетаскивания события отбрасываются
1.5.0 #
Changes #
AudioPlayerBloc.SeekизIdle/Completed/Errorбольше не теряется молча и не требуетPlay— источник готовится черезsetSourceи встаёт на нужную позицию без запуска звука, состояние становитсяPaused. Раньше вызывающему коду приходилось самому выбиратьSeek(когда уже играет/на паузе) илиPlay(startAt:)(холодный старт) — теперьSeekбезопасен в любом состоянии и никогда не запускает воспроизведение сам.Play(startAt:)остаётся для явного «начать играть с места» одним событием
1.4.0 #
Fixes #
AudioPlayerBloc— тап по позиции до первогоPlay(состояниеIdle/Completed) всегда стартовал с начала:Seekбез загруженного источника молча теряется уaudioplayers, аPlayне принимал стартовую позицию.AudioPlayerEvent$Playполучил опциональныйstartAt— холодный старт сразу играет с нужной позиции, без промежуточногоSeek
1.3.0 #
Fixes #
AudioRecordingBloc— PCM с платформенного канала читался какchunk.buffer.asInt16List(): от начала буфера сообщения и на всю его длину, мимоoffsetInBytesи длины самого чанка. В визуализацию и спектр уходили служебные байты сообщения и соседние куски, а при нечётном смещении PCM16 разъезжался на байт — любой звук превращался в шум почти полной громкости, одинаковый и в тишине, и под речь. На записанный файл это не влияло: сервис пишет чанк сам. ТеперьInt16List.sublistView(chunk), с копией при нечётном смещенииAudioUtils.wavToSamples— та же ошибка вwavBytes.buffer.asInt16List(44): для вью читались чужие байтыStringSnapshotPainter— размах «струны» не зависел от громкости: каждый кадр нормировался на собственный пик (scale = 1 / peak), поэтому и тихий шум, и громкая речь рисовались во всю высоту. Теперь размах задаётся уровнем сигнала (RMS) по логарифмической шкалеfloorDb…ceilingDb: тишина — прямая линия, шум комнаты — мелкая дрожь, речь — полный размах. Линейного усиления нет: между шумом и речью 30–40 дБ, одним множителем оба конца шкалы не показатьStringSnapshotPainter— форма волны нормируется отдельно от размаха, поэтому кривая больше не срезается по границе боксаStringSnapshotPainter— сырые сэмплы рисовались точка-в-сэмпл (2048 точек на ~360 px), на экране это полоса шума. Теперь усредняются до одной точки наpointSpacingлогических пикселей; усреднение работает как ФНЧStringSnapshotPainter— окно снапшота съезжало по фазе на каждый чанк, волна ползла по горизонтали. Добавлен триггер по переходу через ноль снизу вверх (alignToZeroCrossing); сдвиг берётся из запаса точек, поэтому горизонтальный масштаб не меняется от кадра к кадруStringSnapshotPainter— контрольной точкой квадратичной кривой брался предыдущий сэмпл вместо текущего: кривая шла на шаг позади данных, а последний сегмент проходил прямой хордой мимо предпоследней точкиStringSnapshotPainter.shouldRepaint— сравнивал толькоsamplesиstringColor; смена остальных параметров не перерисовывала холстAudioRecordingBloc.waveformSamples— точки брались мгновенным отсчётом (каждый 441-й сэмпл): на самом громком слоге могла попасться фаза перехода через ноль и дать 0. Теперь это RMS-энергия по окнам ~10 мс — честная огибающая, неотрицательные значения. Окно RMS также было захардкожено под 44100 Гц (441 сэмпл); теперь считается отspectrumConfig.sampleRate, а остаток окна переносится через границу чанка вместо того, чтобы терятьсяRecordingLevelPainter— высота столбика бралась из мгновенного отсчёта линейно; после смены семантикиwaveformSamples(см. выше) индикатор всегда выглядел почти пустым на речи (−22 dBFS RMS = 8% высоты). Теперь высота — по той же логарифмической шкалеfloorDb/ceilingDb, что и у струныRecordingLevelPainter.shouldRepaint— сравнивал толькоsamplesиbarColor;barSpacing,minBarHeightFractionи новыеfloorDb/ceilingDbне перерисовывали холстWaveformPainter(WaveformStyle.envelope) — каждый кадр нормировался на собственный пик (scale = 1 / peak), та же болезнь, что была у струны: тишина и речь рисовались во всю высоту. Теперь размах — по логарифмической шкалеfloorDb/ceilingDbWaveformDisplay(WaveformStyle.string) — рисовал дубль струны на 56 децимированных, после смены семантикиwaveformSamplesуже неотрицательных (знака нет) отсчётах. Теперь приstyle: WaveformStyle.stringрендеритсяStringSnapshotDisplay— он читает полныеsnapshotSamplesи уже умеет форму, размах и триггер по нулюMessengerWaveformDisplay(живой виджет) — дефолтlogarithmic: false(нормировка на глобальный пик) на скроллящемся окне прыгал на каждом кадре, а без окна один громкий всплеск пережимал всё, что было до него. Дефолт изменён наlogarithmic: true.StaticMessengerWaveformDisplay(весь клип целиком) не тронут — пиковая нормировка там осознанный «телеграмный» видMessengerWaveformPainter.shouldRepaint— не сравнивалsilenceThreshold,minDbThreshold,barSpacing
Changes #
StringSnapshotDisplay,StaticStringSnapshotDisplay,StringSnapshotPainter— новые параметрыfloorDb,ceilingDb,autoGain,maxGain,pointSpacing,alignToZeroCrossing. Сигнатуры совместимы, но вид по умолчанию изменился. Прежний возвращается комбинациейautoGain: true, pointSpacing: 0, alignToZeroCrossing: falseRecordingLevelDisplay,StaticLevelDisplay,RecordingLevelPainter— новые параметрыfloorDb(-55.0),ceilingDb(-15.0)WaveformDisplay,WaveformPainter— новые параметрыfloorDb(-55.0),ceilingDb(-15.0) дляWaveformStyle.envelope; дляWaveformStyle.stringигнорируютсяMessengerWaveformDisplay— дефолтlogarithmicизменён сfalseнаtrue(см. Fixes)- dB-математика (
rmsOf,amplitudeForRms) вынесена в один файлlib/src/utils/level_scale.dart— раньше жила отдельно вStringSnapshotPainterи дублировалась бы вMessengerWaveformPainter. Не экспортируется изaudio_waveform_kit.dart, публичный API не растёт
1.2.0 #
Features #
RmsBucketAccumulatorэкспортирован изaudio_waveform_kit.dart: своя реализацияAudioRecordingServiceможет копить RMS-огибающую для хранения тем же алгоритмом, что иAudioRecordingBloc, не дублируя его
Fixes #
AudioRecordingBloc— поток PCM, закрытый самим сервисом (остановка из уведомления foreground service, микрофон отобран системой), теперь переводит bloc вFinishedтак же, какAudioRecordingEvent$Stop. Раньше bloc оставался вRecordingс тикающим таймером. Реакция наonErrorне изменилась
1.1.0 #
Features #
AudioWaveformScope— опциональный параметрrecordingService: позволяет подставить свою реализациюAudioRecordingService(например, пишущую PCM на диск по ходу записи вместо накопления в памяти, или работающую в foreground service) и при этом пользоваться виджетами и блоком пакета. Приnullповедение прежнее — scope сам создаётAudioRecordingServiceImplи освобождает его. Переданный извне сервис scope не диспозит: его жизненным циклом управляет вызывающая сторона
1.0.1 #
Fixes #
AudioRecordingBloc— убран повторныйdispose()AudioRecordingService: при демонтажеAudioWaveformScopeсервис закрывался дважды (вBloc.close()и вRepositoryProvider.dispose), что могло бросать исключение на уже освобождённомAudioRecorder. Теперь жизненным циклом сервиса управляет только DI-scopeStringSnapshotPainter— исправленыNaN-координаты пути при снапшоте из одного сэмплаTimelineSpectrumPainter— исправлено деление на ноль приSpectrumConfig.frequencyBands == 1LiveSpectrumDisplay,SpectrumDisplay— чтениеspectrumConfigвbuild()переведено сcontext.read()наcontext.select()AudioWaveformPlayer— убран небезопасныйcontext.findRenderObject()!в пользу явной проверки наnullWaveformDisplay,RecordingLevelDisplay,StringSnapshotDisplay,MessengerWaveformDisplay,LiveSpectrumDisplay— сравнение списков сэмплов вbuildWhenпереведено с!=(по ссылке) наListEquality(по содержимому)AudioRecordingBloc— дефолтmaxSnapshotSamplesприведён к2048, согласован с дефолтомAudioWaveformScope
Changes #
WaveformStyleперенесён изwidgets/waveform_display.dartвpainters/waveform_painter.dart— устранена обратная зависимость painter → widget; публичный API не изменился (WaveformStyleпо-прежнему доступен изaudio_waveform_kit.dart)- Добавлен
AudioRecordingState$Finished.toRecordingResult()— единая точка маппинга state вRecordingResultвместо ручной сборки вAudioRecordButton collectionдобавлен в прямые зависимости пакета (ранее использовался транзитивно)
1.0.0 #
Features #
AudioWaveformScope— DI-scope виджет; оборачивает дерево и предоставляетAudioRecordingService,SpectrumAnalyzerиAudioRecordingBlocAudioRecordButton— кнопка записи/остановки с фабричным конструкторомdefaultStyle()RecordingTimer— отображение прошедшего времени записиAudioWaveformPlayer— самодостаточный виджет воспроизведения с прогресс-баром; работает с файлом (native) и WAV-байтами (web)
Визуализация (live, во время записи)
WaveformDisplay— огибающая волновой формы; стилиWaveformStyle.envelopeиWaveformStyle.stringMessengerWaveformDisplay— RMS-волновая форма в стиле мессенджераRecordingLevelDisplay— VU-метр реального времениLiveSpectrumDisplay— живой FFT-спектр, обновляется каждые 50 мсStringSnapshotDisplay— осциллограф из сырых PCM-семплов
Визуализация (static, после записи)
SpectrumDisplay— FFT-спектр; линейный и логарифмический режимыTimelineSpectrumDisplay— тепловая карта время–частотаStaticLevelDisplay— статический VU-метрStaticMessengerWaveformDisplay— статическая RMS-волновая формаStaticStringSnapshotDisplay— статическая осциллограмма
BLoC и состояния
AudioRecordingBloc— конечный автомат записи:$Idle → $Recording → $Finished / $Error; события$Start,$Stop,$ResetAudioPlayerBloc— конечный автомат воспроизведения:$Idle / $Playing / $Paused / $Completed / $Error; события$Play,$Pause,$Stop,$Seek
Анализ и модели
SpectrumAnalyzer— чистый Dart FFT с оконной функцией Ханна, 50 % перекрытием, авто-нормализацией; методыanalyze(),computeTimeline(),analyzeRaw()SpectrumConfig— настройки FFT:fftSize,frequencyBands,frequencyMin/Max,sampleRate,dynamicRangeDb,displayTypeRecordingResult— контейнер результата:filePath,wavBytes,duration,waveformSamples,rmsSamples,snapshotSamples,spectrumData,spectrumTimeline
Утилиты
AudioUtils—encodeWav(),wavToSamples(),formatDuration()PlatformUtils—isWeb,hasMicrophonePermission()
Platform support #
| Platform | Recording | Playback |
|---|---|---|
| Android | ✓ file | ✓ file |
| iOS | ✓ file | ✓ file |
| macOS | ✓ file | ✓ file |
| Windows | ✓ file | ✓ file |
| Linux | ✓ file | ✓ file |
| Web | ✓ WAV bytes | ✓ WAV bytes |