Cara menulis plugin Flutter

Selamat datang di codelab "Cara menulis plugin Flutter".

Apa yang dimaksud dengan plugin?

Plugin adalah software yang menambahkan kemampuan pada aplikasi Anda. Misalnya, Anda mungkin ingin aplikasi seluler berinteraksi dengan kamera di perangkat. Plugin adalah bagian penting dari ekosistem Flutter. Pertama-tama, Anda harus memeriksa pub.dev dahulu untuk mengetahui apakah plugin yang Anda butuhkan sudah ada. Penulis Flutter SDK, dan juga anggota komunitas Flutter, telah menulis banyak plugin dan memublikasikan plugin ke pub.dev untuk dibagikan kepada komunitas.

Secara khusus, Anda harus melihat paket dan plugin Flutter Favorite. Tag Favorites Flutter Favorite mengidentifikasi plugin yang sebaiknya Anda pertimbangkan terlebih dahulu saat membuat aplikasi.

Flutter memudahkan pengguna berinteraksi dengan library Dart lintas platform, tetapi Flutter terkadang ideal untuk berinteraksi dengan kode khusus platform. Misalnya, Anda mungkin ingin berkomunikasi dengan database yang tidak memiliki library Dart yang ditulis untuk database. Flutter menyediakan mekanisme untuk penulisan plugin yang memungkinkan Anda berkomunikasi dengan kode khusus platform dan juga memungkinkan Anda memublikasikan plugin di pub.dev sehingga pengguna lain dapat menggunakan plugin.

Dalam codelab ini, Anda akan mempelajari cara menulis plugin sendiri untuk iOS dan Android. Anda akan mengimplementasikan plugin musik sederhana yang memproses audio di platform host, lalu Anda akan membuat aplikasi contoh yang menggunakan plugin Anda untuk membuat keyboard musik.

Berikut adalah screenshot versi final dari aplikasi:

f4275505c0be0bd7.png 746b8f48aa63e2ff.png

Yang Anda pelajari

  • Cara menulis plugin Flutter untuk iOS dan Android.
  • Cara membuat API untuk plugin Anda.
  • Cara menulis aplikasi yang menggunakan plugin Anda.
  • Cara memublikasikan plugin Anda sehingga pengguna lain dapat menggunakannya.

Apa yang ingin Anda pelajari dari codelab ini?

Saya baru mengenal topik ini, jadi saya ingin melihat ringkasan yang bagus. Saya sedikit paham soal topik ini, tetapi saya butuh penyegaran. Saya sedang mencari kode contoh untuk digunakan dalam project saya. Saya sedang mencari penjelasan tentang sesuatu yang spesifik.

Anda memerlukan dua software untuk menyelesaikan lab ini: Flutter SDK dan editor. Anda dapat menggunakan editor pilihan Anda, seperti Android Studio atau IntelliJ dengan plugin Flutter dan Dart yang sudah diinstal, atau Visual Studio Code dengan ekstensi Flutter dan Dart Code.

Beberapa alat untuk pengembangan plugin baru-baru ini berubah, sehingga codelab ini mengasumsikan Flutter SDK v1.15.19 atau yang lebih baru. Anda dapat memeriksa versi Anda dengan perintah berikut:

$ flutter doctor

Anda dapat menjalankan codelab dengan menggunakan perangkat berikut:

Flutter dilengkapi dengan template untuk plugin yang memudahkan pengguna untuk memulai. Saat membuat template plugin, Anda dapat menentukan bahasa pemrograman apa yang ingin digunakan. Setelan defaultnya adalah iOS menggunakan Swift, sedangkan Android menggunakan Kotlin. Untuk codelab ini, Anda menggunakan Objective-C dan Java.

Jalankan perintah berikut di direktori kerja untuk membuat template plugin dan migrasikan ke Keamanan Null:

$ flutter create --template=plugin --org com.example --template=plugin --platforms=android,ios -a java -i objc plugin_codelab
$ cd plugin_codelab
$ dart migrate --apply-changes
$ cd example
$ flutter pub upgrade
$ dart migrate --apply-changes

Perintah di atas membuat struktur direktori berikut:

