Skip to content

Commit 31988c2

Browse files
authored
Merge pull request #255 from alnitak/streamPosition
improve buffer stream functionalities
2 parents c50abc8 + 1c30949 commit 31988c2

25 files changed

+489
-105
lines changed

.vscode/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"name": "Flutter debug",
99
"type": "dart",
1010
"request": "launch",
11-
"program": "lib/main.dart",
11+
"program": "lib/buffer_stream/simple_noise_stream.dart",
1212
"flutterMode": "debug",
1313
// "env": {
1414
// "NO_OPUS_OGG_LIBS": "1"
@@ -89,7 +89,7 @@
8989
"cwd": "${workspaceFolder}"
9090
},
9191
{
92-
"name": "Chrome release",
92+
"name": "Chrome wasm release",
9393
"type": "chrome",
9494
"preLaunchTask": "compile web wasm release",
9595
"request": "launch"

.vscode/tasks.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
{
1111
"label": "compile linux debug",
12-
"command": "cd ${workspaceFolder}/example; flutter build linux -t lib/main.dart --debug",
12+
"command": "cd ${workspaceFolder}/example; flutter build linux -t lib/buffer_stream/simple_noise_stream.dart --debug",
1313
"type": "shell"
1414
},
1515
{
@@ -29,7 +29,7 @@
2929
},
3030
{
3131
"label": "compile web wasm release",
32-
"command": "cd ${workspaceFolder}/example; flutter run -d chrome --wasm --web-browser-flag '--disable-web-security' -t lib/audio_data/audio_data.dart --release",
32+
"command": "cd ${workspaceFolder}/example; flutter run -d chrome --wasm --web-browser-flag '--disable-web-security' -t lib/buffer_stream/simple_noise_stream.dart --release",
3333
"type": "shell"
3434
},
3535
{

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#### 3.1.12 (21 Jun 2025)
2+
- added `getStreamTimeConsumed()` to get the time consumed by a buffer stream of kind `BufferingType.released`. Since the position of this kind of stream is always 0, this method is useful to know the time already played.
3+
- fix pause/unpause on audio stream buffering.
4+
- added `buffer_stream/simple_noise_stream.dart` example.
5+
16
#### 3.1.11 (16 Jun 2025)
27
- fix: Loading the same AudioSource twice (in parallel) crashes #247
38
- fix win: force cmake to build the plugin in release mode even if building in debug

example/lib/buffer_stream/generate.dart

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,24 @@ class _GenerateState extends State<Generate> {
261261
child: const Text('dispose all sounds'),
262262
),
263263
gap,
264-
BufferBar(sound: tone, startingMb: 1, label: 'tone'),
265-
BufferBar(sound: siren, startingMb: 1, label: 'siren'),
266-
BufferBar(sound: bouncing, startingMb: 1, label: 'bouncing'),
264+
BufferBar(
265+
bufferingType: BufferingType.preserved,
266+
sound: tone,
267+
startingMb: 1,
268+
label: 'tone',
269+
),
270+
BufferBar(
271+
bufferingType: BufferingType.preserved,
272+
sound: siren,
273+
startingMb: 1,
274+
label: 'siren',
275+
),
276+
BufferBar(
277+
bufferingType: BufferingType.preserved,
278+
sound: bouncing,
279+
startingMb: 1,
280+
label: 'bouncing',
281+
),
267282
],
268283
),
269284
),
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import 'dart:developer' as dev;
2+
import 'dart:math';
3+
4+
import 'package:flutter/foundation.dart';
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_soloud/flutter_soloud.dart';
7+
import 'package:flutter_soloud_example/buffer_stream/ui/buffer_widget.dart';
8+
import 'package:logging/logging.dart';
9+
10+
/// Example to demonstrate how to use the buffer stream generating random noise
11+
/// data and how to handle buffering events.
12+
///
13+
/// It also shows how to use the `SoLoud.instance.setBufferStream` method
14+
/// to set up a buffer stream and how to add audio data to it
15+
/// using `SoLoud.instance.addAudioDataStream`.
16+
///
17+
/// As you can see, the playing position of the sound is always at 0 when the
18+
/// buffering type is `BufferingType.released`.
19+
/// When the buffering type is `BufferingType.preserved`, all is normal as for
20+
/// the other "normal" sounds, and the playing position can be get using
21+
/// `SoLoud.instance.getPosition(handle)`.
22+
/// When the buffering type is `BufferingType.released`, the sound is
23+
/// always at the beginning, and the position is get using
24+
/// `SoLoud.instance.getStreamTimeConsumed(sound.)`.
25+
26+
void main() async {
27+
// The `flutter_soloud` package logs everything
28+
// (from severe warnings to fine debug messages)
29+
// using the standard `package:logging`.
30+
// You can listen to the logs as shown below.
31+
Logger.root.level = kDebugMode ? Level.FINE : Level.INFO;
32+
Logger.root.onRecord.listen((record) {
33+
dev.log(
34+
record.message,
35+
time: record.time,
36+
level: record.level.value,
37+
name: record.loggerName,
38+
zone: record.zone,
39+
error: record.error,
40+
stackTrace: record.stackTrace,
41+
);
42+
});
43+
44+
WidgetsFlutterBinding.ensureInitialized();
45+
46+
/// Initialize the player.
47+
await SoLoud.instance.init();
48+
49+
runApp(
50+
const MaterialApp(
51+
home: SimpleNoise(),
52+
),
53+
);
54+
}
55+
56+
class SimpleNoise extends StatefulWidget {
57+
const SimpleNoise({super.key});
58+
59+
@override
60+
State<SimpleNoise> createState() => _SimpleNoiseState();
61+
}
62+
63+
class _SimpleNoiseState extends State<SimpleNoise> {
64+
/// The size of the chunks to be sent to the buffer stream in bytes.
65+
static const chunkSize = 1024 * 1024 / 10; // 1 MB
66+
67+
/// The type of the buffer stream.
68+
final bufferingType = ValueNotifier<BufferingType>(BufferingType.released);
69+
70+
/// The time needed to wait before unpausing the audio stream.
71+
final bufferingTimeNeeds = 1.0;
72+
73+
AudioSource? noise;
74+
bool isBuffering = false;
75+
76+
@override
77+
Widget build(BuildContext context) {
78+
return Scaffold(
79+
appBar: AppBar(title: const Text('Generate noise')),
80+
body: Align(
81+
alignment: Alignment.topCenter,
82+
child: Column(
83+
mainAxisSize: MainAxisSize.min,
84+
spacing: 16,
85+
children: [
86+
// Choose between BufferingType.released and
87+
// BufferingType.preserved.
88+
Row(
89+
mainAxisAlignment: MainAxisAlignment.center,
90+
children: [
91+
const Text('BufferingType: '),
92+
ValueListenableBuilder(
93+
valueListenable: bufferingType,
94+
builder: (context, value, child) {
95+
return DropdownButton<BufferingType>(
96+
value: value,
97+
items: BufferingType.values.map((type) {
98+
return DropdownMenuItem(
99+
value: type,
100+
child: Text(type.name),
101+
);
102+
}).toList(),
103+
onChanged: (value) {
104+
bufferingType.value = value ?? BufferingType.released;
105+
},
106+
);
107+
},
108+
),
109+
],
110+
),
111+
Text('Buffering time needs: $bufferingTimeNeeds seconds'),
112+
OutlinedButton(
113+
onPressed: () async {
114+
/// Setup the buffer stream
115+
noise = SoLoud.instance.setBufferStream(
116+
format: BufferType.f32le,
117+
bufferingTimeNeeds: bufferingTimeNeeds,
118+
bufferingType: bufferingType.value,
119+
onBuffering: (bool buffering, int handle, double time) {
120+
isBuffering = buffering;
121+
debugPrint('ON BUFFERING: $buffering, handle: $handle, '
122+
'at time: $time');
123+
setState(() {});
124+
},
125+
);
126+
await SoLoud.instance.play(noise!);
127+
128+
/// Just to rebuild [BufferBar] widgets
129+
setState(() {});
130+
},
131+
child: const Text('set buffer stream'),
132+
),
133+
OutlinedButton(
134+
onPressed: () async {
135+
if (noise == null) return;
136+
137+
final random = Random();
138+
// floats have 4 bytes each
139+
final randomFloats = Float32List(chunkSize.toInt() >> 2);
140+
141+
for (var i = 0; i < randomFloats.length; i++) {
142+
// Generate noise in range [-1, 1]
143+
randomFloats[i] = random.nextDouble() * 2 - 1;
144+
}
145+
// Add [chunkSize] bytes of random noise data to the buffer
146+
SoLoud.instance.addAudioDataStream(
147+
noise!,
148+
randomFloats.buffer.asUint8List(),
149+
);
150+
},
151+
child:
152+
const Text('push ${chunkSize / 1024 / 1024}MB of noise data'),
153+
),
154+
OutlinedButton(
155+
onPressed: () async {
156+
if (noise == null) return;
157+
SoLoud.instance.setDataIsEnded(noise!);
158+
},
159+
child: const Text('set data ended'),
160+
),
161+
OutlinedButton(
162+
onPressed: () async {
163+
await SoLoud.instance.disposeAllSources();
164+
noise = null;
165+
isBuffering = false;
166+
setState(() {});
167+
},
168+
child: const Text('dispose all sounds'),
169+
),
170+
BufferBar(
171+
bufferingType: bufferingType.value,
172+
sound: noise,
173+
startingMb: 1,
174+
label: 'noise',
175+
),
176+
if (isBuffering) const Text('Buffering...'),
177+
],
178+
),
179+
),
180+
);
181+
}
182+
}

