Files
namida/lib/controller/waveform_controller.dart
2025-12-26 00:05:39 +02:00

176 lines
6.1 KiB
Dart

import 'package:namida/controller/platform/waveform_extractor/waveform_extractor.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/controller/vibrator_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/utils.dart';
class WaveformController {
static WaveformController get inst => _instance;
static final WaveformController _instance = WaveformController._internal();
WaveformController._internal();
int get _defaultUserBarsCount => settings.waveformTotalBars.value;
RxBaseCore<bool> get isWaveformUIEnabled => _isWaveformUIEnabled;
final _isWaveformUIEnabled = false.obso;
late final currentWaveformUIRx = List<double>.filled(_defaultUserBarsCount, -1, growable: false).obs;
List<double> _currentWaveform = [];
var _currentScaleLookup = <double>[];
int _currentScaleMaxIndex = -1;
bool get isDummy => _currentWaveform.isEmpty;
void resetWaveform() {
_currentWaveform = [];
_currentScaleLookup = [];
_currentScaleMaxIndex = -1;
_isWaveformUIEnabled.value = false;
}
/// Extracts waveform data from a given track, or immediately read from .wave file if exists, then assigns wavedata to [_currentWaveform].
Future<void> generateWaveform({required String path, required Duration duration, required bool Function(String path) stillPlaying}) async {
final samplePerSecond = _waveformExtractor.getSampleRateFromDuration(
audioDuration: duration,
maxSampleRate: 400,
scaleFactor: 0.4,
);
List<num> waveformData = [];
await Future.wait([
_waveformExtractor.extractWaveformData(path, samplesPerSecond: samplePerSecond).catchError((_) => <num>[]).then((value) async {
if (value.isNotEmpty) {
waveformData = value;
} else if (stillPlaying(path)) {
waveformData = await _waveformExtractor.extractWaveformData(path).catchError((_) => <num>[]); // re-extracting without samples (out of boundaries error)
}
}),
Future.delayed(const Duration(milliseconds: 800)),
]);
if (waveformData.isNotEmpty && stillPlaying(path)) {
// ----- Updating [_currentWaveform]
const maxWaveformCount = 2000;
final numberOfScales = duration.inMilliseconds ~/ _positionDividor;
final downscaledLists = _downscaledWaveformLists(
targetSizes: [maxWaveformCount, numberOfScales],
original: waveformData,
);
_currentWaveform = downscaledLists[maxWaveformCount] ?? [];
_currentScaleLookup = downscaledLists[numberOfScales] ?? [];
_currentScaleMaxIndex = _currentScaleLookup.length - 1;
calculateUIWaveform();
}
}
void calculateUIWaveform() async {
if (_currentWaveform.isEmpty) return;
final waveform = _getCalculatedUIWaveform(
targetSize: _defaultUserBarsCount,
original: _currentWaveform,
);
currentWaveformUIRx.value = waveform;
_isWaveformUIEnabled.value = true;
}
static List<double> _getCalculatedUIWaveform({required List<double> original, required int targetSize}) {
const maxClamping = 64.0;
final clamping = original.isEmpty ? null : maxClamping;
final downscaled = original.changeListSize(
targetSize: targetSize,
multiplier: 0.9,
clampToMax: clamping,
enforceClampToMax: (minValue, maxValue) => false,
);
return downscaled;
}
static Map<num, List<double>> _downscaledWaveformLists({required List<num> original, required List<int> targetSizes}) {
final newLists = <num, List<double>>{};
const maxClamping = 64.0;
targetSizes.loop((targetSize) {
newLists[targetSize] = original.changeListSize(
targetSize: targetSize,
clampToMax: maxClamping,
enforceClampToMax: (minValue, maxValue) {
// -- checking if max value is greater than `maxClamping`;
// -- since clamping tries to normalize among all lists variations
return maxValue > maxClamping * 2.0;
},
);
});
return newLists;
}
static const _positionDividor = 50;
static const _positionDividorWithOffset = isKuru ? _positionDividor - 3.58 : 50;
double getCurrentAnimatingScale(int positionInMs) {
return _getCurrentAnimatingScaleGeneral(positionInMs, settings.animatingThumbnailIntensity.value);
}
double getCurrentAnimatingScaleLyrics(int positionInMs) {
return _getCurrentAnimatingScaleGeneral(positionInMs, settings.animatingThumbnailIntensityLyrics.value);
}
double getCurrentAnimatingScaleMinimized(int positionInMs) {
return _getCurrentAnimatingScaleGeneral(positionInMs, settings.animatingThumbnailIntensityMinimized.value);
}
double _getCurrentAnimatingScaleGeneral(int positionInMs, int intensity) {
if (intensity > 0) {
final posInMap = positionInMs ~/ _positionDividorWithOffset;
final dynamicScale = posInMap > _currentScaleMaxIndex ? _defaultMinimumScale : _currentScaleLookup[posInMap];
final finalScale = dynamicScale * intensity * 0.00005;
if (finalScale.isNaN || finalScale > 0.35) return _defaultMinimumScale;
if (settings.extra.mediaWaveHaptic == true) {
_vibrateHaptic(finalScale);
}
return finalScale;
}
return _defaultMinimumScale;
}
void _vibrateHaptic(double scale) {
if (Player.inst.isPlaying.value == false) return;
if (scale > _defaultMinimumScale) {
final haptic = VibratorController.interfaceHapticIgnoreSettings;
if (scale < 0.03) {
haptic.light();
haptic.light();
haptic.light();
} else if (scale < 0.06) {
haptic.light();
haptic.medium();
haptic.medium();
haptic.light();
} else if (scale < 0.10) {
haptic.medium();
haptic.high();
haptic.high();
haptic.high();
haptic.medium();
} else {
haptic.high();
haptic.high();
haptic.high();
haptic.high();
}
}
}
static const _defaultMinimumScale = 0.01;
final _waveformExtractor = WaveformExtractor.platform()..init();
}