// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package preferences /* #include #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 }