feat: sound detection
This commit is contained in:
1 parent
51751b5dbf
commit
05a7c1cd83
2 files changed
+75
-68
No files matched your search
@@ -35,7 +35,8 @@ transui # same as record
|
||||
```
|
||||
|
||||
Bind `transui record` to a shortcut in your desktop environment. Transui does
|
||||
not register a global shortcut itself.
|
||||
not register a global shortcut itself. Recording stops automatically after two
|
||||
continuous seconds without detected speech.
|
||||
|
||||
The **Hosted first, local fallback** setting tries the Hosted endpoint, then
|
||||
the Local endpoint when the first request fails. With it disabled, Transui uses
|
||||
|
||||
+73
-67
@@ -19,6 +19,8 @@ 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,
|
||||
@@ -43,7 +45,10 @@ 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<bool> cancelled{false};
|
||||
@@ -285,6 +290,55 @@ 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<App*>(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<App*>(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) {
|
||||
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) {
|
||||
if (!app->recording_path.empty()) {
|
||||
g_remove(app->recording_path.c_str());
|
||||
@@ -293,10 +347,7 @@ void remove_recording(App* app) {
|
||||
}
|
||||
|
||||
void close_recorder(App* app) {
|
||||
if (app->elapsed_source) {
|
||||
g_source_remove(app->elapsed_source);
|
||||
app->elapsed_source = 0;
|
||||
}
|
||||
remove_recording_sources(app);
|
||||
if (app->pipeline) {
|
||||
gst_element_set_state(app->pipeline, GST_STATE_NULL);
|
||||
gst_object_unref(app->pipeline);
|
||||
@@ -506,10 +557,7 @@ void show_processing(App* app) {
|
||||
void stop_recording(GtkButton*, gpointer data) {
|
||||
auto* app = static_cast<App*>(data);
|
||||
if (!app->pipeline) return;
|
||||
if (app->elapsed_source) {
|
||||
g_source_remove(app->elapsed_source);
|
||||
app->elapsed_source = 0;
|
||||
}
|
||||
remove_recording_sources(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,
|
||||
@@ -537,10 +585,7 @@ void cancel_recording(GtkButton*, gpointer data) {
|
||||
void recorder_closed(GtkWidget*, gpointer data) {
|
||||
auto* app = static_cast<App*>(data);
|
||||
app->recorder = nullptr;
|
||||
if (app->elapsed_source) {
|
||||
g_source_remove(app->elapsed_source);
|
||||
app->elapsed_source = 0;
|
||||
}
|
||||
remove_recording_sources(app);
|
||||
if (app->pipeline) {
|
||||
gst_element_set_state(app->pipeline, GST_STATE_NULL);
|
||||
gst_object_unref(app->pipeline);
|
||||
@@ -562,7 +607,10 @@ void start_recording(App* app) {
|
||||
close(fd);
|
||||
app->recording_path = template_path;
|
||||
g_free(template_path);
|
||||
gchar* pipeline_text = g_strdup_printf("autoaudiosrc ! audioconvert ! audioresample ! wavenc ! filesink location=\"%s\"", app->recording_path.c_str());
|
||||
gchar* pipeline_text = g_strdup_printf(
|
||||
"autoaudiosrc ! audioconvert ! audioresample ! "
|
||||
"level interval=100000000 post-messages=true ! wavenc ! filesink location=\"%s\"",
|
||||
app->recording_path.c_str());
|
||||
GError* error = nullptr;
|
||||
app->pipeline = gst_parse_launch(pipeline_text, &error);
|
||||
g_free(pipeline_text);
|
||||
@@ -572,6 +620,9 @@ 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);
|
||||
@@ -608,7 +659,9 @@ 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);
|
||||
@@ -618,83 +671,36 @@ void start_recording(App* app) {
|
||||
|
||||
void install_css() {
|
||||
const char* css = R"CSS(
|
||||
window {
|
||||
background-color: #181b1a;
|
||||
color: #eef0e8;
|
||||
font-family: Cantarell, sans-serif;
|
||||
}
|
||||
headerbar.transui-header {
|
||||
background: #202421;
|
||||
color: #eef0e8;
|
||||
border-bottom: 1px solid #3b413c;
|
||||
box-shadow: none;
|
||||
}
|
||||
.settings-panel {
|
||||
background-color: #202421;
|
||||
border: 1px solid #3b413c;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
padding: 12px 4px;
|
||||
}
|
||||
.field-label {
|
||||
color: #aeb7ae;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
opacity: 0.72;
|
||||
}
|
||||
entry, combobox button {
|
||||
background: #2a2f2b;
|
||||
color: #eef0e8;
|
||||
border: 1px solid #4a514b;
|
||||
border-radius: 4px;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
entry:focus, combobox button:focus, button:focus {
|
||||
border-color: #d4a44c;
|
||||
box-shadow: 0 0 0 1px #d4a44c;
|
||||
}
|
||||
label { color: #d8ddd5; }
|
||||
checkbutton { color: #d8ddd5; }
|
||||
.record-panel, .process-panel {
|
||||
background-color: #202421;
|
||||
border: 1px solid #3b413c;
|
||||
border-radius: 12px;
|
||||
padding: 28px 36px;
|
||||
}
|
||||
.record-light {
|
||||
background: #e64a3c;
|
||||
border: 6px solid #552a25;
|
||||
background: #df493e;
|
||||
border: 6px solid alpha(#df493e, 0.22);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px #f17a6f;
|
||||
}
|
||||
.record-state {
|
||||
color: #f17a6f;
|
||||
color: #c93e35;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.elapsed {
|
||||
color: #eef0e8;
|
||||
font-family: "DejaVu Sans Mono", monospace;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.state-title { color: #eef0e8; font-size: 17px; font-weight: 700; }
|
||||
.signal-spinner { color: #d4a44c; }
|
||||
button {
|
||||
background: #2a2f2b;
|
||||
color: #eef0e8;
|
||||
border: 1px solid #4a514b;
|
||||
border-radius: 4px;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
button:hover { background: #343a35; }
|
||||
button.suggested-action {
|
||||
background: #d4a44c;
|
||||
color: #181b1a;
|
||||
border-color: #d4a44c;
|
||||
font-weight: 800;
|
||||
}
|
||||
button.suggested-action:hover { background: #e1b760; }
|
||||
.state-title { font-size: 17px; font-weight: 700; }
|
||||
button.suggested-action { font-weight: 700; }
|
||||
)CSS";
|
||||
GtkCssProvider* provider = gtk_css_provider_new();
|
||||
gtk_css_provider_load_from_data(provider, css, -1, nullptr);
|
||||
|
||||
Reference in new issue
Block a user