ساخت اپلیکیشن موسیقی در فلاتر | به زبان ساده

ساخت وبلاگ

«فلاتر» (Flutter) ‌یک SDK برای ساخت اپلیکیشن‌های چندپلتفرمی است که از سوی گوگل در سال 2018 معرفی شده است. فلاتر از زمان معرفی خود محبوبیت زیادی کسب کرده است. شرکت‌ها امروزه در تلاش برای کاهش هزینه‌ها هستند و از این رو به دنبال روش‌های بهتر و کارآمدتر برای ساخت اپلیکیشن‌های موبایل و به طور کلی نرم‌افزار‌های چندپلتفرمی هستند. فلاتر از همه پلتفرم‌های عمده پشتیبانی می‌کند. همچنین پشتیبانی از وب و همه سیستم‌های عامل دسکتاپ اصلی نیز در حال توسعه است. در این مقاله یک اپلیکیشن موسیقی در فلاتر توسعه می‌دهیم و در این مسیر با الگوهای طراحی اصلی توسعه اپلیکیشن‌های فلاتر آشنا خواهیم شد.

برای آشنایی با روش نصب فلاتر به این صفحه مستندات (+) مراجعه کنید. همچنین سورس کد کامل این پروژه در این ریپازیتوری (+) ارائه شده است.

ساخت اپلیکیشن <strong>موسیقی</strong> در فلاتر

مفاهیم ابتدایی

چند مفهوم کلیدی در فلاتر وجود دارد که به طور گسترده‌ای در طراحی این اپلیکیشن دمو مورد استفاده قرار گرفته است. به این ترتیب از قابلیت‌های زبان Dart بهره گرفته‌ایم و دیگر نیازی به استفاده از حجم بالایی از کد تکراری وجود ندارد. این موضوع تأثیر عمیقی بر بهبود قابلیت خوانایی، پایداری و عملکرد کد می‌گذارد.

بهره‌گیری بهینه از قابلیت‌های یک زبان، تفاوتی به اندازه تبدیل یک اپلیکیشن شلوغ و مستعد باگ به یک شاهکار هنری ایجاد می‌کند. زبان دارت قابلیت‌های زیادی دارد که به ساخت UX ناهمگام و با تعامل‌پذیری بالا به همراه «مدیریت حالت» کمک می‌کند.

مفاهیم کلیدی مورد اشاره به شرح زیر هستند:

  • ویجت‌های فلاتر از قبیل Container ،SizedBox و Column
  • استفاده از Timer و Stopwatch برای کار با بازه‌های زمانی
  • پیاده‌سازی سرویس‌ها با Stream / StreamController

این مفاهیم به روش‌های مختلفی در سراسر این اپلیکیشن دمو ترکیب شده‌اند تا به یک طراحی دست یابیم که رندرینگ UI و منطق کنترلی به طور تمیزی در کلاس‌هایی با اینترفیس‌ها و مشخصه‌های با کاربرد آسان ترکیب شوند.

شرح اجمالی پروژه

معماری و UX اپلیکیشن ایجاد بیت موسیقی (Beat) تا حد امکان ساده حفظ شده است تا شبیه دستگاه‌های بیت‌ساز دهه 190 و 1980 میلادی به نظر برسد. در این دستگاه‌ها از منابعی مانند سوئیچ‌های مکانیکی، اجزای مسی و CPU-های جذاب و جدید 80 بیتی محدود بودند. در آن دوره ساخت دستگاه‌هایی که نوازندگان توانایی خرید آن را داشته باشند، نیازمند این بود که هزینه‌های طراحی و ساخت در کمترین حد ممکن باقی بمانند.

چارچوب اصلی UI به چهار ویجت «پنل جلویی» تقسیم شده است که هر یک بخشی از تعامل‌پذیری را عرضه می‌کنند و منطق دستگاه درون یک سرویس playback ساده و سرویس موتور صوتی قرار گرفته است. این معماری شبیه اجزای یک آلت موسیقی سخت‌افزاری واقعی است که داده‌ها را از طریق کابل‌های MIDI ارسال می‌کند.

نقطه ورودی اپلیکیشن