plugin_codelab
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── local.properties
│   ├── plugin_codelab_android.iml
│   ├── settings.gradle
│   └── src
│       └── main
│           ├── AndroidManifest.xml
│           └── java
│               └── com
│                   └── example
│                       └── plugin_codelab
│                           └── PluginCodelabPlugin.java
├── example
│   ├── README.md
│   ├── android
│   ├── build
│   │   └── ios
│   │       └── Runner.build
│   │           └── Release-iphoneos
│   │               └── Runner.build
│   │                   └── dgph
│   ├── ios
│   ├── lib
│   │   └── main.dart
│   ├── plugin_codelab_example.iml
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   └── test
│       └── widget_test.dart
├── ios
│   ├── Assets
│   ├── Classes
│   │   ├── PluginCodelabPlugin.h
│   │   └── PluginCodelabPlugin.m
│   └── plugin_codelab.podspec
├── lib
│   └── plugin_codelab.dart
├── plugin_codelab.iml
├── pubspec.lock
├── pubspec.yaml
└── test
    └── plugin_codelab_test.dart

Berikut adalah deskripsi dari beberapa file penting:

  • pubspec.yaml—File YAML yang mendefinisikan plugin Anda. File menentukan nama plugin, dependensi, versi, sistem operasi yang didukung, dan sebagainya. Selain itu, file ini digunakan di halaman pub.dev plugin Anda.
  • CHANGELOG.md—Tiap kali Anda ingin memublikasikan versi plugin yang baru, Anda harus memperbarui file markdown ini untuk mengindikasikan perubahan di versi baru.
  • README.md—File markdown ini ditampilkan di halaman depan listingan pub.dev plugin. File harus menjelaskan secara mendetail apa plugin yang digunakan dan bagaimana cara menggunakannya.
  • lib/plugin_codelab.dart—Kode Dart yang mengimplementasikan frontend ke plugin Anda. Klien plugin memiliki akses ke fungsi dan class publik di direktori ini.
  • android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java—Kode Java native yang mengimplementasikan fitur Android yang dijelaskan di plugin_codelab.dart.
  • ios/Classes/PluginCodelabPlugin.m—Kode Objective-C yang mengimplementasikan fitur iOS yang dijelaskan di plugin_codelab.dart. (Juga terdapat file header yang sesuai.)
  • example/—Direktori ini berisi klien dari plugin Anda. Saat mengembangkan plugin, Anda mengedit file ini untuk melihat cara kerja plugin-nya.
  • example/lib/main.dart—Kode Dart yang mencoba plugin Anda. Anda membuat UI contoh di sini.

Jalankan contoh di perangkat iOS atau Android Anda dengan petunjuk berikut:

$ cd plugin_codelab/example
$ flutter run

Anda akan melihat yang seperti ini:

52b7d03a33b9cbfa.png

Lihat kode yang dibuat untuk frontend plugin:

lib/plugin_codelab.dart

class PluginCodelab {
  static const MethodChannel _channel =
      const MethodChannel('plugin_codelab');

  static Future<String> get platformVersion async {
    final String version =
      await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

cf1e10b838bf60ee.png Pengamatan

  • PluginCodelab adalah class yang dipanggil pengguna plugin Anda.
  • Class ini membuat MethodChannel yang memungkinkan kode Dart berkomunikasi dengan platform host.
  • API plugin hanya memiliki satu metode, yakni pengambil properti platformVersion. Saat seseorang memanggil pengambil ini di Dart, MethodChannel memanggil metode getPlatformVersion() dan menunggu secara asinkron sampai String ditampilkan.
  • Hanya kode khusus platform yang dapat menginterpretasikan arti dari pesan getPlatformVersion, dan Anda akan melihatnya nanti.

example/lib/main.dart

Future<void> initPlatformState() async {
  String platformVersion;
  // Platform messages may fail, so we use a try/catch PlatformException.
  try {
    platformVersion = await PluginCodelab.platformVersion;
  } on PlatformException {
    platformVersion = 'Failed to get platform version.';
  }

  // If the widget was removed from the tree while the asynchronous platform
  // message was in flight, we want to discard the reply rather than calling
  // setState to update our non-existent appearance.
  if (!mounted) return;

  setState(() {
    _platformVersion = platformVersion;
  });
}

cf1e10b838bf60ee.png Pengamatan

  • Ini adalah klien plugin.
  • Kode ini memanggil pengambil yang ditentukan di lib/plugin_codelab.dart.
  • Perhatikan bahwa panggilan digabungkan ke dalam try-block. Jika kode khusus platform untuk iOS menampilkan FlutterError, atau pengecualian ditampilkan di Java, berarti kode khusus platform ditampilkan kembali di sistem Dart.

ios/Classes/PluginCodelabPlugin.m

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  FlutterMethodChannel* channel = [FlutterMethodChannel
      methodChannelWithName:@"plugin_codelab"
            binaryMessenger:[registrar messenger]];
  PluginCodelabPlugin* instance = [[PluginCodelabPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

cf1e10b838bf60ee.png Pengamatan

  • Kode inisialisasi ini dipanggil saat mesin baru disiapkan.
  • Kode ini membuat saluran untuk berkomunikasi dengan plugin.
  • Perhatikan bahwa nama saluran yang ditentukan di sini harus cocok dengan nama yang ditentukan di lib/plugin_codelab.dart.
  • Menyiapkannya sebagai methodCallDelegate berarti instance yang dibuat menerima pesan yang dikaitkan dengan messenger biner yang disediakan.

ios/Classes/PluginCodelabPlugin.m

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else {
    result(FlutterMethodNotImplemented);
  }
}

cf1e10b838bf60ee.png Pengamatan

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
  channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "plugin_codelab");
  channel.setMethodCallHandler(this);
}

cf1e10b838bf60ee.png Pengamatan

