Menghapus jeda dari rekaman suara pada dasarnya diatur oleh empat angka: level di bawahnya audio dianggap hening, berapa lama bagian sunyi harus berlangsung sebelum dibuang, dan berapa banyak yang disisakan di kiri dan kanan setiap potongan yang tersisa. Sound Editor di aplikasi desktop osFoundry menampilkan persis empat angka ini di dialog Strip Silence, dengan nilai default −48 dB, 50 ms, 5 ms dan 20 ms. Di bawah ini operasi yang sama dibuat ulang dengan sekitar 40 baris numpy, dicocokkan sampel demi sampel dengan file yang diekspor aplikasi, lalu dipakai untuk mengukur apa sebenarnya efek tiap angka. Di bagian akhir, pekerjaan yang sama dicoba dengan silenceremove dari ffmpeg, yang perilakunya berbeda dengan cara yang mudah terlewat.

Rekaman uji

Ada dua rekaman uji yang dibuat khusus untuk ini: masing-masing berisi lima kalimat pendek hasil text-to-speech (bahasa Inggris dan Jepang), dipisahkan jeda sunyi 2.4 sampai 3.4 detik, dengan desis ruangan buatan di bawahnya. 48 kHz, mono, 16-bit.

rekaman durasi RMS desis puncak desis
Inggris 27.57 s −64.6 dBFS −52.4 dBFS
Jepang 33.55 s −64.6 dBFS −52.4 dBFS

Selisih antara dua kolom terakhir itu nanti jadi penting. Desis yang rata-ratanya −64.6 dBFS tetap punya sampel-sampel tertentu yang mencapai −52.4.

Aturannya

Sebuah sampel dianggap sunyi jika nilai absolutnya sama dengan atau di bawah threshold. Deretan sampel sunyi boleh dibuang jika panjangnya minimal sama dengan durasi minimum. Semua yang ada di antara deretan yang dibuang menjadi klip, lalu diperlebar dengan pad awal dan pad akhir:

import numpy as np def silent_runs(x, sr, threshold_db, min_ms): quiet = np.abs(x) <= 10 ** (threshold_db / 20) edges = np.flatnonzero(np.diff(np.concatenate(([0], quiet.view(np.int8), [0])))) runs = edges.reshape(-1, 2) # [start, end) of every quiet run  keep = (runs[:, 1] - runs[:, 0]) >= min_ms * sr / 1000 keep |= (runs[:, 0] == 0) | (runs[:, 1] == len(x)) # file edges: always strippable  return runs[keep] def strip_silence(x, sr, threshold_db=-48, min_ms=50, pad_start_ms=5, pad_end_ms=20): runs = silent_runs(x, sr, threshold_db, min_ms) bounds = np.concatenate(([0], runs.ravel(), [len(x)])).reshape(-1, 2) ps, pe = int(pad_start_ms * sr / 1000), int(pad_end_ms * sr / 1000) clips = [] for s, e in bounds: if e <= s: continue s, e = max(0, s - ps), min(len(x), e + pe) if clips and s <= clips[-1][1]: # pads made two clips touch: merge  clips[-1][1] = e else: clips.append([s, e]) return clips 

Strip hanya membuang. Menutup celahnya adalah langkah terpisah, yang di Sound Editor disebut Space Clips: klip-klip disambung ujung ke ujung, dan klip pertama tetap di posisi awalnya.

def space_clips(x, clips): return np.concatenate([np.zeros(clips[0][0], x.dtype)] + [x[s:e] for s, e in clips]) 

Mencocokkan dengan aplikasi

Kedua rekaman diproses di Sound Editor dengan −48 dB, 500 ms, 100 ms dan 300 ms, lalu Space Clips, lalu Bounce Mix yang dinormalisasi ke −0.1 dBFS. File dari aplikasi berformat 24-bit stereo dengan kedua channel identik. Dibandingkan dengan hasil numpy yang dinormalisasi dengan cara yang sama:

