// 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" "math" "path/filepath" "sort" "strings" "unsafe" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/speech" ) 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("interruptAllKeysRadio", values.SpeechInterruption.Mode != input.InterruptCommonKeys && values.SpeechInterruption.Mode != input.InterruptCustomKeys) setCheck("interruptCommonKeysRadio", values.SpeechInterruption.Mode == input.InterruptCommonKeys) setCheck("interruptCustomKeysRadio", values.SpeechInterruption.Mode == input.InterruptCustomKeys) C.prefs_interruption_clear() for _, name := range values.SpeechInterruption.Keys { key := C.CString(name) C.prefs_interruption_append(key) C.free(unsafe.Pointer(key)) } 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, defaults speech.Settings) { for _, item := range []struct { id string value *float64 inherited *float64 server *int scale float64 offset float64 fallback float64 }{ {"rate", values.Rate, inherited.Rate, defaults.Rate, 2, 98, 50}, {"pitch", values.Pitch, inherited.AveragePitch, defaults.Pitch, 20, 90, 5}, {"volume", values.Volume, inherited.Gain, defaults.Volume, 15, 35, 5}, } { id := C.CString(item.id) value := item.fallback if item.server != nil { value = math.Round((float64(*item.server)+item.offset)/item.scale*10) / 10 } if item.inherited != nil { value = *item.inherited } if item.value != nil { value = *item.value } C.prefs_number_set(id, C.double(value)) C.free(unsafe.Pointer(id)) } } func set_speech_presentation(values Values) { setChoice("punctuation", values.Punctuation) setCheck("raisePitchForCapitals", values.RaisePitchForCapitals) setCheck("beepForCapitals", values.BeepForCapitals) setCheck("sayCapForCapitals", values.SayCapForCapitals) setReading(values) } func get_speech_support(values *Values) { values.SpeechInterruption.Mode = input.InterruptAllKeys if getCheck("interruptCommonKeysRadio") { values.SpeechInterruption.Mode = input.InterruptCommonKeys } else if getCheck("interruptCustomKeysRadio") { values.SpeechInterruption.Mode = input.InterruptCustomKeys } for i := C.int(0); i < C.prefs_interruption_count(); i++ { values.SpeechInterruption.Keys = append(values.SpeechInterruption.Keys, C.GoString(C.prefs_interruption_key(i))) } 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() // The synthesizer dropdown now owns this choice directly. Retain the // existing settings field so older profiles keep their meaning on load. values.UseCustomEchoSpeechServer = true 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") values.RaisePitchForCapitals = getCheck("raisePitchForCapitals") values.BeepForCapitals = getCheck("beepForCapitals") values.SayCapForCapitals = getCheck("sayCapForCapitals") return nil } func (session *uiSession) load_voice_editor() { voice := session.voiceDrafts[session.activeVoiceType] settings := config.Settings{RaisePitchForCapitals: true, DefaultVoice: session.voiceDrafts[0], UppercaseVoice: session.voiceDrafts[1], HyperlinkVoice: session.voiceDrafts[2], SystemVoice: session.voiceDrafts[3]} kind := []string{"default", "uppercase", "hyperlink", "system"}[session.activeVoiceType] defaults := session.speechDefaults if session.activeSpeechSystem == 1 { defaults = speech.Settings{} } set_speech_numbers(Values{Rate: voice.Rate, Pitch: voice.AveragePitch, Volume: voice.Gain}, config.EffectiveVoice(settings, kind), defaults) if session.activeSpeechSystem != 1 { session.populateSpeechLanguages(session.selectedSpeechModule(), "", voice.Family) } } func (session *uiSession) read_voice_editor() (config.Voice, error) { voice := session.voiceDrafts[session.activeVoiceType] 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) edited := C.prefs_number_edited(id) != 0 value := math.Round(float64(C.prefs_number_value(id))*10) / 10 C.free(unsafe.Pointer(id)) if !edited { 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 }