Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 2.1.0 (#unreleased)

- Feature [#468](https://github.com/SimformSolutionsPvtLtd/audio_waveforms/pull/468) - Add macOS support
- Feature [#91](https://github.com/SimformSolutionsPvtLtd/audio_waveforms/issues/91) - Add `WaveStyle.maxDuration` to bound recording waveform width proportionally

## 2.0.2

Expand Down
29 changes: 29 additions & 0 deletions doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,34 @@ AudioWaveforms(
)
```

## Bounded Recording Width

By default the recording waveform scrolls once it reaches the middle of the
available width. Set `WaveStyle.maxDuration` to instead bound the waveform to a
fixed maximum duration: the waveform fills only
`elapsedRecordingDuration / maxDuration` of the width and never scrolls. For
example, with a `maxDuration` of 10 seconds, stopping the recording at 5 seconds
draws the waveform across half of the width; reaching 10 seconds fills the full
width (recording past it stays clamped to the full width).

This is useful when you want the final waveform length to reflect how long the
recording is, relative to a cap.

```dart
AudioWaveforms(
controller: recorderController,
size: Size(MediaQuery.of(context).size.width, 100),
waveStyle: const WaveStyle(
maxDuration: Duration(seconds: 10),
),
)
```

> Note: `maxDuration` only applies to `WaveformRenderMode.ltr` (ignored for
> `rtl`, like `extendWaveform`) and takes precedence over `extendWaveform` when
> both are set. A non-positive value (`Duration.zero` or less) is ignored and
> falls back to the default scrolling behavior.

## Waveform Gradients

Apply gradients to waveforms:
Expand Down Expand Up @@ -1708,6 +1736,7 @@ This section provides a quick reference for the main classes and their propertie
| `durationLinesHeight` | `double` | - | Duration line height |
| `gradient` | `Shader?` | `null` | Wave gradient |
| `scaleFactor` | `double` | `20.0` | Wave scaling factor |
| `maxDuration` | `Duration?` | `null` | Bound recording width |

## PlayerWaveStyle (for Player)

Expand Down
1 change: 1 addition & 0 deletions lib/src/audio_waveforms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class _AudioWaveformsState extends State<AudioWaveforms> {
scaleFactor: _waveStyle.scaleFactor,
currentlyRecordedDuration: currentlyRecordedDuration,
isRtl: _isRtl,
maxDuration: _waveStyle.maxDuration,
),
),
),
Expand Down
20 changes: 20 additions & 0 deletions lib/src/base/wave_style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class WaveStyle {
this.gradient,
this.scaleFactor = 20.0,
this.waveformRenderMode = WaveformRenderMode.ltr,
this.maxDuration,
}) : assert(
waveThickness < spacing,
"waveThickness can't be greater than spacing",
Expand Down Expand Up @@ -133,4 +134,23 @@ class WaveStyle {
/// to left. Older waves will be pushed to the left to make space for new
/// waves.
final WaveformRenderMode waveformRenderMode;

/// Bounds the recording waveform to a fixed maximum duration.
///
/// When provided, the waveform is no longer scrolled. Instead it fills only
/// the fraction of the available width equal to
/// `elapsedRecordingDuration / maxDuration`. e.g. with `maxDuration` of 10s,
/// stopping the recording at 5s draws the waveform across half of the width;
/// reaching `maxDuration` fills the full width. Recording past `maxDuration`
/// is clamped to the full width.
///
/// Can only be used with [WaveformRenderMode.ltr] (ignored for
/// [WaveformRenderMode.rtl], like [extendWaveform]). When set, it takes
/// precedence over [extendWaveform].
///
/// A non-positive value (less than or equal to [Duration.zero]) is ignored
/// and falls back to the default scrolling behavior.
///
/// Defaults to `null`, which keeps the default scrolling behavior.
final Duration? maxDuration;
}
57 changes: 49 additions & 8 deletions lib/src/painters/recorder_wave_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class RecorderWavePainter extends CustomPainter {
required this.currentlyRecordedDuration,
required this.labels,
required this.isRtl,
required this.maxDuration,
}) : _wavePaint = Paint()
..color = waveColor
..strokeWidth = waveThickness
Expand Down Expand Up @@ -98,6 +99,15 @@ class RecorderWavePainter extends CustomPainter {
final List<Label> labels;
final bool isRtl;

/// When non-null (LTR only), the waveform is bounded to this duration and
/// fills `currentlyRecordedDuration / maxDuration` of the width instead of
/// scrolling. See [WaveStyle.maxDuration].
final Duration? maxDuration;

/// Whether bounded (max-duration) mode is active. A non-positive
/// [maxDuration] is ignored and falls back to the default scrolling behavior.
bool get _isBounded => maxDuration != null && maxDuration!.inMicroseconds > 0;

static const int durationBuffer = 5;

@override
Expand All @@ -121,9 +131,12 @@ class RecorderWavePainter extends CustomPainter {
}
} else {
for (var i = 0; i < waveData.length; i++) {
if (((spacing * i) + dragOffset.dx + spacing >
size.width / (extendWaveform ? 1 : 2) +
totalCurrentBackDistance.dx) &&
// When [maxDuration] is set the waveform is bounded to the width and
// never scrolls, so pushBack is skipped.
if (!_isBounded &&
((spacing * i) + dragOffset.dx + spacing >
size.width / (extendWaveform ? 1 : 2) +
totalCurrentBackDistance.dx) &&
callPushback) {
pushBack();
}
Expand Down Expand Up @@ -207,10 +220,17 @@ class RecorderWavePainter extends CustomPainter {
/// Draw wave for LTR direction
void _drawLtrWave(Canvas canvas, Size size, int i) {
final height = size.height;
final dx = -totalCurrentBackDistance.dx +
dragOffset.dx +
(spacing * i) -
initialPosition;
final double dx;
if (_isBounded) {
// Bounded mode: distribute the current samples across the fraction of
// the width equal to currentlyRecordedDuration / maxDuration. No scroll.
dx = _boundedDx(size, i);
} else {
dx = -totalCurrentBackDistance.dx +
dragOffset.dx +
(spacing * i) -
initialPosition;
}
final scaledWaveHeight = waveData[i] * scaleFactor;
final upperDy = height - (showTop ? scaledWaveHeight : 0) - bottomPadding;
final lowerDy =
Expand All @@ -223,7 +243,13 @@ class RecorderWavePainter extends CustomPainter {
// portions of waves are being drawn to user
// and [dx > 0] will ensure only fully visible waves are drawn,
// if any wave is half visible this will cut out that wave too.
if (dx > 0 && dx < size.width) {
//
// In bounded ([maxDuration]) mode the waves are clamped within the width,
// so the first wave at dx == 0 and the last at dx == size.width are kept.
final isVisible = _isBounded
? (dx >= 0 && dx <= size.width)
: (dx > 0 && dx < size.width);
if (isVisible) {
canvas.drawLine(
Offset(dx, upperDy),
Offset(dx, lowerDy),
Expand All @@ -232,6 +258,21 @@ class RecorderWavePainter extends CustomPainter {
}
}

/// Horizontal position of wave [i] when [maxDuration] is set.
///
/// The current samples are spread across `fraction * size.width`, where
/// `fraction = currentlyRecordedDuration / maxDuration` (clamped to 1). So
/// the waveform grows left-to-right and reaches the full width exactly at
/// [maxDuration].
double _boundedDx(Size size, int i) {
final maxMicros = maxDuration!.inMicroseconds;
if (maxMicros <= 0 || waveData.length <= 1) return 0;
final fraction =
(currentlyRecordedDuration.inMicroseconds / maxMicros).clamp(0.0, 1.0);
final effectiveSpacing = (fraction * size.width) / (waveData.length - 1);
return effectiveSpacing * i;
}

/// Draw wave for RTL direction
void _drawRtlWave(Canvas canvas, int i, Size size) {
final height = size.height;
Expand Down