  • Kode Java ini mengimplementasikan getPlatformVersion() untuk Android.

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  if (call.method.equals("getPlatformVersion")) {
    result.success("Android " + android.os.Build.VERSION.RELEASE);
  } else {
    result.notImplemented();
  }
}

cf1e10b838bf60ee.png Pengamatan

  • Kode Java ini menangani pesan yang dikirim dari Dart. Perhatikan bahwa kode ini bentuknya serupa dengan plugin iOS, tetapi memiliki beberapa perbedaan kecil.

72ad6e60941a67c6.pngAnda telah menemukan hal yang istimewa!

Sekarang, Anda memberikan implementasi khusus platform untuk synthesizer yang mengeluarkan suara saat menekan tombol di keyboard. Anda dapat menganggap kode ini sebagai library yang akan Anda tampilkan ke Flutter. Sering kali (untuk kasus ini), saat membuat plugin, Anda sudah memiliki API platform yang ditentukan, tempat Anda mengerjakan plugin.

Kini, Anda memiliki dua implementasi terpisah untuk fungsi yang sama, satu untuk iOS dan satu lagi untuk Android. Anda harus membuat kompilasi tersebut menjadi bagian dari aplikasi Anda sehingga plugin dapat memanggil kompilasi.

Menambahkan ke iOS

Tambahkan file berikut ke project Anda:

Dengan menempatkan file tersebut di lokasi ios/Classes, file akan dikompilasi sebagai bagian dari build iOS untuk plugin Anda. Anda dapat melihat ios/plugin_codelab.podspec untuk mengetahui bahwa file secara default menggunakan glob untuk menentukan sumber mana yang akan dikompilasi. Anda hanya perlu meletakkan file di lokasi yang benar.

Menambahkan ke Android

Tambahkan file ini ke project Anda:

android/src/main/java/com/example/plugin_codelab/Synth.java

Dengan menempatkan file Java ini di lokasi android/src/main/java/com/example, file akan dikompilasi sebagai bagian dari build Android untuk plugin Anda. Anda dapat melihat sistem build Gradle untuk mengetahui bahwa Anda hanya butuh menempatkan file di direktori yang tepat agar file dikompilasi.

Penjelasan antarmuka synthesizer

Antarmuka synthesizer di iOS dan Android mirip, dan terdiri dari empat metode:

class Synthesizer {
  void start();
  void stop();
  int keyDown(int key);
  int keyUp(int key);
}

Metode keyUp() dan keyDown() mewakili peristiwa yang dikirim saat tombol di keyboard musik ditekan dan dilepaskan. Argumen key mewakili tombol mana yang ditekan atau dilepaskan. Ini adalah fungsi enumerasi dari semua tombol di keyboard musik. Standar MIDI menentukan enumerasi untuk tombol tersebut, dengan 60 sebagai nilai C Tengah dan bertambah satu untuk tiap tombol hitam atau putih (setengah nada). Plugin Anda menggunakan definisi ini.

Langkah berikutnya dalam membuat plugin adalah mempertimbangkan informasi macam apa yang ingin Anda kirim terus-menerus di antara Flutter dan platform host. Jika Anda mencoba mewakili library dengan API yang sudah ditentukan, Anda cukup meniru antarmuka tersebut.

Dalam codelab ini, kami menyediakan kode synthesizer untuk tiap platform, sehingga Anda dapat meniru antarmukanya di kode Dart:

lib/plugin_codelab.dart

import 'dart:async';

import 'package:flutter/services.dart';

class PluginCodelab {
  static const MethodChannel _channel = const MethodChannel('plugin_codelab');

