Membaca Suara Dengan Perangkat Android

Perangkat Android dalam bentuk ponsel biasanya dilengkapi dengan microphone untuk menerima suara selama penggunaan telepon. Android juga menyediakan API untuk berinteraksi dengan microphone, misalnya MediaRecorder untuk merekam suara ke dalam format terkompresi atau AudioRecord untuk membaca data mentah dari microphone secara langsung.

Pada dasarnya suara dihasilkan oleh sumber suara yang membuat molekul di udara yang bergetar. Molekul di udara tidak bergerak, mereka hanya saling meneruskan getaran. Semakin jauh getaran molekul dari posisi semula, semakin nyaring suara yang terdengar. Untuk menerjemahkan data suara dalam bentuk analog (getaran molekul) menjadi digital (angka), AudioRecord menggunakan metode Pulse-code Modulation (PCM). Setiap perubahan posisi molekul dari jarak semula akan direkam dalam bentuk angka (nilainya ditentukan oleh ENCODING_PCM_16BIT atau ENCODING_PCM_8BIT). Angka ini adalah nilai voltase yang dihasilkan oleh microphone akibat getaran suara. Nilai voltase dari microphone diambil secara periodik tergantung pada sampling rate yang dipakai. Berdasarkan informasi dari dokumentasi Android, nilai 44.100 Hz (mengambil 44.100 sample per detik) adalah nilai sampling rate yang dijamin bekerja dengan baik pada semua perangkat.

Sebagai latihan, saya akan membuat sebuah proyek Android sederhana yang menampilkan sample suara yang dibaca ke layar. Untuk itu, saya membuat sebuah proyek baru di Android Studio. Saya kemudian membuat sebuah activity dengan isi kode program seperti berikut ini:

public class MainActivity extends Activity implements View.OnClickListener {

    private TextView output;
    private Switch aSwitch;
    private int captureSize;
    private AudioRecord audioRecord;
    private boolean isRunning = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RelativeLayout layout = new RelativeLayout(this);
        aSwitch = new Switch(this);
        aSwitch.setId(View.generateViewId());
        aSwitch.setTextOn("Rekam");
        aSwitch.setOnClickListener(this);
        layout.addView(aSwitch);

        output = new TextView(this);
        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
        lp.addRule(RelativeLayout.BELOW, aSwitch.getId());
        output.setLayoutParams(lp);
        layout.addView(output);

        setContentView(layout);

        captureSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100,
            AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, captureSize);
    }

    @Override
    public void onClick(View v) {
        if (aSwitch.isChecked()) {
            audioRecord.startRecording();
            isRunning = true;
            new CaptureThread().start();
        } else {
            isRunning = false;
            audioRecord.stop();
        }
    }

    class CaptureThread extends Thread {

        @Override
        public void run() {
            final short[] buffer = new short[captureSize];
            while(isRunning) {
                audioRecord.read(buffer, 0, captureSize);
                final StringBuilder text = new StringBuilder();
                for (int i=0; i<captureSize; i++) {
                    text.append(buffer[i]);
                    text.append(' ');
                }
                output.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        output.setText(text.toString());
                    }
                }, 100);
            }
        }

    }

}

Sebelum menjalankan aplikasi, saya perlu menambahkan permission berikut ini pada AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

Bila saya menjalankan aplikasi, saya bisa melihat data suara yang dibaca melalui microphone seperti pada gambar berikut ini:

Tampilan Aplikasi

Tampilan Aplikasi

Pada program di atas, sebelum merekam, saya memanggil startRecording() dari AudioRecord. Setelah selesai merekam, saya memanggil stop() dari AudioRecord. Untuk membaca data suara yang direkam, saya memanggil method read() dari AudioRecord. Method ini membutuhkan sebuah array (bertipe byte atau short tergantung apakah memakai encoding 8-bit atau 16-bit). Ukuran array ini tidak boleh kurang dari nilai yang dikembalikan oleh AudioRecord.getMinBufferSize().

Membaca data dalam bentuk angka lumayan membingungkan bukan? Oleh sebab itu, saya akan mencoba untuk menampilkannya dalam bentuk grafis. Saya akan mulai dengan memindahkan thread ke dalam sebuah class tersendiri yang saya beri nama AnimasiThread dengan isi kode program seperti berikut ini:

public class AnimasiThread extends Thread {

    private SurfaceHolder surfaceHolder;
    private int captureSize;
    private AudioRecord audioRecord;
    private boolean running;
    private int width, height;
    private float midLine, scale;
    private final Paint warnaGaris;