example/lib/buffer_stream/ui/buffer_widget.dart

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter_soloud/flutter_soloud.dart';
77

88
class BufferBar extends StatefulWidget {
99
const BufferBar({
10+
required this.bufferingType,
1011
super.key,
1112
this.label = '',
1213
this.sound,
@@ -16,16 +17,18 @@ class BufferBar extends StatefulWidget {
1617
final String? label;
1718
final AudioSource? sound;
1819
final int startingMb;
20+
final BufferingType bufferingType;
1921

2022
@override
2123
State<BufferBar> createState() => _BufferBarState();
2224
}
2325

2426
class _BufferBarState extends State<BufferBar> {
25-
final width = 200.0;
27+
final width = 190.0;
2628
final height = 30.0;
2729
Timer? timer;
2830
int currentMaxBytes = 1024 * 1024; // 1 MB
31+
String firstHandleHumanPos = '';
2932

3033
@override
3134
void initState() {
@@ -38,9 +41,7 @@ class _BufferBarState extends State<BufferBar> {
3841

3942
@override
4043
Widget build(BuildContext context) {
41-
if (widget.sound == null) {
42-
return const SizedBox.shrink();
43-
}
44+
if (widget.sound == null) return const SizedBox.shrink();
4445

4546
final int bufferSize;
4647
try {
@@ -56,9 +57,13 @@ class _BufferBarState extends State<BufferBar> {
5657

5758
/// [soundLength] reflects the value of [progressValue].
5859
final soundLength = SoLoud.instance.getLength(widget.sound!);
59-
final humanDuration = '${soundLength.inMinutes % 60}:'
60-
'${(soundLength.inSeconds % 60).toString().padLeft(2, '0')}.'
61-
'${(soundLength.inMilliseconds % 1000).toString().padLeft(3, '0')}';
60+
final humanDuration = toHuman(soundLength);
61+
final firstHandlePos = widget.bufferingType == BufferingType.preserved
62+
? (widget.sound!.handles.isNotEmpty
63+
? SoLoud.instance.getPosition(widget.sound!.handles.first)
64+
: Duration.zero)
65+
: SoLoud.instance.getStreamTimeConsumed(widget.sound!);
66+
firstHandleHumanPos = toHuman(firstHandlePos);
6267

6368
/// The current progress value
6469
final progressValue = bufferSize > 0.0 ? bufferSize / currentMaxBytes : 0.0;
@@ -81,9 +86,10 @@ class _BufferBarState extends State<BufferBar> {
8186
return Padding(
8287
padding: const EdgeInsets.all(8),
8388
child: Row(
89+
mainAxisSize: MainAxisSize.min,
8490
children: [
8591
SizedBox(
86-
width: 140,
92+
width: 150,
8793
child: Column(
8894
crossAxisAlignment: CrossAxisAlignment.start,
8995
mainAxisAlignment: MainAxisAlignment.center,
@@ -95,6 +101,7 @@ class _BufferBarState extends State<BufferBar> {
95101
),
96102
Text('using $mb MB'),
97103
Text('length $humanDuration'),
104+
Text('position: $firstHandleHumanPos'),
98105
],
99106
),
100107
),
@@ -131,4 +138,10 @@ class _BufferBarState extends State<BufferBar> {
131138
),
132139
);
133140
}
141+
142+
String toHuman(Duration time) {
143+
return '${time.inMinutes % 60}:'
144+
'${(time.inSeconds % 60).toString().padLeft(2, '0')}.'
145+
'${(time.inMilliseconds % 1000).toString().padLeft(3, '0')}';
146+
}
134147
}

example/lib/buffer_stream/websocket.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,10 @@ class _WebsocketExampleState extends State<WebsocketExample> {
320320
],
321321
),
322322
const SizedBox(height: 16),
323-
BufferBar(sound: currentSound),
323+
BufferBar(
324+
bufferingType: BufferingType.preserved,
325+
sound: currentSound,
326+
),
324327
const SizedBox(height: 16),
325328
ValueListenableBuilder(
326329
valueListenable: streamBuffering,

example/pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ packages:
108108
path: ".."
109109
relative: true
110110
source: path
111-
version: "3.1.11"
111+
version: "3.1.12"
112112
flutter_test:
113113
dependency: "direct dev"
114114
description: flutter

lib/src/bindings/bindings_player.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ abstract class FlutterSoLoud {
165165
@mustBeOverridden
166166
PlayerErrors resetBufferStream(SoundHash soundHash);
167167

168+
/// Get the current stream time consumed in seconds of this sound of
169+
/// type `BufferingType.RELEASED` with hash [hash].
170+
@mustBeOverridden
171+
({PlayerErrors error, double value}) getStreamTimeConsumed(
172+
SoundHash soundHash,
173+
);
174+
168175
/// Add a chunk of audio data to the buffer stream.
169176
///
170177
/// [hash] the hash of the sound.

0 commit comments

Comments
 (0)