Skip to content

Commit

Permalink
core(windows): waveform support
Browse files Browse the repository at this point in the history
  • Loading branch information
MSOB7YY committed Oct 24, 2024
1 parent 694066a commit b87cd4d
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 22 deletions.
14 changes: 14 additions & 0 deletions lib/controller/platform/base.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import 'dart:io';

import 'package:flutter/foundation.dart';

import 'package:path/path.dart' as p;

class NamidaPlatformBuilder {
static T init<T>({required T Function() android, required T Function() windows}) {
return switch (Platform.operatingSystem) {
Expand All @@ -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');
}
}
}
2 changes: 0 additions & 2 deletions lib/controller/platform/ffmpeg_executer/ffmpeg_executer.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions lib/controller/platform/waveform_extractor/waveform_extractor.dart
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
part of 'waveform_extractor.dart';

class _WaveformExtractorAndroid extends WaveformExtractor {
_WaveformExtractorAndroid._internal();

final _extractor = pkgwaveform.WaveformExtractor();

@override
void init() {}

@override
Future<List<num>> extractWaveformData(
String source, {
bool useCache = true,
String? cacheKey,
int? samplesPerSecond,
}) {
return _extractor.extractWaveformDataOnly(
source,
useCache: useCache,
cacheKey: cacheKey,
samplePerSecond: samplesPerSecond,
);
}
}
Original file line number Diff line number Diff line change
@@ -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<List<num>> 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,
);
}
}
Original file line number Diff line number Diff line change
@@ -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 = <String>{'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<List<num>> 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<num>? _parseWaveformFile(File cacheFile) {
if (cacheFile.existsSync()) return LineSplitter.split(cacheFile.readAsStringSync()).map((e) => num.parse(e)).toList();
return null;
}

void _encodeWaveformFile(File cacheFile, List<num> list) {
if (list.isNotEmpty) cacheFile.writeAsStringSync(list.join('\n'));
}

Future<List<num>> _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 <num>[];
}

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<num>() ?? [];

final combinedList = <num>[];
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;
}
}
15 changes: 7 additions & 8 deletions lib/controller/waveform_controller.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -36,13 +35,13 @@ class WaveformController {
scaleFactor: 0.4,
);

List<int> waveformData = [];
List<num> waveformData = [];
await Future.wait([
_waveformExtractor.extractWaveformDataOnly(path, samplePerSecond: samplePerSecond).catchError((_) => <int>[]).then((value) async {
_waveformExtractor.extractWaveformData(path, samplesPerSecond: samplePerSecond).catchError((_) => <num>[]).then((value) async {
if (value.isNotEmpty) {
waveformData = value;
} else if (stillPlaying(path)) {
waveformData = await _waveformExtractor.extractWaveformDataOnly(path).catchError((_) => <int>[]); // re-extracting without samples (out of boundaries error)
waveformData = await _waveformExtractor.extractWaveformData(path).catchError((_) => <num>[]); // re-extracting without samples (out of boundaries error)
}
}),
Future.delayed(const Duration(milliseconds: 800)),
Expand Down Expand Up @@ -88,8 +87,8 @@ class WaveformController {
return downscaled;
}

static Map<int, List<double>> _downscaledWaveformLists(({List<int> original, List<int> targetSizes}) params) {
final newLists = <int, List<double>>{};
static Map<num, List<double>> _downscaledWaveformLists(({List<num> original, List<int> targetSizes}) params) {
final newLists = <num, List<double>>{};
const maxClamping = 64.0;
params.targetSizes.loop((targetSize) {
newLists[targetSize] = params.original.changeListSize(
Expand All @@ -114,5 +113,5 @@ class WaveformController {
return finalScale.isNaN ? 0.01 : finalScale;
}

final _waveformExtractor = WaveformExtractor();
final _waveformExtractor = WaveformExtractor.platform()..init();
}
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit b87cd4d

Please sign in to comment.