    public AnimasiThread(SurfaceHolder surfaceHolder) {
        this.surfaceHolder = surfaceHolder;
        this.running = false;
        captureSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100, AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT, captureSize);
        warnaGaris = new Paint();
        warnaGaris.setColor(Color.GREEN);
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
        this.midLine = height / 2;
        this.scale =  height / (float) (2 * Short.MAX_VALUE);
    }

    public void startCapture() {
        audioRecord.startRecording();
        running = true;
        start();
    }

    public void stopCapture() {
        running = false;
        audioRecord.stop();
    }

    @Override
    public void run() {
        final short[] buffer = new short[captureSize];
        while(running) {
            audioRecord.read(buffer, 0, captureSize);
            Canvas c = null;
            Float lastX = null, lastY = null;
            try {
                c = surfaceHolder.lockCanvas();
                c.drawColor(Color.BLACK);
                for (int i = 0; i < captureSize; i++) {
                    float trY = midLine - buffer[i] * scale;
                    if ((lastX != null) && (lastY != null)) {
                        c.drawLine(lastX, lastY, i, trY, warnaGaris);
                    } else {
                        c.drawPoint(i, trY, warnaGaris);
                    }
                    lastX = (float) i;
                    lastY = trY;
                }
            } finally {
                if (c != null) {
                    surfaceHolder.unlockCanvasAndPost(c);
                }
            }
        }
    }

}

Thread di atas akan terus menerus membaca data suara dan menampilkannya dalam bentuk grafis dimana garis tengah layar dari height mewakili nilai 0 (tidak ada suara). Nilai yang berada di atas atau di bawah nilai 0 menunjukkan terdapat suara yang menyebabkan membran microphone bergetar.

Selanjutnya, saya membuat sebuah turunan SurfaceView dengan isi seperti berikut ini:

public class GrafikSuara extends SurfaceView implements SurfaceHolder.Callback {

    private AnimasiThread animasiThread;
    private boolean ready = false;
    private int width, height;

    public GrafikSuara(Context context) {
        super(context);
        getHolder().addCallback(this);
    }

    public void mulai() {
        if (ready) {
            animasiThread = new AnimasiThread(getHolder());
            animasiThread.setWidth(width);
            animasiThread.setHeight(height);
            animasiThread.startCapture();
        }
    }

    public void selesai() {
        if (animasiThread != null) {
            animasiThread.stopCapture();
            animasiThread = null;
        }
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        ready = true;
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        this.width = width;
        this.height = height;
        if (animasiThread != null) {
            animasiThread.setWidth(width);
            animasiThread.setHeight(height);
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (animasiThread != null) {
            animasiThread.stopCapture();
        }
    }

}

Sebagai langkah terakhir, saya mengubah isi MainActivity menjadi seperti berikut ini:

public class MainActivity extends Activity implements View.OnClickListener {

    private GrafikSuara output;
    private Switch aSwitch;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RelativeLayout layout = new RelativeLayout(this);
        aSwitch = new Switch(this);
        aSwitch.setId(View.generateViewId());
        aSwitch.setTextOn("Rekam");
        aSwitch.setOnClickListener(this);
        layout.addView(aSwitch);

        output = new GrafikSuara(this);
        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
        lp.addRule(RelativeLayout.BELOW, aSwitch.getId());
        output.setLayoutParams(lp);
        layout.addView(output);

        setContentView(layout);
    }

    @Override
    protected void onPause() {
        super.onPause();
        output.selesai();
    }

    @Override
    public void onClick(View v) {
        if (aSwitch.isChecked()) {
            output.mulai();
        } else {
            output.selesai();
        }
    }

}

Sekarang, bila saya menjalankan aplikasi, saya akan memperoleh grafis yang terus berubah sesuai suara yang diterima oleh microphone, seperti yang terlihat pada gambar berikut ini:

Tampilan Aplikasi

Tampilan Aplikasi

Membuat Program Pemutar Musik Dengan Mudah Memakai JFugue

Pada tulisan MIDI: Bermain piano dengan Java, saya memakai Java Sound API untuk memutar musik di Java.   Terlihat bahwa dibutuhkan kode program yang cukup panjang untuk memutar sebuah nada (termasuk diantaranya perhitungan frekuensi).   Sebagai alternatif untuk membuat kode program yang lebih singkat, terdapat library JFugue (www.jfugue.org/download.html).   Dengan JFugue, saya dapat memainkan nada dalam bentuk String.   Hal ini mirip seperti di Turbo Basic dulu, tetapi tentu saja dengan syntax String yang jauh lebih lengkap.

Setelah men-download JFugue, saya perlu menambahkan JAR tersebut ke proyek NetBeans saya.   Caranya adalah dengan men-klik kanan nama proyek, memilih Properties, Libraries, Compile, kemudian men-klik tombol Add JAR/Folder.   Setelah itu saya men-browse lokasi penyimpanan file JAR JFugue yang telah di-download sebelumnya.

Berikut ini adalah contoh kode program yang memakai JFugue:

import org.jfugue.Player;

public class Main {

  public void start() {
    Player player = new Player();
    player.play("C D E F G A B C6 R C6 B A G F E D C");
  }

  public static void main(String[] args) {
    new Main().start();
  }

}

Dengan hanya 2 baris perintah yang mudah dipahami, deretan nada akan dimainkan saat program dijalankan. C, D, E, F, G, A, dan B masing-masing mewakili not (note) musik. R dipakai untuk mewakili jeda (rest). Oktaf (octave) diwakili oleh angka 0 hingga 10. Secara default, octave untuk note adalah 5, sehingga C6 pada kode program di atas menyebabkan naik 1 octave.

