From 19fd7fb899656b594b763b2edb0250ca786b5271 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 9 Aug 2025 06:42:11 -0400 Subject: [PATCH] Set-voice updated to new format. Also, search for and set voices using wine registry instead of old sed method. --- speech/set-voice.sh | 559 ++++++++++++++++++++++++++------------------ 1 file changed, 338 insertions(+), 221 deletions(-) diff --git a/speech/set-voice.sh b/speech/set-voice.sh index 595694e..b07cda2 100755 --- a/speech/set-voice.sh +++ b/speech/set-voice.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# Set Voice -# Set the default wine voice based on installed options. +# Set Speech +# Configure SAPI voices and speech rate for wine32 bottle -# -# âe contents of this file are subject to the Common Public Attribution -# License Version 1.0 (the âcenseâ you may not use this file except in +# License header +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0 (the "License"); you may not use this file except in # compliance with the License. You may obtain a copy of the License at # https://opensource.org/licenses/CPAL-1.0. The License is based on the Mozilla Public License Version # 1.1 but Sections 14 and 15 have been added to cover use of software over a @@ -12,7 +12,7 @@ # Developer. In addition, Exhibit A has been modified to be consistent with # Exhibit B. # -# Software distributed under the License is distributed on an â ISâasis, +# Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. @@ -40,8 +40,6 @@ # Works which are defined in the CPAL as a work which combines Covered Code # or portions thereof with code not governed by the terms of the CPAL. -#--code-- - # Detect dialog interface type BEFORE potentially setting DISPLAY if [[ -z "$DISPLAY" ]]; then dialogType="dialog" @@ -52,215 +50,296 @@ fi # Source dialog interface wrapper source "${0%/*}/../.includes/dialog-interface.sh" +# Settings to improve accessibility of dialog +export DIALOGOPTS='--insecure --no-lines --visit-items' +# Turn off debug messages +export WINEDEBUG="-all" +# Set DISPLAY if needed +if [[ -z "$DISPLAY" ]]; then + export DISPLAY=:0 +fi + +# Set wine prefix to wine32 - this is the only supported bottle for SAPI +export WINEPREFIX="$HOME/.local/wine32" +export bottle="$HOME/.local/wine32" + help() { echo "${0##*/}" echo "Released under the terms of the Common Public Attribution License Version 1.0" echo -e "This is a Stormux project: https://stormux.org\n" echo -e "Usage:\n" - echo "With no arguments, open the game launcher." - for i in "${!command[@]}" ; do - echo "-${i/:/ }: ${command[${i}]}" - done | sort + echo "With no arguments, open the voice configuration menu." + echo "-h: Show this help screen." + echo "-r : Set voice rate (0-10, where 10 is fastest)." + echo "-v : Set voice by name." exit 0 } - -# Set a variable to make mac compatibility easier... -sed="sed" -grep="grep" -if [[ "$(uname)" == "Darwin" ]]; then - sed="gsed" - grep="ggrep" -fi -export grep -export sed -# Settings to improve accessibility of dialog. -export DIALOGOPTS='--insecure --no-lines --visit-items' -# Turn off debug messages -export WINEDEBUG="-all" -# Set DISPLAY, as needed -if [ -z "$DISPLAY" ] ; then -export DISPLAY=:0 -fi -# handle arguments -declare -A command=( - [b:]="the wine bottle to use." - [h]="This help screen." - [r:]="Specify voice rate, default is 7, options are 0-9 or A for fastest." - [v:]="Voice name, the voice to use, same as in the menu." -) - - msgbox() { -# Returns: None -# Shows the provided message on the screen with an ok button. -agm_msgbox "Set Voice" "Set Voice" "$*" + agm_msgbox "Set Speech" "Set Speech" "$*" } infobox() { - # Returns: None - # Shows the provided message on the screen with no buttons. - local timeout=3 - agm_infobox "Set Voice" "Set Voice" "$*" - read -n1 -t $timeout continue - # Clear any keypresses from the buffer - read -t 0.01 continue + local timeout=3 + agm_infobox "Set Speech" "Set Speech" "$*" + read -r -n1 -t $timeout + read -r -t 0.01 } yesno() { - # Returns: Yes or No - # Args: Question to user. - # Called in if $(yesno) == "Yes" - # Or variable=$(yesno) - agm_yesno "Set Voice" "Set Voice" "$*" - if [[ $? -eq 0 ]]; then + if agm_yesno "Set Speech" "Set Speech" "$*"; then echo "Yes" else echo "No" fi } -menulist() { - # Args: List of items for menu. - # returns: selected tag - declare -a menuList - for i in "${@}" ; do +voice_menu() { + declare -a menuList=() + for i in "${@}"; do menuList+=("$i" "$i") done - agm_menu "Set Voice" "Set Voice" "Please select one" "${menuList[@]}" - return $? + agm_menu "Set Speech" "Set Speech" "Please select a voice:" "${menuList[@]}" } -restore_voice() { - if [[ $doRestore -eq 0 ]]; then - ${wine}server -k # If we don't do this it's likely wine will overwrite our reverted change or even clobber the registry key entirely - $sed -i -E -e 's/"DefaultTokenId"="HKEY_LOCAL_MACHINE\\\\(SOFTWARE|Software)\\\\(Wow6432Node\\\\|)Microsoft\\\\Speech\\\\Voices\\\\Token(Enum|)s\\\\[^"]+"/"DefaultTokenId"="'"${oldVoice//\\/\\\\}"'"/g' "${WINEPREFIX}/user.reg" +rate_menu() { + declare -a rateList=() + for i in {0..10}; do + local desc="Rate $i" + if [[ $i -eq 0 ]]; then + desc="$desc (Slowest)" + elif [[ $i -eq 5 ]]; then + desc="$desc (Default)" + elif [[ $i -eq 10 ]]; then + desc="$desc (Fastest)" + fi + rateList+=("$i" "$desc") + done + agm_menu "Set Speech" "Set Speech" "Please select speech rate:" "${rateList[@]}" +} + +get_voices() { + # Get list of available voices using wine reg query + declare -a voices + declare -a voiceKeys + + # First, get Microsoft SAPI voices + local allOutput + allOutput=$(WINEPREFIX="$WINEPREFIX" "$wine" reg query "HKLM\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens" /s 2>/dev/null) + + # Parse output to find voice keys and names + local currentKey="" + local currentName="" + local inMainVoiceToken=false + + while IFS= read -r line; do + # Clean up the line first + line=$(echo "$line" | tr -d '\r\n' | sed 's/[[:space:]]*$//') + + # Check if this is a main voice token registry key line (exactly one level deep) + if [[ "$line" =~ ^HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\([^\\]+)$ ]]; then + # Save previous voice if we found a name and had collected both key and name + if [[ -n "$currentKey" && -n "$currentName" ]]; then + voices+=("$currentName") + voiceKeys+=("$currentKey") + fi + # Start new voice - clean up any carriage returns or whitespace + currentKey="${BASH_REMATCH[1]}" + currentName="" + inMainVoiceToken=true + # Check if this is a subkey (contains additional parts after token name) + elif [[ "$line" =~ ^HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\[^\\]+\\ ]]; then + # This is a subkey, not a main voice token + inMainVoiceToken=false + # Check if this is a default value line with voice name and we're in main token + elif [[ "$line" =~ ^[[:space:]]*\(Default\)[[:space:]]+REG_SZ[[:space:]]+(.+)$ ]] && [[ "$inMainVoiceToken" == true ]]; then + # Only capture voice name if we haven't already got one for this token + if [[ -z "$currentName" ]]; then + currentName="${BASH_REMATCH[1]}" + # Clean up the voice name as well + currentName=$(echo "$currentName" | tr -d '\r\n' | sed 's/[[:space:]]*$//') + fi + fi + done <<< "$allOutput" + + # Don't forget the last voice + if [[ -n "$currentKey" && -n "$currentName" ]]; then + voices+=("$currentName") + voiceKeys+=("$currentKey") + fi + + # Now search for L&H TTS voices + local lhOutput + lhOutput=$(WINEPREFIX="$WINEPREFIX" "$wine" reg query "HKLM\\SOFTWARE\\L&H\\TTS\\V6.0\\Voice" /s 2>/dev/null) + + if [[ -n "$lhOutput" ]]; then + while IFS= read -r line; do + # Clean up the line first + line=$(echo "$line" | tr -d '\r\n' | sed 's/[[:space:]]*$//') + + # Look for voice name entries like "Carol REG_SZ {227A0E40-A92A-11d1-B17B-0020AFED142E}" + if [[ "$line" =~ ^[[:space:]]*([A-Za-z]+)[[:space:]]+REG_SZ[[:space:]]+\{([^}]+)\}$ ]]; then + local lhVoiceName="${BASH_REMATCH[1]}" + local lhVoiceGuid="${BASH_REMATCH[2]}" + + # Add L&H prefix to distinguish from SAPI voices (escape ampersand for YAD) + voices+=("L&H $lhVoiceName") + voiceKeys+=("LH_$lhVoiceGuid") + fi + done <<< "$lhOutput" + fi + + if [[ ${#voices[@]} -eq 0 ]]; then + return 1 + fi + + # Export arrays for use in other functions + export voiceList=("${voices[@]}") + export voiceKeyList=("${voiceKeys[@]}") + return 0 +} + +get_current_voice() { + # Get current default voice + local currentVoice + currentVoice=$(WINEPREFIX="$WINEPREFIX" "$wine" reg query "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTokenId" 2>/dev/null | grep "REG_SZ" | sed 's/.*REG_SZ[[:space:]]*//') + + if [[ -n "$currentVoice" ]]; then + # Extract just the voice key from the full registry path + # The path format is: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens\MSMike + if [[ "$currentVoice" =~ Tokens\\([^\\]+)$ ]]; then + local voiceKey="${BASH_REMATCH[1]}" + # Clean up any whitespace or special characters + voiceKey=$(echo "$voiceKey" | tr -d '\r\n' | sed 's/[[:space:]]*$//') + echo "$voiceKey" + fi + fi +} + +get_current_rate() { + # Get current speech rate + local currentRate + currentRate=$(WINEPREFIX="$WINEPREFIX" "$wine" reg query "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTTSRate" 2>/dev/null | grep "REG_DWORD" | sed 's/.*REG_DWORD[[:space:]]*//' | sed 's/^0x//') + + if [[ -n "$currentRate" && "$currentRate" =~ ^[0-9a-fA-F]+$ ]]; then + # Convert hex to decimal + echo $((16#$currentRate)) + else + echo "5" # Default rate fi } set_voice() { - doRestore=1 - local tmp="$1" - local fullVoice - local counter=0 - for x in "${voiceList[@]}" ; do - [[ "$x" = "$tmp" ]] && break - counter=$(( $counter + 1 )) - done - local RHVoiceName="" - local RHVoicePath="${WINEPREFIX}/drive_c/ProgramData/Olga Yakovleva/RHVoice/data/voices/" - if [[ -d "$RHVoicePath" ]]; then - RHVoiceName="$(find "$RHVoicePath" -maxdepth 1 -type d -not -path "$RHVoicePath" | head -1)" - if [[ -n "$RHVoiceName" ]]; then - RHVoiceName="${RHVoiceName##*/}" + local voiceName="$1" + local voiceKey="" + + # Find the voice key for the given voice name + local i + for i in "${!voiceList[@]}"; do + if [[ "${voiceList[i]}" == "$voiceName" ]]; then + voiceKey="${voiceKeyList[i]}" + break fi + done + + if [[ -z "$voiceKey" ]]; then + msgbox "Error: Voice '$voiceName' not found." + return 1 fi - fullVoice="${voiceListFullName[$counter]}" - if [[ -n "$RHVoiceName" ]]; then - fullVoice="${fullVoice/RHVoice/RHVoice\\\\${RHVoiceName}}" + + # Check if this is an L&H voice (cannot be set as system default) + if [[ "$voiceKey" =~ ^LH_ ]]; then + msgbox "L&H voices cannot be set as the system default SAPI voice. They use a proprietary API and must be selected by individual applications that support L&H TTS." + return 1 + fi + + # Kill wine server to ensure registry changes take effect + WINEPREFIX="$WINEPREFIX" "$wineserver" -k 2>/dev/null + + # Set the default voice using wine reg + local fullVoicePath="HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\$voiceKey" + if WINEPREFIX="$WINEPREFIX" "$wine" reg add "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTokenId" /t REG_SZ /d "$fullVoicePath" /f 2>/dev/null; then + infobox "Voice set to: $voiceName" + return 0 + else + msgbox "Error: Failed to set voice." + return 1 + fi +} + +set_rate() { + local rate="$1" + + # Validate rate (0-10) + if ! [[ "$rate" =~ ^[0-9]$|^10$ ]]; then + msgbox "Error: Rate must be between 0 and 10." + return 1 + fi + + # Kill wine server to ensure registry changes take effect + WINEPREFIX="$WINEPREFIX" "$wineserver" -k 2>/dev/null + + # Set the speech rate using wine reg (convert to hex) + local hexRate + hexRate=$(printf "%x" "$rate") + if WINEPREFIX="$WINEPREFIX" "$wine" reg add "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTTSRate" /t REG_DWORD /d "0x$hexRate" /f 2>/dev/null; then + infobox "Speech rate set to: $rate" + return 0 + else + msgbox "Error: Failed to set speech rate." + return 1 fi - ${wine}server -k # If we don't do this it's likely wine will overwrite our reverted change or even clobber the registry key entirely - # Remove any existing rate change for voices - $sed -i '/"DefaultTTSRate"=dword:/d' "${WINEPREFIX}/user.reg" - $sed -i -E -e 's/"DefaultTokenId"="HKEY_LOCAL_MACHINE\\\\(SOFTWARE|Software)\\\\(Wow6432Node\\\\|)Microsoft\\\\Speech\\\\Voices\\\\Token(Enum|)s\\\\[^"]+"/"DefaultTokenId"="HKEY_LOCAL_MACHINE\\\\'"${fullVoice//\\/\\\\}"'"\n"DefaultTTSRate"=dword:0000000'${rate:-7}'/g' "${WINEPREFIX}/user.reg" } test_voice() { - doRestore=0 - local tmp="$1" - local fullVoice - local counter=0 - for x in "${voiceList[@]}" ; do - [ "$x" = "$tmp" ] && break - counter=$(( $counter + 1 )) - done - local RHVoiceName="" - local RHVoicePath="${WINEPREFIX}/drive_c/ProgramData/Olga Yakovleva/RHVoice/data/voices/" - if [[ -d "$RHVoicePath" ]]; then - RHVoiceName="$(find "$RHVoicePath" -maxdepth 1 -type d -not -path "$RHVoicePath" | head -1)" - if [[ -n "$RHVoiceName" ]]; then - RHVoiceName="${RHVoiceName##*/}" - fi - fi - fullVoice="${voiceListFullName[$counter]}" - if [[ -n "$RHVoiceName" ]]; then - fullVoice="${fullVoice/RHVoice/RHVoice\\\\${RHVoiceName}}" - fi - ${wine}server -k # If we don't do this it's likely wine will overwrite our reverted change or even clobber the registry key entirely - $sed -i -E -e 's/"DefaultTokenId"="HKEY_LOCAL_MACHINE\\\\(SOFTWARE|Software)\\\\(Wow6432Node\\\\|)Microsoft\\\\Speech\\\\Voices\\\\Token(Enum|)s\\\\[^"]+"/"DefaultTokenId"="HKEY_LOCAL_MACHINE\\\\'"${fullVoice//\\/\\\\}"'"/g' "${WINEPREFIX}/user.reg" + local voiceName="$1" + + # Set the voice temporarily for testing + set_voice "$voiceName" || return 1 + + # Create test script + mkdir -p "${WINEPREFIX}/drive_c/windows/temp" cat << "EOF" > "${WINEPREFIX}/drive_c/windows/temp/speak.vbs" - dim speechobject - set speechobject=createobject("sapi.spvoice") - speechobject.speak "This is a test of your chosen voice. It contains multiple sentences and punctuation, and is designed to give a full representation of this voices qualities." +dim speechObject +set speechObject = createObject("sapi.spvoice") +speechObject.speak "This is a test of your chosen voice. It contains multiple sentences and punctuation, and is designed to give a full representation of this voice's qualities." EOF - ${wine} cscript "c:\windows\temp\speak.vbs" + + # Test the voice + WINEPREFIX="$WINEPREFIX" "$wine" cscript "c:\\windows\\temp\\speak.vbs" 2>/dev/null } -# Handle voice restore, but only if voice changed -doRestore=1 -trap restore_voice SIGINT +initialize_sapi() { + # Initialize SAPI if not already done + mkdir -p "${WINEPREFIX}/drive_c/windows/temp" + cat << "EOF" > "${WINEPREFIX}/drive_c/windows/temp/init.vbs" +dim speechObject +set speechObject = createObject("sapi.spvoice") +speechObject.speak "" +EOF + WINEPREFIX="$WINEPREFIX" "$wine" cscript "c:\\windows\\temp\\init.vbs" 2>/dev/null +} -# Convert the keys of the command associative array to a format usable by getopts -args="${!command[*]}" -args="${args//[[:space:]]/}" -while getopts "${args}" i ; do - case "$i" in - b) - # Check for valid bottles in all possible locations - if [[ -d ~/.local/wine/${OPTARG} ]]; then - export bottle=~/.local/wine/${OPTARG} - export WINEPREFIX=~/.local/wine/${OPTARG} - elif [[ -d ~/.local/wine32 && ${OPTARG} == "wine32" ]]; then - export bottle=~/.local/wine32 - export WINEPREFIX=~/.local/wine32 - elif [[ -d ~/.local/wine64 && ${OPTARG} == "wine64" ]]; then - export bottle=~/.local/wine64 - export WINEPREFIX=~/.local/wine64 - else - echo "Invalid wine bottle specified." +# Handle command line arguments +while getopts "hr:v:" option; do + case "$option" in + h) help ;; + r) + if ! [[ "$OPTARG" =~ ^[0-9]$|^10$ ]]; then + echo "Error: Rate must be between 0 and 10." exit 1 fi - ;; - h) help;; - r) - if ! [[ "${OPTARG}" =~ ^[0-9A]$ ]]; then - echo "Invalid rate specified. Arguments must be 0-9 or A." - exit 1 - fi - rate="${OPTARG}" - ;; - v) voice="${OPTARG}";; + requestedRate="$OPTARG" + ;; + v) requestedVoice="$OPTARG" ;; + *) help ;; esac done - -# Get the desired wine bottle - -# Offer a list of wine bottles if one isn't specified on the command line. -if [[ -z "${bottle}" ]]; then - declare -a bottles=() - - # Check for wine32 and wine64 bottles - [[ -d ~/.local/wine32 ]] && bottles+=("$HOME/.local/wine32" "wine32") - [[ -d ~/.local/wine64 ]] && bottles+=("$HOME/.local/wine64" "wine64") - - # Add bottles from ~/.local/wine directory - if [[ -d ~/.local/wine ]]; then - for i in $(find ~/.local/wine/ -maxdepth 1 -type d -not -name 'wine' | sort); do - bottles+=("$i" "${i##*/}") - done - fi - - if [[ ${#bottles[@]} -eq 0 ]]; then - echo "No wine bottles found in ~/.local/wine32, ~/.local/wine64, or ~/.local/wine/" - exit 1 - fi - - export WINEPREFIX="$(agm_menu "Set Voice" "Set Voice" "Select A Wine Bottle" "${bottles[@]}")" -fi - -if [[ -z "${WINEPREFIX}" ]]; then - exit 0 +# Check if wine32 bottle exists +if [[ ! -d "$WINEPREFIX" ]]; then + msgbox "Error: Wine32 bottle not found at $WINEPREFIX. Please create it first." + exit 1 fi # Get wine version if available @@ -269,58 +348,96 @@ if [[ -r "${WINEPREFIX}/agm.conf" ]]; then export WINE export WINESERVER fi -wine="${WINE:-$(command -v wine)}" -wineserver="${WINESERVER:-$(command -v wineserver)}" +wine="${WINE:-$HOME/.local/share/audiogame-manager/wine32/bin/wine}" +wineserver="${WINESERVER:-$HOME/.local/share/audiogame-manager/wine32/bin/wineserver}" -# Debug information - comment out or delete when not needed -# echo "Using wine bottle: ${WINEPREFIX}" -# echo "Wine executable: ${wine}" -# echo "Wineserver executable: ${wineserver}" - -# In case the user hasn't run a game using sapi in this prefix yet, let's try to initialize all the registry keys properly. -mkdir -p "${WINEPREFIX}/drive_c/windows/temp" -cat << "EOF" > "${WINEPREFIX}/drive_c/windows/temp/speak.vbs" -dim speechobject -set speechobject=createobject("sapi.spvoice") -speechobject.speak "" -EOF -${wine} cscript "c:\windows\temp\speak.vbs" - -# Create an array of available voices. -ifs="$IFS" -IFS=$'\n' -voiceListFullName=($($grep -P '\[Software\\\\(Wow6432Node\\\\|)Microsoft\\\\Speech\\\\Voices\\\\Token(Enum|)s\\\\[^\\]+\].*' "${WINEPREFIX}/system.reg" | $sed -E -e 's/\[([^]]+)\].*/\1/g')) -IFS="$ifs" - -# Debug the voice list - comment out or delete when not needed -# echo "Found ${#voiceListFullName[@]} voices in registry" -# for voice in "${voiceListFullName[@]}"; do -# echo "Voice: $voice" -# done - -voiceList=() -for x in "${voiceListFullName[@]}" ; do - voiceList+=("$(echo "$x" | $sed -E -e 's/Software\\\\(Wow6432Node\\\\|)Microsoft\\\\Speech\\\\Voices\\\\Token(Enum|)s\\\\(.+)/\3/g')") -done -oldVoice="$($grep -P '"DefaultTokenId"="HKEY_LOCAL_MACHINE\\\\(SOFTWARE|Software)\\\\(Wow6432Node\\\\|)Microsoft\\\\Speech\\\\Voices\\\\Token(Enum|)s\\\\[^"]+"' "${WINEPREFIX}/user.reg" | $sed -E -e 's/"DefaultTokenId"="([^"]+)"/\1/g')" -exit=1 -if [[ "${#voiceList[@]}" -eq 0 ]]; then - agm_msgbox "Set Voice" "Set Voice" "No voices found in ${WINEPREFIX}. Make sure SAPI voices are installed in this Wine prefix." +# Check if wine executable exists +if [[ ! -x "$wine" ]]; then + msgbox "Error: Wine executable not found at $wine" exit 1 fi -while [[ $exit -ne 0 ]] ; do - if [[ -z "${voice}" ]]; then - voice="$(menulist "${voiceList[@]}")" + +# Initialize SAPI +initialize_sapi + +# Get available voices +if ! get_voices; then + msgbox "No SAPI voices found in wine32 bottle. Please install SAPI voices first." + exit 1 +fi + +# Handle command line options +if [[ -n "$requestedRate" ]]; then + set_rate "$requestedRate" +fi + +if [[ -n "$requestedVoice" ]]; then + set_voice "$requestedVoice" + exit 0 +fi + +# Show interactive menu if no voice specified +currentVoice=$(get_current_voice) +currentRate=$(get_current_rate) + +# Verify arrays are properly set +if [[ ${#voiceList[@]} -eq 0 ]]; then + msgbox "Error: No voices loaded." + exit 1 +fi + + +while true; do + # Build menu with current settings + declare -a menuOptions=() + + # Find current voice name for display + currentVoiceName="None" + if [[ -n "$currentVoice" ]]; then + for i in "${!voiceKeyList[@]}"; do + # Clean up the voice key for comparison + cleanKey=$(echo "${voiceKeyList[i]}" | tr -d '\r\n' | sed 's/[[:space:]]*$//') + if [[ "$cleanKey" == "$currentVoice" ]]; then + currentVoiceName="${voiceList[i]}" + break + fi + done fi - - case $? in - 0) - set_voice "$voice" "${rate:-7}" ; exit=0 ;; - 3) - test_voice "$voice";; - *) - restore_voice ; exit=0 ;; + + menuOptions+=("voice" "Select Voice (Current: $currentVoiceName)") + menuOptions+=("rate" "Set Rate (Current: $currentRate)") + menuOptions+=("test" "Test Current Voice") + menuOptions+=("quit" "Exit") + + choice=$(agm_menu "Set Speech" "SAPI Voice Configuration" "Choose an option:" "${menuOptions[@]}") + + case "$choice" in + "voice") + if selectedVoice=$(voice_menu "${voiceList[@]}"); then + if [[ -n "$selectedVoice" ]] && set_voice "$selectedVoice"; then + currentVoice=$(get_current_voice) + fi + fi + ;; + "rate") + if newRate=$(rate_menu); then + if [[ -n "$newRate" ]] && set_rate "$newRate"; then + currentRate=$(get_current_rate) + fi + fi + ;; + "test") + if [[ -n "$currentVoice" && "$currentVoiceName" != "None" ]]; then + infobox "Testing voice: $currentVoiceName" + test_voice "$currentVoiceName" + else + msgbox "No voice selected. Please select a voice first." + fi + ;; + "quit"|"") + break + ;; esac - done +done -exit 0 +exit 0 \ No newline at end of file