Files

402 lines
13 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package preferences
/*
#include <stdlib.h>
#include "native.h"
*/
import "C"
import (
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"unsafe"
"git.stormux.org/storm/cthulhu/internal/config"
)
func (session *uiSession) setSpeechVoice(values Values) {
session.voiceDrafts = [4]config.Voice{
{Rate: values.Rate, AveragePitch: values.Pitch, Gain: values.Volume, Family: values.SpeechVoiceFamily},
values.UppercaseVoice, values.HyperlinkVoice, values.SystemVoice,
}
setCombo("voiceTypesCombo", session.activeVoiceType)
session.activeSpeechSystem = 0
if values.SpeechServerFactory == "hardwarefactory" {
session.activeSpeechSystem = 1
}
setCombo("speechSystems", session.activeSpeechSystem)
devices := []string{}
for _, pattern := range []string{"/dev/ttyS*", "/dev/ttyUSB*", "/dev/ttyACM*", "/dev/serial/by-id/*"} {
matches, _ := filepath.Glob(pattern)
devices = append(devices, matches...)
}
sort.Strings(devices)
C.prefs_hardware_devices_clear()
for _, device := range devices {
label := C.CString(device)
C.prefs_hardware_device_append(label)
C.free(unsafe.Pointer(label))
}
device := C.CString(values.HardwareSpeechDevice)
C.prefs_hardware_device_set(device)
C.free(unsafe.Pointer(device))
baudIndex := 3
for index, baud := range hardwareBaudRates {
if baud == values.HardwareSpeechBaudRate {
baudIndex = index
}
}
setCombo("hardwareBaudCombo", baudIndex)
C.prefs_hardware_sensitivity(C.int(session.activeSpeechSystem))
session.populateSpeechModules(values)
session.load_voice_editor()
}
var hardwareBaudRates = []int{1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200}
func (session *uiSession) populateSpeechModules(values Values) {
if session.activeSpeechSystem == 1 {
session.speechModules = []string{"litetalk", "doubletalk", "tripletalk", "dectalk"}
selected := 0
for index, module := range session.speechModules {
if module == values.SpeechOutputModule {
selected = index
}
}
comboItems("speechServers", []string{"LiteTalk", "DoubleTalk", "TripleTalk", "DECtalk"}, selected)
session.activeSpeechModule = session.speechModules[selected]
comboItems("speechLanguages", []string{"Default"}, 0)
comboItems("speechFamilies", []string{"Default"}, 0)
return
}
session.speechModules = []string{""}
for module := range session.catalog {
if module != "" {
session.speechModules = append(session.speechModules, module)
}
}
sort.Strings(session.speechModules[1:])
selected := 0
if values.SpeechOutputModule != "" {
selected = -1
for i, module := range session.speechModules {
if module == values.SpeechOutputModule {
selected = i
}
}
if selected < 0 {
selected = len(session.speechModules)
session.speechModules = append(session.speechModules, values.SpeechOutputModule)
}
}
labels := append([]string(nil), session.speechModules...)
labels[0] = "Default"
comboItems("speechServers", labels, selected)
session.populateSpeechLanguages(values.SpeechOutputModule, "", values.SpeechVoiceFamily)
}
func (session *uiSession) populateSpeechLanguages(module, language string, selected *config.VoiceFamily) {
session.activeSpeechModule = module
session.speechLanguages = []string{""}
seen := map[string]bool{"": true}
for _, voice := range session.catalog[module] {
if seen[voice.Language] {
continue
}
seen[voice.Language] = true
session.speechLanguages = append(session.speechLanguages, voice.Language)
}
index := 0
if selected != nil {
language = strings.TrimSuffix(selected.Lang+"-"+selected.Dialect, "-")
}
for i, candidate := range session.speechLanguages {
if strings.EqualFold(candidate, language) {
index = i
break
}
}
if selected != nil && index == 0 && language != "" {
session.speechLanguages = append(session.speechLanguages, language)
index = len(session.speechLanguages) - 1
}
labels := append([]string(nil), session.speechLanguages...)
labels[0] = "Default"
comboItems("speechLanguages", labels, index)
session.populateSpeechFamilies(module, session.speechLanguages[index], selected)
}
func (session *uiSession) populateSpeechFamilies(module, language string, selected *config.VoiceFamily) {
session.activeSpeechLanguage = language
session.speechFamilies = []*config.VoiceFamily{nil}
labels := []string{"Default"}
index := 0
voices := session.catalog[module]
if language == "" {
voices = session.default_voices(module)
}
for _, voice := range voices {
if language != "" && !strings.EqualFold(voice.Language, language) {
continue
}
family := &config.VoiceFamily{Name: voice.Name, Lang: voice.Language, Variant: voice.Variant}
session.speechFamilies = append(session.speechFamilies, family)
labels = append(labels, voice.Name+" ("+voice.Language+")")
if selected != nil && selected.Name == voice.Name &&
strings.EqualFold(strings.TrimSuffix(selected.Lang+"-"+selected.Dialect, "-"), voice.Language) {
index = len(labels) - 1
}
}
if selected != nil && selected.Name != "" && index == 0 {
index = len(labels)
session.speechFamilies = append(session.speechFamilies, selected)
labels = append(labels, selected.Name+" ("+selected.Lang+")")
}
comboItems("speechFamilies", labels, index)
}
func (session *uiSession) selectedSpeechModule() string {
i := getCombo("speechServers")
if i >= 0 && i < len(session.speechModules) {
return session.speechModules[i]
}
return ""
}
func (session *uiSession) selectedSpeechFamily() *config.VoiceFamily {
i := getCombo("speechFamilies")
if i >= 0 && i < len(session.speechFamilies) {
if family := session.speechFamilies[i]; family != nil {
return family
}
languageIndex := getCombo("speechLanguages")
if languageIndex > 0 && languageIndex < len(session.speechLanguages) {
return &config.VoiceFamily{Lang: session.speechLanguages[languageIndex]}
}
}
return nil
}
func (session *uiSession) refreshSpeechVoices() {
voiceType := getCombo("voiceTypesCombo")
if voiceType >= 0 && voiceType < len(session.voiceDrafts) && voiceType != session.activeVoiceType {
voice, err := session.read_voice_editor()
if err != nil {
status(err.Error(), true)
setCombo("voiceTypesCombo", session.activeVoiceType)
return
}
session.voiceDrafts[session.activeVoiceType] = voice
session.activeVoiceType = voiceType
session.load_voice_editor()
return
}
system := getCombo("speechSystems")
if system != session.activeSpeechSystem {
if voice, err := session.read_voice_editor(); err == nil {
session.voiceDrafts[session.activeVoiceType] = voice
}
if session.activeSpeechSystem == 0 {
session.softwareModule = session.selectedSpeechModule()
} else {
session.hardwareModule = session.selectedSpeechModule()
}
session.activeSpeechSystem = system
module := session.softwareModule
if system == 1 {
module = session.hardwareModule
}
session.populateSpeechModules(Values{SpeechOutputModule: module})
C.prefs_hardware_sensitivity(C.int(system))
if system == 0 {
session.reconcile_voice_families(session.selectedSpeechModule())
session.load_voice_editor()
}
}
if system == 1 {
return
}
module := session.selectedSpeechModule()
if module != session.activeSpeechModule {
if voice, err := session.read_voice_editor(); err == nil {
session.voiceDrafts[session.activeVoiceType] = voice
}
session.reconcile_voice_families(module)
session.populateSpeechLanguages(module, session.activeSpeechLanguage, session.voiceDrafts[session.activeVoiceType].Family)
return
}
languageIndex := getCombo("speechLanguages")
if languageIndex < 0 || languageIndex >= len(session.speechLanguages) {
return
}
language := session.speechLanguages[languageIndex]
if language != session.activeSpeechLanguage {
session.populateSpeechFamilies(module, language, nil)
}
}
func (session *uiSession) reconcile_voice_families(module string) {
for index, voice := range session.voiceDrafts {
family := voice.Family
if family == nil {
continue
}
language := strings.TrimSuffix(family.Lang+"-"+family.Dialect, "-")
languageFound, exact := false, false
for _, candidate := range session.catalog[module] {
if strings.EqualFold(candidate.Language, language) {
languageFound = true
if candidate.Name == family.Name {
exact = true
break
}
}
}
if exact {
continue
}
if languageFound {
session.voiceDrafts[index].Family = &config.VoiceFamily{Lang: language}
} else {
session.voiceDrafts[index].Family = nil
}
}
}
func set_speech_support(values Values) {
setCheck("speechSupportCheckButton", values.EnableSpeech)
setSpin("speechHistoryLimitSpinButton", float64(values.SpeechHistoryLimit))
setCheck("speechBriefButton", values.SpeechVerbosityLevel == 0)
setCheck("speechVerboseButton", values.SpeechVerbosityLevel != 0)
setCombo("sayAllStyle", map[string]int{"sentence": 0, "line": 1}[values.SayAllStyle])
mode := values.Reading.IndentationPresentationMode
setCheck("indentationSpeechButton", mode == "" || mode == "speech")
setCheck("indentationBeepsButton", mode == "beeps")
setCheck("indentationSpeechAndBeepsButton", mode == "both")
}
func set_speech_numbers(values Values, inherited config.Voice) {
for _, item := range []struct {
id string
value *float64
inherited *float64
fallback float64
}{
{"rate", values.Rate, inherited.Rate, 50}, {"pitch", values.Pitch, inherited.AveragePitch, 5}, {"volume", values.Volume, inherited.Gain, 5},
} {
id := C.CString(item.id)
value := item.fallback
if item.inherited != nil {
value = *item.inherited
}
if item.value != nil {
value = *item.value
}
C.prefs_number_set(id, C.double(value), boolean(item.value == nil))
C.free(unsafe.Pointer(id))
}
}
func set_speech_presentation(values Values) {
setChoice("punctuation", values.Punctuation)
setCombo("capitalization", map[string]int{"none": 0, "spell": 1, "icon": 2}[values.Capitalization])
setReading(values)
}
func get_speech_support(values *Values) {
values.EnableSpeech = getCheck("speechSupportCheckButton")
values.SpeechHistoryLimit = int(getSpin("speechHistoryLimitSpinButton"))
values.Reading.IndentationPresentationMode = "speech"
if getCheck("indentationBeepsButton") {
values.Reading.IndentationPresentationMode = "beeps"
} else if getCheck("indentationSpeechAndBeepsButton") {
values.Reading.IndentationPresentationMode = "both"
}
values.SpeechVerbosityLevel = 1
if getCheck("speechBriefButton") {
values.SpeechVerbosityLevel = 0
}
if getCombo("sayAllStyle") == 0 {
values.SayAllStyle = "sentence"
} else {
values.SayAllStyle = "line"
}
}
func (session *uiSession) get_speech_values(values *Values) error {
voice, err := session.read_voice_editor()
if err != nil {
return err
}
session.voiceDrafts[session.activeVoiceType] = voice
defaultVoice := session.voiceDrafts[0]
values.Rate, values.Pitch, values.Volume = defaultVoice.Rate, defaultVoice.AveragePitch, defaultVoice.Gain
values.SpeechVoiceFamily = defaultVoice.Family
values.UppercaseVoice, values.HyperlinkVoice, values.SystemVoice = session.voiceDrafts[1], session.voiceDrafts[2], session.voiceDrafts[3]
values.VoiceType = []string{"default", "uppercase", "hyperlink", "system"}[session.activeVoiceType]
values.EchoVoice, values.EchoSpeechServer = session.getEchoVoice()
values.SpeechServerFactory = "speechdispatcherfactory"
if session.activeSpeechSystem == 1 {
values.SpeechServerFactory = "hardwarefactory"
}
values.HardwareSpeechDevice = C.GoString(C.prefs_hardware_device_get())
indexBaud := getCombo("hardwareBaudCombo")
if indexBaud < 0 || indexBaud >= len(hardwareBaudRates) {
return errors.New("Choose a serial baud rate.")
}
values.HardwareSpeechBaudRate = hardwareBaudRates[indexBaud]
values.SpeechOutputModule = session.selectedSpeechModule()
values.Punctuation = getChoice("punctuation")
index := getCombo("capitalization")
if index < 0 || index > 2 {
return errors.New("Choose a capitalization style.")
}
values.Capitalization = []string{"none", "spell", "icon"}[index]
return nil
}
func (session *uiSession) load_voice_editor() {
voice := session.voiceDrafts[session.activeVoiceType]
settings := config.Settings{DefaultVoice: session.voiceDrafts[0], UppercaseVoice: session.voiceDrafts[1], HyperlinkVoice: session.voiceDrafts[2], SystemVoice: session.voiceDrafts[3]}
kind := []string{"default", "uppercase", "hyperlink", "system"}[session.activeVoiceType]
set_speech_numbers(Values{Rate: voice.Rate, Pitch: voice.AveragePitch, Volume: voice.Gain}, config.EffectiveVoice(settings, kind))
C.prefs_voice_defaults(C.int(session.activeVoiceType))
if session.activeSpeechSystem != 1 {
session.populateSpeechLanguages(session.selectedSpeechModule(), "", voice.Family)
}
}
func (session *uiSession) read_voice_editor() (config.Voice, error) {
var voice config.Voice
for _, item := range []struct {
id string
target **float64
max float64
}{
{"rate", &voice.Rate, 99}, {"pitch", &voice.AveragePitch, 9}, {"volume", &voice.Gain, 9},
} {
id := C.CString(item.id)
useDefault := C.prefs_default(id) != 0
value := float64(C.prefs_number_value(id))
C.free(unsafe.Pointer(id))
if useDefault {
continue
}
if !(value >= 0 && value <= item.max) {
return voice, fmt.Errorf("Enter a %s between 0 and %.0f.", item.id, item.max)
}
*item.target = &value
}
if session.activeSpeechSystem != 1 {
voice.Family = session.selectedSpeechFamily()
}
return voice, nil
}