Pengenalan array audio

Sampel, laju sampling, dan bentuk array audio.

Daftar isi

Suara yang telah didigitalisasi hanyalah deretan angka, sehingga array NumPy satu dimensi merupakan representasi alami untuk audio mono, dan array dua dimensi untuk audio stereo. Topik ini membahas konsep sampel dan laju sampling, hubungan jumlah sampel dengan durasi, cara membentuk gelombang sinus sintetis dengan frekuensi tertentu, konversi sinyal float ke tipe int16, perbedaan bentuk array mono dan stereo, serta pola penyimpanan dan pembacaan berkas WAV. Seluruh sinyal dibangun secara sintetis, sehingga setiap angka pada keluaran dapat dihitung ulang.

Sampel dan laju sampling#

Digitalisasi suara merekam besaran gelombang pada saat-saat yang berjarak tetap, dan setiap nilai yang terekam pada satu saat disebut sampel (sample). Banyaknya perekaman per detik disebut laju sampling (sampling rate) dan dinyatakan dalam hertz (Hz). Audio pada compact disc memakai laju 44100 Hz, sedangkan 8000 Hz merupakan laju historis pada sistem telepon yang tetap berguna untuk latihan karena ukuran arraynya kecil. Semakin tinggi lajunya, semakin halus gelombang yang tertangkap, tetapi semakin besar pula jumlah datanya.

Hubungan ketiga besaran pokoknya bersifat aritmetis dan menjadi dasar hampir semua perhitungan audio.

N=fs×t N = f_s \times t

Notasi NN menyatakan jumlah sampel, fsf_s menyatakan laju sampling, dan tt menyatakan durasi dalam detik. Karena lajunya tetap, durasi sebuah array audio selalu bisa dihitung kembali dengan membagi jumlah elemennya terhadap lajunya. Kode berikut menghitung ukuran array untuk rekaman tiga detik pada laju 8000 Hz, sekaligus menyusun sumbu waktunya.

python
import numpy as np

laju = 8000
durasi = 3.0
n_sampel = int(laju * durasi)
waktu = np.arange(n_sampel) / laju
print(n_sampel)
print(waktu[0], waktu[1], waktu[-1])
text Keluaran
24000
0.0 0.000125 2.999875

Array tiga detik pada laju 8000 Hz memuat 24000 sampel, dan sumbu waktunya dibentuk dengan membagi indeks arange terhadap laju. Sampel pertama berada pada detik ke-0.0, sampel kedua pada 0.000125 detik, yaitu 1/8000, dan sampel terakhir pada 2.999875 detik karena indeks terakhir adalah 23999, bukan 24000.

Gelombang sinus sintetis#

Gelombang sinus adalah bahan uji standar untuk audio karena bentuknya diketahui secara matematis, yaitu np.sin(2 * np.pi * f * t) dengan ff sebagai frekuensi yang diinginkan dalam hertz. Frekuensi menentukan tinggi rendahnya nada yang terdengar, sedangkan amplitudo fungsi sinus, yang berada pada rentang -1.0 sampai 1.0, menentukan kerasnya nada. Frekuensi 440 Hz merupakan nada standar A4 dalam musik, sehingga dipakai sebagai contoh yang mudah dikenali. Membangun sinyal dari rumusnya juga menghilangkan kebutuhan berkas suara apa pun pada tahap belajar.

Hasil rumus ini juga dapat diperiksa secara manual, karena gelombang 440 Hz menempuh 440 siklus setiap detiknya, sehingga rekaman tiga detik memuat 1320 siklus penuh. Pada laju 8000 Hz, satu siklus hanya berlangsung sekitar 18 sampel, angka yang kecil namun cukup untuk mengikuti bentuk gelombangnya. Laju yang lebih rendah akan menyisakan lebih sedikit sampel per siklus, sehingga bentuk gelombang yang direkonstruksi menjadi bergerigi.

python
import numpy as np

