From 86b306897ae6108a63c62ad5ad2fd224cc2f7eb7 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 14 Aug 2025 03:39:36 -0400 Subject: [PATCH] Updated set-voice.sh from the version in master. Change it to only search ~/.local/wine32 since that's where all the 32 bit games go. --- speech/set-voice.sh | 544 +++++++++++++------------------------------- 1 file changed, 156 insertions(+), 388 deletions(-) mode change 100755 => 100644 speech/set-voice.sh diff --git a/speech/set-voice.sh b/speech/set-voice.sh old mode 100755 new mode 100644 index b07cda2..c09ffea --- a/speech/set-voice.sh +++ b/speech/set-voice.sh @@ -1,443 +1,211 @@ #!/usr/bin/env bash -# Set Speech -# Configure SAPI voices and speech rate for wine32 bottle +# Set Voice - Fixed version for audiogame-manager -# 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 -# computer network and provide for limited attribution for the Original -# Developer. In addition, Exhibit A has been modified to be consistent with -# Exhibit B. -# -# 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. -# -# The Original Code is audiogame manager. -# -# The Original Developer is not the Initial Developer and is . If -# left blank, the Original Developer is the Initial Developer. -# -# The Initial Developer of the Original Code is Billy "Storm Dragon" Wolfe. All portions of -# the code written by Billy Wolfe are Copyright (c) 2020. All Rights -# Reserved. -# -# Contributor Michael Taboada. -# -# Attribution Copyright Notice: Audiogame manager copyright 2020 Storm Dragon. All rights reserved. -# -# Attribution Phrase (not exceeding 10 words): A Stormux project -# -# Attribution URL: https://stormgames.wolfe.casa -# -# Graphic Image as provided in the Covered Code, if any. -# -# Display of Attribution Information is required in Larger -# 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. - -# Detect dialog interface type BEFORE potentially setting DISPLAY -if [[ -z "$DISPLAY" ]]; then - dialogType="dialog" -else - dialogType="yad" +# 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 -# Source dialog interface wrapper -source "${0%/*}/../.includes/dialog-interface.sh" - -# Settings to improve accessibility of dialog +# 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 +# Set DISPLAY, as 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" +# 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." +) 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 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." + echo "With no arguments, open the game launcher." + for i in "${!command[@]}" ; do + echo "-${i/:/ }: ${command[${i}]}" + done | sort exit 0 } msgbox() { - agm_msgbox "Set Speech" "Set Speech" "$*" +dialog --clear --msgbox "$*" 0 0 } -infobox() { - local timeout=3 - agm_infobox "Set Speech" "Set Speech" "$*" - read -r -n1 -t $timeout - read -r -t 0.01 -} - -yesno() { - if agm_yesno "Set Speech" "Set Speech" "$*"; then - echo "Yes" - else - echo "No" - fi -} - -voice_menu() { - declare -a menuList=() - for i in "${@}"; do +menulist() { + declare -a menuList + for i in "${@}" ; do menuList+=("$i" "$i") done - agm_menu "Set Speech" "Set Speech" "Please select a voice:" "${menuList[@]}" + dialog --backtitle "Use the up and down arrow keys to find the option you want, then press enter to select it." \ + --clear \ + --extra-button \ + --extra-label "Test Voice" \ + --no-tags \ + --menu "Please select one" 0 0 0 "${menuList[@]}" --stdout + return $? } -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 +restore_voice() { + if [[ $doRestore -eq 0 ]]; then + ${wine}server -k + $sed -i -E -e 's/"DefaultTokenId"="HKEY_LOCAL_MACHINE\\\\(SOFTWARE|Software)\\\\(Wow6432Node\\\\|)Microsoft\\\\Speech\\\\Voices\\\\Token(Enum|)s\\\\[^"]+"/"DefaultTokenId"="'"${oldVoice//\\/\\\\}"'"/g' "${WINEPREFIX}/user.reg" fi } set_voice() { - 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 + doRestore=1 + local tmp="$1" + local fullVoice + local counter=0 + for x in "${voiceList[@]}" ; do + [[ "$x" = "$tmp" ]] && break + counter=$(( $counter + 1 )) done - - if [[ -z "$voiceKey" ]]; then - msgbox "Error: Voice '$voiceName' not found." - return 1 - fi - - # 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 + local RHVoiceName="$(find "${WINEPREFIX}/drive_c/ProgramData/Olga Yakovleva/RHVoice/data/voices/" -maxdepth 1 -type d 2>/dev/null | head -1)" + RHVoiceName="${RHVoiceName##*/}" + fullVoice="${voiceListFullName[$counter]}" + fullVoice="${fullVoice/RHVoice/RHVoice\\\\${RHVoiceName}}" + ${wine}server -k + # 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() { - 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 voice's qualities." + doRestore=0 + local tmp="$1" + local fullVoice + local counter=0 + for x in "${voiceList[@]}" ; do + [ "$x" = "$tmp" ] && break + counter=$(( $counter + 1 )) + done + local RHVoiceName="$(find "${WINEPREFIX}/drive_c/ProgramData/Olga Yakovleva/RHVoice/data/voices/" -maxdepth 1 -type d 2>/dev/null | head -1)" + RHVoiceName="${RHVoiceName##*/}" + fullVoice="${voiceListFullName[$counter]}" + fullVoice="${fullVoice/RHVoice/RHVoice\\\\${RHVoiceName}}" + ${wine}server -k +$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" + cat << "EOF" > "${bottle}/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." EOF - - # Test the voice - WINEPREFIX="$WINEPREFIX" "$wine" cscript "c:\\windows\\temp\\speak.vbs" 2>/dev/null + ${wine} cscript "c:\windows\temp\speak.vbs" } -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 -} +# Handle voice restore, but only if voice changed +doRestore=1 +trap restore_voice SIGINT -# 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." +# 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) + if ! [[ -d ~/".local/wine/${OPTARG}" ]]; then + echo "Invalid wine bottle specified." exit 1 fi - requestedRate="$OPTARG" - ;; - v) requestedVoice="$OPTARG" ;; - *) help ;; + export bottle=~/".local/wine/${OPTARG}" + export WINEPREFIX=~/".local/wine/${OPTARG}" + ;; + 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}";; esac done -# Check if wine32 bottle exists -if [[ ! -d "$WINEPREFIX" ]]; then - msgbox "Error: Wine32 bottle not found at $WINEPREFIX. Please create it first." - exit 1 +# 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 bottle + for i in $(find ~/.local/wine/ -maxdepth 1 -type d -not -name 'wine' | sort) ; do + bottle+=("$i" "${i##*/}") + done + export WINEPREFIX="$(dialog --backtitle "Use the up and down arrow keys to find the option you want, then press enter to select it." \ + --clear \ + --no-tags \ + --menu "Select A Wine Bottle" 0 0 0 "${bottle[@]}" --stdout)" +fi + +if [[ -z "${WINEPREFIX}" ]]; then + exit 0 fi -# Get wine version if available +export bottle="$WINEPREFIX" + +# Get wine version if available - Use wine32 for SAPI games if [[ -r "${WINEPREFIX}/agm.conf" ]]; then source "${WINEPREFIX}/agm.conf" export WINE export WINESERVER fi -wine="${WINE:-$HOME/.local/share/audiogame-manager/wine32/bin/wine}" -wineserver="${WINESERVER:-$HOME/.local/share/audiogame-manager/wine32/bin/wineserver}" -# Check if wine executable exists -if [[ ! -x "$wine" ]]; then - msgbox "Error: Wine executable not found at $wine" - exit 1 +# Use wine32 installation from audiogame-manager +wine32Dir="${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/wine32" +if [[ -f "$wine32Dir/bin/wine" ]]; then + wine="$wine32Dir/bin/wine" +else + wine="${WINE:-$(command -v wine)}" fi -# Initialize SAPI -initialize_sapi +# 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. +cat << "EOF" > "${bottle}/drive_c/windows/temp/speak.vbs" +dim speechobject +set speechobject=createobject("sapi.spvoice") +speechobject.speak "" +EOF +${wine} cscript "c:\windows\temp\speak.vbs" -# 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 - - 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 +# 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" +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 + dialog --msgbox "No voices found." -1 -1 + exit 1 +fi +while [[ $exit -ne 0 ]] ; do + if [[ -z "${voice}" ]]; then + voice="$(menulist "${voiceList[@]}")" + fi -exit 0 \ No newline at end of file + case $? in + 0) + set_voice "$voice" "${rate:-7}" ; exit=0 ;; + 3) + test_voice "$voice";; + *) + restore_voice ; exit=0 ;; + esac + done + +exit 0