वॉइस रिकॉर्डिंग से रुकावटें हटाने का पूरा काम चार नंबरों पर टिका है: वह लेवल जिसके नीचे ऑडियो को साइलेंट माना जाए, किसी शांत हिस्से को हटाए जाने से पहले कम से कम कितना लंबा होना चाहिए, और बचे हुए हर टुकड़े के दोनों ओर कितना हिस्सा छोड़ा जाए। osFoundry डेस्कटॉप ऐप का Sound Editor अपने Strip Silence डायलॉग में ठीक यही चार चीज़ें देता है, जिनकी डिफ़ॉल्ट वैल्यू −48 dB, 50 ms, 5 ms और 20 ms हैं। नीचे यही ऑपरेशन numpy की लगभग 40 लाइनों में दोबारा बनाया गया है, ऐप से एक्सपोर्ट की गई फ़ाइल से सैंपल-दर-सैंपल मिलाया गया है, और फिर इससे मापा गया है कि हर नंबर असल में क्या करता है। आख़िर में यही काम ffmpeg के silenceremove से किया गया है, जो एक ऐसे तरीके से अलग बर्ताव करता है जिस पर आसानी से ध्यान नहीं जाता।
टेस्ट रिकॉर्डिंग
इसके लिए दो टेस्ट रिकॉर्डिंग बनाई गईं: हर एक में text-to-speech से बने पाँच छोटे वाक्य (अंग्रेज़ी और जापानी), जिनके बीच 2.4 से 3.4 सेकंड की शांति है, और नीचे जनरेट किया गया कमरे का हिस (hiss)। 48 kHz, मोनो, 16-bit।
| रिकॉर्डिंग | लंबाई | हिस RMS | हिस पीक |
|---|---|---|---|
| अंग्रेज़ी | 27.57 s | −64.6 dBFS | −52.4 dBFS |
| जापानी | 33.55 s | −64.6 dBFS | −52.4 dBFS |
आख़िरी दो कॉलम के बीच का फ़र्क आगे चलकर अहम होगा। जिस हिस का औसत −64.6 dBFS है, उसके कुछ अलग-अलग सैंपल फिर भी −52.4 तक पहुँचते हैं।
नियम
कोई सैंपल शांत है अगर उसकी absolute वैल्यू threshold के बराबर या उससे कम है। शांत सैंपल्स की कोई लगातार कड़ी तभी हटाई जा सकती है जब वह कम से कम minimum लंबाई की हो। हटाई जाने वाली कड़ियों के बीच जो कुछ है वह क्लिप बनता है, और फिर उसे start और end pad से चौड़ा किया जाता है:
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 सिर्फ़ हटाता है। खाली जगहों को बंद करना एक अलग स्टेप है, जिसे Sound Editor में Space Clips कहा जाता है: क्लिप्स को एक के बाद एक जोड़ दिया जाता है, और पहली क्लिप अपनी जगह पर रहती है।
def space_clips(x, clips): return np.concatenate([np.zeros(clips[0][0], x.dtype)] + [x[s:e] for s, e in clips]) ऐप से मिलान
दोनों रिकॉर्डिंग Sound Editor में −48 dB, 500 ms, 100 ms और 300 ms के साथ प्रोसेस की गईं, फिर Space Clips, फिर −0.1 dBFS पर नॉर्मलाइज़ करके Bounce Mix। ऐप की फ़ाइल 24-bit स्टीरियो है, जिसके दोनों चैनल एक जैसे हैं। इसे उसी तरह नॉर्मलाइज़ किए गए numpy नतीजे से मिलाने पर:
| रिकॉर्डिंग | ऐप का bounce | numpy | सबसे बड़ा अंतर |
|---|---|---|---|
| अंग्रेज़ी | 660,500 सैंपल (13.76 s) | 660,500 सैंपल | −135.0 dB |
| जापानी | 815,030 सैंपल (16.98 s) | 815,030 सैंपल | −132.5 dB |
लंबाई एक-एक सैंपल तक बराबर है, और अंतर 24-bit राउंडिंग के स्तर का है। ऐप जो एक्सपोर्ट करता है उसे दोबारा बनाने के लिए सीधा सैंपल-दर-सैंपल मिलान काफ़ी है, न RMS विंडो की ज़रूरत है न smoothing की।
दोबारा बनाए गए कोड का पहला वर्शन ठीक एक region से चूक गया था, और यही वह इकलौती जगह है जहाँ सीधा-सादा implementation गलत हो जाता है। हर रिकॉर्डिंग पहले शब्द से पहले 0.47 सेकंड की शांति से शुरू होती है, जो 500 ms के minimum से छोटी है। नियम को जस का तस लगाएँ तो यह हिस्सा बच जाता है, और आउटपुट 0.47 s के हिस से शुरू होता है। जबकि ऐप की फ़ाइल 0.376 s के डिजिटल ज़ीरो से शुरू होती है, फिर पहला शब्द आता है: फ़ाइल की शुरुआत या अंत को छूने वाली शांति, चाहे जितनी लंबी हो, हटा दी जाती है, और पहली क्लिप अपनी जगह पर रहती है (0.47 s में से 100 ms का start pad घटाकर)। ऊपर की keep |= लाइन यही करती है।
Minimum लंबाई तय करती है कि कट कहाँ लगेंगे
डिफ़ॉल्ट pad (5 ms और 20 ms) के साथ, सिर्फ़ minimum लंबाई बदलती है:
| minimum strip लंबाई | अंग्रेज़ी क्लिप्स | अंग्रेज़ी आउटपुट | जापानी क्लिप्स | जापानी आउटपुट |
|---|---|---|---|---|
| 20 ms | 18 | 11.40 s | 19 | 14.08 s |
| 50 ms (डिफ़ॉल्ट) | 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 |
दोनों रिकॉर्डिंग में पाँच वाक्य हैं। डिफ़ॉल्ट 50 ms पर अंग्रेज़ी रिकॉर्डिंग 11 क्लिप्स में और जापानी 14 क्लिप्स में लौटती है: वाक्यों के अंदर के छोटे ठहराव (अंग्रेज़ी में 50 से 500 ms के बीच के 6, जापानी में 9) साइलेंस गिने जाते हैं और कट जाते हैं। आउटपुट 500 ms वाले से सिर्फ़ आधा सेकंड छोटा है, लेकिन हर कट किसी साँस या दो शब्दों के बीच की जगह को हटा देता है, और नतीजा कटा-कटा सुनाई देता है। 300 ms और उससे ऊपर हर रिकॉर्डिंग ठीक पाँच क्लिप्स बनती है, हर वाक्य की एक। कुल लंबाई मुश्किल से बदलती है; बदलती है कट की जगह।
Pad तय करते हैं लय
Minimum को 500 ms पर रखकर, अब pad बदलते हैं:
| start / end pad | अंग्रेज़ी आउटपुट | जापानी आउटपुट |
|---|---|---|
| 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 |
खाली जगहें बंद होने के बाद, दो वाक्यों के बीच का ठहराव एक वाक्य का end pad और अगले का start pad जोड़कर बनता है। 0/0 पर वाक्य एक-दूसरे से टकराते हैं; 100/300 पर हर जोड़ 0.4 सेकंड की साँस बन जाता है, जो लगभग उतना ही है जितना कोई वक्ता स्थिर रफ़्तार से पढ़ते हुए छोड़ता है। Start pad का एक दूसरा काम भी है: शब्दों की नरम शुरुआत अक्सर threshold से नीचे होती है, और इसके बिना वाक्य का पहला व्यंजन गायब हो जाता है। End pad वह है जिसे कान से सुनकर ट्यून करना चाहिए।
Threshold को नॉइज़ के औसत से नहीं, उसके पीक से ऊपर होना चाहिए
500 ms और 100/300 pad के साथ threshold बदलकर देखते हैं:
| threshold | अंग्रेज़ी क्लिप्स | अंग्रेज़ी आउटपुट | जापानी क्लिप्स | जापानी आउटपुट |
|---|---|---|---|---|
| −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 |
−36 से −48 dB तक नतीजा स्थिर रहता है; ऊँचा threshold हर शब्द की धीमी पूँछ को बस थोड़ा और काटता है। −52 dB पर, यानी हिस के पीक से ठीक ऊपर, बीच-बीच में हिस के कुछ सैंपल लाइन पार कर जाते हैं और लंबे ठहरावों को 500 ms से छोटे टुकड़ों में तोड़ देते हैं, इसलिए उनमें से कई बच जाते हैं। −56 dB पर हिस हर जगह आवाज़ गिनी जाती है और कुछ भी नहीं हटता। अगर आपको सिर्फ़ इतना पता है कि noise floor लगभग −64 dB RMS है, तो −56 सुरक्षित चुनाव लगता है। लेकिन क्योंकि नियम एक-एक सैंपल की तुलना करता है, मायने हिस के पीक का है, और threshold को उससे कुछ dB ऊपर जगह चाहिए।
यही काम ffmpeg से
ffmpeg में इसके लिए दो फ़िल्टर हैं। silencedetect सिर्फ़ रिपोर्ट करता है, और उसी threshold और minimum के साथ यह वही ठहराव ढूँढता है जो दोबारा बनाया गया कोड:
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 # ... पहला वाक्य 2.4118 s पर खत्म होता है, और दोबारा बनाए गए कोड की पहली क्लिप 2.712 s पर, यानी वही बिंदु और उसमें 300 ms का end pad। ध्यान दें कि इस आउटपुट में क्या नहीं है: फ़ाइल की शुरुआत की 0.47 s की शांति, जो d से छोटी है, रिपोर्ट नहीं होती।
silenceremove सीधे काटता है, और यहीं उम्मीदें गलत साबित होती हैं। 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 | फ़िल्टर सेटिंग्स | अंग्रेज़ी आउटपुट | जापानी आउटपुट |
|---|---|---|
| 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 |
stop_silence=0 के साथ भी आउटपुट बिना pad वाले strip से लगभग 2 सेकंड लंबा है। उस पर silencedetect चलाने से वजह दिखती है: हटाए गए हर ठहराव का लगभग 0.52 s पीछे छूट गया है। stop_duration एक साथ minimum लंबाई भी है और साइलेंस की वह मात्रा भी जिसे फ़िल्टर काटना शुरू करने से पहले जाने देता है, इसलिए "सिर्फ़ 500 ms से लंबे ठहराव हटाओ" कहने का मतलब यह भी है कि "हर एक में से 500 ms छोड़ दो"। stop_silence फिर इसमें जुड़ता है, इसकी जगह नहीं लेता, और बोलने से पहले pad के लिए कोई विकल्प नहीं है। अगर ठहराव को अपनी चुनी हुई लंबाई का रखना है, तो silencedetect के साथ अपनी कट लिस्ट (या ऊपर वाला numpy वर्शन) यह कंट्रोल देता है; silenceremove तब ठीक है जब आपको वैसे भी आधे सेकंड का ठहराव ही चाहिए था।
ऊपर के सभी sweep वाली पूरी स्क्रिप्ट numpy और soundfile के साथ Python की लगभग 90 लाइनें है। osFoundry डेस्कटॉप ऐप में यही स्टेप्स Sound Editor में Edit → Strip Silence और Edit → Space Clips… हैं, जो ऐप के local-first डिज़ाइन के मुताबिक आपकी अपनी मशीन पर चलते हैं। ऐप osFoundry डाउनलोड पेज से मुफ़्त मिलता है।

