monowave 0.4.0 copy "monowave: ^0.4.0" to clipboard
monowave: ^0.4.0 copied to clipboard

Headless audio for Flutter: microphone capture, waveform peaks, and non-destructive editing, from one C core across all six Flutter targets.

example/lib/main.dart

// Monokit re-exports `package:flutter/widgets.dart`, so this one import is the
// whole UI layer. Material is not used anywhere in this app, which is the
// point: monowave ships no widgets, so a host is free to bring any design
// system - and every painter here is host code, written to be copied.

import 'dart:io';

import 'package:monokit_ui/monokit_ui.dart';
import 'package:monowave/monowave.dart';

import 'editor/editor_page.dart';
import 'fixtures.dart';
import 'record_page.dart';
import 'result_page.dart';
import 'ui/wave_chrome.dart';

/// The version the front door advertises, and the one `widget_test.dart`
/// asserts on.
///
/// One constant rather than a literal in each: the badge said `0.2.0` and the
/// test agreed with it for two releases, because agreeing with each other is
/// all two hardcoded copies ever have to do. Bump it with `pubspec.yaml` at the
/// root - nothing reads that file at runtime.
const monowaveVersion = '0.3.1';

void main() => runApp(const MonowaveExampleApp());

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

  @override
  Widget build(BuildContext context) {
    // Monokit leaves haptics off until the host asks, since a haptic is a
    // product decision rather than a component one. Recording is exactly the
    // case for turning them on: the moment a take starts and stops has to be
    // felt, because the screen is not where your attention is.
    const haptics = MonokitHaptics(enabled: true);

    return MonokitApp(
      title: 'monowave',
      theme: MonokitThemeData.light().copyWith(haptics: haptics),
      darkTheme: MonokitThemeData.dark().copyWith(haptics: haptics),
      themeMode: MonokitThemeMode.dark,
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String? _error;
  bool _isLoading = false;

  Future<void> _run(Future<void> Function() body) async {
    setState(() => _error = null);
    try {
      await body();
    } on Object catch (failure) {
      if (mounted) setState(() => _error = failure.toString());
    }
  }

  Future<void> _record() => _run(() async {
    final path = await Navigator.of(
      context,
    ).push<String>(WaveRoute(builder: (_) => const RecordPage()));
    if (path != null && mounted) await _edit(path);
  });

  Future<void> _openSample() => _run(() async {
    // The system takes a moment to write the fixture; without this the row
    // gives no sign that the tap landed.
    setState(() => _isLoading = true);
    try {
      await _edit(await Fixtures.sourceFile());
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  });

  Future<void> _edit(String path) async {
    final peaks = await MonowavePlatform.instance.decodeBytes(
      await File(path).readAsBytes(),
    );
    if (!mounted) {
      peaks.dispose();
      return;
    }

    final exported = await Navigator.of(context).push<String>(
      WaveRoute(
        builder: (_) => EditorPage(source: path, peaks: peaks),
      ),
    );
    peaks.dispose();

    if (exported == null || !mounted) return;
    await Navigator.of(
      context,
    ).push(WaveRoute(builder: (_) => ResultPage(path: exported)));
  }

  @override
  Widget build(BuildContext context) {
    final theme = MonokitTheme.of(context);

    // Centred rather than stacked from the top: there is not enough here to
    // fill a phone, and content crammed under the status bar with a void
    // beneath it reads as unfinished rather than as composed.
    return MonoScreen(
      body: LayoutBuilder(
        builder: (context, constraints) => SingleChildScrollView(
          padding: EdgeInsets.symmetric(
            horizontal: theme.spacing.xl,
            vertical: theme.spacing.xxl,
          ),
          child: ConstrainedBox(
            constraints: BoxConstraints(
              minHeight: (constraints.maxHeight - theme.spacing.xxl * 2).clamp(
                0.0,
                double.infinity,
              ),
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                const _Hero(),
                SizedBox(height: theme.spacing.xxxl),
                _RecordTile(onPressed: _record),
                SizedBox(height: theme.spacing.md),
                MonoSurface(
                  padding: EdgeInsets.symmetric(vertical: theme.spacing.xs),
                  child: _SampleRow(busy: _isLoading, onPressed: _openSample),
                ),
                SizedBox(height: theme.spacing.xxxl),
                Text(
                  'IN THE EDITOR',
                  style: theme.typography.labelMedium.copyWith(
                    color: theme.colors.foregroundSubtle,
                    fontSize: 10,
                    letterSpacing: 1.2,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                SizedBox(height: theme.spacing.md),
                const _CapabilityCloud(),
                if (_error != null) ...<Widget>[
                  SizedBox(height: theme.spacing.xl),
                  MonoAlert(
                    variant: MonoAlertVariant.destructive,
                    title: const Text('Something went wrong'),
                    description: Text(_error!),
                  ),
                ],
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _Hero extends StatelessWidget {
  const _Hero();

  @override
  Widget build(BuildContext context) {
    final theme = MonokitTheme.of(context);
    final colors = theme.colors;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Row(
          children: <Widget>[
            Container(
              width: 34,
              height: 34,
              decoration: BoxDecoration(
                color: colors.tint,
                borderRadius: BorderRadius.circular(theme.radii.md),
              ),
              child: Center(
                child: MonoIcon(MonoIcons.mic, size: 20, color: colors.canvas),
              ),
            ),
            SizedBox(width: theme.spacing.md),
            Text(
              'monowave',
              style: theme.typography.headlineLarge.copyWith(
                color: colors.foreground,
              ),
            ),
            const Spacer(),
            const MonoBadge(
              // `outline` is gone in monokit_ui 3.0.0: the variant ladder is the
              // state vocabulary now, and `primary`, `secondary` and `outline`
              // all collapsed into `neutral`. A version chip reports no status,
              // so `neutral` is the one it was always asking for.
              variant: MonoBadgeVariant.neutral,
              size: MonoBadgeSize.sm,
              child: Text(monowaveVersion),
            ),
          ],
        ),
        SizedBox(height: theme.spacing.lg),
        Text(
          'Headless capture, waveforms and editing.\n'
          'Every pixel of this app is host code.',
          style: theme.typography.bodyLarge.copyWith(
            color: colors.foregroundMuted,
          ),
        ),
      ],
    );
  }
}

/// The way in. Sized as a tile rather than listed as a button because it is the
/// screen's subject, not a menu option.
class _RecordTile extends StatelessWidget {
  const _RecordTile({required this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    final theme = MonokitTheme.of(context);
    final colors = theme.colors;

    return MonoPressable(
      onPressed: onPressed,
      semanticLabel: 'Record',
      child: (context, states) => AnimatedScale(
        duration: theme.motion.reduced(context, theme.motion.fast),
        curve: theme.motion.standard,
        scale: states.contains(MonoState.pressed) ? 0.97 : 1,
        child: Container(
          height: 148,
          padding: EdgeInsets.all(theme.spacing.lg),
          decoration: BoxDecoration(
            color: colors.primary,
            borderRadius: BorderRadius.circular(theme.radii.xxl),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              MonoIcon(MonoIcons.mic, size: 22, color: colors.onPrimary),
              const Spacer(),
              Text(
                'Record',
                style: theme.typography.titleLarge.copyWith(
                  color: colors.onPrimary,
                ),
              ),
              SizedBox(height: theme.spacing.xs),
              Text(
                'Reduced on the audio thread',
                style: theme.typography.bodyMedium.copyWith(
                  color: colors.onPrimary.withValues(alpha: 0.7),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _SampleRow extends StatelessWidget {
  const _SampleRow({required this.busy, required this.onPressed});

  final bool busy;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    final theme = MonokitTheme.of(context);
    final colors = theme.colors;

    return MonoPressable(
      onPressed: busy ? null : onPressed,
      enabled: !busy,
      semanticLabel: 'Open the bundled sample',
      child: (context, states) => Container(
        color: states.contains(MonoState.pressed) ? colors.fill : null,
        padding: EdgeInsets.symmetric(
          horizontal: theme.spacing.lg,
          vertical: theme.spacing.md + 2,
        ),
        child: Row(
          children: <Widget>[
            MonoIcon(MonoIcons.play, size: 18, color: colors.foregroundMuted),
            SizedBox(width: theme.spacing.md),
            Expanded(
              child: Text(
                'Open the bundled sample',
                style: theme.typography.bodyLarge.copyWith(
                  color: colors.foreground,
                ),
              ),
            ),
            if (busy)
              const MonoSpinner(size: 16)
            else
              MonoIcon(
                MonoIcons.chevronRight,
                size: 16,
                color: colors.foregroundSubtle,
              ),
          ],
        ),
      ),
    );
  }
}

/// What the editor can do, stated once so the front door is not silent about
/// the actual surface area.
class _CapabilityCloud extends StatelessWidget {
  const _CapabilityCloud();

  // monokit's catalogue has no scissors or waveform, so these lean on the
  // nearest honest glyph rather than reusing one for two different things.
  // monolens solves the same problem with its own LensIcons set; if this app
  // grows, that is the move.
  static const List<(MonoIconData, String)> _items = [
    (MonoIcons.mic, 'Capture'),
    (MonoIcons.play, 'Scrub'),
    (MonoIcons.search, 'Zoom'),
    (MonoIcons.location, 'Snap'),
    (MonoIcons.filter, 'Trim'),
    (MonoIcons.close, 'Delete'),
    (MonoIcons.sparkles, 'Fade'),
    (MonoIcons.arrowRight, 'Undo'),
    (MonoIcons.download, 'Export WAV'),
  ];

  @override
  Widget build(BuildContext context) {
    final theme = MonokitTheme.of(context);
    final colors = theme.colors;

    return Wrap(
      spacing: theme.spacing.sm,
      runSpacing: theme.spacing.sm,
      children: <Widget>[
        for (final (icon, label) in _items)
          Container(
            padding: EdgeInsets.symmetric(
              horizontal: theme.spacing.md,
              vertical: theme.spacing.sm,
            ),
            decoration: BoxDecoration(
              color: colors.fill,
              borderRadius: BorderRadius.circular(theme.radii.full),
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                MonoIcon(icon, size: 15, color: colors.foregroundMuted),
                SizedBox(width: theme.spacing.sm - 2),
                Text(
                  label,
                  style: theme.typography.labelMedium.copyWith(
                    color: colors.foregroundMuted,
                  ),
                ),
              ],
            ),
          ),
      ],
    );
  }
}
1
likes
150
points
202
downloads

Documentation

Documentation
API reference

Publisher

verified publishermonorithm.dev

Weekly Downloads

Headless audio for Flutter: microphone capture, waveform peaks, and non-destructive editing, from one C core across all six Flutter targets.

Repository (GitHub)
View/report issues

Topics

#audio #waveform #ffi #media

License

MIT (license)

Dependencies

code_assets, ffi, flutter, hooks, native_toolchain_c

More

Packages that depend on monowave