402 lines
13 KiB
Go
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
|
|
}
|