0

I'm trying to make a script that monitors a directory (where recordings from a VHF radio are saved by iSpy) for new files and applies a noise reduction process to them using the SoX binary. The simplest part of the script is where I'm failing: I'm not passing parameters to the executable properly. I've tried multiple iterations of the command I'm using based on solutions found on this site only to run into new problems. I tried various quoting strategies I do not understand and piping to CMD but there's obviously something basic that I'm missing. I'm very new to PS and scripting in general, so help would be appreciated.

The whole script (early WIP) is at the bottom of this post.

The binary expects a simple syntax:

sox.exe <file to analyze> <"-n noiseprof"> <analysis output filename>
sox.exe <file to process> <file to save> <"noisered"> <analysis filename> <"0.21 silence -l 1 0.3 5% -1 2.0 5%">

I'm trying to input the binary's path and all parameters/arguments as variables, expand and run the command. Examples of all variables used:

        $soxpath = "C:\Program Files (x86)\sox-14-4-2"
        $path = "E:\SEC Surveillance\audio\EOJNP\2_2021-07-17_10-18-00.mp3"
        $args1 = "-n noiseprof"
        $profileoutput = "E:\SEC Surveillance\audio\EOJNP\2_2021-07-17_10-18-00_noiseprofile.prof"
        $processoutput = "E:\SEC Surveillance\audio\EOJNP\processed\2_2021-07-17_10-18-00_processed.mp3"
        $args3 = "noisered"
        $args4 = "0.21 silence -l 1 0.3 5% -1 2.0 5%"
        #& $soxpath\sox.exe $path $args1 $profileoutput
        #& $soxpath\sox.exe $path $processoutput $args3 $profileoutput $args4

The executable will run just fine if I do not use variables to input paths and arguments, from a PS terminal as well as from a simple script:

& "C:\Program Files (x86)\sox-14-4-2\sox.exe" "E:\SEC Surveillance\audio\EOJNP\2_2021-07-17_10-18-00.mp3" -n noiseprof "E:\SEC Surveillance\audio\EOJNP\2_2021-07-17_10-18-00_noiseprofile.prof"

After various simpler attempts my reasoning was that if I replicate this exact syntax in a script's output, it should work. The result is not pretty:

& "`"$($soxpath)\sox.exe`"" "`"$($path)`"" $args1 "`"$($profileoutput)`""
& : The term '"C:\Program Files (x86)\sox-14-4-2\sox.exe"' is not recognized...

Going back a step results in the executable launching but not getting arguments (based on a Write-Host output I'd expect the command to be spot on):

& $soxpath\sox.exe "`"$($path)`"" $args1 "`"$($profileoutput)`""
sox.exe : C:\Program Files (x86)\sox-14-4-2\sox.exe WARN getopt: option ` ' not recognized
C:\Program Files (x86)\sox-14-4-2\sox.exe FAIL sox: invalid option

Also tried to create a parameter array using variables:

$prm = $ExecutionContext.InvokeCommand.ExpandString("'$path', $args1, '$profileoutput'")
& $soxpath\sox.exe $prm

But honestly I have no idea what I'm doing.

### SETTINGS
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = "E:\SEC Surveillance\audio\EOJNP"
    $watcher.Filter = "*.mp3"
    $watcher.IncludeSubdirectories = $false
    $watcher.EnableRaisingEvents = $true  

### ACTION
    $action = { 
                ### VAR STATIC
                $args1 = "-n noiseprof"
                #$args2 = "null"
                $args3 = "noisered"
                $args4 = "0.21 silence -l 1 0.3 5% -1 2.0 5%"

                ### VAR BASIC PATHS
                $path = $Event.SourceEventArgs.FullPath
                $parentpath = Split-Path -Path "$path"
                $name = $Event.SourceEventArgs.Name
                $namenoext = [IO.Path]::GetFileNameWithoutExtension($name)
                $pathnoext = ("$parentpath" + "\" + "$namenoext")

                ### VAR PROCESS PATHS
                $soxpath = "C:\Program Files (x86)\sox-14-4-2"
                $profileoutput = ("$pathnoext" + "_noiseprofile.prof")
                $processoutput = ("$parentpath" + "\processed\" + "$namenoext" + "_processed.mp3")

                ### PROCESSING AND FILE MANAGEMENT
                # Double quotes escaped
                & "`"$($soxpath)\sox.exe`"" "`"$($path)`"" $args1 "`"$($profileoutput)`""
                & "`"$($soxpath)\sox.exe`"" "`"$($path)`"" "`"$($processoutput)`"" $args3 "`"$($profileoutput)`"" $args4

                }    