اپلیکیشن ما درون فایل main.dart مقداردهی می‌شود:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_drum_machine_demo/services/sampler.dart';
import 'package:flutter_drum_machine_demo/views/display.dart';
import 'package:flutter_drum_machine_demo/views/sequencer.dart';
import 'package:flutter_drum_machine_demo/views/transport.dart';
import 'package:flutter_drum_machine_demo/views/pad-bank.dart';
void main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await Sampler.init(); runApp(Game());
}
class Game extends StatelessWidget { final String _title = "Flutter Beat Machine Demo"; @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: _title, theme: ThemeData.dark(), home: Scaffold( appBar: AppBar(title: Center(child: Text(_title))), body: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Display(), Sequencer(), Transport(), PadBank() ] ), ) ); }
}

تابع main موجب می‌شود که اپلیکیشن در همان ابتدا در حالت افقی قفل شود و تنها پس از اطمینان یافتن از این که ویجت‌ها مقداردهی شد‌ند، جهت‌گیری دستگاه مورد استفاده قرار می‌گیرد. چارچوب UI ساده است و یک ستون برای نمایش چهار ویجت اینترفیس وجود دارد. در ادامه این ویجت‌ها و کلاس‌های مورد استفاده برای مدیریت ورودی کاربر و رندر کارآمد و رفرش UI در موارد نیاز را مورد بررسی قرار می‌دهیم.

ویجت پایه

چهار ویجت اصلی در چارچوب (main) ‌یک کلاس مشترک مبنا را بسط می‌‌دهند تا به موتور صوتی وصل شوند. این کلاس در مسیر views/base-class.dart تعریف شده است:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_drum_machine_demo/services/audio-engine.dart';
class BaseWidget extends StatefulWidget { BaseWidget({ key: Key }) : super(key: key); @override BaseState createState() => BaseState();
}
class BaseState<T extends BaseWidget> extends State<T> { StreamSubscription<Signal> _stream; void on<Signal>(Signal s) => setState(() => null); @override void initState() { _stream = AudioEngine.listen(on); super.initState(); } @override void dispose() { if (_stream != null) { _stream.cancel(); } super.dispose(); } @override Widget build(BuildContext context) => Container();
}

کلاس‌های BaseWidget و BaseeState به ترتیب اقدام به بسط StatefulWidget و State می‌کنند و یک استریم داخلی را پیاده‌سازی می‌کنند که در زمان مقداردهی اولیه، یک شنونده را به AudioEngine متصل می‌سازد و هنگامی که سیگنالی از موتور صوتی دریافت شود، حالت را رفرش می‌کند. از این رو هر ویجت که AudioEngine را بسط دهد، ‌در مواردی که موتور صوتی سیگنالی ارسال کند که رویدادی درون موتور رخ داده است، بازسازی می‌شود و از این رو UI باید از نو ساخته شود.

پنل نمایش

بالاترین کامپوننت در ستون scaffold به نام پنل نمایش در مسیر views/display.dart قرار دارد:

import 'package:flutter/material.dart';
import 'package:flutter_drum_machine_demo/services/audio-engine.dart';
import 'package:flutter_drum_machine_demo/views/base-widget.dart';
class BPMSelector extends StatelessWidget { final ScrollController _controller = ScrollController(); final int itemHeight = 32; @override Widget build(BuildContext context) { double listHeight = 256.toDouble() * itemHeight; double offset = (listHeight / AudioEngine.bpm * itemHeight); WidgetsBinding.instance.addPostFrameCallback((_) => _controller.jumpTo(offset)); TextStyle style = Theme.of(context).textTheme.headline5; return Dialog( backgroundColor: Colors.black38, child: SizedBox.expand( child: Container( padding: EdgeInsets.all(24), child: ListView.builder( controller: _controller, itemCount: 255, itemBuilder: (context, i) => InkWell( onTap: () { AudioEngine.bpm = (i + 1); Navigator.pop(context); }, child: Container( height: 32, child: Center(child: Text((i+1).toString(), style: style)) ) ) ) ) ) ); }
}
class Display extends BaseWidget { Display({Key key}) : super(key: key); @override _DisplayState createState() => _DisplayState();
}
class _DisplayState extends BaseState<Display> { Color _color = Color.lerp(Colors.brown, Colors.black, 0.7); String get _label => AudioEngine.bpm.toString() + 'bpm'; bool get _isRunning => AudioEngine.state != ControlState.READY; int get _step => AudioEngine.step; @override Widget build(BuildContext context) { double labelWidth = MediaQuery.of(context).size.width / 5; TextStyle style = Theme.of(context).textTheme.overline; return Container( height: 48, color: _color, child: Row( children: <Widget>[ Container( padding: EdgeInsets.all(4), width: labelWidth, child: Container( decoration: BoxDecoration( color: Colors.black26, border: Border.all(color: Colors.pink.withOpacity(0.32)), borderRadius: BorderRadius.circular(2), ), child: SizedBox.expand( child: MaterialButton( padding: EdgeInsets.zero, onPressed: () => showDialog(context: context, builder: (_) => BPMSelector()), child: Text(_label, style: style) ) ) ) ), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: List<Widget>.generate(8, (i) => Expanded( child: Container( margin: EdgeInsets.all(4), decoration: BoxDecoration( color: (_step == i && _isRunning) ? Colors.grey.withOpacity(0.2) : Colors.black26, border: Border.all(color: Colors.yellow.withOpacity(0.12)), borderRadius: BorderRadius.circular(2) ) ) ) ) ) ) ] ) ); }
}

