faust_min 0.2.0 copy "faust_min: ^0.2.0" to clipboard
faust_min: ^0.2.0 copied to clipboard

High-fidelity physical modeling synthesis (Flute, Bell, Singing Bowl, Tabla, Sitar), powered by Faust. Lightweight, cross-platform, and high-performance PCM generation.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:typed_data';
import 'dart:math' as math;

import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:faust_min/faust_min.dart';
import 'package:flutter_soloud/flutter_soloud.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SoLoud.instance.init();
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(useMaterial3: true).copyWith(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.dark,
        ),
      ),
      home: const FaustExampleHome(),
    );
  }
}

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

  @override
  State<FaustExampleHome> createState() => _FaustExampleHomeState();
}

class _FaustExampleHomeState extends State<FaustExampleHome> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 8,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Faust Classical Suite'),
          bottom: const TabBar(
            isScrollable: true,
            tabs: [
              Tab(icon: Icon(Icons.air), text: "Flute"),
              Tab(icon: Icon(Icons.blur_circular), text: "Bowl"),
              Tab(icon: Icon(Icons.circle), text: "Dayan"),
              Tab(icon: Icon(Icons.lens), text: "Bayan"),
              Tab(icon: Icon(Icons.music_note), text: "Sitar"),
              Tab(icon: Icon(Icons.grid_view), text: "Drums"),
              Tab(icon: Icon(Icons.notifications), text: "Bell"),
              Tab(icon: Icon(Icons.playlist_play), text: "Patterns"),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            FluteSection(),
            BowlSection(),
            DayanSection(),
            BayanSection(),
            SitarSection(),
            DrumKitSection(),
            BellSection(),
            SequencerSection(),
          ],
        ),
      ),
    );
  }
}

// ... helper code ...

class DrumKitSection extends StatefulWidget {
  const DrumKitSection({super.key});
  @override
  State<DrumKitSection> createState() => _DrumKitSectionState();
}

class _DrumKitSectionState extends State<DrumKitSection> {
  late FaustKickInstrument _kick;
  late FaustSnareInstrument _snare;
  late FaustTomInstrument _tom;
  late FaustHiHatInstrument _hat;
  late FaustRideInstrument _ride;

  double _tomFreq = 120.0;
  double _hatOpenness = 0.2;

  @override
  void initState() {
    super.initState();
    _kick = FaustKickInstrument();
    _snare = FaustSnareInstrument();
    _tom = FaustTomInstrument();
    _hat = FaustHiHatInstrument();
    _ride = FaustRideInstrument();
  }

  @override
  void dispose() {
    _kick.dispose();
    _snare.dispose();
    _tom.dispose();
    _hat.dispose();
    _ride.dispose();
    super.dispose();
  }

  void _trigger(FaustInstrument inst, String name) async {
    final pcm = Float32List(11025); // 0.25 seconds
    if (inst is FaustHiHatInstrument) inst.setOpenness(_hatOpenness);
    if (inst is FaustTomInstrument) inst.setFrequency(_tomFreq);

    // Type-specific strike
    if (inst is FaustKickInstrument) inst.strike(1.0);
    if (inst is FaustSnareInstrument) inst.strike(1.0);
    if (inst is FaustTomInstrument) inst.strike(1.0);
    if (inst is FaustHiHatInstrument) inst.strike(0.8);
    if (inst is FaustRideInstrument) inst.strike(0.8);

    inst.render(pcm);
    final source = await SoLoud.instance.loadMem(
      '${name}_${DateTime.now().microsecondsSinceEpoch}',
      createWavFile(pcm, 44100),
    );
    await SoLoud.instance.play(source);
    await Future.delayed(const Duration(milliseconds: 250));
    await SoLoud.instance.disposeSource(source);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(15),
      child: Column(
        children: [
          Expanded(
            child: GridView.count(
              crossAxisCount: 3, // 3 columns for better fit
              mainAxisSpacing: 8,
              crossAxisSpacing: 8,
              childAspectRatio: 1.2, // Make pads slightly wider/shorter
              children: [
                _pad("KICK", Colors.deepOrange, () => _trigger(_kick, "kick")),
                _pad("SNARE", Colors.blueGrey, () => _trigger(_snare, "snare")),
                _pad("TOM", Colors.blue, () => _trigger(_tom, "tom")),
                _pad(
                  "HI-HAT",
                  Colors.yellow.shade800,
                  () => _trigger(_hat, "hat"),
                ),
                _pad("RIDE", Colors.teal, () => _trigger(_ride, "ride")),
              ],
            ),
          ),
          const Divider(),
          Text(
            "Tom Tuning: ${_tomFreq.toStringAsFixed(0)} Hz",
            style: const TextStyle(fontSize: 12),
          ),
          Slider(
            value: _tomFreq,
            min: 60,
            max: 400,
            onChanged: (v) => setState(() => _tomFreq = v),
          ),
          Text(
            "Hi-Hat Openness: ${_hatOpenness.toStringAsFixed(2)}",
            style: const TextStyle(fontSize: 12),
          ),
          Slider(
            value: _hatOpenness,
            min: 0,
            max: 1.0,
            onChanged: (v) => setState(() => _hatOpenness = v),
          ),
        ],
      ),
    );
  }

  Widget _pad(String label, Color color, VoidCallback onTap) {
    return InkWell(
      onTap: onTap,
      child: Container(
        decoration: BoxDecoration(
          color: color.withOpacity(0.3),
          borderRadius: BorderRadius.circular(15),
          border: Border.all(color: color, width: 2),
        ),
        child: Center(
          child: Text(
            label,
            style: TextStyle(
              color: color,
              fontWeight: FontWeight.bold,
              fontSize: 18,
            ),
          ),
        ),
      ),
    );
  }
}