rekaman bounce aplikasi numpy selisih terbesar
Inggris 660,500 sampel (13.76 s) 660,500 sampel −135.0 dB
Jepang 815,030 sampel (16.98 s) 815,030 sampel −132.5 dB

Panjangnya sama persis sampai ke sampel, dan selisihnya setingkat pembulatan 24-bit. Perbandingan per sampel biasa, tanpa window RMS dan tanpa smoothing, sudah cukup untuk mereproduksi hasil ekspor aplikasi.

Versi pertama dari implementasi ulang ini meleset tepat di satu region, dan itulah satu-satunya tempat implementasi naif biasanya salah. Setiap rekaman diawali 0.47 detik sunyi sebelum kata pertama, lebih pendek dari minimum 500 ms. Kalau aturannya diterapkan apa adanya, bagian itu dipertahankan dan output diawali 0.47 s desis. File dari aplikasi justru diawali 0.376 s nol digital, baru kemudian kata pertama: bagian sunyi yang menempel di awal atau akhir file selalu dibuang berapa pun panjangnya, dan klip pertama tetap di posisinya (0.47 s dikurangi pad awal 100 ms). Itulah fungsi baris keep |= di atas.

Durasi minimum menentukan bentuknya

Dengan pad default (5 ms dan 20 ms), hanya durasi minimum yang diubah:

durasi strip minimum klip Inggris output Inggris klip Jepang output Jepang
20 ms 18 11.40 s 19 14.08 s
50 ms (default) 11 11.46 s 14 14.15 s
100 ms 7 11.63 s 11 14.30 s
200 ms 6 11.73 s 6 15.00 s
300 ms 5 11.98 s 5 15.20 s
500 ms 5 11.98 s 5 15.20 s

Kedua rekaman berisi lima kalimat. Dengan default 50 ms, rekaman Inggris jadi 11 klip dan rekaman Jepang 14: jeda pendek di dalam kalimat (6 di antaranya berada antara 50 dan 500 ms di rekaman Inggris, 9 di rekaman Jepang) dihitung sebagai sunyi dan ikut dipotong. Outputnya cuma setengah detik lebih pendek dibanding 500 ms, tapi setiap potongan itu membuang tarikan napas atau jarak antara dua kata, sehingga hasilnya terdengar terpotong-potong. Mulai 300 ms ke atas, setiap rekaman jadi tepat lima klip, satu per kalimat. Total durasinya hampir tidak berubah; yang berubah adalah letak potongannya.

Pad menentukan ritmenya

Dengan minimum 500 ms, sekarang pad-nya yang diubah:

pad awal / akhir output Inggris output Jepang
0 / 0 ms 11.86 s 15.08 s
5 / 20 ms 11.98 s 15.20 s
50 / 100 ms 12.56 s 15.78 s
100 / 300 ms 13.76 s 16.98 s
200 / 500 ms 15.16 s 18.38 s

Setelah celahnya ditutup, jeda antara dua kalimat sama dengan pad akhir kalimat pertama ditambah pad awal kalimat berikutnya. Di 0/0 kalimat-kalimatnya langsung bertabrakan; di 100/300 setiap sambungan jadi jeda napas 0.4 detik, kurang lebih seperti jeda pembicara yang membaca dengan tempo stabil. Pad awal punya tugas kedua: awal kata yang lembut sering berada di bawah threshold, dan tanpa pad ini konsonan pertama sebuah kalimat bisa hilang. Pad akhir adalah yang sebaiknya disetel dengan telinga.

Threshold harus di atas puncak noise, bukan rata-ratanya

Dengan 500 ms dan pad 100/300, threshold-nya digeser:

threshold klip Inggris output Inggris klip Jepang output Jepang
−36 dB 5 13.29 s 5 16.79 s
−40 dB 5 13.34 s 5 16.87 s
−44 dB 5 13.40 s 5 16.94 s
−48 dB 5 13.76 s 5 16.98 s
−52 dB 10 17.48 s 13 22.78 s
−56 dB 1 27.57 s 1 33.55 s