کلاس DisplayPanel نشانگرهای موقعیت BPM و Step را در ابتدای صفحه رندر می‌کند و زمانی که کلاس مبنا سیگنالی دریافت کند، به صورت خودکار رفرش می‌شود. با کلیک کردن روی نشانگر BPM یک دیالوگ BPMSelector به همراه فهرستی از گزینه‌های عددی از 1 تا 256 باز می‌شود. با انتخاب یکی از این گزینه‌ها میزان BPM روی موتور صوتی تنظیم می‌شود.

نشانگرهای Step نیز تولید می‌شوند که هر یک از آن‌ها زمانی که موتور اجرا شود و گام کنونی با شاخص روی چرخه رندر تطبیق پیدا کند، روشن می‌شوند.

تقطیع‌کننده الگو

ویجت ادیتور «تقطیع‌کننده الگو» (Pattern Sequencer) در مسیر views/sequencer.dart تعریف می‌شود:

import 'package:flutter/material.dart';
import 'package:flutter_drum_machine_demo/services/audio-engine.dart';
import 'package:flutter_drum_machine_demo/services/sampler.dart';
import 'package:flutter_drum_machine_demo/views/track.dart';
class Sequencer extends StatelessWidget { Sequencer({Key key}) : super(key: key); final BorderSide _border = BorderSide(color: Colors.amber.withOpacity(0.4)); @override Widget build(BuildContext context) { double labelWidth = MediaQuery.of(context).size.width / 5; return Expanded( child: Container( decoration: BoxDecoration( border: Border(top: _border), color: Colors.black45, ), child: Column( children: List<Widget>.generate(Sampler.samples.length, (i) => Expanded( child: Container( decoration: BoxDecoration(border: Border(bottom: _border)), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ InkWell( enableFeedback: false, onTap: () => AudioEngine.on<PadEvent>(PadEvent(DRUM_SAMPLE.values[i])), child: Container( width: labelWidth, color: Sampler.colors[i].withOpacity(0.2), child: Center(child: Text(Sampler.samples[DRUM_SAMPLE.values[i]])) ), ), Track(sample: DRUM_SAMPLE.values[i]) ] ) ) ) ), ) ) ); }
}

این سکوئنسر یک «ویجت بی‌حالت» (StatelessWidget) است، چون هیچ نوع تعامل‌پذیری از خود ندارد، اما به جای آن ویجت‌های Track را که UX هر ترک را ارائه می‌کنند رندر می‌کند.

یک ردیف بسط‌یافته نیز برای هر سمپل (نمونه) تولید می‌شود که دارای یک برچسب در سمت چپ است و یک Track دارد که به صورت خودکار باز شده و باقی فضای ردیف را پر می‌کند.

ترک سکوئنسر

ادیتور تِرَک سکانس در مسیر views/track.dart تعریف شده است:

import 'package:flutter/material.dart';
import 'package:flutter_drum_machine_demo/services/audio-engine.dart';
import 'package:flutter_drum_machine_demo/services/sampler.dart';
import 'package:flutter_drum_machine_demo/views/base-widget.dart';
class Track extends BaseWidget { Track({ Key key, @required this.sample }) : super(key: key); final DRUM_SAMPLE sample; @override _TrackState createState() => _TrackState();
}
class _TrackState extends BaseState<Track> { List<bool> _data = List.generate(8, (i) => false); bool get isRunning => AudioEngine.state != ControlState.READY; Color get color => Sampler.colors[widget.sample.index]; Color getItemColor(int i) => (_data[i] == true) ? (i == AudioEngine.step && isRunning) ? color.withOpacity(0.6) : color.withOpacity(0.4) : (i % 2 == 0) ? Colors.black38 : Colors.transparent; @override void on<Signal>(Signal signal) => setState(() => _data = AudioEngine.trackdata[widget.sample]); @override Widget build(BuildContext context) { return Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: List<Widget>.generate(8, (i) => Expanded( child: SizedBox.expand( child: InkWell( enableFeedback: false, onTap: () => AudioEngine.on<EditEvent>(EditEvent(widget.sample, i)), child: Container( margin: EdgeInsets.symmetric(horizontal: 1), color: getItemColor(i) ) ) ) ) ) ) ); }
}

ویجت Track در واقع «ویجت پایه» (BaseWidget) را بسط داده است و از این رو هر زمان که یک سیگنال از موتور صوتی دریافت کند، بازسازی می‌شود. هر ترک درون خود یک فهرست از هشت نشانگر نُت دارد که وقتی کلیک شود، یک رویداد به موتور صوتی ارسال می‌کند. به این ترتیب حالت نت به صورت داخلی خاموش/روشن می‌شود و علامتی برای یک رفرش فرستاده می‌شود. رنگ هر نشانگر بلوک نت از روی این مسئله که نت در موقعیت کنونی موجود باشد و این که نت در حال پخش باشد یا نه، تعیین می‌شود. زمانی که یک نت موجود نباشد، رنگ ستون‌های مجاور به منظور خوانایی و UX تغییر می‌یابد.

کنترل انتقال

در این بخش ویجت «کنترل انتقال» (Transport Control) را در مسیر views/transport.dart تعریف می‌کنیم:

import 'package:flutter/material.dart';
import 'package:flutter_drum_machine_demo/services/audio-engine.dart';
import 'package:flutter_drum_machine_demo/views/base-widget.dart';
class Transport extends BaseWidget { Transport({Key key}) : super(key: key); @override _TransportState createState() => _TransportState();
}
class _TransportState extends BaseState<Transport> { ControlState get state => AudioEngine.state; Map<ControlState, Icon> get _icons => { ControlState.READY: Icon(Icons.stop, color: (state == ControlState.READY) ? Colors.blue : Colors.white), ControlState.PLAY: Icon(Icons.play_arrow, color: (state == ControlState.PLAY) ? Colors.green : Colors.white), ControlState.RECORD: Icon(Icons.fiber_manual_record, color: (state == ControlState.RECORD) ? Colors.red : Colors.white) }; final BoxDecoration _decoration = BoxDecoration( color: Colors.black54, border: Border(bottom: BorderSide(color: Colors.blueGrey.withOpacity(0.6))) ); void onTap(ControlState state) => AudioEngine.on<ControlEvent>(ControlEvent(state)); @override Widget build(BuildContext context) { return Container( decoration: _decoration, height: 64, padding: EdgeInsets.symmetric(horizontal: 8), child: Row( children: List<Widget>.generate(ControlState.values.length, (i) => Expanded( child: SizedBox.expand( child: Container( padding: EdgeInsets.symmetric(horizontal: 4), child: MaterialButton( disabledColor: Colors.black54, onPressed: (state == ControlState.values[i]) ? null : () => onTap(ControlState.values[i]), child: _icons[ControlState.values[i]] ) ) ) ) ) ) ); }
}

کلاس Transport یک ردیف از دکمه‌های کنترل انتقال ایجاد می‌کند که هر کدام از آن‌ها را وقتی بزنیم یک فراخوانی onTap ایجاد می‌کند و رویداد تغییر حالت به موتور ارسال می‌شود که به نوبه خود از طریق ویجت پایه یک رفرش را روی این ویجت علامت‌دهی می‌کند. زمانی که یک دکمه با حالت کنونی موتور تطبیق پیدا کند، با ارسال مقدار null به متد onPressed مربوط به MaterialButton غیر فعال می‌شود.

پد بانک

پد بانک درام در مسیر views/pad-bank.dart تعریف می‌شود:

