From 37fbe5d787e1d8a1ad1a857f8372b2290333753e Mon Sep 17 00:00:00 2001 From: intelligence Date: Fri, 14 Aug 2026 14:00:52 +0200 Subject: [PATCH] feat: shortcut --- README.md | 4 +-- src/main.cpp | 72 ++++++++++------------------------------------------ 2 files changed, 15 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index a6f0867..0f3e778 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ transui # same as record ``` Bind `transui record` to a shortcut in your desktop environment. Transui does -not register a global shortcut itself. Recording stops automatically after two -continuous seconds without detected speech. +not register a global shortcut itself. Press the same shortcut again to stop +and transcribe the current recording. The **Hosted first, local fallback** setting tries the Hosted endpoint, then the Local endpoint when the first request fails. With it disabled, Transui uses diff --git a/src/main.cpp b/src/main.cpp index b6c9806..70a4608 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,8 +19,6 @@ namespace { constexpr const char* kAppId = "io.github.transui"; constexpr const char* kConfigGroup = "transui"; -constexpr gint64 kSilenceTimeoutUs = 2 * G_USEC_PER_SEC; -constexpr double kSoundThresholdDb = -42.0; const SecretSchema kSecretSchema = { "io.github.transui.credentials", SECRET_SCHEMA_NONE, @@ -45,10 +43,7 @@ struct App { GtkWidget* processing = nullptr; GtkWidget* elapsed_label = nullptr; guint elapsed_source = 0; - guint silence_source = 0; - guint level_watch = 0; guint elapsed_seconds = 0; - gint64 last_sound_us = 0; GstElement* pipeline = nullptr; std::string recording_path; std::atomic cancelled{false}; @@ -290,53 +285,11 @@ gboolean tick_elapsed(gpointer data) { return G_SOURCE_CONTINUE; } -void stop_recording(GtkButton*, gpointer data); - -gboolean monitor_audio_level(GstBus*, GstMessage* message, gpointer data) { - if (GST_MESSAGE_TYPE(message) != GST_MESSAGE_ELEMENT) return G_SOURCE_CONTINUE; - const GstStructure* structure = gst_message_get_structure(message); - if (!structure || !gst_structure_has_name(structure, "level")) return G_SOURCE_CONTINUE; - const GValue* rms = gst_structure_get_value(structure, "rms"); - if (!rms || !GST_VALUE_HOLDS_LIST(rms)) return G_SOURCE_CONTINUE; - - auto* app = static_cast(data); - for (guint index = 0; index < gst_value_list_get_size(rms); ++index) { - const GValue* channel = gst_value_list_get_value(rms, index); - if (G_VALUE_HOLDS_DOUBLE(channel) && g_value_get_double(channel) > kSoundThresholdDb) { - app->last_sound_us = g_get_monotonic_time(); - break; - } - } - return G_SOURCE_CONTINUE; -} - -gboolean detect_silence(gpointer data) { - auto* app = static_cast(data); - if (!app->pipeline) { - app->silence_source = 0; - return G_SOURCE_REMOVE; - } - if (g_get_monotonic_time() - app->last_sound_us < kSilenceTimeoutUs) - return G_SOURCE_CONTINUE; - - app->silence_source = 0; - stop_recording(nullptr, app); - return G_SOURCE_REMOVE; -} - -void remove_recording_sources(App* app) { +void stop_elapsed_timer(App* app) { if (app->elapsed_source) { g_source_remove(app->elapsed_source); app->elapsed_source = 0; } - if (app->silence_source) { - g_source_remove(app->silence_source); - app->silence_source = 0; - } - if (app->level_watch) { - g_source_remove(app->level_watch); - app->level_watch = 0; - } } void remove_recording(App* app) { @@ -347,7 +300,7 @@ void remove_recording(App* app) { } void close_recorder(App* app) { - remove_recording_sources(app); + stop_elapsed_timer(app); if (app->pipeline) { gst_element_set_state(app->pipeline, GST_STATE_NULL); gst_object_unref(app->pipeline); @@ -557,7 +510,7 @@ void show_processing(App* app) { void stop_recording(GtkButton*, gpointer data) { auto* app = static_cast(data); if (!app->pipeline) return; - remove_recording_sources(app); + stop_elapsed_timer(app); gst_element_send_event(app->pipeline, gst_event_new_eos()); GstBus* bus = gst_element_get_bus(app->pipeline); GstMessage* message = gst_bus_timed_pop_filtered(bus, 3 * GST_SECOND, @@ -585,7 +538,7 @@ void cancel_recording(GtkButton*, gpointer data) { void recorder_closed(GtkWidget*, gpointer data) { auto* app = static_cast(data); app->recorder = nullptr; - remove_recording_sources(app); + stop_elapsed_timer(app); if (app->pipeline) { gst_element_set_state(app->pipeline, GST_STATE_NULL); gst_object_unref(app->pipeline); @@ -595,7 +548,14 @@ void recorder_closed(GtkWidget*, gpointer data) { } void start_recording(App* app) { - if (app->recorder) { gtk_window_present(GTK_WINDOW(app->recorder)); return; } + if (app->recorder) { + stop_recording(nullptr, app); + return; + } + if (app->processing) { + gtk_window_present(GTK_WINDOW(app->processing)); + return; + } if (app->config.local_url.empty() && (!app->config.hosted_first || app->config.hosted_url.empty())) { open_settings(app); show_error(GTK_WINDOW(app->settings), "Set an endpoint first."); @@ -608,8 +568,7 @@ void start_recording(App* app) { app->recording_path = template_path; g_free(template_path); gchar* pipeline_text = g_strdup_printf( - "autoaudiosrc ! audioconvert ! audioresample ! " - "level interval=100000000 post-messages=true ! wavenc ! filesink location=\"%s\"", + "autoaudiosrc ! audioconvert ! audioresample ! wavenc ! filesink location=\"%s\"", app->recording_path.c_str()); GError* error = nullptr; app->pipeline = gst_parse_launch(pipeline_text, &error); @@ -620,9 +579,6 @@ void start_recording(App* app) { remove_recording(app); return; } - GstBus* level_bus = gst_element_get_bus(app->pipeline); - app->level_watch = gst_bus_add_watch(level_bus, monitor_audio_level, app); - gst_object_unref(level_bus); if (gst_element_set_state(app->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { show_error(nullptr, "Could not access an audio input."); close_recorder(app); @@ -659,9 +615,7 @@ void start_recording(App* app) { gtk_box_pack_start(GTK_BOX(actions), cancel, false, false, 0); gtk_box_pack_start(GTK_BOX(actions), stop, false, false, 0); app->elapsed_seconds = 0; - app->last_sound_us = g_get_monotonic_time(); app->elapsed_source = g_timeout_add_seconds(1, tick_elapsed, app); - app->silence_source = g_timeout_add(100, detect_silence, app); g_signal_connect(cancel, "clicked", G_CALLBACK(cancel_recording), app); g_signal_connect(stop, "clicked", G_CALLBACK(stop_recording), app); g_signal_connect(window, "destroy", G_CALLBACK(recorder_closed), app);