### START
                Register-ObjectEvent $watcher "Created" -Action $action
                #Register-ObjectEvent $watcher "Changed" -Action $action
                #Register-ObjectEvent $watcher "Deleted" -Action $action
                #Register-ObjectEvent $watcher "Renamed" -Action $action
                while ($true) {sleep 5}
    
Alex Oja
  • 11
  • 3
  • 1
    see: https://stackoverflow.com/questions/3592851/executing-a-command-stored-in-a-variable-from-powershell. I do think that you should not do: `$args1 = "-n noiseprof"`, but `$args1 = "-n","noiseprof" (which is an array) – Luuk Jul 17 '21 at 10:39
  • The post you linked is one I already tried to apply. The $prm array attempt was the result, but it did not work. Trying to use Invoke-Expression as also suggested in that thread results in: "Invoke-Expression : A positional parameter cannot be found that accepts argument 'C:\Program Files (x86)\sox-14-4-2\sox.exe'" When should I use an array instead of a simple variable? When the string contains spaces? – Alex Oja Jul 17 '21 at 11:02
  • On second thought I seem to have been hasty. The post linked by Luuk mentions the use of Invoke-Expression with a single parameter. I previously tried to run it with two. When I get it down to "Invoke-Expression $prm" it does in fact seem to function. I'll have to run tests. Is there a reason why my other attempts are failing; is using Invoke-Expression not quite far from ideal? – Alex Oja Jul 17 '21 at 11:13
  • The description in `help Invoke-Expression` make the use for this clear. ("The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command. Without Invoke-Expression , a string submitted at the command line would be returned (echoed) unchanged") – Luuk Jul 17 '21 at 11:18
  • Thank you! Do you have tips on using the ampersand as well? Does work for others, it seems, and would seem preferable: "Runs a command, script, or script block. The call operator, also known as the "invocation operator", lets you run commands that are stored in variables and represented by strings or script blocks." – Alex Oja Jul 17 '21 at 11:29
  • I don have `sox` installed, but this works for me: `$cmd="c:\Program Files (x86)\Notepad++\notepad++.exe"; $prm="a","b"; & $cmd $prm`. It will open file `a` and file `b` in Notepad++. – Luuk Jul 17 '21 at 11:34

1 Answers1

0
  • &, the call operator, is situationally required for invocation of commands (including external executables), namely if the command is specified via a quoted string and/or a variable or expression, possibly as _part of an expandable (interpolating) string ("...").

    • However, it's safe to always use &
  • To programmatically pass multiple arguments to external executables, use an array.

    • Be sure to make each argument its own array element; that is, use $array = '-n', 'noiseprof' and not $array = '-n noiseprof' - the latter would be passed as "-n noiseprof", i.e. as a single, double-quoted argument.
  • Except in edge cases due to legacy bugs relating to values with embedded " chars. and empty strings - see this answer - do not enclose arguments in escaped " chars., e.g. "`"$var`"" - if the value of $var contains spaces, PowerShell will automatically double-quoted it behind the scenes.

  • Do not use Invoke-Expression, which should generally be avoided, to invoke external programs - see this answer.

Applied to your case:

$soxpath = "C:\Program Files (x86)\sox-14-4-2"
$path = "E:\SEC Surveillance\audio\EOJNP\2_2021-07-17_10-18-00.mp3"
$args1 = '-n', 'noiseprof'
$profileoutput = "E:\SEC Surveillance\audio\EOJNP\2_2021-07-17_10-18-00_noiseprofile.prof"
$processoutput = "E:\SEC Surveillance\audio\EOJNP\processed\2_2021-07-17_10-18-00_processed.mp3"
$args3 = 'noisered'
$args4 = '0.21', 'silence', '-l', '1', '0.3', '5%', '-1', '2.0', '5%'

& $soxpath\sox.exe $path $args1 $profileoutput

& $soxpath\sox.exe $path $processoutput $args3 $profileoutput $args4

As an aside: in order to pass multiple arguments to a PowerShell-native command using named arguments (values targeting parameters by name), you must use parameter splatting.

mklement0
  • 382,024
  • 64
  • 607
  • 775