mic_stream 0.6.0 mic_stream: ^0.6.0 copied to clipboard
MicStream is a plugin to receive raw byte streams from a device's microphone. Configurations allow for 8- and 16-bit PCM streams, and mono or stereo. Audio is returned as `Stream<Uint8list>`.
import 'dart:async';
import 'dart:math';
import 'dart:core';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
import 'package:mic_stream/mic_stream.dart';
enum Command {
start,
stop,
change,
}
const AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
void main() => runApp(MicStreamExampleApp());
class MicStreamExampleApp extends StatefulWidget {
@override
_MicStreamExampleAppState createState() => _MicStreamExampleAppState();
}
class _MicStreamExampleAppState extends State<MicStreamExampleApp>
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
Stream? stream;
late StreamSubscription listener;
List<int>? currentSamples = [];
List<int> visibleSamples = [];
int? localMax;
int? localMin;
Random rng = new Random();
// Refreshes the Widget for every possible tick to force a rebuild of the sound wave
late AnimationController controller;
Color _iconColor = Colors.white;
bool isRecording = false;
bool memRecordingState = false;
late bool isActive;
DateTime? startTime;
int page = 0;
List state = ["SoundWavePage", "IntensityWavePage", "InformationPage"];
@override
void initState() {
print("Init application");
super.initState();
WidgetsBinding.instance!.addObserver(this);
setState(() {
initPlatformState();
});
}
void _controlPage(int index) => setState(() => page = index);
// Responsible for switching between recording / idle state
void _controlMicStream({Command command: Command.change}) async {
switch (command) {
case Command.change:
_changeListening();
break;
case Command.start:
_startListening();
break;
case Command.stop:
_stopListening();
break;
}
}
Future<bool> _changeListening() async =>
!isRecording ? await _startListening() : _stopListening();
late int bytesPerSample;
late int samplesPerSecond;
Future<bool> _startListening() async {
print("START LISTENING");
if (isRecording) return false;
// if this is the first time invoking the microphone()
// method to get the stream, we don't yet have access
// to the sampleRate and bitDepth properties
print("wait for stream");
// Default option. Set to false to disable request permission dialogue
MicStream.shouldRequestPermission(true);
stream = await MicStream.microphone(
audioSource: AudioSource.DEFAULT,
sampleRate: 1000 * (rng.nextInt(50) + 30),
channelConfig: ChannelConfig.CHANNEL_IN_MONO,
audioFormat: AUDIO_FORMAT);
// after invoking the method for the first time, though, these will be available;
// It is not necessary to setup a listener first, the stream only needs to be returned first
print("Start Listening to the microphone, sample rate is ${await MicStream.sampleRate}, bit depth is ${await MicStream.bitDepth}, bufferSize: ${await MicStream.bufferSize}");
bytesPerSample = (await MicStream.bitDepth)! ~/ 8;
samplesPerSecond = (await MicStream.sampleRate)!.toInt();
localMax = null;
localMin = null;
setState(() {
isRecording = true;
startTime = DateTime.now();
});
visibleSamples = [];
listener = stream!.listen(_calculateSamples);
return true;
}
void _calculateSamples(samples) {
if (page == 0)
_calculateWaveSamples(samples);
else if (page == 1)
_calculateIntensitySamples(samples);
}
void _calculateWaveSamples(samples) {
bool first = true;
visibleSamples = [];
int tmp = 0;
for (int sample in samples) {
if (sample > 128) sample -= 255;
if (first) {
tmp = sample * 128;
} else {
tmp += sample;
visibleSamples.add(tmp);
localMax ??= visibleSamples.last;
localMin ??= visibleSamples.last;
localMax = max(localMax!, visibleSamples.last);
localMin = min(localMin!, visibleSamples.last);
tmp = 0;
}
first = !first;
}
print(visibleSamples);
}
void _calculateIntensitySamples(samples) {
currentSamples ??= [];
int currentSample = 0;
eachWithIndex(samples, (i, int sample) {
currentSample += sample;
if ((i % bytesPerSample) == bytesPerSample-1) {
currentSamples!.add(currentSample);
currentSample = 0;
}
});
if (currentSamples!.length >= samplesPerSecond/10) {
visibleSamples.add(currentSamples!.map((i) => i).toList().reduce((a, b) => a+b));
localMax ??= visibleSamples.last;
localMin ??= visibleSamples.last;
localMax = max(localMax!, visibleSamples.last);
localMin = min(localMin!, visibleSamples.last);
currentSamples = [];
setState(() {});
}
}
bool _stopListening() {
if (!isRecording) return false;
print("Stop Listening to the microphone");
listener.cancel();
setState(() {
isRecording = false;
currentSamples = null;
startTime = null;
});
return true;
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
if (!mounted) return;
isActive = true;
Statistics(false);
controller =
AnimationController(duration: Duration(seconds: 1), vsync: this)
..addListener(() {
if (isRecording) setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed)
controller.reverse();
else if (status == AnimationStatus.dismissed) controller.forward();
})
..forward();
}
Color _getBgColor() => (isRecording) ? Colors.red : Colors.cyan;
Icon _getIcon() =>
(isRecording) ? Icon(Icons.stop) : Icon(Icons.keyboard_voice);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin: mic_stream :: Debug'),
),
floatingActionButton: FloatingActionButton(
onPressed: _controlMicStream,
child: _getIcon(),
foregroundColor: _iconColor,
backgroundColor: _getBgColor(),
tooltip: (isRecording) ? "Stop recording" : "Start recording",
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.broken_image),
label: "Sound Wave",
),
BottomNavigationBarItem(
icon: Icon(Icons.broken_image),
label: "Intensity Wave",
),
BottomNavigationBarItem(
icon: Icon(Icons.view_list),
label: "Statistics",
)
],
backgroundColor: Colors.black26,
elevation: 20,
currentIndex: page,
onTap: _controlPage,
),
body: (page == 0 || page == 1)
? CustomPaint(
painter: WavePainter(
samples: visibleSamples,
color: _getBgColor(),
localMax: localMax,
localMin: localMin,
context: context,
),
)
: Statistics(
isRecording,
startTime: startTime,
)),
);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
isActive = true;
print("Resume app");
_controlMicStream(
command: memRecordingState ? Command.start : Command.stop);
} else if (isActive) {
memRecordingState = isRecording;
_controlMicStream(command: Command.stop);
print("Pause app");
isActive = false;
}
}
@override
void dispose() {
listener.cancel();
controller.dispose();
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
}
class WavePainter extends CustomPainter {
int? localMax;
int? localMin;
List<int>? samples;
late List<Offset> points;
Color? color;
BuildContext? context;
Size? size;
// Set max val possible in stream, depending on the config
// int absMax = 255*4; //(AUDIO_FORMAT == AudioFormat.ENCODING_PCM_8BIT) ? 127 : 32767;
// int absMin; //(AUDIO_FORMAT == AudioFormat.ENCODING_PCM_8BIT) ? 127 : 32767;
WavePainter({this.samples, this.color, this.context, this.localMax, this.localMin});
@override
void paint(Canvas canvas, Size? size) {
this.size = context!.size;
size = this.size;
Paint paint = new Paint()
..color = color!
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
if (samples!.length == 0)
return;
points = toPoints(samples);
Path path = new Path();
path.addPolygon(points, false);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldPainting) => true;
// Maps a list of ints and their indices to a list of points on a cartesian grid
List<Offset> toPoints(List<int>? samples) {
List<Offset> points = [];
if (samples == null)
samples = List<int>.filled(size!.width.toInt(), (0.5).toInt());
double pixelsPerSample = size!.width/samples.length;
for (int i = 0; i < samples.length; i++) {
var point = Offset(i * pixelsPerSample, 0.5 * size!.height * pow((samples[i] - localMin!)/(localMax! - localMin!), 5));
points.add(point);
}
return points;
}
double project(int val, int max, double height) {
double waveHeight = (max == 0) ? val.toDouble() : (val / max) * 0.5 * height;
return waveHeight + 0.5 * height;
}
}
class Statistics extends StatelessWidget {
final bool isRecording;
final DateTime? startTime;
final String url = "https://github.com/anarchuser/mic_stream";
Statistics(this.isRecording, {this.startTime});
@override
Widget build(BuildContext context) {
return ListView(children: <Widget>[
ListTile(
leading: Icon(Icons.title),
title: Text("Microphone Streaming Example App")),
ListTile(
leading: Icon(Icons.keyboard_voice),
title: Text((isRecording ? "Recording" : "Not recording")),
),
ListTile(
leading: Icon(Icons.access_time),
title: Text((isRecording
? DateTime.now().difference(startTime!).toString()
: "Not recording"))),
]);
}
}
Iterable<T> eachWithIndex<E, T>(
Iterable<T> items, E Function(int index, T item) f) {
var index = 0;
for (final item in items) {
f(index, item);
index = index + 1;
}
return items;
}