laju = 8000
durasi = 3.0
frekuensi = 440.0
waktu = np.arange(int(laju * durasi)) / laju
sinyal = np.sin(2 * np.pi * frekuensi * waktu)
print(sinyal.shape, sinyal.dtype)
print(sinyal[:4])
print(sinyal.min(), sinyal.max())
text Keluaran
(24000,) float64
[0.         0.33873792 0.63742399 0.86074203]
-1.0 1.0

Sinyalnya adalah array satu dimensi dengan 24000 elemen bertipe float64. Empat sampel pertamanya naik dari 0.0 ke arah puncak positif, sesuai fase awal gelombang sinus yang mulai dari nol. Nilai minimum dan maksimumnya berada tepat pada -1.0 dan 1.0, sesuai amplitudo fungsi sinus sepanjang tiga detik penuh.

Konversi ke int16#

Format berkas WAV yang paling umum menyimpan sampel sebagai bilangan bulat 16 bit, yaitu tipe int16, dengan jangkauan -32768 sampai 32767. Sinyal float berjangkauan -1.0 sampai 1.0 perlu diskalakan terlebih dahulu, misalnya dikalikan 32000 agar mendekati penuh tanpa melampaui batas, kemudian dikonversi dengan astype. Amplitudo pada tahap float dan besaran skalanya menentukan kerasnya nada hasil, sehingga skala 32000 yang mendekati batas maksimal dipakai agar nada terdengar jelas.

python
import numpy as np

laju = 8000
durasi = 3.0
waktu = np.arange(int(laju * durasi)) / laju
sinyal = np.sin(2 * np.pi * 440.0 * waktu)
pcm = (sinyal * 32000).astype(np.int16)
print(pcm.dtype)
print(pcm[:4])
print(pcm.min(), pcm.max())
print(np.iinfo(np.int16).min, np.iinfo(np.int16).max)
text Keluaran
int16
[    0 10839 20397 27543]
-32000 32000
-32768 32767

Array pcm kini bertipe int16 dan siap disimpan sebagai berkas WAV. Nilai ekstremnya berada tepat pada -32000 dan 32000, yaitu hasil skala 32000 kali amplitudo penuh, dan masih berada di dalam jangkauan -32768 sampai 32767 yang dilaporkan np.iinfo. Setiap sampel int16 memakai 2 byte, sehingga array 24000 sampel memakai 48000 byte, sebagaimana dapat diverifikasi melalui atribut nbytes. Sinyal int16 semacam ini biasa disebut PCM (pulse-code modulation), yaitu nama format penyimpanan sampel digitalnya.

Mono dan stereo#

Audio mono memakai satu kanal, sehingga arraynya satu dimensi dengan shape (n,), sedangkan audio stereo memakai dua kanal yang biasanya disebut kiri dan kanan, sehingga arraynya dua dimensi dengan shape (n, 2). Konstruksi stereo yang lazim adalah menumpuk dua sinyal satu dimensi sebagai kolom dengan np.column_stack, sehingga setiap baris berisi satu pasangan sampel yang dibunyikan bersamaan. Satu baris dan satu kolom array stereo membaca hal yang berbeda, yaitu satu momen waktu atau satu sisi telinga.

python
import numpy as np

laju = 8000
durasi = 3.0
waktu = np.arange(int(laju * durasi)) / laju
kiri = np.sin(2 * np.pi * 440.0 * waktu)
kanan = np.sin(2 * np.pi * 550.0 * waktu)
stereo = np.column_stack((kiri, kanan))
print(stereo.shape)
print(stereo[0])
print(stereo[:, 0][:3])
text Keluaran
(24000, 2)
[0. 0.]
[0.         0.33873792 0.63742399]

Shape (24000, 2) berarti 24000 momen waktu dengan 2 kanal, dan stereo[0] menghasilkan pasangan [0. 0.] untuk momen pertama karena kedua sinyal dimulai dari fase nol. Pemisahan stereo[:, 0] mengambil seluruh baris pada kolom pertama, sehingga mengembalikan sinyal kiri sebagai array satu dimensi, dan tiga nilainya sama persis dengan sinyal 440 Hz pada bagian Gelombang sinus sintetis. Apabila kedua kanal ingin dijadikan satu sinyal mono, rata-rata keduanya dihitung dengan (stereo[:, 0] + stereo[:, 1]) / 2, dan hasilnya kembali berbentuk (n,).

