Files
LBJ_Console/lib/widgets/audio_waterfall_widget.dart

235 lines
6.6 KiB
Dart

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<int> 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<AudioWaterfallWidget> createState() => _AudioWaterfallWidgetState();
}
class _AudioWaterfallWidgetState extends State<AudioWaterfallWidget> {
static const platform = MethodChannel('org.noxylva.lbjconsole/audio_input');
final _cache = _WaterfallCache.instance;
ui.Image? _waterfallImage;
List<double> _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<double>();
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<void> _rebuildImage() async {
_imageNeedsUpdate = false;
final completer = Completer<ui.Image>();
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<double> 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;
}