  static Future<String?> get platformVersion async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  static Future<int?> onKeyDown(int key) async {
    final int? numNotesOn = await _channel.invokeMethod('onKeyDown', [key]);
    return numNotesOn;
  }

  static Future<int?> onKeyUp(int key) async {
    final int? numNotesOn = await _channel.invokeMethod('onKeyUp', [key]);
    return numNotesOn;
  }
}

Perhatikan bahwa parameter kedua untuk invokeMethod() mencantumkan parameter yang dikirim ke panggilan metode.

Kini, Anda memiliki library khusus platform untuk membuat suara dan kode Dart yang mengontrol kode tersebut, tetapi library dan kode tersebut tidak terhubung. Jika Anda memanggil metode Dart tersebut sekarang, metode menampilkan pengecualian "Tidak Diimplementasikan" karena Anda belum mengimplementasikan sistem host di plugin. Itulah langkah berikutnya.

Menghubungkan semuanya di iOS

Pertama, ubah plugin untuk membuat dan memulai instance synthesizer:

ios/Classes/PluginCodelabPlugin.m

@implementation PluginCodelabPlugin {
  int _numKeysDown;
  FLRSynthRef _synth;
}
- (instancetype)init {
  self = [super init];
  if (self) {
    _synth = FLRSynthCreate();
    FLRSynthStart(_synth);
  }
  return self;
}

- (void)dealloc {
  FLRSynthDestroy(_synth);
}

Berikutnya, mulai tangani pesan yang dikirim melalui saluran:

- (void)handleMethodCall:(FlutterMethodCall *)call
                  result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS "
        stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else if ([@"onKeyDown" isEqualToString:call.method]) {
    FLRSynthKeyDown(_synth, [call.arguments[0] intValue]);
    _numKeysDown += 1;
    result(@(_numKeysDown));
  } else if ([@"onKeyUp" isEqualToString:call.method]) {
    FLRSynthKeyUp(_synth, [call.arguments[0] intValue]);

    _numKeysDown -= 1;
    result(@(_numKeysDown));
  } else {
    result(FlutterMethodNotImplemented);
  }
}

Perhatikan bahwa kode kini juga mencari pesan onKeyDown dan onKeyUp. Untuk mendapatkan argumen key, tarik argumen dari call.arguments. Nilai yang ditampilkan dikotakkan sebagai NSNumber (dijelaskan dalam Dokumentasi saluran platform), jadi Anda harus mengonversi nilai yang ditampilkan dengan intValue.

Lihat file yang lengkap, PluginCodelabPlugin.m.

Menghubungkan semuanya di Android

Pertama, ubah plugin untuk membuat dan memulai instance synthesizer:

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

public class PluginCodelabPlugin implements FlutterPlugin, MethodCallHandler {
  private MethodChannel channel;
  private Synth synth;
  private static final String channelName = "plugin_codelab";

  private static void setup(PluginCodelabPlugin plugin, BinaryMessenger binaryMessenger) {
    plugin.channel = new MethodChannel(binaryMessenger, channelName);
    plugin.channel.setMethodCallHandler(plugin);
    plugin.synth = new Synth();
    plugin.synth.start();
  }

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    setup(this, flutterPluginBinding.getBinaryMessenger());
  }

Berikutnya, mulai tangani pesan yang dikirim melalui saluran:

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  if (call.method.equals("getPlatformVersion")) {
    result.success("Android " + android.os.Build.VERSION.RELEASE);
  } else if (call.method.equals("onKeyDown")) {
    try {
      ArrayList arguments = (ArrayList) call.arguments;
      int numKeysDown = synth.keyDown((Integer) arguments.get(0));
      result.success(numKeysDown);
    } catch (Exception ex) {
      result.error("1", ex.getMessage(), ex.getStackTrace());
    }
  } else if (call.method.equals("onKeyUp")) {
    try {
      ArrayList arguments = (ArrayList) call.arguments;
      int numKeysDown = synth.keyUp((Integer) arguments.get(0));
      result.success(numKeysDown);
    } catch (Exception ex) {
      result.error("1", ex.getMessage(), ex.getStackTrace());
    }
  } else {
    result.notImplemented();
  }
}

Seperti halnya iOS, kode kini mencari pesan onKeyDown dan onKeyUp. Gunakan arguments.get() juga untuk mengekstrak nilai key di sini. Pastikan bahwa, di Android, plugin Anda menangani setiap pengecualian yang mungkin muncul.

Lihat file yang lengkap, PluginCodelabPlugin.java.

Karena sekarang plugin mengimplementasikan semua pembuatan saluran, Anda mungkin ingin melihat cara kerjanya. Untuk itu, Anda mengimplementasikan aplikasi contoh UI keyboard sederhana:

