Ashlar UI Ashlar UI ashlarui

Soundwave

How loud a voice is, as a row of bars, from the levels your audio gives.

Soundwave, as it looks in an app Soundwave, as it looks in an app, dark
// The soundwave demo: the page's picture and what runs live.
import 'dart:math' as math;

import 'package:flutter/widgets.dart';

import 'ui/soundwave.dart';
import 'ui/theme.dart';

/// Levels as speech gives them: louder in bursts, quiet between words.
final _levels = [
  for (var i = 0; i < 90; i++) (math.sin(i / 3) * .5 + .5) * (math.sin(i / 11) * .4 + .6) * (i % 17 < 3 ? .15 : 1),
];

class SoundwaveDemo extends StatelessWidget {
  const SoundwaveDemo({super.key});

  @override
  Widget build(BuildContext context) {
    final t = AshlarTheme.of(context);
    return SizedBox(
      width: 360,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Soundwave(levels: _levels, height: 40, semanticLabel: 'Listening'),
          const SizedBox(height: 8),
          Text('Listening… 0:07', style: t.text.sm.copyWith(color: t.colors.mutedForeground)),
          const SizedBox(height: 20),
          Soundwave(levels: const [], height: 24),
        ],
      ),
    );
  }
}

Add it

In your Flutter project:

$ ashlar add soundwave

It writes theme.dart, palette.dart and soundwave.dart into lib/ui, keeping any you already have.

API

Read from the source above: each parameter with its type, its default, and what it is for.

Soundwaveclass

How loud something is, as a row of rounded bars: a person speaking to an assistant, or a voice note. Give it levels from 0 (silence) to 1, newest last, as your audio gives them; it draws as many of the newest as fit, and a row of dots when there are none. Listening to the microphone needs a plugin, so that part is your app's.

Parameter Type Default What it is for
levelsrequired List<double> From 0 to 1, oldest first.
heightdouble32
barWidthdouble3
gapdouble2
color Color? The bars' colour; the foreground when not given.
semanticLabel String? What a screen reader hears: "Listening", "Voice note, 12 seconds".

Source

What lands in your app. It is yours from then on: change it as you would your own code, and ashlar add will not write over it.

lib/ui/soundwave.dart 112 lines
// Ashlar UI: Soundwave. Copied into your app by `ashlar add soundwave`; it is yours to change.

import 'dart:math' as math;

import 'package:flutter/widgets.dart';

import 'theme.dart';

/// How loud something is, as a row of rounded bars: a person speaking to an assistant, or
/// a voice note. Give it [levels] from 0 (silence) to 1, newest last, as your audio gives
/// them; it draws as many of the newest as fit, and a row of dots when there are none.
/// Listening to the microphone needs a plugin, so that part is your app's.
///
/// ```dart
/// Soundwave(levels: recorder.levels, semanticLabel: 'Listening')
/// ```
class Soundwave extends StatelessWidget {
  const Soundwave({
    super.key,
    required this.levels,
    this.height = 32,
    this.barWidth = 3,
    this.gap = 2,
    this.color,
    this.semanticLabel,
  });

  /// From 0 to 1, oldest first.
  final List<double> levels;
  final double height, barWidth, gap;

  /// The bars' colour; the foreground when not given.
  final Color? color;

  /// What a screen reader hears: "Listening", "Voice note, 12 seconds".
  final String? semanticLabel;

  @override
  Widget build(BuildContext context) {
    final c = AshlarTheme.of(context).colors;
    return Semantics(
      label: semanticLabel,
      image: semanticLabel != null,
      excludeSemantics: true,
      child: SizedBox(
        height: height,
        width: double.infinity,
        child: CustomPaint(
          painter: _Bars(
            levels: levels,
            barWidth: barWidth,
            gap: gap,
            color: color ?? c.foreground,
            quiet: c.mutedForeground.withValues(alpha: .5),
            rtl: Directionality.of(context) == TextDirection.rtl,
          ),
        ),
      ),
    );
  }
}

class _Bars extends CustomPainter {
  _Bars({
    required this.levels,
    required this.barWidth,
    required this.gap,
    required this.color,
    required this.quiet,
    required this.rtl,
  });

  final List<double> levels;
  final double barWidth, gap;
  final Color color, quiet;
  final bool rtl;

  @override
  void paint(Canvas canvas, Size size) {
    final step = barWidth + gap;
    final fit = math.max(1, ((size.width + gap) / step).floor());
    final shown = levels.length > fit
        ? levels.sublist(levels.length - fit)
        : levels;
    final mid = size.height / 2;
    final paint = Paint();
    // The newest bar at the end the text reads to; the bars before it towards the start.
    for (var i = 0; i < fit; i++) {
      final k = i - (fit - shown.length);
      final level = k < 0 ? 0.0 : shown[k].clamp(0.0, 1.0);
      final h = math.max(barWidth, level * size.height);
      final x = rtl ? size.width - (i + 1) * step + gap : i * step;
      paint.color = k < 0 || level == 0 ? quiet : color;
      canvas.drawRRect(
        RRect.fromRectAndRadius(
          Rect.fromLTWH(x, mid - h / 2, barWidth, h),
          Radius.circular(barWidth / 2),
        ),
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(_Bars old) =>
      old.levels != levels ||
      old.color != color ||
      old.quiet != quiet ||
      old.barWidth != barWidth ||
      old.gap != gap ||
      old.rtl != rtl;
}