Pemutaran dan berkas WAV#

Menyimpan array int16 ke berkas WAV dan memuatnya kembali dilakukan dengan scipy.io.wavfile, dan polanya ditunjukkan sebagai berikut. Fungsi wavfile.write menerima laju sampling sebagai argumen kedua, sehingga informasi waktu tersimpan bersama datanya di dalam berkas, bukan dihitung ulang dari tebakan. Pemutaran suara sendiri memerlukan perangkat keluaran audio yang tidak tersedia di lingkungan eksekusi ini, sehingga prosesnya cukup dipahami sebagai pola, misalnya melalui IPython.display.Audio pada notebook Jupyter.

python
# tidak-dijalankan
from scipy.io import wavfile

wavfile.write("nada.wav", laju, pcm)
laju_baca, audio = wavfile.read("nada.wav")
print(laju_baca, audio.shape, audio.dtype)

Pola di atas menuliskan pcm bertipe int16 bersama laju samplingnya, lalu membaca berkas yang sama dan mendapatkan laju beserta arraynya kembali. Array yang dibaca mempertahankan shape dan dtypenya, sehingga bentuk mono atau stereo dapat dikenali langsung dari audio.ndim tanpa membuka isi berkasnya. Laju sampling dan array selalu berpasangan karena array tanpa catatan laju samplingnya kehilangan makna waktunya, yaitu tidak dapat ditebak berapa lama durasinya.

Kesalahan umum#

Menskalakan sinyal melampaui batas int16 sebelum konversi. Hasil perkalian seperti 1.0 dikali 40000 menghasilkan 40000, dan konversi astype(np.int16) melipatnya menjadi bilangan negatif tanpa peringatan apa pun.

python
import numpy as np

sinyal = np.array([1.0, 0.9])
melampaui = (sinyal * 40000).astype(np.int16)
print(melampaui)
text Keluaran
[-25536 -29536]

Nilai 40000 dan 36000 berbalik menjadi -25536 dan -29536 karena melampaui 32767, sehingga nada yang dihasilkan berlompatan tidak wajar. Perbaikannya adalah membatasi amplitudo pada tahap float, misalnya mengalikan dengan 0.8 terlebih dahulu, atau memanggil np.clip(sinyal, -1.0, 1.0) sebelum penskalaan.

Mengira jumlah elemen array adalah durasinya. Array dengan 24000 elemen berdurasi 3.0 detik pada laju 8000 Hz, tetapi berdurasi sekitar 0.54 detik pada laju 44100 Hz. Perbaikannya adalah selalu menghitung durasi dengan len(sinyal) / laju dan membawa laju sampling sebagai variabel yang menyertai array.

Membalik orientasi array stereo. np.column_stack((kiri, kanan)) menghasilkan (n, 2) dengan baris sebagai momen waktu, sedangkan np.stack((kiri, kanan)) tanpa parameter axis menghasilkan (2, n). Perbaikannya adalah menetapkan konvensi (n, 2) sejak awal dan memeriksa shape sebelum mengindeks, karena keliru orientasi tidak memunculkan error, melainkan membunyikan kanal yang tertukar dengan durasi yang salah.

Ringkasan#

  • Jumlah sampel audio memenuhi N=fs×tN = f_s \times t, sehingga laju sampling harus selalu diketahui untuk menafsirkan durasi sebuah array.
  • Gelombang sinus dibentuk dengan np.sin(2 * np.pi * f * t) pada array waktu hasil np.arange(n_sampel) / laju dan beramplitudo antara -1.0 sampai 1.0.
  • Penyimpanan WAV yang umum memakai int16 dengan jangkauan -32768 sampai 32767, sehingga sinyal float diskalakan lalu dikonversi dengan astype.
  • Audio mono berbentuk (n,) dan stereo berbentuk (n, 2), dengan np.column_stack sebagai cara penumpukan dua kanal yang lazim.

Referensi#