Skip to main content
Synthesize natural-sounding speech from text using on-device Piper TTS models.

Basic Usage

// Load a TTS voice
await RunAnywhere.loadTTSVoice('vits-piper-en_US-lessac-medium');

// Synthesize speech
final result = await RunAnywhere.synthesize(
  'Hello! Welcome to RunAnywhere.',
  rate: 1.0,
  pitch: 1.0,
);

// result.samples contains PCM Float32 audio
// result.sampleRate is typically 22050 Hz
print('Duration: ${result.durationSeconds}s');

TTSResult

PropertyTypeDescription
samplesFloat32ListAudio samples (PCM float)
sampleRateintSample rate in Hz (typically 22050)
durationMsintDuration in milliseconds
durationSecondsdoubleDuration in seconds
numSamplesintNumber of audio samples

Parameters

ParameterTypeDefaultDescription
ratedouble1.0Speech rate (0.5 to 2.0)
pitchdouble1.0Voice pitch (0.5 to 2.0)
volumedouble1.0Volume (0.0 to 1.0)
// Slower, lower pitch for emphasis
final result = await RunAnywhere.synthesize(
  'Important announcement!',
  rate: 0.8,
  pitch: 0.9,
  volume: 1.0,
);

Setup

1. Register ONNX Backend

import 'package:runanywhere_onnx/runanywhere_onnx.dart';

await Onnx.register();

2. Add TTS Voice

Onnx.addModel(
  id: 'vits-piper-en_US-lessac-medium',
  name: 'Piper US English (Lessac)',
  url: 'https://github.com/RunanywhereAI/sherpa-onnx/releases/download/runanywhere-models-v1/vits-piper-en_US-lessac-medium.tar.bz2',
  modality: ModelCategory.textToSpeech,
);

3. Download & Load

// Download
await for (final progress in RunAnywhere.downloadModel('vits-piper-en_US-lessac-medium')) {
  print('${(progress.percentage * 100).toStringAsFixed(1)}%');
  if (progress.state.isCompleted) break;
}

// Load
await RunAnywhere.loadTTSVoice('vits-piper-en_US-lessac-medium');

Playing Audio

Use a package like audioplayers to play the synthesized audio:
import 'package:audioplayers/audioplayers.dart';

final player = AudioPlayer();

Future<void> speakText(String text) async {
  final result = await RunAnywhere.synthesize(text);

  // Convert Float32 samples to bytes for playback
  final bytes = _convertToBytes(result.samples, result.sampleRate);

  // Play using audioplayers
  await player.play(BytesSource(bytes));
}

Uint8List _convertToBytes(Float32List samples, int sampleRate) {
  // Convert Float32 PCM to Int16 PCM WAV format
  final int16Samples = Int16List(samples.length);
  for (var i = 0; i < samples.length; i++) {
    int16Samples[i] = (samples[i] * 32767).clamp(-32768, 32767).toInt();
  }

  // Create WAV header + data
  return _createWavFile(int16Samples, sampleRate);
}

Complete Example

class TextToSpeechDemo extends StatefulWidget {
  @override
  _TextToSpeechDemoState createState() => _TextToSpeechDemoState();
}

class _TextToSpeechDemoState extends State<TextToSpeechDemo> {
  final _controller = TextEditingController();
  final _player = AudioPlayer();
  bool _isSpeaking = false;
  bool _isLoaded = false;

  @override
  void initState() {
    super.initState();
    _initTTS();
  }

  Future<void> _initTTS() async {
    // Download if needed
    final models = await RunAnywhere.availableModels();
    final ttsModel = models.firstWhere(
      (m) => m.id == 'vits-piper-en_US-lessac-medium',
    );

    if (!ttsModel.isDownloaded) {
      await for (final p in RunAnywhere.downloadModel(ttsModel.id)) {
        if (p.state.isCompleted) break;
      }
    }

    // Load voice
    await RunAnywhere.loadTTSVoice('vits-piper-en_US-lessac-medium');
    setState(() => _isLoaded = true);
  }

  Future<void> _speak() async {
    if (_controller.text.isEmpty) return;

    setState(() => _isSpeaking = true);

    try {
      final result = await RunAnywhere.synthesize(_controller.text);
      // Play audio (implementation depends on your audio setup)
      await _playAudio(result);
    } finally {
      setState(() => _isSpeaking = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _controller,
          decoration: InputDecoration(
            hintText: 'Enter text to speak...',
          ),
          maxLines: 3,
        ),
        SizedBox(height: 16),
        ElevatedButton.icon(
          onPressed: _isLoaded && !_isSpeaking ? _speak : null,
          icon: Icon(_isSpeaking ? Icons.stop : Icons.volume_up),
          label: Text(_isSpeaking ? 'Speaking...' : 'Speak'),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    _player.dispose();
    super.dispose();
  }
}

See Also