import 'package:flutter/material.dart';
import 'package:flutter_drum_machine_demo/views/pad.dart';
class PadBank extends StatelessWidget { @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; double padBankHeight = (size.height / 3); double padHeight = padBankHeight / 2; double padWidth = size.width / 3; return Container( height: padBankHeight, color: Colors.black38, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List<Widget>.generate(2, (i) => Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List<Widget>.generate(3, (j) => Pad( height: padHeight, width: padWidth, value: 3 * i + j ) ) ) ) ) ); }
}

ویجت PadBank نیز ویجت StatelessWidget را بسط می‌دهد و از این رو هیچ مشخصه «تغییرپذیر» (mutable) ندارد. به همین جهت این ویجت نیازی به حالت ندارد. این ویجت یک کانتینر با 3/1 ارتفاع فضای موجود روی والا خود با دو ردیف ویجت‌های Pad رندر می‌کند که هر یک اندازه تعریف‌شده‌ای دارند و مقدار آن نیز از اندیس لیست جاری گرفته می‌شود.

پد درام

ویجت پد درام در مسیر views/pad.dart تعریف می‌شود:

import 'package:flutter/material.dart';
import 'package:flutter_drum_machine_demo/services/audio-engine.dart';
import 'package:flutter_drum_machine_demo/services/sampler.dart';
class Pad extends StatelessWidget { Pad({ this.height, this.width, this.value }); final double height; final double width; final int value; DRUM_SAMPLE get _sample => DRUM_SAMPLE.values[value]; String get _name => Sampler.samples[_sample]; Color get _color => Sampler.colors[_sample.index]; @override Widget build(BuildContext context) { return SizedBox( height: height * .82, width: width * .88, child: Container( alignment: Alignment.center, decoration: BoxDecoration( border: Border.all(color: _color), borderRadius: BorderRadius.all(Radius.circular(4)), color: _color.withOpacity(0.12) ), child: SizedBox.expand( child: InkWell( enableFeedback: false, onTap: () => AudioEngine.on<PadEvent>(PadEvent(_sample)), child: Center(child: Text(_name)), ) ) ) ); }
}

ویجت پد یک ویجت بی‌حالت است که سه پارامتر تغییرناپذیر نهایی (final) به عنوان آرگومان می‌گیرد. سه مشخصه get تعریف شده‌اند تا DRUM_SAMPLE را همراه با نام و رنگ سمپل مربوطه دریافت کنند. زمانی که روی یک پد بزنید، یک PadEvent به موتور صوتی ارسال می‌شود تا در آنجا پردازش شود. در ادامه به بررسی طرز کار داخلی این اپلیکیشن ایجاد بیت می‌پردازیم.

سمپلر

تعاریف سمپل و بارگذاری/بازپخش در مسیر services/sampler.dart تعریف شده‌اند:

import 'dart:async';
import 'package:audioplayers/audio_cache.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
enum DRUM_SAMPLE { KICK, SNARE, HAT, TOM1, TOM2, CRASH }
abstract class Sampler { static String _ext = '.wav'; static Map<DRUM_SAMPLE, String> samples = const { DRUM_SAMPLE.KICK: 'kick', DRUM_SAMPLE.SNARE: 'snare', DRUM_SAMPLE.HAT: 'hat', DRUM_SAMPLE.TOM1: 'tom1', DRUM_SAMPLE.TOM2: 'tom2', DRUM_SAMPLE.CRASH: 'crash' }; static List<Color> colors = [ Colors.red, Colors.amber, Colors.purple, Colors.blue, Colors.cyan, Colors.pink, ]; static List<String> _files = List.generate(samples.length, (i) => samples[DRUM_SAMPLE.values[i]] + _ext); static AudioCache _cache = AudioCache(respectSilence: true); static Future<void> init() => _cache.loadAll(_files); static void play(DRUM_SAMPLE sample) => _cache.play(samples[sample] + _ext, mode: PlayerMode.LOW_LATENCY);
}

انواع سمپل با استفاده از DRUM_SAMPLE تعریف شده‌اند و نام‌های فایل و رنگ‌های متناظر روی سرویس مقداردهی می‌شوند که فایل‌های صوتی را در طی فاز مقداردهی اولیه اپلیکیشن بارگذاری می‌کند. زمانی که متد play روی سمپلر از سوی موتور صوتی فراخوانی شود، فایل صوتی کش‌شده متناظر نواخته می‌شود.

موتور صوتی

