Simplified speech settings.
This commit is contained in:
1 parent
03e2b31e09
commit
54722e985e
20 files changed
+403
-92
No files matched your search
@@ -78,11 +78,12 @@ type preferencesWorker struct {
|
||||
announceEchoPreview func(string, string)
|
||||
catalog speech.Catalog
|
||||
discover func(context.Context, string) (speech.Catalog, error)
|
||||
readSpeechDefaults func(context.Context, string) (speech.Settings, error)
|
||||
}
|
||||
|
||||
func newPreferencesWorker(logger *slog.Logger, settings config.Settings, updater any) *preferencesWorker {
|
||||
return &preferencesWorker{logger: logger, settings: settings, globalSettings: settings, appSettings: make(map[string]config.Settings), updater: updater,
|
||||
extensionRequests: make(chan readerSettingsCall, 16), extensions: make(map[string]*readerExtension), speechCommands: make(chan input.Command, 16), remoteChanges: make(chan remoteSettingsChange, 16), discover: speech.Discover, open: make(chan string, 1), applicationChanges: make(chan string, 1), done: make(chan struct{})}
|
||||
extensionRequests: make(chan readerSettingsCall, 16), extensions: make(map[string]*readerExtension), speechCommands: make(chan input.Command, 16), remoteChanges: make(chan remoteSettingsChange, 16), discover: speech.Discover, readSpeechDefaults: speech.ReadDefaults, open: make(chan string, 1), applicationChanges: make(chan string, 1), done: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (worker *preferencesWorker) Start(ctx context.Context) error {
|
||||
|
||||
@@ -132,6 +132,14 @@ func (worker *preferencesWorker) initial_response(childCtx context.Context) (pre
|
||||
if err != nil {
|
||||
worker.logger.Debug("discover echo voices", "error", err)
|
||||
}
|
||||
if worker.editing.SpeechServerFactory != "hardwarefactory" {
|
||||
defaultsCtx, cancelDefaults := context.WithTimeout(childCtx, 500*time.Millisecond)
|
||||
initial.SpeechDefaults, err = worker.readSpeechDefaults(defaultsCtx, worker.socket)
|
||||
cancelDefaults()
|
||||
if err != nil {
|
||||
worker.logger.Debug("read speech defaults", "error", err)
|
||||
}
|
||||
}
|
||||
if worker.editApp == "" {
|
||||
if err := worker.load_extension_pages(childCtx); err != nil {
|
||||
worker.logger.Warn("load tentacle preference pages", "error", err)
|
||||
|
||||
@@ -32,6 +32,7 @@ func TestPreferencesInitialRetainsLargeVoiceCatalog(t *testing.T) {
|
||||
}
|
||||
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), config.DefaultSettings(), nil)
|
||||
worker.discover = func(context.Context, string) (speech.Catalog, error) { return catalog, nil }
|
||||
worker.readSpeechDefaults = func(context.Context, string) (speech.Settings, error) { return speech.Settings{}, nil }
|
||||
response, err := worker.initial_response(t.Context())
|
||||
if err != nil || !reflect.DeepEqual(response.Catalog, catalog) {
|
||||
t.Fatalf("initial response lost discovered voices: err=%v", err)
|
||||
@@ -41,6 +42,7 @@ func TestPreferencesInitialRetainsLargeVoiceCatalog(t *testing.T) {
|
||||
func TestPreferencesWindowReportsHelperExit(t *testing.T) {
|
||||
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), config.DefaultSettings(), nil)
|
||||
worker.discover = func(context.Context, string) (speech.Catalog, error) { return nil, nil }
|
||||
worker.readSpeechDefaults = func(context.Context, string) (speech.Settings, error) { return speech.Settings{}, nil }
|
||||
// The test executable does not implement --preferences-ui. It exits before
|
||||
// opening a dialog, as a broken helper would on the affected machine.
|
||||
err := worker.window(t.Context())
|
||||
|
||||
@@ -73,13 +73,14 @@ static gboolean key(GtkWidget *window, GdkEventKey *event, gpointer data) {
|
||||
if (event->keyval == GDK_KEY_Escape) { action = 3; return TRUE; }
|
||||
return FALSE;
|
||||
}
|
||||
static void defaults(GtkToggleButton *button, gpointer spin) {
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(spin), !gtk_toggle_button_get_active(button));
|
||||
}
|
||||
static void voice_value_changed(GtkWidget *control, gpointer data) {
|
||||
(void)control; (void)data;
|
||||
voice_changed = 1;
|
||||
}
|
||||
static void speech_number_changed(GtkWidget *control, gpointer data) {
|
||||
g_object_set_data(G_OBJECT(control), "number-edited", GINT_TO_POINTER(1));
|
||||
voice_value_changed(control, data);
|
||||
}
|
||||
static gboolean echo_control_focused(GtkWidget *control) {
|
||||
GtkWidget *focus = gtk_window_get_focus(GTK_WINDOW(prefs_widget("window")));
|
||||
return focus && (focus == control || gtk_widget_is_ancestor(focus, control));
|
||||
@@ -176,11 +177,7 @@ int prefs_init(const char *xml) {
|
||||
g_signal_connect(prefs_widget("window"), "key-press-event", G_CALLBACK(key), NULL);
|
||||
const char *ids[] = {"rate", "pitch", "volume"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
gchar *id = g_strconcat(ids[i], "_default", NULL);
|
||||
g_signal_connect(prefs_widget(id), "toggled", G_CALLBACK(defaults), prefs_widget(ids[i]));
|
||||
g_signal_connect(prefs_widget(id), "toggled", G_CALLBACK(voice_value_changed), NULL);
|
||||
g_signal_connect(prefs_widget(ids[i]), "value-changed", G_CALLBACK(voice_value_changed), NULL);
|
||||
g_free(id);
|
||||
g_signal_connect(prefs_widget(ids[i]), "value-changed", G_CALLBACK(speech_number_changed), NULL);
|
||||
}
|
||||
const char *voiceScales[] = {"rate", "pitch", "volume", "echoRateScale", "echoPitchScale", "echoVolumeScale"};
|
||||
for (unsigned i = 0; i < G_N_ELEMENTS(voiceScales); i++) {
|
||||
|
||||
@@ -12,10 +12,9 @@ void prefs_busy(int busy, int can_save);
|
||||
void prefs_present(void);
|
||||
void prefs_close(void);
|
||||
void prefs_status(const char *message, int focus);
|
||||
void prefs_number_set(const char *id, double value, int use_default);
|
||||
void prefs_voice_defaults(int kind);
|
||||
void prefs_number_set(const char *id, double value);
|
||||
double prefs_number_value(const char *id);
|
||||
int prefs_default(const char *id);
|
||||
int prefs_number_edited(const char *id);
|
||||
void prefs_choice_set(const char *id, int value);
|
||||
int prefs_choice_get(const char *id);
|
||||
void prefs_hardware_devices_clear(void);
|
||||
|
||||
@@ -3,30 +3,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "native_internal.h"
|
||||
|
||||
void prefs_voice_defaults(int kind) {
|
||||
const char *ids[] = {"rate_default", "pitch_default", "volume_default"};
|
||||
const char *server[] = {"Use server default rate", "Use server default pitch", "Use server default volume"};
|
||||
const char *inherited[] = {"Use Default voice rate", "Use Default voice pitch", "Use Default voice volume"};
|
||||
for (unsigned i = 0; i < G_N_ELEMENTS(ids); i++) {
|
||||
const char *label = kind == 0 ? server[i] : inherited[i];
|
||||
if (kind == 1 && i == 1) label = "Use automatic uppercase pitch";
|
||||
GtkButton *button = GTK_BUTTON(prefs_widget(ids[i]));
|
||||
if (g_strcmp0(gtk_button_get_label(button), label) != 0) gtk_button_set_label(button, label);
|
||||
}
|
||||
}
|
||||
|
||||
void prefs_number_set(const char *id, double value, int use_default) {
|
||||
gchar *check = g_strconcat(id, "_default", NULL);
|
||||
gtk_range_set_value(GTK_RANGE(prefs_widget(id)), value);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(prefs_widget(check)), use_default);
|
||||
gtk_widget_set_sensitive(prefs_widget(id), !use_default);
|
||||
g_free(check);
|
||||
void prefs_number_set(const char *id, double value) {
|
||||
GtkWidget *control = prefs_widget(id);
|
||||
gtk_range_set_value(GTK_RANGE(control), value);
|
||||
// Loading an effective value must not turn inheritance into an override.
|
||||
g_object_set_data(G_OBJECT(control), "number-edited", NULL);
|
||||
}
|
||||
double prefs_number_value(const char *id) { return gtk_range_get_value(GTK_RANGE(prefs_widget(id))); }
|
||||
int prefs_default(const char *id) {
|
||||
gchar *check = g_strconcat(id, "_default", NULL);
|
||||
int result = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(prefs_widget(check)));
|
||||
g_free(check); return result;
|
||||
int prefs_number_edited(const char *id) {
|
||||
return GPOINTER_TO_INT(g_object_get_data(G_OBJECT(prefs_widget(id)), "number-edited"));
|
||||
}
|
||||
void prefs_choice_set(const char *id, int value) {
|
||||
gchar *name = g_strdup_printf("%s_%d", id, value);
|
||||
|
||||
@@ -114,24 +114,21 @@
|
||||
<child><object class="GtkComboBoxText" id="speechFamilies"><property name="can-focus">False</property><items><item>Default</item></items></object><packing><property name="left-attach">1</property><property name="top-attach">3</property></packing></child>
|
||||
<child><object class="GtkComboBoxText" id="voiceTypesCombo"><property name="can-focus">False</property><items><item>Default</item><item>Uppercase</item><item>Hyperlink</item><item>System</item></items></object><packing><property name="left-attach">1</property><property name="top-attach">4</property></packing></child>
|
||||
<child><object class="GtkLabel"><property name="label">_Voice type:</property><property name="use-underline">True</property><property name="mnemonic-widget">voiceTypesCombo</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">4</property></packing></child>
|
||||
<child><object class="GtkCheckButton" id="volume_default"><property name="label">Use server default volume</property><property name="can-focus">True</property></object><packing><property name="left-attach">0</property><property name="top-attach">5</property><property name="width">2</property></packing></child>
|
||||
<child><object class="GtkLabel" id="volume_label"><property name="label">Vo_lume:</property><property name="use-underline">True</property><property name="mnemonic-widget">volume</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">6</property></packing></child>
|
||||
<child><object class="GtkScale" id="volume"><property name="adjustment">volume_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">6</property></packing></child>
|
||||
<child><object class="GtkCheckButton" id="pitch_default"><property name="label">Use server default pitch</property><property name="can-focus">True</property></object><packing><property name="left-attach">0</property><property name="top-attach">7</property><property name="width">2</property></packing></child>
|
||||
<child><object class="GtkLabel" id="pitch_label"><property name="label">Pi_tch:</property><property name="use-underline">True</property><property name="mnemonic-widget">pitch</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">8</property></packing></child>
|
||||
<child><object class="GtkScale" id="pitch"><property name="adjustment">pitch_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">8</property></packing></child>
|
||||
<child><object class="GtkCheckButton" id="rate_default"><property name="label">Use server default rate</property><property name="can-focus">True</property></object><packing><property name="left-attach">0</property><property name="top-attach">9</property><property name="width">2</property></packing></child>
|
||||
<child><object class="GtkLabel" id="rate_label"><property name="label">_Rate:</property><property name="use-underline">True</property><property name="mnemonic-widget">rate</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">10</property></packing></child>
|
||||
<child><object class="GtkScale" id="rate"><property name="adjustment">rate_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">10</property></packing></child>
|
||||
<child><object class="GtkLabel" id="volume_label"><property name="label">Vo_lume:</property><property name="use-underline">True</property><property name="mnemonic-widget">volume</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">5</property></packing></child>
|
||||
<child><object class="GtkScale" id="volume"><property name="adjustment">volume_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">5</property></packing></child>
|
||||
<child><object class="GtkLabel" id="pitch_label"><property name="label">Pi_tch:</property><property name="use-underline">True</property><property name="mnemonic-widget">pitch</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">6</property></packing></child>
|
||||
<child><object class="GtkScale" id="pitch"><property name="adjustment">pitch_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">6</property></packing></child>
|
||||
<child><object class="GtkLabel" id="rate_label"><property name="label">_Rate:</property><property name="use-underline">True</property><property name="mnemonic-widget">rate</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">7</property></packing></child>
|
||||
<child><object class="GtkScale" id="rate"><property name="adjustment">rate_adjustment</property><property name="can-focus">True</property><property name="digits">1</property><property name="hexpand">True</property><property name="value-pos">right</property></object><packing><property name="left-attach">1</property><property name="top-attach">7</property></packing></child>
|
||||
<child><object class="GtkFrame" id="capitalLettersFrame"><property name="label">Capital letters</property><child><object class="GtkBox"><property name="orientation">vertical</property><property name="spacing">6</property><property name="border-width">8</property>
|
||||
<child><object class="GtkCheckButton" id="raisePitchForCapitals"><property name="label">Raise pitch for capital letters</property><property name="can-focus">True</property></object></child>
|
||||
<child><object class="GtkCheckButton" id="beepForCapitals"><property name="label">Beep for capital letters</property><property name="can-focus">True</property></object></child>
|
||||
<child><object class="GtkCheckButton" id="sayCapForCapitals"><property name="label">Say “cap” before capital letters</property><property name="can-focus">True</property></object></child>
|
||||
</object></child></object><packing><property name="left-attach">0</property><property name="top-attach">11</property><property name="width">2</property></packing></child>
|
||||
<child><object class="GtkEntry" id="hardwareDeviceEntry"><property name="can-focus">True</property><property name="tooltip-text">Serial device path; completion offers detected devices</property></object><packing><property name="left-attach">1</property><property name="top-attach">15</property></packing></child>
|
||||
<child><object class="GtkLabel"><property name="label">Serial _device:</property><property name="use-underline">True</property><property name="mnemonic-widget">hardwareDeviceEntry</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">15</property></packing></child>
|
||||
<child><object class="GtkLabel"><property name="label">Serial baud rate:</property><property name="mnemonic-widget">hardwareBaudCombo</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">17</property></packing></child>
|
||||
<child><object class="GtkComboBoxText" id="hardwareBaudCombo"><property name="can-focus">False</property><items><item>1200</item><item>2400</item><item>4800</item><item>9600</item><item>19200</item><item>38400</item><item>57600</item><item>115200</item></items></object><packing><property name="left-attach">1</property><property name="top-attach">17</property></packing></child>
|
||||
</object></child></object><packing><property name="left-attach">0</property><property name="top-attach">8</property><property name="width">2</property></packing></child>
|
||||
<child><object class="GtkEntry" id="hardwareDeviceEntry"><property name="can-focus">True</property><property name="tooltip-text">Serial device path; completion offers detected devices</property></object><packing><property name="left-attach">1</property><property name="top-attach">12</property></packing></child>
|
||||
<child><object class="GtkLabel"><property name="label">Serial _device:</property><property name="use-underline">True</property><property name="mnemonic-widget">hardwareDeviceEntry</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">12</property></packing></child>
|
||||
<child><object class="GtkLabel"><property name="label">Serial baud rate:</property><property name="mnemonic-widget">hardwareBaudCombo</property><property name="xalign">1</property></object><packing><property name="left-attach">0</property><property name="top-attach">14</property></packing></child>
|
||||
<child><object class="GtkComboBoxText" id="hardwareBaudCombo"><property name="can-focus">False</property><items><item>1200</item><item>2400</item><item>4800</item><item>9600</item><item>19200</item><item>38400</item><item>57600</item><item>115200</item></items></object><packing><property name="left-attach">1</property><property name="top-attach">14</property></packing></child>
|
||||
<child>
|
||||
<object class="GtkBox" id="speech_settings">
|
||||
<property name="orientation">vertical</property>
|
||||
@@ -169,7 +166,7 @@
|
||||
<child><object class="GtkRadioButton" id="indentationSpeechAndBeepsButton"><property name="label">Speech and beeps</property><property name="can-focus">True</property><property name="group">indentationSpeechButton</property></object></child>
|
||||
</object></child></object></child>
|
||||
</object>
|
||||
<packing><property name="left-attach">0</property><property name="top-attach">18</property><property name="width">2</property></packing>
|
||||
<packing><property name="left-attach">0</property><property name="top-attach">15</property><property name="width">2</property></packing>
|
||||
</child>
|
||||
</object></child>
|
||||
</object>
|
||||
|
||||
@@ -124,6 +124,7 @@ type Response struct {
|
||||
Tentacles *tentacle.Snapshot `json:",omitempty"`
|
||||
ChooseApplication bool
|
||||
Catalog speech.Catalog
|
||||
SpeechDefaults speech.Settings
|
||||
CatalogPending bool // More catalog parts follow before the UI can initialize.
|
||||
Application string
|
||||
Present bool
|
||||
|
||||
@@ -17,6 +17,7 @@ type uiSession struct {
|
||||
extensionPages []ExtensionPage
|
||||
progressBar config.ProgressBarSettings
|
||||
catalog speech.Catalog
|
||||
speechDefaults speech.Settings
|
||||
contractionTables []braille.TableOption
|
||||
soundThemes []string
|
||||
echoModules []string
|
||||
|
||||
@@ -13,6 +13,7 @@ import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
|
||||
"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) {
|
||||
@@ -218,8 +220,8 @@ func (session *uiSession) refreshSpeechVoices() {
|
||||
C.prefs_hardware_sensitivity(C.int(system))
|
||||
if system == 0 {
|
||||
session.reconcile_voice_families(session.selectedSpeechModule())
|
||||
session.load_voice_editor()
|
||||
}
|
||||
session.load_voice_editor()
|
||||
}
|
||||
if system == 1 {
|
||||
return
|
||||
@@ -292,24 +294,32 @@ func set_speech_support(values Values) {
|
||||
setCheck("indentationSpeechAndBeepsButton", mode == "both")
|
||||
}
|
||||
|
||||
func set_speech_numbers(values Values, inherited config.Voice) {
|
||||
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, 50}, {"pitch", values.Pitch, inherited.AveragePitch, 5}, {"volume", values.Volume, inherited.Gain, 5},
|
||||
{"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), boolean(item.value == nil))
|
||||
C.prefs_number_set(id, C.double(value))
|
||||
C.free(unsafe.Pointer(id))
|
||||
}
|
||||
}
|
||||
@@ -388,15 +398,18 @@ 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]
|
||||
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))
|
||||
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) {
|
||||
var voice config.Voice
|
||||
voice := session.voiceDrafts[session.activeVoiceType]
|
||||
for _, item := range []struct {
|
||||
id string
|
||||
target **float64
|
||||
@@ -405,10 +418,10 @@ func (session *uiSession) read_voice_editor() (config.Voice, error) {
|
||||
{"rate", &voice.Rate, 99}, {"pitch", &voice.AveragePitch, 9}, {"volume", &voice.Gain, 9},
|
||||
} {
|
||||
id := C.CString(item.id)
|
||||
useDefault := C.prefs_default(id) != 0
|
||||
edited := C.prefs_number_edited(id) != 0
|
||||
value := float64(C.prefs_number_value(id))
|
||||
C.free(unsafe.Pointer(id))
|
||||
if useDefault {
|
||||
if !edited {
|
||||
continue
|
||||
}
|
||||
if !(value >= 0 && value <= item.max) {
|
||||
|
||||
@@ -64,7 +64,7 @@ func Run(ctx context.Context, input io.Reader, output io.Writer) error {
|
||||
C.prefs_set_application(name)
|
||||
C.free(unsafe.Pointer(name))
|
||||
}
|
||||
session := &uiSession{catalog: initial.Catalog, soundThemes: []string{"default", "none"}}
|
||||
session := &uiSession{catalog: initial.Catalog, speechDefaults: initial.SpeechDefaults, soundThemes: []string{"default", "none"}}
|
||||
session.setValues(initial.Values)
|
||||
if err := session.set_extension_pages(initial.ExtensionPages); err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package speech
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ReadDefaults queries a fresh client, leaving the speaking client's settings
|
||||
// alone. Unsupported or invalid fields stay nil so preferences can use sensible
|
||||
// display values. The caller bounds the entire optional query with a deadline.
|
||||
func ReadDefaults(ctx context.Context, socket string) (Settings, error) {
|
||||
var defaults Settings
|
||||
endpoint, err := resolveServerAddress(socket)
|
||||
if err != nil {
|
||||
return defaults, err
|
||||
}
|
||||
conn, err := (&net.Dialer{Timeout: connectTimeout}).DialContext(ctx, endpoint.network, endpoint.address)
|
||||
if err != nil {
|
||||
return defaults, err
|
||||
}
|
||||
defer conn.Close()
|
||||
finish, err := prepareIO(ctx, conn, ioTimeout)
|
||||
if err != nil {
|
||||
return defaults, err
|
||||
}
|
||||
defer finish()
|
||||
client := &Client{conn: conn, reader: bufio.NewReader(conn)}
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
target **int
|
||||
}{
|
||||
{"RATE", &defaults.Rate}, {"PITCH", &defaults.Pitch}, {"VOLUME", &defaults.Volume},
|
||||
} {
|
||||
if _, err := fmt.Fprintf(conn, "GET %s\r\n", field.name); err != nil {
|
||||
return defaults, err
|
||||
}
|
||||
response, err := client.readReply()
|
||||
if err != nil {
|
||||
return defaults, err
|
||||
}
|
||||
if response.code < 200 || response.code >= 300 || len(response.data) != 1 {
|
||||
continue
|
||||
}
|
||||
value, err := strconv.Atoi(response.data[0])
|
||||
if err == nil && value >= -100 && value <= 100 {
|
||||
*field.target = &value
|
||||
}
|
||||
}
|
||||
return defaults, nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package speech
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestReadDefaults(t *testing.T) {
|
||||
path, server := newSettingsServer(t, func(command string) string {
|
||||
switch command {
|
||||
case "GET RATE":
|
||||
return "200-20\r\n200 OK\r\n"
|
||||
case "GET PITCH":
|
||||
return "200--10\r\n200 OK\r\n"
|
||||
case "GET VOLUME":
|
||||
return "200-100\r\n200 OK\r\n"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
defaults, err := ReadDefaults(t.Context(), path)
|
||||
if err != nil || defaults.Rate == nil || *defaults.Rate != 20 ||
|
||||
defaults.Pitch == nil || *defaults.Pitch != -10 || defaults.Volume == nil || *defaults.Volume != 100 {
|
||||
t.Fatalf("defaults=%+v, err=%v", defaults, err)
|
||||
}
|
||||
for _, command := range []string{"SET SELF RATE 20", "SET SELF PITCH -10", "SET SELF VOLUME 100", "SPEAK"} {
|
||||
if server.saw(1, command) {
|
||||
t.Fatalf("query changed speech: %s", command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDefaultsRejectsUnsupportedAndInvalidValues(t *testing.T) {
|
||||
for _, response := range []string{"500 UNSUPPORTED\r\n", "200 OK\r\n", "200-invalid\r\n200 OK\r\n",
|
||||
"200-101\r\n200 OK\r\n", "200-0\r\n200-1\r\n200 OK\r\n"} {
|
||||
t.Run(response, func(t *testing.T) {
|
||||
path, _ := newSettingsServer(t, func(string) string { return response })
|
||||
defaults, err := ReadDefaults(t.Context(), path)
|
||||
if err != nil || defaults.Rate != nil || defaults.Pitch != nil || defaults.Volume != nil {
|
||||
t.Fatalf("invalid defaults=%+v, err=%v", defaults, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDefaultsRetainsPartialValuesAndHonorsDeadline(t *testing.T) {
|
||||
path, _ := newSettingsServer(t, func(command string) string {
|
||||
if command == "GET RATE" {
|
||||
return "200-0\r\n200 OK\r\n"
|
||||
}
|
||||
if command == "GET PITCH" {
|
||||
return "200-unfinished\r\n"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
defaults, err := ReadDefaults(ctx, path)
|
||||
if err == nil || defaults.Rate == nil || *defaults.Rate != 0 || defaults.Pitch != nil || defaults.Volume != nil {
|
||||
t.Fatalf("partial defaults=%+v, err=%v", defaults, err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user