import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class _WaterfallCache { static final _WaterfallCache instance = _WaterfallCache._(); _WaterfallCache._(); static const int waterfallWidth = 500; static const int waterfallHeight = 500; Uint32List? pixels; late List colorLUT; void initialize() { if (pixels == null) { pixels = Uint32List(waterfallWidth * waterfallHeight); for (int i = 0; i < pixels!.length; i++) { pixels![i] = 0xFF000033; } _buildColorLUT(); } } void _buildColorLUT() { colorLUT = List.filled(256, 0); for (int i = 0; i < 256; i++) { final intensity = i / 255.0; final color = _intensityToColor(intensity); colorLUT[i] = (color.alpha << 24) | (color.red << 16) | (color.green << 8) | color.blue; } } Color _intensityToColor(double intensity) { if (intensity < 0.2) { return Color.lerp( const Color(0xFF000033), const Color(0xFF0000FF), intensity / 0.2)!; } else if (intensity < 0.4) { return Color.lerp(const Color(0xFF0000FF), const Color(0xFF00FFFF), (intensity - 0.2) / 0.2)!; } else if (intensity < 0.6) { return Color.lerp(const Color(0xFF00FFFF), const Color(0xFF00FF00), (intensity - 0.4) / 0.2)!; } else if (intensity < 0.8) { return Color.lerp(const Color(0xFF00FF00), const Color(0xFFFFFF00), (intensity - 0.6) / 0.2)!; } else { return Color.lerp(const Color(0xFFFFFF00), const Color(0xFFFF0000), (intensity - 0.8) / 0.2)!; } } } class AudioWaterfallWidget extends StatefulWidget { const AudioWaterfallWidget({super.key}); @override State createState() => _AudioWaterfallWidgetState(); } class _AudioWaterfallWidgetState extends State { static const platform = MethodChannel('org.noxylva.lbjconsole/audio_input'); final _cache = _WaterfallCache.instance; ui.Image? _waterfallImage; List _currentSpectrum = []; Timer? _updateTimer; bool _imageNeedsUpdate = false; @override void initState() { super.initState(); _cache.initialize(); _startUpdating(); } @override void dispose() { _updateTimer?.cancel(); _waterfallImage?.dispose(); super.dispose(); } void _startUpdating() { _updateTimer = Timer.periodic(const Duration(milliseconds: 20), (timer) async { try { final result = await platform.invokeMethod('getSpectrum'); if (result != null && result is List && mounted) { final fftData = result.cast(); final pixels = _cache.pixels!; pixels.setRange( _WaterfallCache.waterfallWidth, pixels.length, pixels, 0); for (int i = 0; i < _WaterfallCache.waterfallWidth && i < fftData.length; i++) { final intensity = (fftData[i].clamp(0.0, 1.0) * 255).toInt(); pixels[i] = _cache.colorLUT[intensity]; } _currentSpectrum = fftData; _imageNeedsUpdate = true; if (_imageNeedsUpdate) { _rebuildImage(); } } } catch (e) {} }); } Future _rebuildImage() async { _imageNeedsUpdate = false; final completer = Completer(); ui.decodeImageFromPixels( _cache.pixels!.buffer.asUint8List(), _WaterfallCache.waterfallWidth, _WaterfallCache.waterfallHeight, ui.PixelFormat.bgra8888, (image) => completer.complete(image), ); final newImage = await completer.future; if (mounted) { setState(() { _waterfallImage?.dispose(); _waterfallImage = newImage; }); } else { newImage.dispose(); } } @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Container( height: 80, width: double.infinity, decoration: BoxDecoration( color: Colors.black, border: Border( left: BorderSide(color: Colors.cyan.withOpacity(0.3), width: 2), right: BorderSide(color: Colors.cyan.withOpacity(0.3), width: 2), top: BorderSide(color: Colors.cyan.withOpacity(0.3), width: 2), ), ), child: _currentSpectrum.isEmpty ? const Center( child: CircularProgressIndicator( color: Colors.cyan, strokeWidth: 2)) : CustomPaint(painter: _SpectrumPainter(_currentSpectrum)), ), Container( height: 100, width: double.infinity, decoration: BoxDecoration( color: Colors.black, border: Border( left: BorderSide(color: Colors.cyan.withOpacity(0.3), width: 2), right: BorderSide(color: Colors.cyan.withOpacity(0.3), width: 2), bottom: BorderSide(color: Colors.cyan.withOpacity(0.3), width: 2), ), ), child: _waterfallImage == null ? const Center( child: CircularProgressIndicator(color: Colors.cyan)) : CustomPaint(painter: _WaterfallImagePainter(_waterfallImage!)), ), ], ); } } class _SpectrumPainter extends CustomPainter { final List spectrum; _SpectrumPainter(this.spectrum); @override void paint(Canvas canvas, Size size) { if (spectrum.isEmpty) return; final path = Path(); final binWidth = size.width / spectrum.length; for (int i = 0; i < spectrum.length; i++) { final x = i * binWidth; final y = size.height - (spectrum[i].clamp(0.0, 1.0) * size.height); i == 0 ? path.moveTo(x, y) : path.lineTo(x, y); } canvas.drawPath( path, Paint() ..color = Colors.cyan ..strokeWidth = 0.5 ..style = PaintingStyle.stroke ..isAntiAlias = true ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round); } @override bool shouldRepaint(_SpectrumPainter old) => true; } class _WaterfallImagePainter extends CustomPainter { final ui.Image image; _WaterfallImagePainter(this.image); @override void paint(Canvas canvas, Size size) { canvas.drawImageRect( image, Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), Rect.fromLTWH(0, 0, size.width, size.height), Paint()..filterQuality = FilterQuality.none, ); } @override bool shouldRepaint(_WaterfallImagePainter old) => old.image != image; }