JFugue juga memungkinkan untuk memutar chord (beberapa nada yang dimainkan pada waktu bersamaan) dengan mudah, seperti pada contoh berikut ini:

import org.jfugue.Player;

public class Main {

  public void start() {
    Player player = new Player();
    player.play("Ch+Eh+Gh C5maj D5maj7 D5maj11 Cmaj7 Cmaj11 Cdim7");
  }

  public static void main(String[] args) {
    new Main().start();
  }

}

Pada contoh di atas, Ch+Eh+Gh adalah chord C-major.   Sebagai alternatifnya, saya dapat menggunakan Cmaj yang lebih singkat. Secara default, chords dimainkan pada octave 3.   C5maj akan memutar chord C-major pada oktave 5.   JFugue juga memiliki nama untuk chords lainnya seperti maj, min, aug, dim, dom7, maj7, dsb (informasi dapat dilihat pada dokumentasinya).

Untuk mengatur durasi, saya dapat menambahkan karakter w (durasi penuh), h (durasi setengah), q (untuk durasi 1/4), i (untuk durasi 1/8), s (untuk durasi 1/16), t (untuk durasi 1/32), x (untuk durasi 1/64), dan o (untuk durasi 1/128).   Berikut ini adalah contoh pemutaran nada yang dilengkapi durasi:

import org.jfugue.Player;

public class Main {

  public void start() {
    Player player = new Player();
    player.play("C D E F Gh Eq C");
  }

  public static void main(String[] args) {
    new Main().start();
  }

}

Untuk berganti alat musik, gunakan I di-ikuti dengan nama instrumen sesuai dengan spesifikasi MIDI.   Alat musik default yang dipakai adalah piano.   Sebagai contoh, saya akan mengubah kode program di atas agar dimainkan dengan alat musik seruling:

import org.jfugue.Player;

public class Main {

  public void start() {
    Player player = new Player();
    player.play("I[Flute] C D E F Gh Eq C");
  }

  public static void main(String[] args) {
    new Main().start();
  }

}

Beberapa hal menarik,  misalnya saya memakai instrument suara tembakan,  seperti yang terlihat pada contoh kode program berikut ini:

import org.jfugue.Player;

public class Main {

  public void start() {
    Player player = new Player();
    player.play("I[Gunshot] Cs Cs Cs Gs Gs Gs Cw");
  }

  public static void main(String[] args) {
    new Main().start();
  }
}

Dan tentunya masih ada banyak lagi fasilitas JFugue yang dapat dibaca di dokumentasi resminya.

MIDI: Bermain piano dengan Java

Tidak dipungkiri lagi hal yang paling menarik minat programmer pemula adalah pemograman grafis dan suara. Dan begitu juga denganku. Di masa-masa saat pertama kali belajar pemograman dengan Turbo Basic, aku menghabiskan banyak waktu mengubah layar monitor dan membuat speaker internal bersuara aneh. Saat itu ada keyword PLAY yang meminta input berupa string yang berisi deretan tangga nada yang hendak dipakai.

Untuk standar zaman sekarang, sudah ada standar MIDI untuk merekam dan menyalurkan instrumen suara. Java mendukung MIDI melalui sejumlah API yang berada di package javax.sound.midi. Berikut ini adalah contoh sederhana untuk memutar nada-nada:

import java.util.Random;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;


public class LatihanMIDI {

  public LatihanMIDI() {
    MidiDevice.Info[] info = MidiSystem.getMidiDeviceInfo();
    System.out.println("Daftar MIDI Device yang tersedia:");
    for (MidiDevice.Info i : info) {
      System.out.println("Name = " + i.getName());
      System.out.println("Description = " + i.getDescription());
      System.out.println("Vendor = " + i.getVendor());
      System.out.println();
    }
    try {
      System.out.println("Memakai device pertama.");
      MidiDevice device = MidiSystem.getMidiDevice(info[0]);
      device.open();
      
      Receiver receiver = device.getReceiver();
      
      for (int i= 0; i<128; i++) {
        receiver.send(createMessage(i), -1);
        Thread.sleep(i);
      }
      for (int i=127; i>0; i--) {
        receiver.send(createMessage(i), -1);
        Thread.sleep(i);
      }
      Random rand = new Random();
      while (true) {
        int i = rand.nextInt(127);
        receiver.send(createMessage(i), -1);
        Thread.sleep(rand.nextInt(1000));
      }
      
      
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  
  public ShortMessage createMessage(int n1) {
    ShortMessage msg = new ShortMessage();
    
    try {
      msg.setMessage(ShortMessage.NOTE_ON, 0, n1, 100);
    } catch (InvalidMidiDataException e) {
      e.printStackTrace();
    }
    return msg;
    
  }
  
  public static void main(String[] args) {
    new LatihanMIDI();
  }

}

Program ini akan terus berulang dan memainkan nada secara acak. O ya, aku memakai MIDI device yang pertama dalam listing (apapun itu), kemudian menulis byte MIDI langsung ke Receiver.