// --- Common Helpers ---

double midiToFreq(double midi) {
  return 440.0 * math.pow(2.0, (midi - 69.0) / 12.0);
}

String getNoteName(double midi) {
  const names = [
    "C",
    "C#",
    "D",
    "D#",
    "E",
    "F",
    "F#",
    "G",
    "G#",
    "A",
    "A#",
    "B",
  ];
  int imidi = midi.round();
  int octave = (imidi / 12).floor() - 1;
  return "${names[imidi % 12]}$octave";
}

Uint8List createWavFile(Float32List samples, int sampleRate) {
  final numSamples = samples.length;
  final dataSize = numSamples * 2;
  final buffer = ByteData(44 + dataSize);
  buffer.setUint32(0, 0x52494646, Endian.big);
  buffer.setUint32(4, 36 + dataSize, Endian.little);
  buffer.setUint32(8, 0x57415645, Endian.big);
  buffer.setUint32(12, 0x666D7420, Endian.big);
  buffer.setUint32(16, 16, Endian.little);
  buffer.setUint16(20, 1, Endian.little);
  buffer.setUint16(22, 1, Endian.little);
  buffer.setUint32(24, sampleRate, Endian.little);
  buffer.setUint32(28, sampleRate * 2, Endian.little);
  buffer.setUint16(32, 2, Endian.little);
  buffer.setUint16(34, 16, Endian.little);
  buffer.setUint32(36, 0x64617461, Endian.big);
  buffer.setUint32(40, dataSize, Endian.little);
  var offset = 44;
  for (var sample in samples) {
    final intSample = (sample * 32767).clamp(-32768, 32767).toInt();
    buffer.setInt16(offset, intSample, Endian.little);
    offset += 2;
  }
  return buffer.buffer.asUint8List();
}

// --- Sections ---

class FluteSection extends StatefulWidget {
  const FluteSection({super.key});
  @override
  State<FluteSection> createState() => _FluteSectionState();
}

class _FluteSectionState extends State<FluteSection> {
  late FaustFluteInstrument _inst;
  double _midi = 60.0;
  double _pressure = 0.8;
  bool _isPlaying = false;

  @override
  void initState() {
    super.initState();
    _inst = FaustFluteInstrument();
  }

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

