| #!/usr/bin/env tclsh |
| ############################################################################### |
| # BRLTTY - A background process providing access to the console screen (when in |
| # text mode) for a blind person using a refreshable braille display. |
| # |
| # Copyright (C) 1995-2023 by The BRLTTY Developers. |
| # |
| # BRLTTY comes with ABSOLUTELY NO WARRANTY. |
| # |
| # This is free software, placed under the terms of the |
| # GNU Lesser General Public License, as published by the Free Software |
| # Foundation; either version 2.1 of the License, or (at your option) any |
| # later version. Please see the file LICENSE-LGPL for details. |
| # |
| # Web Page: http://brltty.app/ |
| # |
| # This software is maintained by Dave Mielke <dave@mielke.cc>. |
| ############################################################################### |
| |
| # BrlTTY GenericSay helper script for the Accent/SA Speech Synthesizer |
| # It should be installed as "/usr/local/bin/say". |
| |
| # Return the name of this script. |
| proc programName {} { |
| global argv0 |
| return [file tail $argv0] |
| } |
| |
| # Write a message, prefixed with the name of this script, to standard error. |
| proc programMessage {message} { |
| global argv0 |
| puts stderr "[programName]: $message" |
| } |
| |
| # Display an error message, and exit with the specified return code. |
| proc programError {code {message ""}} { |
| if {[string length $message] > 0} { |
| programMessage $message |
| } |
| exit $code |
| } |
| |
| # Standard exit routine when command line errors are detected. |
| proc syntaxError {message} { |
| programError 2 $message |
| } |
| |
| # Standard exit routine when something is wrong with the parameter values. |
| proc semanticError {message} { |
| programError 3 $message |
| } |
| |
| # Close the speech synthesizer device. |
| proc closeDevice {} { |
| global deviceStream |
| close $deviceStream; unset deviceStream |
| } |
| |
| # Maintain an event which automatically closes the speech synthesizer |
| # device if no new data has arrived for a while. |
| proc scheduleClose {} { |
| global closeEvent closeTimeout |
| if {[info exists closeEvent]} { |
| after cancel $closeEvent; unset closeEvent |
| } |
| set closeEvent [after $closeTimeout {unset closeEvent; closeDevice}] |
| } |
| |
| # Send data to the speech synthesizer. |
| proc sendData {data} { |
| global deviceStream |
| puts -nonewline $deviceStream $data |
| flush $deviceStream |
| } |
| |
| # Send a command to the speech synthesizer. |
| proc sendCommand {command} { |
| sendData "\x1b$command" |
| } |
| |
| # Configure a speech synthesizer setting. |
| proc configureParameter {settingVariable command} { |
| upvar #0 $settingVariable setting |
| if {[info exists setting]} { |
| if {[llength $command] == 1} { |
| sendCommand "$command$setting" |
| } else { |
| foreach sequence [lindex $command $setting] { |
| sendCommand $sequence |
| } |
| } |
| } |
| } |
| |
| # Open and configure the speech synthesizer device. |
| # If it's already open, then don't do anything. |
| # If it can't be opened, then silently exit with a non-zero return code. |
| proc openDevice {} { |
| global devicePath deviceStream |
| if {![info exists deviceStream]} { |
| if {[catch [list open $devicePath {WRONLY NOCTTY}] response] != 0} { |
| exit 10 |
| } |
| set deviceStream $response |
| sendCommand "=F" |
| sendCommand "=B" |
| sendCommand "Oi" |
| configureParameter speechVolume "A" |
| configureParameter speechRate "R" |
| configureParameter voicePitch "P" |
| configureParameter voiceType "V" |
| configureParameter spacePause "S" |
| configureParameter sentenceIntonation "M" |
| configureParameter punctuationMode {Op {OP Or} {OP OR}} |
| configureParameter hyphenMode {Om OM ON} |
| } |
| } |
| |
| # Insure that the speech synthesizer device is open, and then |
| # instruct it to speak the content of the supplied string. |
| # Arrange for the device to be automatically closed if nothing more |
| # needs to be spoken for a reasonable amount of time. |
| proc sayString {string} { |
| openDevice |
| scheduleClose |
| sendData "$string\r" |
| } |
| |
| # Tell the speech synthesizer to stop speaking immediately, |
| # and to flush its input buffer. |
| proc flushSynthesizer {} { |
| sendCommand "=x" |
| } |
| |
| # Check a device path. |
| proc checkDevice {description path} { |
| if {![file exists $path]} { |
| semanticError "$description not found: $path" |
| } |
| if {[string compare [set type [file type $path]] characterSpecial] != 0} { |
| semanticError "incorrect $description type: $type: $path" |
| } |
| if {![file writable $path]} { |
| semanticError "$description not writable: $path" |
| } |
| } |
| |
| # Check a keyword value. |
| proc checkKeyword {description value keywords} { |
| if {[regexp -nocase {^[a-z]+$} $value]} { |
| if {[set index [lsearch -glob $keywords [set value [string tolower $value]]*]] >= 0} { |
| if {[lsearch -glob [lreplace $keywords $index $index] $value*] >= 0} { |
| syntaxError "ambiguous $description: $value" |
| } |
| return [lsearch -glob $keywords $value*] |
| } |
| } |
| syntaxError "invalid $description: $value" |
| } |
| |
| # Check an integer value. |
| proc checkInteger {description value {minimum ""} {maximum ""}} { |
| if {![regexp {^(0|-?[1-9][0-9]*)$} $value]} { |
| syntaxError "invalid $description: $value" |
| } |
| if {[string length $minimum] > 0} { |
| if {$value < $minimum} { |
| syntaxError "$description less than $minimum: $value" |
| } |
| } |
| if {[string length $maximum] > 0} { |
| if {$value > $maximum} { |
| syntaxError "$description greater than $maximum: $value" |
| } |
| } |
| } |
| |
| # Set the "device" parameter. |
| # It must be either the absolute or the relative path to the speech synthesizer device. |
| proc set-device {path} { |
| global devicePath |
| set devicePath $path |
| } |
| |
| # Set the "close" parameter. |
| # It must be a non-negative integral number of seconds. |
| proc set-close {close} { |
| global closeTimeout |
| checkInteger "close timeout" $close 0 |
| set closeTimeout $close |
| } |
| |
| # Set the "volume" parameter. |
| # It must be an integer from 0 through 9. |
| proc set-volume {volume} { |
| global speechVolume |
| checkInteger "speech volume" $volume 0 9 |
| set speechVolume $volume |
| } |
| |
| # Set the "rate" parameter. |
| # It must be an integer from 0 through 17. |
| proc set-rate {rate} { |
| global speechRate |
| set rates "0123456789ABCDEFGH" |
| checkInteger "speech rate" $rate 0 [expr {[string length $rates] - 1}] |
| set speechRate [string index $rates $rate] |
| } |
| |
| # Set the "pitch" parameter. |
| # It must be an integer from 0 through 9. |
| proc set-pitch {pitch} { |
| global voicePitch |
| checkInteger "voice pitch" $pitch 0 9 |
| set voicePitch $pitch |
| } |
| |
| # Set the "voice" parameter. |
| # It must be an integer from 0 through 9. |
| proc set-voice {voice} { |
| global voiceType |
| checkInteger "voice type" $voice 0 9 |
| set voiceType $voice |
| } |
| |
| # Set the "pause" parameter. |
| # It must be an integer from 0 through 9. |
| proc set-pause {pause} { |
| global spacePause |
| checkInteger "space pause" $pause 0 9 |
| set spacePause $pause |
| } |
| |
| # Set the "intonation" parameter. |
| # It must be an integer from 0 through 4. |
| proc set-intonation {intonation} { |
| global sentenceIntonation |
| checkInteger "sentence intonation" $intonation 0 4 |
| set sentenceIntonation [expr {($intonation + 1) % 5}] |
| } |
| |
| # Set the "punctuation" parameter. |
| # It must be one of none, common, all. |
| proc set-punctuation {punctuation} { |
| global punctuationMode |
| set punctuationMode [checkKeyword "punctuation mode" $punctuation {none common all}] |
| } |
| |
| # Set the "hyphen" parameter. |
| # It must be one of no, dash, minus. |
| proc set-hyphen {hyphen} { |
| global hyphenMode |
| set hyphenMode [checkKeyword "hyphen mode" $hyphen {no dash minus}] |
| } |
| |
| # Set a parameter. |
| # For the list of settable parameters, see the set-... handlers. |
| # They may be specified within the system configuration file, |
| # within the user configuration file, or as command line options. |
| proc setParameter {name value} { |
| if {[set count [llength [set handlers [info procs "set-[string tolower $name]*"]]]] == 0} { |
| syntaxError "invalid parameter name: $name" |
| } elseif {$count > 1} { |
| syntaxError "ambiguous parameter name: $name" |
| } elseif {[string length $value] == 0} { |
| syntaxError "missing parameter value: $name" |
| } else { |
| eval [lindex $handlers 0] [list $value] |
| } |
| } |
| |
| # Process the configuration file. |
| # If it does not exist, then silently continue. |
| # If it does exist but cannot be opened, then display a warning. |
| proc setParameters {path} { |
| if {[catch [list open $path {RDONLY}] response] != 0} { |
| global errorCode |
| set type [lindex $errorCode 0] |
| set code [lindex $errorCode 1] |
| set warn 1 |
| if {[string compare $type POSIX] == 0} { |
| if {[lsearch -exact {ENOENT} $code] >= 0} { |
| set warn 0 |
| } |
| } |
| if {$warn} { |
| programMessage $response |
| } |
| } else { |
| set file $response |
| while {[gets $file line] >= 0} { |
| if {[set length [string length [set line [string trim $line]]]] == 0} { |
| continue |
| } |
| if {[string compare [string index $line 0] #] == 0} { |
| continue |
| } |
| regexp {^([^ ]+)(.*)$} $line x name value |
| setParameter $name [string trim $value] |
| } |
| close $file; unset file |
| } |
| } |
| |
| # Process the command line options, and remove them from the argument list. |
| proc processOptions {} { |
| global argv |
| while {[llength $argv] > 0} { |
| set name [lindex $argv 0] |
| if {[string length $name] == 0} { |
| break |
| } |
| if {[string compare [set character [string index $name 0]] -] != 0} { |
| break |
| } |
| set argv [lrange $argv 1 end] |
| if {[string length [set name [string trimleft $name $character]]] == 0} { |
| break |
| } |
| if {[llength $argv] == 0} { |
| set value "" |
| } else { |
| set value [lindex $argv 0] |
| set argv [lrange $argv 1 end] |
| } |
| setParameter $name $value |
| } |
| } |
| |
| proc prepareParameters {} { |
| global devicePath closeTimeout |
| checkDevice "synthesizer device" $devicePath |
| set closeTimeout [expr {$closeTimeout * 1000}]; # After wants milliseconds. |
| } |
| |
| # Assign defaults to the parameters. |
| setParameter device "/dev/ttyS0" |
| setParameter close 5 |
| |
| # Process the system configuration file. |
| setParameters "/usr/local/etc/[programName].conf" |
| |
| # Process the user configuration file. |
| set variable env(HOME) |
| if {[info exists $variable]} { |
| if {[string length [set directory [set $variable]]] > 0} { |
| setParameters [file join $directory ".[programName]rc"] |
| } |
| } |
| |
| # If no arguments have been supplied, then be a brltty GenericSay helper command. |
| if {[llength $argv] == 0} { |
| prepareParameters |
| set inputStream stdin |
| fconfigure $inputStream -blocking 0 |
| fileevent $inputStream readable { |
| if {[eof $inputStream]} { |
| if {[info exists deviceStream]} { |
| flushSynthesizer |
| } |
| set returnCode 0 |
| } else { |
| sayString [read $inputStream] |
| } |
| } |
| vwait returnCode |
| exit $returnCode |
| } |
| |
| # Process the command line options. |
| processOptions |
| prepareParameters |
| |
| # Speak the positional arguments as a sequence of words. |
| sayString [join $argv " "] |
| exit 0 |