From 260389307d684ecd67dd8e76c8e3dd05285659f4 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Mon, 8 Nov 2021 02:20:58 +1100 Subject: [PATCH] Add zoom parameter and docs. --- README.md | 18 +++++++++ .../just_waveform/JustWaveformPlugin.java | 9 +++-- .../just_waveform/WaveformExtractor.java | 14 +++++-- darwin/Classes/JustWaveformPlugin.m | 18 ++++++--- example/lib/main.dart | 3 ++ lib/just_waveform.dart | 37 ++++++++++++++++--- 6 files changed, 81 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 205c959..ed9e296 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ # just_waveform This plugin extracts waveform data from an audio file that can be used to render waveform visualisations. + +waveform screenshot + +## Usage + +```dart +final progressStream = JustWaveform.extract( + audioInFile: '/path/to/audio.mp3', + waveOutFile: '/path/to/waveform.wave', + zoom: const WaveformZoom.pixelsPerSecond(100), +); +progressStream.listen((waveformProgress) { + print('Progress: %${(100 * waveformProgress.progress).toInt()}'); + if (waveformProgress.waveform != null) { + // Use the waveform. + } +}); +``` diff --git a/android/src/main/java/com/ryanheise/just_waveform/JustWaveformPlugin.java b/android/src/main/java/com/ryanheise/just_waveform/JustWaveformPlugin.java index 834a67a..71fc3f9 100644 --- a/android/src/main/java/com/ryanheise/just_waveform/JustWaveformPlugin.java +++ b/android/src/main/java/com/ryanheise/just_waveform/JustWaveformPlugin.java @@ -26,10 +26,11 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { switch (call.method) { case "extract": - List args = (List)call.arguments; - String audioInPath = (String)args.get(0); - String waveOutPath = (String)args.get(1); - WaveformExtractor waveformExtractor = new WaveformExtractor(audioInPath, waveOutPath); + String audioInPath = call.argument("audioInPath"); + String waveOutPath = call.argument("waveOutPath"); + Integer samplesPerPixel = call.argument("samplesPerPixel"); + Integer pixelsPerSecond = call.argument("pixelsPerSecond"); + WaveformExtractor waveformExtractor = new WaveformExtractor(audioInPath, waveOutPath, samplesPerPixel, pixelsPerSecond); waveformExtractor.start(new WaveformExtractor.OnProgressListener() { @Override public void onProgress(int progress) { diff --git a/android/src/main/java/com/ryanheise/just_waveform/WaveformExtractor.java b/android/src/main/java/com/ryanheise/just_waveform/WaveformExtractor.java index 6f92faa..85f1a01 100644 --- a/android/src/main/java/com/ryanheise/just_waveform/WaveformExtractor.java +++ b/android/src/main/java/com/ryanheise/just_waveform/WaveformExtractor.java @@ -23,6 +23,8 @@ public class WaveformExtractor { private String inPath; private String wavePath; + private Integer samplesPerPixel; + private Integer pixelsPerSecond; private OnProgressListener onProgressListener; private MediaExtractor extractor; private ProcessThread processThread; @@ -30,9 +32,11 @@ public class WaveformExtractor { private MediaFormat inFormat; private String inMime; - public WaveformExtractor(String inPath, String wavePath) { + public WaveformExtractor(String inPath, String wavePath, Integer samplesPerPixel, Integer pixelsPerSecond) { this.inPath = inPath; this.wavePath = wavePath; + this.samplesPerPixel = samplesPerPixel; + this.pixelsPerSecond = pixelsPerSecond; } public void start(OnProgressListener onProgressListener) { @@ -95,8 +99,12 @@ void processAudio() { BufferInfo bufferInfo = new BufferInfo(); // For the wave - int pixelsPerSecond = 50; // 50 min/max pairs - int samplesPerPixel = sampleRate / pixelsPerSecond; + int samplesPerPixel; + if (this.samplesPerPixel != null) { + samplesPerPixel = this.samplesPerPixel; + } else { + samplesPerPixel = sampleRate / pixelsPerSecond; + } System.out.println("samples per pixel: " + samplesPerPixel + " = " + sampleRate + " / " + pixelsPerSecond); // Multiply by 2 since 2 bytes are needed for each short, and multiply by 2 again because for each sample we store a pair of (min,max) int scaledByteSamplesLength = 2*2*(int)(expectedSampleCount / samplesPerPixel); diff --git a/darwin/Classes/JustWaveformPlugin.m b/darwin/Classes/JustWaveformPlugin.m index 73fe589..06d5a8e 100644 --- a/darwin/Classes/JustWaveformPlugin.m +++ b/darwin/Classes/JustWaveformPlugin.m @@ -20,10 +20,12 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { ExtAudioFileRef audioFileRef = NULL; + NSDictionary *request = (NSDictionary *)call.arguments; if ([@"extract" isEqualToString:call.method]) { - NSArray *args = (NSArray *)call.arguments; - NSString *audioInPath = (NSString *)args[0]; - NSString *waveOutPath = (NSString *)args[1]; + NSString *audioInPath = (NSString *)request[@"audioInPath"]; + NSString *waveOutPath = (NSString *)request[@"waveOutPath"]; + NSNumber *samplesPerPixelArg = (NSNumber *)request[@"samplesPerPixel"]; + NSNumber *pixelsPerSecondArg = (NSNumber *)request[@"pixelsPerSecond"]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ OSStatus status; @@ -65,9 +67,13 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSLog(@"frames per packet: %d", fileFormat.mFramesPerPacket); - int pixelsPerSecond = 50; // 50 min/max pairs - int samplesPerPixel = (int)(fileFormat.mSampleRate / pixelsPerSecond); - NSLog(@"samples per pixel: %d = %f / %d", samplesPerPixel, fileFormat.mSampleRate, pixelsPerSecond); + int samplesPerPixel; + if (samplesPerPixelArg != (id)[NSNull null]) { + samplesPerPixel = [samplesPerPixelArg intValue]; + } else { + samplesPerPixel = (int)(fileFormat.mSampleRate / [pixelsPerSecondArg intValue]); + } + // Multiply by 2 since 2 bytes are needed for each short, and multiply by 2 again because for each sample we store a pair of (min,max) UInt32 scaledByteSamplesLength = 2*2*(UInt32)(expectedSampleCount / samplesPerPixel); UInt32 waveLength = (UInt32)(scaledByteSamplesLength / 2); // better name: numPixels? diff --git a/example/lib/main.dart b/example/lib/main.dart index 337fe6d..4802238 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -131,6 +131,9 @@ class _AudioWaveformState extends State { waveform: widget.waveform, start: widget.start, duration: widget.duration, + scale: widget.scale, + strokeWidth: widget.strokeWidth, + pixelsPerStep: widget.pixelsPerStep, ), ), ); diff --git a/lib/just_waveform.dart b/lib/just_waveform.dart index b7c48e5..561e885 100644 --- a/lib/just_waveform.dart +++ b/lib/just_waveform.dart @@ -4,16 +4,20 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; +/// A utility for extracting a [Waveform] from an audio file suitable for visual +/// rendering. class JustWaveform { static const MethodChannel _channel = MethodChannel('com.ryanheise.just_waveform'); - /// Extract a waveform from [audioInFile] and write it to [waveOutFile]. + /// Extract a [Waveform] from [audioInFile] and write it to [waveOutFile] at + /// the specified [zoom] level. // XXX: It would be better to return a stream of the actual [Waveform], with // progress => wave.data.length / (wave.length*2) static Stream extract({ required File audioInFile, required File waveOutFile, + WaveformZoom zoom = const WaveformZoom.pixelsPerSecond(100), }) { final progressController = StreamController.broadcast(); progressController.add(WaveformProgress._(0.0, null)); @@ -35,13 +39,16 @@ class JustWaveform { break; } }); - _channel.invokeMethod('extract', [ - audioInFile.path, - waveOutFile.path, - ]).catchError(progressController.addError); + _channel.invokeMethod('extract', { + 'audioInPath': audioInFile.path, + 'waveOutPath': waveOutFile.path, + 'samplesPerPixel': zoom._samplesPerPixel, + 'pixelsPerSecond': zoom._pixelsPerSecond, + }).catchError(progressController.addError); return progressController.stream; } + /// Reads [Waveform] data from an audiowaveform-formatted data file. static Future parse(File waveformFile) async { final bytes = Uint8List.fromList(await waveformFile.readAsBytes()).buffer; const headerLength = 20; @@ -58,8 +65,12 @@ class JustWaveform { } } +/// The progress of the [Waveform] extraction. class WaveformProgress { + /// The progress value between 0.0 to 1.0. final double progress; + + /// The finished [Waveform] when extraction is complete. final Waveform? waveform; WaveformProgress._(this.progress, this.waveform); @@ -118,3 +129,19 @@ class Waveform { double positionToPixel(Duration position) => position.inMicroseconds * sampleRate / (samplesPerPixel * 1000000); } + +/// The resolution at which a [Waveform] should be generated. +class WaveformZoom { + final int? _samplesPerPixel; + final int? _pixelsPerSecond; + + /// Specify a zoom level via samples per pixel. + const WaveformZoom.samplesPerPixel(int samplesPerPixel) + : _samplesPerPixel = samplesPerPixel, + _pixelsPerSecond = null; + + /// Specify a zoom level via pixels per second. + const WaveformZoom.pixelsPerSecond(int pixelsPerSecond) + : _pixelsPerSecond = pixelsPerSecond, + _samplesPerPixel = null; +}