Continuously play generated PCM audio in Flutter

229 Views Asked by At

I have found a way to generate and play a finite snippet of PCM audio using the just_audio plugin. When the user clicks the button, one second of PCM audio is generated, appended to a WAV header, and streamed to the AudioPlayer:

import 'dart:async';
import 'dart:math';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Chromatic scale',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Chromatic scale'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

const int _kRIFF = 0x52494646; // "RIFF"
const int _kWAVE = 0x57415645; // "WAVE"
const int _kfmt_ = 0x666d7420; // "fmt "
const int _kdata = 0x64617461; // "data"
const int _kMaxSize = 0xffffffff;
const int _kWavHeaderSize = 44;

// Generate a WAV header for a much-too-long file, as length may be unknown ahead of time, or infinite
int _writeWavHeader(ByteData dst, int hz, int nChannels, int nBytes) {
  dst.setUint32(0, _kRIFF, Endian.big);
  dst.setUint32(4, _kMaxSize, Endian.little);
  dst.setUint32(8, _kWAVE, Endian.big);
  dst.setUint32(12, _kfmt_, Endian.big);
  dst.setUint32(16, 16, Endian.little);
  dst.setUint16(20, 1, Endian.little);
  dst.setUint16(22, nChannels, Endian.little);
  dst.setUint32(24, hz, Endian.little);
  dst.setUint32(28, hz * nChannels * nBytes, Endian.little);
  dst.setUint16(32, nBytes * nChannels, Endian.little);
  dst.setUint16(34, nBytes * 8, Endian.little);
  dst.setUint32(36, _kdata, Endian.big);
  dst.setUint32(40, _kMaxSize, Endian.little);
  return _kWavHeaderSize;
}

// Audio source that generates PCM data and returns it with a WAV header
class StreamingSource extends StreamAudioSource {
  static const int _sampleRate = 44100;
  double frequency;

  StreamingSource([this.frequency = 440]);

  @override
  Future<StreamAudioResponse> request([int? start, int? end]) async {
    int t0 = start ?? 0;
    int nSecs = 1;
    int t1 = end ?? (_sampleRate * nSecs + _kWavHeaderSize);
    int len = t1 - t0;
    assert(len >= _kWavHeaderSize);
    Uint8List data = Uint8List(len);
    int endOfHeader = _writeWavHeader(data.buffer.asByteData(), _sampleRate, 1, 1);
    for (int i = 0; i < data.length - endOfHeader; i++) {
      double samp = sin(i * 2 * pi * frequency / _sampleRate) * 0.5 + 0.5;
      data[endOfHeader + i] = (samp * 255).toInt();
    }
    return StreamAudioResponse(
      sourceLength: len,
      contentLength: len,
      offset: t0,
      stream: Stream.fromIterable([data]),
      contentType: 'audio/wav',);
  }

}

class _MyHomePageState extends State<MyHomePage> {
  late AudioPlayer _player;
  late StreamingSource _source;

  @override
  void initState() {
    super.initState();
    _player = AudioPlayer();
    _source = StreamingSource(440);
    _initPlayer();
  }

  Future<void> _initPlayer() async {
    await _player.pause();
    await _player.setAudioSource(_source, preload: false);
  }

  Future<void> _playTone() async {
    _source.frequency *= pow(2.0, 1.0/12);
    await _player.stop();
    await _player.seek(Duration.zero); // Stop and seek necesssary to request audio again
    await _player.play();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _playTone,
        tooltip: 'Play',
        child: const Icon(Icons.add),
      ),
    );
  }
}

For example, if I instead wanted to continuously generate and play PCM audio, and whenever the button is pressed, it begins to generate audio of a new frequency, how might I do so? I suppose what I'd ideally be looking for is something like SDL Audio's callback that is called when the device needs new audio, from which I could then just return new generated audio with the current desired pitch, but I do not think something quite like that exists for Flutter. Either using just_audio, or another widespread audio plugin that supports all major platforms, how could one generate and play PCM audio in realtime?

0

There are 0 best solutions below