Dari −36 sampai −48 dB hasilnya stabil; threshold yang lebih tinggi hanya memangkas sedikit lebih banyak ekor pelan di setiap kata. Di −52 dB, sedikit di atas puncak desis, sesekali ada sampel desis yang melewati garis dan memecah jeda panjang menjadi potongan-potongan yang lebih pendek dari 500 ms, sehingga beberapa di antaranya lolos. Di −56 dB desisnya dianggap suara di mana-mana dan tidak ada yang dibuang sama sekali. −56 kelihatannya pilihan aman kalau yang kamu tahu hanya noise floor-nya sekitar −64 dB RMS. Karena aturannya membandingkan sampel satu per satu, yang menentukan adalah puncak desisnya, dan threshold perlu beberapa dB ruang di atasnya.

Pekerjaan yang sama dengan ffmpeg

ffmpeg punya dua filter yang relevan. silencedetect hanya melaporkan, dan dengan threshold dan durasi minimum yang sama, filter ini menemukan jeda yang sama dengan implementasi ulang tadi:

ffmpeg -i en-take.wav -af silencedetect=noise=-48dB:d=0.5 -f null - 2>&1 | grep silence_ # silence_start: 2.411833 # silence_end: 6.070167 | silence_duration: 3.658333 # ... 

Kalimat pertama berakhir di 2.4118 s, dan klip pertama implementasi ulang berakhir di 2.712 s, titik yang sama ditambah pad akhir 300 ms. Perhatikan apa yang tidak ada di output itu: 0.47 s sunyi di awal file, yang lebih pendek dari d, tidak dilaporkan.

silenceremove langsung memotong, dan di sinilah ekspektasi sering meleset. Dengan ffmpeg 7.1:

ffmpeg -i en-take.wav -af "silenceremove=start_periods=1:start_threshold=-48dB:\ stop_periods=-1:stop_threshold=-48dB:stop_duration=0.5:stop_silence=0:detection=peak" out.wav 
pengaturan filter output Inggris output Jepang
detection=peak, stop_silence=0 13.98 s 17.20 s
detection=peak, stop_silence=0.3 15.48 s 18.70 s
detection=rms, stop_silence=0 13.50 s 17.03 s
numpy, 500 ms, pad 0/0 11.86 s 15.08 s

Dengan stop_silence=0 pun output-nya masih sekitar 2 detik lebih panjang dibanding strip tanpa pad. Menjalankan silencedetect pada hasilnya menunjukkan alasannya: setiap jeda yang dibuang masih menyisakan sekitar 0.52 s. stop_duration berfungsi sebagai durasi minimum sekaligus jumlah sunyi yang dibiarkan lewat sebelum filter mulai memotong, jadi meminta "hanya buang jeda di atas 500 ms" juga berarti "sisakan 500 ms dari setiap jeda". stop_silence kemudian menambah ke angka itu, bukan menggantinya, dan tidak ada opsi untuk pad sebelum ucapan. Kalau panjang jeda ingin ditentukan sendiri, silencedetect ditambah daftar potongan buatan sendiri (atau versi numpy di atas) memberi kontrol itu; silenceremove cukup kalau jeda setengah detik memang yang kamu mau.

Dialog Strip Silence di Sound Editor osFoundry, diatur ke −48 dB, 500 ms, 100 ms dan 300 ms

Skrip lengkap dengan semua pengujian di atas berisi sekitar 90 baris Python dengan numpy dan soundfile. Di aplikasi desktop osFoundry, langkah yang sama ada di Edit → Strip Silence dan Edit → Space Clips… di Sound Editor, berjalan di komputer kamu sendiri sesuai desain local-first aplikasinya. Aplikasinya gratis dari halaman download osFoundry.