Skip to content

Commit

Permalink
Add zoom parameter and docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanheise committed Nov 7, 2021
1 parent 8160367 commit 2603893
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 18 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
# just_waveform

This plugin extracts waveform data from an audio file that can be used to render waveform visualisations.

<img src="https://user-images.githubusercontent.com/19899190/138703227-6263c233-945c-4b60-8f0a-f652fbba9a3f.png" alt="waveform screenshot" width="350" />

## 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.
}
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ public class WaveformExtractor {

private String inPath;
private String wavePath;
private Integer samplesPerPixel;
private Integer pixelsPerSecond;
private OnProgressListener onProgressListener;
private MediaExtractor extractor;
private ProcessThread processThread;
private MediaCodec decoder;
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) {
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 12 additions & 6 deletions darwin/Classes/JustWaveformPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ - (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)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;
Expand Down Expand Up @@ -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?
Expand Down
3 changes: 3 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ class _AudioWaveformState extends State<AudioWaveformWidget> {
waveform: widget.waveform,
start: widget.start,
duration: widget.duration,
scale: widget.scale,
strokeWidth: widget.strokeWidth,
pixelsPerStep: widget.pixelsPerStep,
),
),
);
Expand Down
37 changes: 32 additions & 5 deletions lib/just_waveform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<WaveformProgress> extract({
required File audioInFile,
required File waveOutFile,
WaveformZoom zoom = const WaveformZoom.pixelsPerSecond(100),
}) {
final progressController = StreamController<WaveformProgress>.broadcast();
progressController.add(WaveformProgress._(0.0, null));
Expand All @@ -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<Waveform> parse(File waveformFile) async {
final bytes = Uint8List.fromList(await waveformFile.readAsBytes()).buffer;
const headerLength = 20;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}

0 comments on commit 2603893

Please sign in to comment.