  void _play() async {
    if (_isPlaying) return;
    setState(() => _isPlaying = true);
    _inst.setFrequency(midiToFreq(_midi));
    _inst.setPressure(_pressure);
    final pcm = Float32List(22050);
    _inst.render(pcm);
    final source = await SoLoud.instance.loadMem(
      'flute_${DateTime.now()}',
      createWavFile(pcm, 44100),
    );
    await SoLoud.instance.play(source);
    await Future.delayed(const Duration(milliseconds: 600));
    await SoLoud.instance.disposeSource(source);
    if (mounted) setState(() => _isPlaying = false);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(30),
      child: Column(
        children: [
          Text(
            "Note: ${getNoteName(_midi)}",
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          Slider(
            value: _midi,
            min: 36,
            max: 84,
            onChanged: (v) => setState(() => _midi = v),
          ),
          Text("Pressure: ${_pressure.toStringAsFixed(2)}"),
          Slider(
            value: _pressure,
            min: 0,
            max: 1.2,
            onChanged: (v) => setState(() => _pressure = v),
          ),
          const Spacer(),
          SizedBox(
            width: double.infinity,
            height: 80,
            child: ElevatedButton.icon(
              onPressed: _isPlaying ? null : _play,
              icon: const Icon(Icons.air),
              label: const Text("Blow Flute"),
            ),
          ),
        ],
      ),
    );
  }
}

class BowlSection extends StatefulWidget {
  const BowlSection({super.key});
  @override
  State<BowlSection> createState() => _BowlSectionState();
}

class _BowlSectionState extends State<BowlSection> {
  late FaustBowlInstrument _inst;
  double _midi = 48.0;
  double _duration = 6.0;
  double _rub = 0.0;
  double _waver = 0.002;
  bool _isPlaying = false;

  @override
  void initState() {
    super.initState();
    _inst = FaustBowlInstrument();
  }

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

  void _strike() async {
    if (_isPlaying) return;
    setState(() => _isPlaying = true);
    _inst.setFrequency(midiToFreq(_midi));
    _inst.setDuration(_duration);
    _inst.setRub(_rub);
    _inst.setWaver(_waver);
    _inst.strike(1.0);

    final pcm = Float32List(44100 * (_duration + 1).toInt());
    _inst.render(pcm);

    final source = await SoLoud.instance.loadMem(
      'bowl_${DateTime.now().microsecondsSinceEpoch}',
      createWavFile(pcm, 44100),
    );
    await SoLoud.instance.play(source);

    await Future.delayed(Duration(seconds: _duration.toInt()));
    await SoLoud.instance.disposeSource(source);
    if (mounted) setState(() => _isPlaying = false);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(30),
      child: Column(
        children: [
          Text(
            "Note: ${getNoteName(_midi)}",
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          Slider(
            value: _midi,
            min: 36,
            max: 72,
            onChanged: (v) => setState(() => _midi = v),
          ),
          const SizedBox(height: 20),
          Text("Sustain (Rub) Intensity: ${_rub.toStringAsFixed(2)}"),
          Slider(
            value: _rub,
            min: 0,
            max: 1.0,
            onChanged: (v) {
              setState(() => _rub = v);
              _inst.setRub(v);
            },
          ),
          const SizedBox(height: 20),
          Text("Waver (Beating): ${(_waver * 100).toStringAsFixed(2)}%"),
          Slider(
            value: _waver,
            min: 0.0,
            max: 0.02,
            onChanged: (v) {
              setState(() => _waver = v);
              _inst.setWaver(v);
            },
          ),
          const SizedBox(height: 20),
          Text("Ring Length: ${_duration.toStringAsFixed(1)}s"),
          Slider(
            value: _duration,
            min: 1.0,
            max: 15.0,
            onChanged: (v) => setState(() => _duration = v),
          ),
          const Spacer(),
          SizedBox(
            width: double.infinity,
            height: 80,
            child: ElevatedButton.icon(
              onPressed: _isPlaying ? null : _strike,
              icon: const Icon(Icons.gavel),
              label: const Text("Strike & Sustain"),
            ),
          ),
        ],
      ),
    );
  }
}

class DayanSection extends StatefulWidget {
  const DayanSection({super.key});
  @override
  State<DayanSection> createState() => _DayanSectionState();
}

class _DayanSectionState extends State<DayanSection> {
  late FaustDayanInstrument _inst;
  double _midi = 62.0; // D
  bool _isPlaying = false;

  @override
  void initState() {
    super.initState();
    _inst = FaustDayanInstrument();
  }

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

