Waveform analysis/Top and tail
The task is to crop a given audio waveform file, removing any leading or trailing silence from the wave file, leaving just the audible sound.
The task should utilize a configurable audio squelch level calibration parameter that enables the user to set the silence level threshold (enabling low level background hiss or hum to be removed from the leading and trailing edges of the audio file.)
Moments of silence (below the squelch threshold), should be removed from the leading and trailing edges of the input wave file, to produce a new cleanly cropped audio file.
Note that the output file format should be the same as the initial input format. This should not be changed by the implemented procedure.
FreeBASIC
Since FreeBASIC does not have any audio support in its standard library, one must invoke the SoX utility to trim any leading or trailing silence from an audio file.
Const sec = "00:00:01"
Dim As String infile = ""
Input "Enter infile of audio file to be trimmed : ", infile
Dim As String outfile = ""
Input "Enter infile of output file : ", outfile
Dim As Integer squelch = 0
Print "Enter squelch level % max (1 to 10) : ";
While squelch < 1 Or squelch > 10
Input "", squelch
Wend
Dim As String squelchS = Str(squelch) & "%"
Dim As String tmp1 = "tmp1_" & infile
Dim As String tmp2 = "tmp2_" & infile
' Trim audio below squelch level from start and output to tmp1.
Shell "sox " & infile & " " & tmp1 & " silence 1 " & Sec & " " & squelchS
' Reverse tmp1 to tmp2.
Shell "sox " & tmp1 & " " & tmp2 & " reverse"
' Trim audio below squelch level from tmp2 and output to tmp1.
Shell "sox " & tmp2 & " " & tmp1 & " silence 1 " & Sec & " " & squelchS
' Reverse tmp1 to the output file.
Shell "sox " & tmp1 & " " & outfile & " reverse"
' Remove the temporary files.
Kill tmp1
Kill tmp2
Sleep
Go
As Go does not have any audio support in its standard library, this invokes the SoX utility to trim any leading or trailing silence from an audio file.
Unfortunately, you must know the length of the silence at the end of the audio file to trim off silence reliably. To work around this, we first trim off leading silence from the file, reverse it and then trim off the leading silence from that file. We then reverse the resulting file to the output file specified by the user.
Setting the squelch level to 2 or 3 per cent of the maximum sample value worked reasonably well in my tests.
package main
import (
"bufio"
"fmt"
"log"
"os"
"os/exec"
"strconv"
)
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
const sec = "00:00:01"
scanner := bufio.NewScanner(os.Stdin)
name := ""
for name == "" {
fmt.Print("Enter name of audio file to be trimmed : ")
scanner.Scan()
name = scanner.Text()
check(scanner.Err())
}
name2 := ""
for name2 == "" {
fmt.Print("Enter name of output file : ")
scanner.Scan()
name2 = scanner.Text()
check(scanner.Err())
}
squelch := 0.0
for squelch < 1 || squelch > 10 {
fmt.Print("Enter squelch level % max (1 to 10) : ")
scanner.Scan()
input := scanner.Text()
check(scanner.Err())
squelch, _ = strconv.ParseFloat(input, 64)
}
squelchS := strconv.FormatFloat(squelch, 'f', -1, 64) + "%"
tmp1 := "tmp1_" + name
tmp2 := "tmp2_" + name
// Trim audio below squelch level from start and output to tmp1.
args := []string{name, tmp1, "silence", "1", sec, squelchS}
cmd := exec.Command("sox", args...)
err := cmd.Run()
check(err)
// Reverse tmp1 to tmp2.
args = []string{tmp1, tmp2, "reverse"}
cmd = exec.Command("sox", args...)
err = cmd.Run()
check(err)
// Trim audio below squelch level from tmp2 and output to tmp1.
args = []string{tmp2, tmp1, "silence", "1", sec, squelchS}
cmd = exec.Command("sox", args...)
err = cmd.Run()
check(err)
// Reverse tmp1 to the output file.
args = []string{tmp1, name2, "reverse"}
cmd = exec.Command("sox", args...)
err = cmd.Run()
check(err)
// Remove the temporary files.
err = os.Remove(tmp1)
check(err)
err = os.Remove(tmp2)
check(err)
}
Julia
Implemented as a Gtk GUI app.
using FileIO, Gtk, LibSndFile, Printf
function dB(buf, i, channels, cutoff=0.001)
xsum = sum(buf[k, j] * buf[k, j] for j in 1:channels, k in i-1:i+1)
sigmean = xsum / (channels * 3)
return sigmean < cutoff ? -60.0 : 20 * log(10, sigmean)
end
function silencecropperapp()
win = GtkWindow("Sound File Silence Cropping Tool", 800, 200) |> (GtkFrame() |> (vbox = GtkBox(:v)))
infilename, outfilename, modifyinputfile, trimmable = "", "", false, true
outchoices = Vector{GtkRadioButton}(undef, 2)
outchoices[1] = GtkRadioButton("Crop the Input File In-Place", active=true)
outchoices[2] = GtkRadioButton(outchoices[1], "Copy Output to File Chosen Below")
inbutton = GtkButton("Click to Choose Input File")
senslabel = GtkLabel("Threshold (db)")
thresholdslider = GtkScale(false, -40.0:10.0)
adj = GtkAdjustment(thresholdslider)
push!(vbox, outchoices[1], outchoices[2], inbutton, senslabel, thresholdslider)
crop = Vector{GtkRadioButton}(undef, 3)
crop[1] = GtkRadioButton("Crop File at Beginning")
crop[2] = GtkRadioButton(crop[1], "Crop File at End")
crop[3] = GtkRadioButton(crop[2], "Crop Both Ends", active=true)
cropchoice() = [get_gtk_property(b, :active, Bool) for b in crop]
trimfilebutton = GtkButton("Trim!")
push!(vbox, crop[1], crop[2], crop[3], trimfilebutton)
hbox = GtkBox(:h)
textentry = GtkEntry()
set_gtk_property!(textentry, :expand, true)
set_gtk_property!(textentry, :text, "Set Output File")
pickoutfilebutton = GtkButton("Choose Existing File for Output")
push!(hbox, textentry, pickoutfilebutton)
push!(vbox, hbox)
function reinitialize()
infilename, outfilename, modifyinputfile, trimmable = "", "", true, true
set_gtk_property!(trimfilebutton, :label, "Trim!")
toggleoutputactive(win)
end
function toggleoutputactive(w)
if get_gtk_property(outchoices[2], :active, Bool)
set_gtk_property!(textentry, :editable, true)
set_gtk_property!(textentry, :text, "Set Output File")
modifyinputfile = false
elseif get_gtk_property(outchoices[1], :active, Bool)
set_gtk_property!(textentry, :editable, false)
set_gtk_property!(textentry, :text, "Set Output File")
outfilename = ""
modifyinputfile = true
end
end
function pickinput(w)
filename = open_dialog("Pick a sound or music file to be trimmed.")
if filesize(filename) > 0
infilename = filename
set_gtk_property!(inbutton, :label, infilename)
end
trimbuttonlabel(win)
end
function pickoutput(w)
if get_gtk_property(outchoices[2], :active, Bool)
outfilename = open_dialog("Pick Output File To Be Overwritten.")
set_gtk_property!(textentry, :text, outfilename)
show(textentry)
end
end
function trimbuttonlabel(w)
tstart, tend, tboth = cropchoice()
toggleoutputactive(win)
if filesize(infilename) > 0
scut, ecut, b, nframes, fs = getsilence(get_gtk_property(adj, :value, Float64))
if (tboth && scut <= 1 && ecut >= nframes) ||
(tstart && scut <= 1) || (tend && ecut >= nframes)
set_gtk_property!(trimfilebutton, :label, "Nothing to trim.")
trimmable = false
else
text = @sprintf("Trim %7.2f seconds at front and %7.2f seconds at back.",
(scut - 1) / fs, (nframes - ecut) / fs)
set_gtk_property!(trimfilebutton, :label, text)
trimmable = true
end
else
set_gtk_property!(trimfilebutton, :label, "Trim!")
end
end
function trimsilence(w)
if trimmable
if modifyinputfile
outfilename = infilename
elseif outfilename == ""
s = get_gtk_property(textentry, :text, String)
outfilename = s != "Set Output File" ? s :
open_dialog("Pick or enter a file for output")
end
if filesize(outfilename) <= 0 || ask_dialog("Really change file $infilename?")
scut, ecut, buf, n, fs = getsilence(get_gtk_property(adj, :value, Float64))
FileIO.save(outfilename, buf[scut:ecut, :])
info_dialog("File $outfilename saved: $(filesize(outfilename)) bytes.", win)
reinitialize()
end
end
end
function getsilence(threshold, granularity=0.1)
buf = load(infilename)
(buflen, channels) = size(buf)
startcut, endcut = 0, buflen
nframes = LibSndFile.nframes(buf)
fs = LibSndFile.samplerate(buf)
cchoices = cropchoice()
if cchoices[1] || cchoices[3]
pos = findfirst(i -> dB(buf, i, channels) > threshold, 2:buflen-1)
if pos == nothing
# all below threshold
return buflen, 0, buf, nframes, fs
else
startcut = Int(floor(((pos / fs) - (pos / fs) % granularity) * fs))
startcut = startcut < 1 ? 1 : startcut
end
end
if cchoices[2] || cchoices[3]
pos = findlast(i -> dB(buf, i, channels) > threshold, 2:buflen-1)
if pos != nothing
endcut = Int(ceil((granularity + (pos / fs) - (pos / fs) % granularity) * fs))
endcut = endcut > nframes ? nframes : endcut
end
end
return startcut, endcut, buf, nframes, fs
end
foreach(i -> signal_connect(toggleoutputactive, outchoices[i], :clicked), 1:2)
foreach(i -> signal_connect(trimbuttonlabel, crop[i], :clicked), 1:3)
signal_connect(pickoutput, pickoutfilebutton, :clicked)
setfromtext(w) = (outfilename = get_gtk_property(textentry, :text, String))
signal_connect(pickinput, inbutton, :clicked)
signal_connect(trimsilence, trimfilebutton, :clicked)
toggleoutputactive(win)
cond = Condition()
endit(w) = notify(cond)
signal_connect(endit, win, :destroy)
showall(win)
wait(cond)
end
silencecropperapp()
Nim
import os, osproc, strutils
const Sec = "00:00:01"
proc getString(prompt: string): string =
while true:
stdout.write prompt
stdout.flushFile()
try:
result = stdin.readLine().strip()
if result.len != 0: break
except EOFError:
quit "\nEOF encountered. Quitting.", QuitFailure
proc getFloatValue(prompt: string; minval, maxval: float): float =
while true:
stdout.write prompt
stdout.flushFile()
try:
result = stdin.readLine.strip().parseFloat()
if result notin minval..maxval:
echo "Invalid value"
else:
return
except ValueError:
echo "Error: invalid value."
except EOFError:
quit "\nEOF encountered. Quitting.", QuitFailure
let infile = getString("Enter name of audio file to be trimmed: ")
let outfile = getString("Enter name of output file: ")
let squelch = getFloatValue("Enter squelch level % max (1 to 10): ", 1, 10)
let squelchS = squelch.formatFloat(ffDecimal, precision = -1) & '%'
let tmp1 = "tmp1_" & infile
let tmp2 = "tmp2_" & infile
# Trim audio below squelch level from start and output to tmp1.
var args = @[infile, tmp1, "silence", "1", Sec, squelchS]
discard execProcess("sox", args = args, options = {poStdErrToStdOut, poUsePath})
# Reverse tmp1 to tmp2.
args = @[tmp1, tmp2, "reverse"]
discard execProcess("sox", args = args, options = {poStdErrToStdOut, poUsePath})
# Trim audio below squelch level from tmp2 and output to tmp1.
args = @[tmp2, tmp1, "silence", "1", Sec, squelchS]
discard execProcess("sox", args = args, options = {poStdErrToStdOut, poUsePath})
# Reverse tmp1 to the output file.
args = @[tmp1, outfile, "reverse"]
discard execProcess("sox", args = args, options = {poStdErrToStdOut, poUsePath})
# Remove the temporary files.
removeFile(tmp1)
removeFile(tmp2)
Phix
The first part is a copy of Musical_scale#version_2 but with first and last 2s written out as silence.
I referred to http://soundfile.sapp.org/doc/WaveFormat/ when writing this.
You may want to check for average volume over some period, rather than just all-0.
This is clearly just a starting point and not a practical/useful/finished product!
without js -- (file i/o) constant sample_rate = 44100, duration = 8, dataLength = sample_rate * duration, hdrSize = 44, fileLen = dataLength + hdrSize - 8, freqs = { 261.6, 293.6, 329.6, 349.2, 392.0, 440.0, 493.9, 523.3 }, wavhdr = "RIFF"& int_to_bytes(fileLen,4)& "WAVE"& "fmt "& int_to_bytes(16,4)& -- length of format data (= 16) int_to_bytes(1,2)& -- type of format (= 1 (PCM)) int_to_bytes(1,2)& -- number of channels (= 1) int_to_bytes(sample_rate,4)& -- sample rate int_to_bytes(sample_rate,4)& -- sample rate * bps(8) * channels(1) / 8 (= sample rate) int_to_bytes(1,2)& -- bps(8) * channels(1) / 8 (= 1) int_to_bytes(8,2)& -- bits per sample (bps) (= 8) "data"& int_to_bytes(dataLength,4) -- size of data section if length(wavhdr)!=hdrSize then ?9/0 end if -- sanity check integer fn = open("notes.wav", "wb") puts(fn, wavhdr) for j=1 to duration do atom omega = 2 * PI * freqs[j] for i=0 to dataLength/duration-1 do atom y = 32 * sin(omega * i / sample_rate) -- integer byte = and_bits(y,#FF) -- (makes for no cropping) integer byte = iff(j<=2 or j>=7?0:and_bits(y,#FF)) puts(fn,byte) end for end for close(fn) -- </copy of Musical_scale> string raw = get_text("notes.wav") if raw[1..4]!="RIFF" then ?9/0 end if integer rawlen = bytes_to_int(raw[5..8]) if rawlen!=fileLen then ?9/0 end if if raw[9..12]!="WAVE" then ?9/0 end if if raw[13..16]!="fmt " then ?9/0 end if if bytes_to_int(raw[17..20])!=16 then ?9/0 end if -- 2 bytes per sample if bytes_to_int(raw[21..22])!=1 then ?9/0 end if -- must be PCM integer channels = bytes_to_int(raw[23..24]) if channels!=1 and channels!=2 then ?9/0 end if -- mono or stereo (else??) integer bps = bytes_to_int(raw[35..36]) if bps!=8 and bps!=16 then ?9/0 end if -- 8 or 16 bits per sample? bps = bps/8*channels*2 -- ===> bytes per sample (pair(s)(s)) if raw[37..40]!="data" then ?9/0 end if integer rawdata = bytes_to_int(raw[41..44]) if rawdata!=dataLength then ?9/0 end if integer bs, be, crop = 0 for bs=45 to length(raw) by bps do -- some volume threshold... if sum(raw[bs..bs+bps-1])>0 then exit end if -- (assumes unsigned) crop += bps end for for be = length(raw) to bs by -bps do if sum(raw[be-bps+1..be])>0 then exit end if crop += bps end for if crop=0 then printf(1,"nothing to crop\n") else printf(1,"cropping %d bytes\n",crop) rawlen -= crop rawdata -= crop raw[5..8] = int_to_bytes(rawlen,4) raw[41..44] = int_to_bytes(rawdata,4) raw = raw[1..be] raw[46..bs-1] = {} fn = open("notes.wav","wb") puts(fn,raw) close(fn) end if -- without cropping 8s, with 4s if platform()=WINDOWS then system("notes.wav") elsif platform()=LINUX then system("aplay notes.wav") end if
Wren
Uses the 'os' and 'io' sub-modules.
import "os" for Process
import "io" for File
import "./ioutil" for Input
var sec = "00:00:01"
var name = Input.text ("Enter name of audio file to be trimmed : ", 1, 80)
var name2 = Input.text ("Enter name of output file : ", 1, 80)
var squelch = Input.number ("Enter squelch level \% max (1 to 10) : ", 1, 10)
var squelchS = squelch.toString + "\%"
var tmp1 = "tmp1_" + name
var tmp2 = "tmp2_" + name
// Trim audio below squelch level from start and output to tmp1.
var args = [name, tmp1, "silence", "1", sec, squelchS]
Process.exec("sox", args)
// Reverse tmp1 to tmp2.
args = [tmp1, tmp2, "reverse"]
Process.exec("sox", args)
// Trim audio below squelch level from tmp2 and output to tmp1.
args = [tmp2, tmp1, "silence", "1", sec, squelchS]
Process.exec("sox", args)
// Reverse tmp1 to the output file.
args = [tmp1, name2, "reverse"]
Process.exec("sox", args)
// Remove the temporary files.
File.delete(tmp1)
File.delete(tmp2)