silk_decoder 0.0.2 copy "silk_decoder: ^0.0.2" to clipboard
silk_decoder: ^0.0.2 copied to clipboard

A Dart library for decoding SILK audio files to PCM format using native bindings, with asynchronous support to avoid blocking the main thread.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:file_picker/file_picker.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:audioplayers/audioplayers.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:silk_decoder/silk_decoder.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Silk V3 Player & Converter',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
        brightness: Brightness.dark,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final AudioPlayer _audioPlayer = AudioPlayer();

  String? _ffmpegPath;
  String _status = 'Please drag and drop or select an audio file';
  String? _currentFilePath;
  String? _currentWavPath;

  PlayerState _playerState = PlayerState.stopped;
  Duration _duration = Duration.zero;
  Duration _position = Duration.zero;

  @override
  void initState() {
    super.initState();
    _loadFFmpegPath();
    _audioPlayer.onPlayerStateChanged.listen((state) {
      if (mounted) setState(() => _playerState = state);
    });
    _audioPlayer.onDurationChanged.listen((d) {
      if (mounted) setState(() => _duration = d);
    });
    _audioPlayer.onPositionChanged.listen((p) {
      if (mounted) setState(() => _position = p);
    });
  }

  @override
  void dispose() {
    _audioPlayer.dispose();
    super.dispose();
  }

  Future<void> _loadFFmpegPath() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _ffmpegPath = prefs.getString('ffmpeg_path');
    });
    if (_ffmpegPath == null) {
      _autoDetectFFmpeg();
    }
  }

  Future<void> _saveFFmpegPath(String path) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('ffmpeg_path', path);
    setState(() {
      _ffmpegPath = path;
    });
  }

  Future<void> _autoDetectFFmpeg() async {
    String command = Platform.isWindows ? 'where' : 'which';
    ProcessResult result = await Process.run(command, ['ffmpeg']);
    if (result.exitCode == 0) {
      String path = result.stdout.toString().trim().split('\n').first;
      await _saveFFmpegPath(path);
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Auto-detected FFmpeg: $path')));
      }
    }
  }

  Future<void> _pickFFmpegPath() async {
    FilePickerResult? result = await FilePicker.platform.pickFiles();
    if (result != null) {
      await _saveFFmpegPath(result.files.single.path!);
    }
  }

  Future<void> _processFile(String filePath) async {
    if (_ffmpegPath == null || _ffmpegPath!.isEmpty) {
      setState(() {
        _status = 'Error: Please set the FFmpeg path first!';
      });
      return;
    }

    setState(() {
      _status = 'Decoding...';
      _currentFilePath = filePath;
      _playerState = PlayerState.stopped;
      _duration = Duration.zero;
      _position = Duration.zero;
    });

    try {
      final tempDir = await getTemporaryDirectory();
      final pcmPath = p.join(tempDir.path, '${p.basename(filePath)}.pcm');
      final wavPath = p.join(tempDir.path, '${p.basename(filePath)}.wav');

      await decodeSilkFileAsync(filePath, pcmPath, 24000);
      setState(() => _status = 'Converting to WAV...');
      await _convertPcmToWav(pcmPath, wavPath);

      await _audioPlayer.setSourceDeviceFile(wavPath);
      await _audioPlayer.resume();
      setState(() {
        _status = 'Ready: ${p.basename(filePath)}';
        _currentWavPath = wavPath;
      });
    } catch (e) {
      setState(() {
        _status = 'Processing failed: $e';
      });
    }
  }

  Future<void> _convertPcmToWav(String pcmPath, String wavPath) async {
    final result = await Process.run(_ffmpegPath!, [
      '-y',
      // Overwrite output file if it exists
      '-f',
      's16le',
      // Format of the input file (signed 16-bit little-endian PCM)
      '-ar',
      '24000',
      // Audio sample rate of the input file
      '-ac',
      '1',
      // Audio channels of the input file (1 for mono)
      '-i',
      pcmPath,
      // Input file path
      '-c:a',
      'pcm_s16le',
      // Explicitly set the audio codec for the output to pcm_s16le
      wavPath,
      // Output file path
    ]);
    if (result.exitCode != 0) {
      throw Exception('FFmpeg conversion failed: ${result.stderr}');
    }
  }

  Future<void> _exportFile(String format) async {
    if (_currentFilePath == null) return;

    String? outputFile = await FilePicker.platform.saveFile(
      dialogTitle: 'Please select a save location:',
      fileName: '${p.basenameWithoutExtension(_currentFilePath!)}.$format',
    );

    if (outputFile == null) return;

    setState(() => _status = 'Exporting to $format...');

    try {
      final tempDir = await getTemporaryDirectory();
      final pcmPath = p.join(
        tempDir.path,
        '${p.basename(_currentFilePath!)}.pcm',
      );
      await File(pcmPath).exists();
      final result = await Process.run(_ffmpegPath!, [
        '-y',
        '-f',
        's16le',
        '-ar',
        '24000',
        '-ac',
        '1',
        '-i',
        pcmPath,
        outputFile,
      ]);

      if (result.exitCode == 0) {
        setState(() => _status = 'Export successful: $outputFile');
      } else {
        throw Exception('FFmpeg export failed: ${result.stderr}');
      }
    } catch (e) {
      setState(() => _status = 'Export failed: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Silk Player & Converter'),
        actions: [
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () => _showSettingsDialog(),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            _buildDropZone(),
            const SizedBox(height: 20),
            if (_currentFilePath != null) _buildPlayerControls(),
            const SizedBox(height: 20),
            _buildInfoZone(),
          ],
        ),
      ),
    );
  }

  Widget _buildDropZone() {
    return DropTarget(
      onDragDone: (details) {
        final path = details.files.first.path;
        if (['.slk', '.aud', '.amr'].any((ext) => path.endsWith(ext))) {
          _processFile(path);
        }
      },
      child: InkWell(
        onTap: () async {
          FilePickerResult? result = await FilePicker.platform.pickFiles(
            type: FileType.custom,
            allowedExtensions: ['slk', 'aud', 'amr'],
          );
          if (result != null) {
            _processFile(result.files.single.path!);
          }
        },
        child: Container(
          height: 200,
          width: double.infinity,
          decoration: BoxDecoration(
            border: Border.all(
              color: Colors.grey.shade600,
              style: BorderStyle.solid,
              width: 2,
            ),
            borderRadius: BorderRadius.circular(12),
          ),
          child: const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.upload_file, size: 50),
                SizedBox(height: 10),
                Text('Drag & drop a file here or click to select'),
                Text('.slk, .aud, .amr', style: TextStyle(color: Colors.grey)),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildPlayerControls() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Slider(
              value: _position.inSeconds.toDouble().clamp(
                0.0,
                _duration.inSeconds.toDouble() + 0.1,
              ),
              max: _duration.inSeconds.toDouble() + 0.1,
              onChanged: (value) async {
                final position = Duration(seconds: value.toInt());
                await _audioPlayer.seek(position);
              },
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(_formatDuration(_position)),
                Text(_formatDuration(_duration)),
              ],
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IconButton(
                  icon: Icon(
                    _playerState == PlayerState.playing
                        ? Icons.pause
                        : Icons.play_arrow,
                  ),
                  iconSize: 48,
                  onPressed: _currentWavPath == null
                      ? null
                      : () {
                          if (_playerState == PlayerState.playing) {
                            _audioPlayer.pause();
                          } else {
                            _audioPlayer.resume();
                          }
                        },
                ),
                const SizedBox(width: 20),
                PopupMenuButton<String>(
                  onSelected: _exportFile,
                  itemBuilder: (BuildContext context) =>
                      <PopupMenuEntry<String>>[
                        const PopupMenuItem<String>(
                          value: 'wav',
                          child: Text('Export as WAV'),
                        ),
                        const PopupMenuItem<String>(
                          value: 'mp3',
                          child: Text('Export as MP3'),
                        ),
                        const PopupMenuItem<String>(
                          value: 'm4a',
                          child: Text('Export as M4A (AAC)'),
                        ),
                      ],
                  child: const Row(
                    children: [
                      Icon(Icons.save_alt),
                      SizedBox(width: 8),
                      Text('Export'),
                    ],
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  void _showSettingsDialog() {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Settings'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'FFmpeg Path:',
                style: Theme.of(context).textTheme.titleSmall,
              ),
              const SizedBox(height: 8),
              Text(_ffmpegPath ?? 'Not set'),
              const SizedBox(height: 16),
              Row(
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  TextButton(
                    onPressed: _autoDetectFFmpeg,
                    child: const Text('Auto-detect'),
                  ),
                  const SizedBox(width: 8),
                  ElevatedButton(
                    onPressed: _pickFFmpegPath,
                    child: const Text('Select manually'),
                  ),
                ],
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  String _formatDuration(Duration d) {
    final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
  }

  Widget _buildInfoZone() {
    return Container(
      height: 120,
      width: double.infinity,
      padding: const EdgeInsets.all(12.0),
      decoration: BoxDecoration(
        color: Theme.of(
          context,
        ).colorScheme.surfaceContainerHighest.withAlpha((255 * 0.3).round()),
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.grey.withAlpha((255 * 0.5).round())),
      ),
      child: SingleChildScrollView(
        child: SelectableText(
          _status,
          style: Theme.of(
            context,
          ).textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'),
        ),
      ),
    );
  }
}
0
likes
150
points
198
downloads

Publisher

unverified uploader

Weekly Downloads

A Dart library for decoding SILK audio files to PCM format using native bindings, with asynchronous support to avoid blocking the main thread.

Homepage
Repository (GitHub)

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

ffi, flutter, plugin_platform_interface

More

Packages that depend on silk_decoder

Packages that implement silk_decoder