  void _strike() async {
    if (_isPlaying) return;
    setState(() => _isPlaying = true);
    _inst.setFrequency(midiToFreq(_midi));
    _inst.strike(0.9);
    final pcm = Float32List(22050);
    _inst.render(pcm);
    final source = await SoLoud.instance.loadMem(
      'dayan_${DateTime.now()}',
      createWavFile(pcm, 44100),
    );
    await SoLoud.instance.play(source);
    await Future.delayed(const Duration(milliseconds: 500));
    await SoLoud.instance.disposeSource(source);
    if (mounted) setState(() => _isPlaying = false);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(30),
      child: Column(
        children: [
          Text(
            "Note: ${getNoteName(_midi)}",
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          Slider(
            value: _midi,
            min: 48,
            max: 72,
            onChanged: (v) => setState(() => _midi = v),
          ),
          const Spacer(),
          SizedBox(
            width: double.infinity,
            height: 80,
            child: ElevatedButton.icon(
              onPressed: _isPlaying ? null : _strike,
              icon: const Icon(Icons.circle),
              label: const Text("Strike Dayan"),
            ),
          ),
        ],
      ),
    );
  }
}

class BayanSection extends StatefulWidget {
  const BayanSection({super.key});
  @override
  State<BayanSection> createState() => _BayanSectionState();
}

class _BayanSectionState extends State<BayanSection> {
  late FaustBayanInstrument _inst;
  double _meend = 1.0;
  bool _isPlaying = false;

  @override
  void initState() {
    super.initState();
    _inst = FaustBayanInstrument();
  }

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

  void _strike() async {
    if (_isPlaying) return;
    setState(() => _isPlaying = true);
    _inst.setFrequency(110.0);
    _inst.setMeend(_meend);
    _inst.strike(1.0);
    final pcm = Float32List(44100);
    _inst.render(pcm);
    final source = await SoLoud.instance.loadMem(
      'bayan_${DateTime.now()}',
      createWavFile(pcm, 44100),
    );
    await SoLoud.instance.play(source);
    await Future.delayed(const Duration(milliseconds: 1000));
    await SoLoud.instance.disposeSource(source);
    if (mounted) setState(() => _isPlaying = false);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(30),
      child: Column(
        children: [
          const Text(
            "Bayan (Bass Drum)",
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 40),
          Text("Meend (Slide): ${_meend.toStringAsFixed(2)}x"),
          Slider(
            value: _meend,
            min: 1.0,
            max: 2.5,
            onChanged: (v) => setState(() {
              _meend = v;
              _inst.setMeend(v);
            }),
          ),
          const Spacer(),
          SizedBox(
            width: double.infinity,
            height: 80,
            child: ElevatedButton.icon(
              onPressed: _isPlaying ? null : _strike,
              icon: const Icon(Icons.lens),
              label: const Text("Strike Bayan"),
            ),
          ),
        ],
      ),
    );
  }
}

class SitarSection extends StatefulWidget {
  const SitarSection({super.key});
  @override
  State<SitarSection> createState() => _SitarSectionState();
}

class _SitarSectionState extends State<SitarSection> {
  late FaustSitarInstrument _inst;
  double _midi = 50.0; // D3
  double _jivari = 0.5;
  bool _isPlaying = false;

  @override
  void initState() {
    super.initState();
    _inst = FaustSitarInstrument();
  }

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

  void _pluck() async {
    if (_isPlaying) return;
    setState(() => _isPlaying = true);
    _inst.setFrequency(midiToFreq(_midi));
    _inst.setJivari(_jivari);
    _inst.pluck(0.8);
    final pcm = Float32List(44100 * 2);
    _inst.render(pcm);

    // Debug: Check if PCM has sound
    double maxAmp = 0;
    for (var s in pcm) {
      if (s.abs() > maxAmp) maxAmp = s.abs();
    }
    debugPrint('Sitar Render - Max Amplitude: $maxAmp');

    final source = await SoLoud.instance.loadMem(
      'sitar_${DateTime.now().microsecondsSinceEpoch}',
      createWavFile(pcm, 44100),
    );
    await SoLoud.instance.play(source);
    await Future.delayed(const Duration(seconds: 2));
    await SoLoud.instance.disposeSource(source);
    if (mounted) setState(() => _isPlaying = false);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(30),
      child: Column(
        children: [
          Text(
            "Note: ${getNoteName(_midi)}",
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          Slider(
            value: _midi,
            min: 36,
            max: 72,
            onChanged: (v) => setState(() => _midi = v),
          ),
          Text("Jivari (Buzz): ${_jivari.toStringAsFixed(2)}"),
          Slider(
            value: _jivari,
            min: 0,
            max: 1,
            onChanged: (v) => setState(() => _jivari = v),
          ),
          const Spacer(),
          SizedBox(
            width: double.infinity,
            height: 80,
            child: ElevatedButton.icon(
              onPressed: _isPlaying ? null : _pluck,
              icon: const Icon(Icons.music_note),
              label: const Text("Pluck Sitar"),
            ),
          ),
        ],
      ),
    );
  }
}
class SequencerSection extends StatefulWidget {
  const SequencerSection({super.key});

