#!/usr/bin/perl -w # constuct and execute a sox command to dial a phone number with DTMF tones. $VERSION = "0.0.1"; ($SELF = $0) =~ s,.*/,,; $|++; =pod =head1 NAME soxdial - generate DTMF (touchtone) audio =head1 SYNOPSIS B [I] [ [I] [I] ... ] =head1 DESCRIPTION B uses B(1) to generate DTMF tones (aka touchtone dialling). Output can be played through the system sound card, or saved to a file of any type supported by sox. By default, letters are accepted and converted to numbers, according to the layout of a touchtone phone (e.g. A = 2, M = 6, etc). If the extended touchtone digits A, B, C, and D are needed, they can be enabled with the B<-x>, B<--extended> option (see below). =head1 OPTIONS Note that option bundling is not supported. Use e.g. B<-v -n>, not B<-vn>. Also, spaces are required between options and their arguments. Use e.g. B<-b 16>, not B<-b16>. Also, GNU style --option=value is not supported (use B<--bits 16>, not B<--bits=16>). =head2 Global Options These options affect the entire output. They should only be given once, before any dial strings or dial options. =over 4 =item B<-o>, B<--output> I Write sox's output to a file, rather than playing it. The file format is determined by the filename extension. Use B<.wav> for RIFF WAVE output, B<.flac> for FLAC, B<.ogg> for Ogg Vorbis, or anything else (including no extension) for raw audio samples. The special filename B<-> writes raw samples to standard output. =item B<-r>, B<--rate> I Set the bitrate in Hz. Default is 8000. You should probably stick with standard bitrates such as 22050, 44100, 48000, etc, although this is not enforced. =item B<-b>, B<--bits> I Set the bits per sample. Default is 8. The only other choice is 16. 8-bit samples will be encoded as unsigned, and 16-bit will be encoded as signed. =item B<-v>, B<--verbose> Print verbose information, including the generated B command on stdout =item B<-n>, B<--no-exec> Do not execute the generated B command. This option also enables B<-v>. =item B<--help> Prints this help text, via B(1). =item B<--man> Prints this help text as a man page, via B(1). Suggested use: soxdial --man > soxdial.1 Then B can be installed in e.g. /usr/man/man1 or /usr/share/man/man1 or wherever your OS keeps its man pages. =back =head2 Dial Options These options can be mixed freely with dial strings, and are applied as they're found on the command line. Each one affects the rest of the dial strings, until the same option is seen again. =over 4 =item B<-l> I, B<--length> I Sets the time each digit's tones are played. Default is 0.25. =item B<-d> I, B<--delay> I Sets the delay between consecutive digits. Default is 0.1. =item B<-c> I, B<--comma> I Sets the delay added by commas in the dial strings. Default is 0.5. =item B<-x>, B<--extended> Allows the extended touchtone pad keys A, B, C, and D. B letter-to-number conversions. =item B<-a>, B<--alphabet> Disables extended touchtone pad keys A, B, C, and D; re-enables letter-to-number conversions. This is the default; this option exists to turn off a prior B<-x>, B<--extended> option. =item B<-t> I, B<--dialtone> I Play I of dialtone. =back =head2 Dial Strings These are the actual digits to be dialled. DTMF digits 0 to 9, #, and * are supported. By default, letters are also supported, and will be converted to digits according to the standard layout of touchtone phones (e.g. A through C convert to 2, D through F are 3, etc). Q and Z weren't present on classic phones, but they will be converted to 7 and 9, respectively (like modern cell phones). To add an extra delay between digits, use a comma. To play the extra DTMF tones for the extended 16-digit keypad (which has A, B, C, and D keys), use the B<-x>, B<--extended> option. This disables letter-to-number conversion, but it can be re-enabled later on the command line with the B<-a>, B<--alphabet> option. All characters that aren't mentioned above, will be silently ignored. This allows you to paste a phone number in the form B<(555) 555-1212> and have it work correctly. =head1 AUTHOR soxdial was written by B. Watson and released under the WTFPL: Do WTF you want with this. =cut %freqs = ( 1 => [697, 1209], 2 => [697, 1336], 3 => [697, 1477], 4 => [770, 1209], 5 => [770, 1336], 6 => [770, 1477], 7 => [852, 1209], 8 => [852, 1336], 9 => [852, 1477], '*' => [941, 1209], 0 => [941, 1336], '#' => [941, 1477], A => [697, 1633], B => [770, 1633], C => [852, 1633], D => [941, 1633], ); @freqs1 = (); @freqs2 = (); @delays = (); $time = 0; $pausetime = 0.5; $digittime = 0.25; $intertime = 0.1; $bits = 8; $encoding = "-eun"; $rate = 8000; $output = "-d"; sub letter2number { my $ret; my $l = uc shift; return $l unless $l =~ /[A-Z]/; for($l) { if(/[A-O]/) { $ret = int((ord($_) - 65) / 3 + 2) . ""; } elsif(/[P-S]/) { $ret = "7"; } elsif(/[T-V]/) { $ret = "8"; } else { # /[W-Z]/ $ret = "9"; } } warn "$SELF: letter2number('$l') => '$ret'\n" if $verbose; return $ret; } # sox -n -d synth 0.25 sine 697 sine 1209 sine 770 sine 1477 delay 0 0 .35 .35 remix - # ...plays DTMF 1 and 6, for 0.25 sec each, with a 0.10 sec delay between them. sub add_digit { my $d = shift; if($d eq ',') { # pause push @freqs1, 0; push @freqs2, 0; push @delays, $time; $time += $pausetime; } else { #print "time $time\n"; #print "freq1 " . ($freqs{$d}->[0]) . "\n"; #print "freq2 " . ($freqs{$d}->[1]) . "\n"; push @freqs1, ($freqs{$d}->[0]); push @freqs2, ($freqs{$d}->[1]); push @delays, $time; $time += $digittime; $time += $intertime; } warn "$SELF: added digit '$d', time now $time\n" if $verbose; } # ( sox -n -p synth 2 sine 350 sine 440; sox -n -p synth 2 sine 697 sine 1209 ) | sox -G -p -d # ...plays 2 sec of dialtone followed by 2 sec of DTMF key 1. Change the -d # to 1.wav to save to a file. Prefix it with -rXXXX -sX if needed. # each dial string (or dialtone) creates a subcommand. they get executed in a # subshell, writing raw audio to stdout, and the final sox command reads all # the audio from its stdin. looks like: # ( ; ; ... ) | sox sub make_sox_subcmd { my $cmd = "sox -n -b$bits $encoding -r$rate -c1 -traw - "; my $synth = " synth $digittime "; my $delay = "delay "; for(0..$#freqs1) { my $f1 = $freqs1[$_]; my $f2 = $freqs2[$_]; my $d = $delays[$_]; $synth .= "sine $f1 sine $f2 "; $delay .= "$d $d "; } $time = 0; @freqs1 = @freqs2 = @delays = (); return $cmd . $synth . $delay; } # used to add the silence after the last digit in a dial string sub silence_subcmd { warn "$SELF: adding $intertime sec silence at end of dial string.\n" if $verbose; return "sox -n -b$bits $encoding -r$rate -c1 -traw - trim 0 $intertime"; } sub dialtone_subcmd { my $sec = shift; if(!defined $sec || ($sec + 0) <= 0) { warn "$SELF: ignoring invalid --dialtone argument.\n"; return; } warn "$SELF: adding $sec sec of dial tone.\n" if $verbose; return "sox -n -b$bits $encoding -r$rate -c1 -traw - synth $sec sine 350 sine 440"; } # final sox command, to which we pipe all the others. sub make_sox_cmd { if($output eq '-') { $output = "-t raw $output"; } else { # support quotes, spaces, etc in filenames. # this can probably be fooled by a determined luser. $output =~ s,",\\",g; $output = "\"$output\""; } my $cmd = "sox -traw -b$bits $encoding -r$rate -c1 - $output "; my $subcmds = join(" ; ", @sox_subcmds); return "( " . $subcmds . " ) | " . $cmd; } # main() if(!@ARGV) { warn "$SELF: no dial strings. Try $SELF --help.\n"; exit 1; } for ($argc = 0; $argc < @ARGV; $argc++) { $_ = $ARGV[$argc]; # note: unusually, if anything starts with - but isn't a recognized # option, it's not an error (it gets treated as a dial string). this # allows e.g. 555 -1212 to work correctly, but mistyped options will # result in them being dialled as alphabetic characters. if(/--?version$/) { print "$SELF $VERSION\n"; exit 0; } elsif(/^--?man$/) { exec "pod2man --stderr -s6 -cUrchlaysStuff -r$VERSION -u $0"; exit 1; } elsif(/^--?(?:\?|h)/) { exec "perldoc $0"; exit 1; } elsif(/^--?v(erbose)?$/) { $verbose = 1; } elsif(/^--?n(oexec)?$/) { $verbose = 1; $noexec = 1; } elsif(/^--?o(?:output)?$/) { $output = $ARGV[++$argc]; warn "$SELF: output set to '$output'\n" if $verbose; } elsif(/^--?b(?:its)?$/) { $bits = $ARGV[++$argc]; if($bits == 8) { $encoding = "-eun"; } elsif($bits == 16) { $encoding = "-esig"; } else { die "$SELF: bad -b/--bits, only 8 or 16 is allowed.\n"; } warn "$SELF: bits set to '$bits'\n" if $verbose; } elsif(/^--?r(?:ate)?$/) { $rate = $ARGV[++$argc]; warn "$SELF: bitrate set to '$rate'\n" if $verbose; die "$SELF: invalid bitrate.\n" unless $rate > 0; } elsif(/^--?l(?:ength)?$/) { $digittime = $ARGV[++$argc]; warn "$SELF: digit length set to '$digittime'\n" if $verbose; die "$SELF: invalid --length argument.\n" unless $digittime > 0; } elsif(/^--?(?:x|extended)$/) { $extended = 1; warn "$SELF: extended DTMF (ABCD) enabled.\n" if $verbose; } elsif(/^--?a(?:lphabet)?$/) { $extended = 0; warn "$SELF: extended DTMF (ABCD) disabled.\n" if $verbose; } elsif(/^--?d(?:elay)?$/) { $intertime = $ARGV[++$argc]; warn "$SELF: inter-digit delay set to '$intertime'\n" if $verbose; die "$SELF: invalid --delay argument.\n" unless $intertime > 0; } elsif(/^--?c(?:omma)?$/) { $pausetime = $ARGV[++$argc]; warn "$SELF: comma delay set to '$pausetime'\n" if $verbose; die "$SELF: invalid --comma argument.\n" unless $pausetime > 0; } elsif(/^--?(?:t|dialtone)$/) { push @sox_subcmds, dialtone_subcmd($ARGV[++$argc]); } else { warn "$SELF: start dial string '$_'\n" if $verbose; for (split "", $_) { my $digit = uc $_; if($extended) { next if $digit !~ /[0-9,#*A-D]/; } else { $digit = letter2number($digit); next if $digit !~ /[0-9,#*]/; } add_digit($digit); } warn "$SELF: end dial string '$_'\n" if $verbose; push @sox_subcmds, make_sox_subcmd(); push @sox_subcmds, silence_subcmd(); } } if(!@sox_subcmds) { die "$SELF: no digits or dialtone to generate.\n"; } my $cmd = make_sox_cmd(); if($verbose) { warn "$SELF: sox command is:\n $cmd\n"; } else { $cmd .= " 2>/dev/null"; } if(!$noexec) { system($cmd); }