در این بخش به بررسی سرویس صوتی می‌پردازیم که در مسیر services/audio-service.dart تعریف شده است:

import 'dart:async';
import 'package:flutter_drum_machine_demo/services/sampler.dart';
enum ControlState { READY, PLAY, RECORD }
class Event { const Event(); }
class TickEvent extends Event {}
class ControlEvent extends Event { const ControlEvent(this.state); final ControlState state;
}
class PadEvent extends Event { const PadEvent(this.sample); final DRUM_SAMPLE sample;
}
class EditEvent extends Event { const EditEvent(this.sample, this.position); final DRUM_SAMPLE sample; final int position;
}
class Signal {}
abstract class AudioEngine { // Each pattern has eight steps static const int _resolution = 8; static int step = 0; // Engine control current state static ControlState _state = ControlState.READY; static get state => _state; // Beats per minute static int _bpm = 120; static get bpm => _bpm; static set bpm(int x) { _bpm = x; if (_state != ControlState.READY) { synchronize();} _signal.add(Signal()); } // Generates a new blank track data structure static Map<DRUM_SAMPLE, List<bool>> get _blanktape => Map.fromIterable(DRUM_SAMPLE.values, key: (k) => k, value: (v) => List.generate(8, (i) => false)); // Track note on/off data static Map<DRUM_SAMPLE, List<bool>> _trackdata = _blanktape; static Map<DRUM_SAMPLE, List<bool>> get trackdata => _trackdata; // Timer tick duration static Duration get _tick => Duration(milliseconds: (60000 / bpm / 2).round()); static Stopwatch _watch = Stopwatch(); static Timer _timer; // Outbound signal driver - allows widgets to listen for signals from audio engine static StreamController<Signal> _signal = StreamController<Signal>.broadcast(); static Future<void> close() => _signal.close(); // Not used but required by SDK static StreamSubscription<Signal> listen(Function(Signal) onData) => _signal.stream.listen(onData); // Incoming event handler static void on<T extends Event>(Event event) { switch (T) { case PadEvent: if (state == ControlState.RECORD) { return processInput(event); } Sampler.play((event as PadEvent).sample); return; case TickEvent: if (state == ControlState.READY) { return; } return next(); case EditEvent: return edit(event); case ControlEvent: return control(event); } } // Controller state change handler static control(ControlEvent event) { switch (event.state) { case ControlState.PLAY: case ControlState.RECORD: if (state == ControlState.READY) { start(); } break; case ControlState.READY: default: reset(); } _state = event.state; _signal.add(Signal()); } // Note block edit event handler static void edit(EditEvent event) { trackdata[event.sample][event.position] = !trackdata[event.sample][event.position]; if (trackdata[event.sample][event.position]) { Sampler.play(event.sample); } _signal.add(Signal()); } // Quantize input using the stopwatch static void processInput(PadEvent event) { int position = (_watch.elapsedMilliseconds < 900) ? step : (step != 7) ? step + 1 : 0; edit(EditEvent(event.sample, position)); } // Reset the engine static void reset() { step = 0; _watch.reset(); if (_timer != null) { _timer.cancel(); } } // Start the sequencer static void start() { reset(); _watch.start(); _timer = Timer.periodic(_tick, (t) => on<TickEvent>(TickEvent())); } // Process the next step static void next() { step = (step == 7) ? 0 : step + 1; _watch.reset(); trackdata.forEach((DRUM_SAMPLE sample, List<bool> track) { if (track[step]) { Sampler.play(sample); } }); _watch.start(); _signal.add(Signal()); } static void synchronize() { _watch.stop(); _timer.cancel(); _watch.start(); _timer = Timer.periodic(_tick, (t) => on<TickEvent>(TickEvent())); }
}

سرویس «موتور صوتی» (AudioEngine) حالت «کنترل انتقال» را مدیریت می‌کند، رویدادهای ورودی را اداره کرده و فرایند «کمّی‌سازی» (quantization) را در زمان ضبط کردن، روی نت‌های ورودی اجرا می‌کند. همچنین موتور صوتی داده‌های ترک را ذخیره کرده و به همه ویجت‌هایی که گوش می‌دهند بسته به نیاز علامت می‌دهد که UI را رفرش کنند.