  @override
  State<SequencerSection> createState() => _SequencerSectionState();
}

class _SequencerSectionState extends State<SequencerSection> {
  bool _isPlaying = false;
  String _status = "Ready to play patterns";

  Future<void> _playPattern(String type) async {
    if (_isPlaying) return;
    setState(() {
      _isPlaying = true;
      _status = "Rendering $type...";
    });

    try {
      const double sampleRate = 44100.0;
      const double tempoBpm = 120.0;
      const double durationSec = 4.0;
      final int totalSamples = (durationSec * sampleRate).toInt();

      final List<FaustTriggerData> triggers = [];
      final double secondsPerBeat = 60.0 / tempoBpm;
      final double secondsPerStep = secondsPerBeat / 4.0;
      final int samplesPerStep = (secondsPerStep * sampleRate).floor();

      if (type == "Tabla") {
        const String pattern = "D . n t G . k . D n t . G k . .";
        for (int i = 0; i < pattern.length; i++) {
          int offset = i * samplesPerStep;
          String stroke = pattern[i];
          if (stroke == ' ' || stroke == '.') continue;
          double vel = (stroke == stroke.toUpperCase()) ? 1.0 : 0.6;
          String s = stroke.toLowerCase();
          if (s == 'd') {
            triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 0, velocity: vel * 0.8, param: 0.0));
            triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 1, velocity: vel, param: 1.1));
          } else if (s == 'n') {
            triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 0, velocity: vel, param: 0.0));
          } else if (s == 't') {
            triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 0, velocity: vel, param: 1.0));
          } else if (s == 'g') {
            triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 1, velocity: vel, param: 1.2));
          } else if (s == 'k') {
            triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 1, velocity: vel, param: 1.0));
          }
        }
      } else if (type == "Zen") {
        const String bowlPattern = "B . . . . . . . B . . . . . . .";
        const String bellPattern = ". . . . . . . . . . . . . . . L";
        for (int i = 0; i < bowlPattern.length; i++) {
          int offset = i * samplesPerStep;
          if (bowlPattern[i] == 'B') {
            triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 8, velocity: 0.8)); // Bowl
          }
          if (bellPattern[i] == 'L') {
            triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 7, velocity: 1.0, param: 880.0)); // Bell
          }
        }
      } else {
        const String pattern = "K . H . S . H . K K H . S . H R";
        for (int i = 0; i < pattern.length; i++) {
          int offset = i * samplesPerStep;
          String stroke = pattern[i];
          if (stroke == ' ' || stroke == '.') continue;
          double vel = (stroke == stroke.toUpperCase()) ? 1.0 : 0.5;
          String s = stroke.toLowerCase();
          if (s == 'k') triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 2, velocity: vel));
          else if (s == 's') triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 3, velocity: vel));
          else if (s == 'h') triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 4, velocity: vel, param: 0.2));
          else if (s == 'r') triggers.add(FaustTriggerData(sampleOffset: offset, instrumentId: 6, velocity: vel));
        }
        // Snare roll
        int lastBarStart = 12 * samplesPerStep;
        for (int i = 0; i < 4; i++) {
          int microOffset = lastBarStart + (i * (samplesPerStep ~/ 2));
          triggers.add(FaustTriggerData(sampleOffset: microOffset, instrumentId: 3, velocity: 0.4 + (i * 0.1)));
        }
      }

      final Pointer<Float> outputBuffer = calloc<Float>(totalSamples);
      try {
        FaustMin.renderSequencedAudio(
          triggers: triggers,
          baseFreq: 140.0,
          sampleRate: sampleRate,
          totalSamples: totalSamples,
          outputBuffer: outputBuffer,
        );

        final pcm = outputBuffer.asTypedList(totalSamples);
        final source = await SoLoud.instance.loadMem(
          'pattern_${DateTime.now().microsecondsSinceEpoch}',
          createWavFile(pcm, 44100),
        );
        
        setState(() => _status = "Playing $type...");
        await SoLoud.instance.play(source);
        await Future.delayed(const Duration(seconds: 4));
        await SoLoud.instance.disposeSource(source);
      } finally {
        calloc.free(outputBuffer);
      }
    } catch (e) {
      setState(() => _status = "Error: $e");
    } finally {
      if (mounted) {
        setState(() {
          _isPlaying = false;
          _status = "Finished playing $type";
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(30),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.queue_music, size: 80, color: Colors.blue),
          const SizedBox(height: 20),
          Text(
            "Native Sequencer Demo",
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const SizedBox(height: 10),
          Text(
            _status,
            style: const TextStyle(color: Colors.grey),
          ),
          const SizedBox(height: 40),
          _patternButton("Tabla: Teental Variation", "Tabla", Colors.orange),
          const SizedBox(height: 15),
          _patternButton("Rock: Classic 4/4 Groove", "Rock", Colors.blueGrey),
          const SizedBox(height: 15),
          _patternButton("Zen: Meditative Chime", "Zen", Colors.amber),
          const SizedBox(height: 40),
          const Text(
            "Patterns are rendered in a single sample-accurate native pass for zero jitter and maximum efficiency.",
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
          ),
        ],
      ),
    );
  }

  Widget _patternButton(String label, String type, Color color) {
    return SizedBox(
      width: double.infinity,
      height: 60,
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          backgroundColor: color.withOpacity(0.2),
          foregroundColor: color,
          side: BorderSide(color: color),
        ),
        onPressed: _isPlaying ? null : () => _playPattern(type),
        child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
      ),
    );
  }
}