example/lib/main.dart

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

import 'package:flutter/services.dart';
import 'package:plugin_codelab/plugin_codelab.dart';

enum _KeyType { Black, White }

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight])
      .then((_) {
    runApp(new MyApp());
  });
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String? _platformVersion = 'Unknown';

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

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String? platformVersion;
    try {
      platformVersion = await PluginCodelab.platformVersion;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  void _onKeyDown(int key) {
    print("key down:$key");
    PluginCodelab.onKeyDown(key).then((value) => print(value));
  }

  void _onKeyUp(int key) {
    print("key up:$key");
    PluginCodelab.onKeyUp(key).then((value) => print(value));
  }

  Widget _makeKey({@required _KeyType keyType, @required int key}) {
    return AnimatedContainer(
      height: 200,
      width: 44,
      duration: Duration(seconds: 2),
      curve: Curves.easeIn,
      child: Material(
        color: keyType == _KeyType.White
            ? Colors.white
            : Color.fromARGB(255, 60, 60, 80),
        child: InkWell(
          onTap: () => _onKeyUp(key),
          onTapDown: (details) => _onKeyDown(key),
          onTapCancel: () => _onKeyUp(key),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Color.fromARGB(255, 250, 30, 0),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Text('Running on: $_platformVersion\n'),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  _makeKey(keyType: _KeyType.White, key: 60),
                  _makeKey(keyType: _KeyType.Black, key: 61),
                  _makeKey(keyType: _KeyType.White, key: 62),
                  _makeKey(keyType: _KeyType.Black, key: 63),
                  _makeKey(keyType: _KeyType.White, key: 64),
                  _makeKey(keyType: _KeyType.White, key: 65),
                  _makeKey(keyType: _KeyType.Black, key: 66),
                  _makeKey(keyType: _KeyType.White, key: 67),
                  _makeKey(keyType: _KeyType.Black, key: 68),
                  _makeKey(keyType: _KeyType.White, key: 69),
                  _makeKey(keyType: _KeyType.Black, key: 70),
                  _makeKey(keyType: _KeyType.White, key: 71),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

Perhatikan hal berikut ini:

  • Anda harus mengimpor 'package:plugin_codelab/plugin_codelab.dart' untuk menggunakan plugin. Dependensi contoh di plugin ditentukan di example/pubspec.yaml, yang membuat plugin berfungsi.
  • Di main(), orientasi dipaksa dalam format lanskap sehingga seluruh bagian keyboard muat di layar.
  • Metode _onKeyDown() dan _onKeyUp() merupakan klien dari API plugin yang dirancang di langkah-langkah sebelumnya.
  • Kode menggunakan InkWell, yang hanya merupakan kotak interaktif, untuk menggambar masing-masing tombol.

Jalankan aplikasi untuk melihat keyboard musik Anda yang berfungsi:

cd example
flutter run

Keyboard akan terlihat seperti ini:

f4275505c0be0bd7.png

Selamat! Anda berhasil membuat plugin Flutter untuk iOS dan Android, dan Anda memiliki keyboard musik yang cocok untuk digunakan bersenang-senang. Project yang selesai dapat didownload dari https://github.com/flutter/codelabs/tree/master/plugin_codelab untuk perbandingan.

Langkah berikutnya

  • Tambahkan pengujian menyeluruh. Tim Flutter menyediakan library untuk membuat pengujian integrasi menyeluruh yang disebut e2e.
  • Publikasikan ke pub.dev. Setelah membuat plugin, Anda mungkin ingin membagikannya secara online sehingga pengguna lain dapat menggunakannya. Anda dapat menemukan dokumentasi lengkap terkait memublikasikan plugin ke pub.dev di Mengembangkan paket plugin.

Memperluas synthesizer

Jika Anda ingin bereksperimen dengan synthesizer (untuk bersenang-senang) dan meningkatkan kualitasnya, inilah langkah-langkah berikutnya yang dapat dipertimbangkan:

  • Saat ini synthesizer membuat gelombang sinus. Ingin membuat gelombang sinus?
  • Apakah Anda mendengar suara yang muncul saat menekan dan melepaskan tombol? Suara tersebut muncul karena osilator nyala dan mati secara tiba-tiba. Biasanya, synthesizer mengatasinya dengan sampul amplitudo.
  • Saat ini, Anda hanya dapat memainkan satu tombol pada satu waktu. Hal ini disebut monofonik. Piano yang asli bersifat polifonik.