کلاس‌های Event برای هر نوع از رویدادی که موتور صوتی نیاز دارد، تعریف شده‌اند و یک کلاس placeholder Signal نیز تعریف شده است تا به عنوان یک سیگنال با کاربرد عمومی برای رفرش کردن UI مورد استفاده قرار گیرد. در سناریوهای پیچیده‌تر، می‌توان کلاس Signal را برای ارسال انواع مختلفی از سیگنال‌ها به UI بسط داد.

«وضوح الگو» (Pattern Resolution) و Step همراه با حالت کنترل، BPM، داده‌های ترک اولیه، محاسبات Timer / Watch / _tick تعریف شده‌اند. همچنین یک StreamController به همراه listener تعریف شده‌اند تا ویجت‌ها بتوانند به سیگنال‌های دریافتی گوش دهند.

زمانی که سطح کنترل (مانند پد درام) on را با یک وهله از Event فراخوانی کند، متد از ژنریک‌ها برای سوئیچ کردن به نوع رویداد و اجرای عمل صحیح استفاده می‌کند. به این ترتیب همه پیام‌های ورودی از طریق یک مکان مسیریابی می‌شوند و بر همین اساس مدیریت خواهند شد. هر نوع رویداد با یک سری از عملیات درون موتور متناظر است. متدهای control ،edit ،next و synchronize در زمانی که همه به‌روزرسانی‌ها تکمیل شدند، هر کدام یک سیگنال را به UI می‌فرستند.

طراحی موتور صوتی به ما امکان می‌دهد که UI را به صورت درجا (on-the-fly) به‌روزرسانی کنیم و نیازی به راه‌اندازی مجدد موتور وجود ندارد. به این ترتیب امکان ضبط با یک تغییر حالت ساده میسر می‌شود و با این تغییر نت‌های ورودی آتی باید از متد process بگذرند تا بتوانند تمپو را در میانه الگو تنظیم کنند. همچنین از یک متد synchronize برای تنظیم تایمر جاری به BPM جدید استفاده می‌شود.

وقتی که رویداد EditEvent دریافت شود، داده‌های این رویداد برای عوض کردن آن مقدار بولی که نمایانگر روشن/خاموش بودن نت برای این ترک و موقعیت گام است مورد استفاده قرار می‌گیرند. زمانی که موتور صوتی آغاز شود، یک تایمر دوره‌ای ایجاد می‌شود که با هر مقدار _tick سکوئنسر را به پیش می‌برد و next را فرا می‌خواند که تایمر را افزایش داده یا ریست می‌کند و سپس نت را برای هر ترک روی گام جاری بررسی کرده و در نهایت کمّی‌سازی ‎_watch را ریست کرده و سیگنالی به UI ارسال می‌کند.

سخن پایانی

ساخت اپلیکیشن موسیقی در فلاتر

در این مقاله یک پروژه را معرفی کردیم که قدرت Flutter SDK را برای طراحی و توسعه سریع اپلیکیشن‌ها نشان می‌دهد. فلاتر گزینه‌ای عالی برای ساخت اپلیکیشن‌های چندپلتفرمی با تعامل‌پذیری و پایداری بالا است. این SDK با پشتیبانی از پلتفرم‌های موبایل، دسکتاپ و وب، ‌به توسعه‌دهندگان امکان می‌دهد که اپلیکیشن‌هایی با کیفیت بالا بسازند که در هر محیطی عملکردی عالی دارند و نگهداری آن‌ها آسان است.

چنین اپلیکیشن‌هایی دارای ساختار مشترک بوده و یک پشتیبانی تقریباً سراسری از کتابخانه‌ها و پکیج‌ها ارائه می‌کنند. به این ترتیب وظیفه به‌روز نگه‌داشتن یک اپلیکیشن بزرگ چندپلتفرمی با فیچرهای سازگار، پایداری کاملاً مستحکم و عملکرد کاملاً سریع به مقدار زیادی ساده می‌شود.

اگر این مطلب برای شما مفید بوده است، آموزش‌ها و مطالب زیر نیز به شما پیشنهاد می‌شوند:

میثم لطفی (+)

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

نوشته ساخت اپلیکیشن موسیقی در فلاتر | به زبان ساده اولین بار در مجله فرادرس. پدیدار شد.

مطالب درسی...
ما را در سایت مطالب درسی دنبال می کنید

برچسب : نویسنده : خنجی darsi بازدید : 464 تاريخ : شنبه 21 تير 1399 ساعت: 16:04

خبرنامه