Waveform analysis/Top and tail

From Rosetta Code
Waveform analysis/Top and tail is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

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.

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

Translation of: Go
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

Translation of: Go

The ability to call external processes such as SoX is expected to be added to Wren-cli in the next release. In the meantime, we embed the following Wren script in a C host to complete this task.

/* Waveform_analysis_Top_and_tail.wren */

class C {
    foreign static getInput(maxSize)

    foreign static sox(args)

    foreign static removeFile(name)
}

var sec = "00:00:01"

var name = ""
while (name == "") {
    System.write("Enter name of audio file to be trimmed : ")
    name = C.getInput(80)
}

var name2 = ""
while (name2 == "") {
    System.write("Enter name of output file              : ")
    name2 = C.getInput(80)
}

var squelch = 0
while (!squelch || squelch < 1 || squelch > 10) {
    System.write("Enter squelch level \% max (1 to 10)    : ")
    squelch = Num.fromString(C.getInput(5))
}
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]
C.sox(args.join(" "))

// Reverse tmp1 to tmp2.
args = [tmp1, tmp2, "reverse"]
C.sox(args.join(" "))

// Trim audio below squelch level from tmp2 and output to tmp1.
args = [tmp2, tmp1, "silence", "1", sec, squelchS]
C.sox(args.join(" "))

// Reverse tmp1 to the output file.
args = [tmp1, name2, "reverse"]
C.sox(args.join(" "))

// Remove the temporary files.
C.removeFile(tmp1)
C.removeFile(tmp2)


We now embed this in the following C program, compile and run it.

/* gcc Waveform_analysis_Top_and_tail.c -o Waveform_analysis_Top_and_tail -lwren -lm */

#include <stdio.h>
#include <stdio_ext.h>
#include <stdlib.h>
#include <string.h>
#include "wren.h"

void C_getInput(WrenVM* vm) {
    int maxSize = (int)wrenGetSlotDouble(vm, 1) + 2;
    char input[maxSize];
    fgets(input, maxSize, stdin);
    __fpurge(stdin);
    input[strcspn(input, "\n")] = 0;
    wrenSetSlotString(vm, 0, (const char*)input);
}

void C_sox(WrenVM* vm) {
    const char *args = wrenGetSlotString(vm, 1);
    char command[strlen(args) + 4];
    strcpy(command, "sox ");
    strcat(command, args);
    system(command);
}

void C_removeFile(WrenVM* vm) {
    const char *name = wrenGetSlotString(vm, 1);
    if (remove(name) != 0) perror("Error deleting file.");
}

WrenForeignMethodFn bindForeignMethod(
    WrenVM* vm,
    const char* module,
    const char* className,
    bool isStatic,
    const char* signature) {
    if (strcmp(module, "main") == 0) {
        if (strcmp(className, "C") == 0) {
            if (isStatic && strcmp(signature, "getInput(_)") == 0)   return C_getInput;
            if (isStatic && strcmp(signature, "sox(_)") == 0)        return C_sox;
            if (isStatic && strcmp(signature, "removeFile(_)") == 0) return C_removeFile;
        }
    }
    return NULL;
}

static void writeFn(WrenVM* vm, const char* text) {
    printf("%s", text);
}

void errorFn(WrenVM* vm, WrenErrorType errorType, const char* module, const int line, const char* msg) {
    switch (errorType) {
        case WREN_ERROR_COMPILE:
            printf("[%s line %d] [Error] %s\n", module, line, msg);
            break;
        case WREN_ERROR_STACK_TRACE:
            printf("[%s line %d] in %s\n", module, line, msg);
            break;
        case WREN_ERROR_RUNTIME:
            printf("[Runtime Error] %s\n", msg);
            break;
    }
}

char *readFile(const char *fileName) {
    FILE *f = fopen(fileName, "r");
    fseek(f, 0, SEEK_END);
    long fsize = ftell(f);
    rewind(f);
    char *script = malloc(fsize + 1);
    fread(script, 1, fsize, f);
    fclose(f);
    script[fsize] = 0;
    return script;
}

int main(int argc, char **argv) {
    WrenConfiguration config;
    wrenInitConfiguration(&config);
    config.writeFn = &writeFn;
    config.errorFn = &errorFn;
    config.bindForeignMethodFn = &bindForeignMethod;
    WrenVM* vm = wrenNewVM(&config);
    const char* module = "main";
    const char* fileName = "Waveform_analysis_Top_and_tail.wren";
    char *script = readFile(fileName);
    WrenInterpretResult result = wrenInterpret(vm, module, script);
    switch (result) {
        case WREN_RESULT_COMPILE_ERROR:
            printf("Compile Error!\n");
            break;
        case WREN_RESULT_RUNTIME_ERROR:
            printf("Runtime Error!\n");
            break;
        case WREN_RESULT_SUCCESS:
            break;
    }
    wrenFreeVM(vm);
    free(script);
    return 0;
}