diff --git a/lib/controller/platform/base.dart b/lib/controller/platform/base.dart index 26542d87..3d23f8c1 100644 --- a/lib/controller/platform/base.dart +++ b/lib/controller/platform/base.dart @@ -1,5 +1,9 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; + +import 'package:path/path.dart' as p; + class NamidaPlatformBuilder { static T init({required T Function() android, required T Function() windows}) { return switch (Platform.operatingSystem) { @@ -8,4 +12,14 @@ class NamidaPlatformBuilder { _ => throw UnimplementedError(), }; } + + static String getExecutablesPath() { + var processDir = p.dirname(Platform.resolvedExecutable); + if (kDebugMode) { + var midway = r'..\..\..\..\..\..\ffmpeg_build'; + return p.normalize(p.join(processDir, midway)); + } else { + return p.join(processDir, 'bin'); + } + } } diff --git a/lib/controller/platform/ffmpeg_executer/ffmpeg_executer.dart b/lib/controller/platform/ffmpeg_executer/ffmpeg_executer.dart index 1d67624b..a6a4038a 100644 --- a/lib/controller/platform/ffmpeg_executer/ffmpeg_executer.dart +++ b/lib/controller/platform/ffmpeg_executer/ffmpeg_executer.dart @@ -1,8 +1,6 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart'; - import 'package:ffmpeg_kit_flutter_min/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_min/ffmpeg_kit_config.dart'; import 'package:ffmpeg_kit_flutter_min/ffprobe_kit.dart'; diff --git a/lib/controller/platform/ffmpeg_executer/ffmpeg_executer_windows.dart b/lib/controller/platform/ffmpeg_executer/ffmpeg_executer_windows.dart index f774ed29..a67662c9 100644 --- a/lib/controller/platform/ffmpeg_executer/ffmpeg_executer_windows.dart +++ b/lib/controller/platform/ffmpeg_executer/ffmpeg_executer_windows.dart @@ -9,16 +9,9 @@ class _FFMPEGExecuterWindows extends FFMPEGExecuter { @override void init() { - if (kDebugMode) { - var processDir = p.dirname(Platform.resolvedExecutable); - var midway = p.normalize(r'..\..\..\..\..\..\ffmpeg_build'); - ffmpegExePath = p.normalize(p.join(processDir, midway, 'ffmpeg.exe')); - ffprobeExePath = p.normalize(p.join(processDir, midway, 'ffprobe.exe')); - } else { - var processDir = p.dirname(Platform.resolvedExecutable); - ffmpegExePath = p.join(processDir, 'bin', 'ffmpeg.exe'); - ffprobeExePath = p.join(processDir, 'bin', 'ffprobe.exe'); - } + final executablesPath = NamidaPlatformBuilder.getExecutablesPath(); + ffmpegExePath = p.join(executablesPath, 'ffmpeg.exe'); + ffprobeExePath = p.join(executablesPath, 'ffprobe.exe'); } @override diff --git a/lib/controller/platform/waveform_extractor/waveform_extractor.dart b/lib/controller/platform/waveform_extractor/waveform_extractor.dart new file mode 100644 index 00000000..dcebe5a6 --- /dev/null +++ b/lib/controller/platform/waveform_extractor/waveform_extractor.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:waveform_extractor/waveform_extractor.dart' as pkgwaveform; + +import 'package:namida/class/file_parts.dart'; +import 'package:namida/controller/ffmpeg_controller.dart'; +import 'package:namida/controller/platform/base.dart'; +import 'package:namida/core/constants.dart'; +import 'package:namida/core/extensions.dart'; + +part 'waveform_extractor_android.dart'; +part 'waveform_extractor_base.dart'; +part 'waveform_extractor_windows.dart'; diff --git a/lib/controller/platform/waveform_extractor/waveform_extractor_android.dart b/lib/controller/platform/waveform_extractor/waveform_extractor_android.dart new file mode 100644 index 00000000..6e1c7cc9 --- /dev/null +++ b/lib/controller/platform/waveform_extractor/waveform_extractor_android.dart @@ -0,0 +1,25 @@ +part of 'waveform_extractor.dart'; + +class _WaveformExtractorAndroid extends WaveformExtractor { + _WaveformExtractorAndroid._internal(); + + final _extractor = pkgwaveform.WaveformExtractor(); + + @override + void init() {} + + @override + Future> extractWaveformData( + String source, { + bool useCache = true, + String? cacheKey, + int? samplesPerSecond, + }) { + return _extractor.extractWaveformDataOnly( + source, + useCache: useCache, + cacheKey: cacheKey, + samplePerSecond: samplesPerSecond, + ); + } +} diff --git a/lib/controller/platform/waveform_extractor/waveform_extractor_base.dart b/lib/controller/platform/waveform_extractor/waveform_extractor_base.dart new file mode 100644 index 00000000..d38d8b6f --- /dev/null +++ b/lib/controller/platform/waveform_extractor/waveform_extractor_base.dart @@ -0,0 +1,31 @@ +part of 'waveform_extractor.dart'; + +abstract class WaveformExtractor { + static WaveformExtractor platform() { + return NamidaPlatformBuilder.init( + android: () => _WaveformExtractorAndroid._internal(), + windows: () => _WaveformExtractorWindows._internal(NamidaFFMPEG.inst), + ); + } + + void init(); + + Future> extractWaveformData( + String source, { + bool useCache = true, + String? cacheKey, + int? samplesPerSecond, + }); + + int getSampleRateFromDuration({ + required Duration audioDuration, + int maxSampleRate = 400, + double scaleFactor = 0.4, + }) { + return pkgwaveform.WaveformExtractor.getSampleRateFromDuration( + audioDuration: audioDuration, + maxSampleRate: maxSampleRate, + scaleFactor: scaleFactor, + ); + } +} diff --git a/lib/controller/platform/waveform_extractor/waveform_extractor_windows.dart b/lib/controller/platform/waveform_extractor/waveform_extractor_windows.dart new file mode 100644 index 00000000..1499a83f --- /dev/null +++ b/lib/controller/platform/waveform_extractor/waveform_extractor_windows.dart @@ -0,0 +1,111 @@ +part of 'waveform_extractor.dart'; + +class _WaveformExtractorWindows extends WaveformExtractor { + final NamidaFFMPEG ffmpegController; + _WaveformExtractorWindows._internal(this.ffmpegController); + + late String ffmpegExePath; + late String waveformExePath; + + static const _supportedFormats = {'wav', 'flac', 'mp3', 'ogg', 'opus', 'webm'}; + + @override + void init() { + final executablesPath = NamidaPlatformBuilder.getExecutablesPath(); + ffmpegExePath = p.join(executablesPath, 'ffmpeg.exe'); + waveformExePath = p.join(executablesPath, 'audiowaveform.exe'); + } + + @override + Future> extractWaveformData( + String source, { + bool useCache = true, + String? cacheKey, + int? sampleRate, + int? samplesPerSecond, + }) async { + File? cacheFile; + if (useCache) { + final cacheDir = AppDirs.APP_CACHE; + cacheKey ??= source.hashCode.toString(); + cacheFile = FileParts.join(cacheDir, '$cacheKey.txt'); + final cachedWaveform = _parseWaveformFile(cacheFile); + if (cachedWaveform != null && cachedWaveform.isNotEmpty) return cachedWaveform; + } + final wavelist = await _extractWaveformNew( + source, + cacheFile: cacheFile, + sampleRate: sampleRate, + samplesPerSecond: samplesPerSecond, + ); + if (cacheFile != null) _encodeWaveformFile(cacheFile, wavelist); + return wavelist; + } + + List? _parseWaveformFile(File cacheFile) { + if (cacheFile.existsSync()) return LineSplitter.split(cacheFile.readAsStringSync()).map((e) => num.parse(e)).toList(); + return null; + } + + void _encodeWaveformFile(File cacheFile, List list) { + if (list.isNotEmpty) cacheFile.writeAsStringSync(list.join('\n')); + } + + Future> _extractWaveformNew( + String source, { + File? cacheFile, + int? sampleRate, + int? samplesPerSecond, + }) async { + final extension = source.getExtension; + const multiplier = 0.05; + final waveformOutputOptions = [ + '--output-format', + 'json', + '-b', + '8', + if (samplesPerSecond != null) ...[ + '--pixels-per-second', + '$samplesPerSecond', + ], + ]; + ProcessResult audioWaveformGenerate; + bool needsConversion = true; + String? convertedFilePath; + final probablyGoodFormat = _supportedFormats.contains(extension); + if (probablyGoodFormat) { + try { + audioWaveformGenerate = await Process.run(waveformExePath, ['-i', source, '--input-format', extension, ...waveformOutputOptions]); + needsConversion = false; + } catch (_) {} + } + if (needsConversion) { + convertedFilePath = FileParts.joinPath(AppDirs.APP_CACHE, '${source.hashCode}.wav'); + final ffmpegConvert = await Process.run( + ffmpegExePath, + ['-i', source, '-f', 'wav', convertedFilePath], + ); + if (ffmpegConvert.exitCode != 0) return []; + } + + audioWaveformGenerate = await Process.run( + waveformExePath, + ['-i', convertedFilePath ?? source, '--input-format', 'wav', ...waveformOutputOptions], + ); + if (convertedFilePath != null) File(convertedFilePath).delete().catchError((_) => File('')); + + final data = jsonDecode(audioWaveformGenerate.stdout)?['data'] as List?; + final finalList = data?.cast() ?? []; + + final combinedList = []; + final maxLength = finalList.length % 2 == 0 ? finalList.length : finalList.length - 1; // ensure even number for pair combination + for (int i = 0; i < maxLength; i += 2) { + final left = finalList[i].abs(); + final right = finalList[i + 1].abs(); + final combined = left > right ? left : right; + final finalnumber = combined * multiplier; + combinedList.add(finalnumber); + } + return combinedList; + } +} diff --git a/lib/controller/waveform_controller.dart b/lib/controller/waveform_controller.dart index 5f47385e..7e17f849 100644 --- a/lib/controller/waveform_controller.dart +++ b/lib/controller/waveform_controller.dart @@ -1,5 +1,4 @@ -import 'package:waveform_extractor/waveform_extractor.dart'; - +import 'package:namida/controller/platform/waveform_extractor/waveform_extractor.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/utils.dart'; @@ -36,13 +35,13 @@ class WaveformController { scaleFactor: 0.4, ); - List waveformData = []; + List waveformData = []; await Future.wait([ - _waveformExtractor.extractWaveformDataOnly(path, samplePerSecond: samplePerSecond).catchError((_) => []).then((value) async { + _waveformExtractor.extractWaveformData(path, samplesPerSecond: samplePerSecond).catchError((_) => []).then((value) async { if (value.isNotEmpty) { waveformData = value; } else if (stillPlaying(path)) { - waveformData = await _waveformExtractor.extractWaveformDataOnly(path).catchError((_) => []); // re-extracting without samples (out of boundaries error) + waveformData = await _waveformExtractor.extractWaveformData(path).catchError((_) => []); // re-extracting without samples (out of boundaries error) } }), Future.delayed(const Duration(milliseconds: 800)), @@ -88,8 +87,8 @@ class WaveformController { return downscaled; } - static Map> _downscaledWaveformLists(({List original, List targetSizes}) params) { - final newLists = >{}; + static Map> _downscaledWaveformLists(({List original, List targetSizes}) params) { + final newLists = >{}; const maxClamping = 64.0; params.targetSizes.loop((targetSize) { newLists[targetSize] = params.original.changeListSize( @@ -114,5 +113,5 @@ class WaveformController { return finalScale.isNaN ? 0.01 : finalScale; } - final _waveformExtractor = WaveformExtractor(); + final _waveformExtractor = WaveformExtractor.platform()..init(); } diff --git a/pubspec.yaml b/pubspec.yaml index 4fedc123..ba980c4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: namida description: A Beautiful and Feature-rich Music Player, With YouTube & Video Support Built in Flutter publish_to: "none" -version: 4.5.85-beta+241024005 +version: 4.5.9-beta+241024013 environment: sdk: ">=3.4.0 <4.0.0" @@ -81,7 +81,7 @@ dependencies: git: url: https://github.com/MSOB7YY/audio_service path: audio_service/ - waveform_extractor: ^1.0.3 + waveform_extractor: ^1.1.3 basic_audio_handler: git: url: https://github.com/namidaco/basic_audio_handler