feat: sound detection

This commit is contained in:
intelligence committed 2026-08-14 13:51:39 +02:00
1 parent 51751b5dbf
commit 05a7c1cd83
2 files changed
+75 -68

No files matched your search

+73 -67
View File
@@ -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);