class BellSection extends StatefulWidget {
  const BellSection({super.key});
  @override
  State<BellSection> createState() => _BellSectionState();
}

class _BellSectionState extends State<BellSection> {
  late FaustBellInstrument _inst;
  double _midi = 67.0; // G4
  double _duration = 6.0;
  bool _isPlaying = false;

  @override
  void initState() {
    super.initState();
    _inst = FaustBellInstrument();
  }

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

  void _strike() async {
    if (_isPlaying) return;
    setState(() => _isPlaying = true);
    _inst.setFrequency(midiToFreq(_midi));
    _inst.setDuration(_duration);
    _inst.strike(1.0);
    final pcm = Float32List(44100 * (_duration + 1).toInt());
    _inst.render(pcm);

    final source = await SoLoud.instance.loadMem(
      'bell_${DateTime.now().microsecondsSinceEpoch}',
      createWavFile(pcm, 44100),
    );
    await SoLoud.instance.play(source);
    await Future.delayed(Duration(seconds: _duration.toInt()));
    await SoLoud.instance.disposeSource(source);
    if (mounted) setState(() => _isPlaying = false);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(30),
      child: Column(
        children: [
          const Text(
            "Meditative Bell",
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 20),
          const Icon(Icons.notifications_active, size: 100, color: Colors.amber),
          const SizedBox(height: 20),
          Text(
            "Note: ${getNoteName(_midi)}",
            style: const TextStyle(fontSize: 18),
          ),
          Slider(
            value: _midi,
            min: 48,
            max: 84,
            onChanged: (v) => setState(() => _midi = v),
          ),
          const SizedBox(height: 20),
          Text(
            "Ring Length: ${_duration.toStringAsFixed(1)}s",
            style: const TextStyle(fontSize: 14),
          ),
          Slider(
            value: _duration,
            min: 1.0,
            max: 15.0,
            onChanged: (v) {
              setState(() => _duration = v);
              _inst.setDuration(v);
            },
          ),
          const Spacer(),
          SizedBox(
            width: double.infinity,
            height: 80,
            child: ElevatedButton.icon(
              onPressed: _isPlaying ? null : _strike,
              icon: const Icon(Icons.notifications),
              label: const Text("Strike Bell"),
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.amber.shade900,
                foregroundColor: Colors.white,
              ),
            ),
          ),
        ],
      ),
    );
  }
}
3
likes
0
points
487
downloads

Publisher

verified publishermoonypanda.com

Weekly Downloads

High-fidelity physical modeling synthesis (Flute, Bell, Singing Bowl, Tabla, Sitar), powered by Faust. Lightweight, cross-platform, and high-performance PCM generation.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

ffi, flutter, plugin_platform_interface

More

Packages that depend on faust_min

Packages that implement faust_min