GTK: move audio bell processing to the application

This fixes #13647 by using at most one GStreamer thread per application.
This was previously addressed in #12815 which used at most one GStreamer
thread per surface. Originally discussed in #12808.
This commit is contained in:
Jeffrey C. Ollie
2026-08-05 20:10:53 -05:00
parent 8eecb8fdbf
commit f0f3f4d8d8
2 changed files with 48 additions and 41 deletions

View File

@@ -45,6 +45,7 @@ const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseCo
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts;
const OpenURI = @import("../portal.zig").OpenURI;
const media = @import("../media.zig");
const log = std.log.scoped(.gtk_ghostty_application);
@@ -223,6 +224,12 @@ pub const Application = extern struct {
open_uri: OpenURI = undefined,
// The audio bell's MediaFile, reused across bells so we don't leak a
// GStreamer pipeline (and its GL threads) on every ring. Built lazily
// on the first audio bell and rebuilt when `bell-audio-path` changes;
// unref'd on dispose. See ringBell and media.zig.
bell_media: ?*gtk.MediaFile = null,
pub var offset: c_int = 0;
};
@@ -1478,6 +1485,7 @@ pub const Application = extern struct {
.init("quit", actionQuit, null),
.init("reload-config", actionReloadConfig, null),
.init("toggle-quick-terminal", actionToggleQuickTerminal, null),
.init("ring-bell", actionRingBell, null),
};
ext.actions.add(Self, self, &actions);
@@ -1549,6 +1557,11 @@ pub const Application = extern struct {
priv.signal_source = null;
}
if (priv.bell_media) |v| {
v.unref();
priv.bell_media = null;
}
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
@@ -1948,6 +1961,40 @@ pub const Application = extern struct {
);
}
pub fn actionRingBell(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const priv: *Private = self.private();
const config = priv.config.get();
// Do our sound
if (config.@"bell-features".audio) audio: {
const config_path = config.@"bell-audio-path" orelse break :audio;
const path, const required = switch (config_path) {
.optional => |path| .{ path, false },
.required => |path| .{ path, true },
};
const volume = std.math.clamp(
config.@"bell-audio-volume",
0.0,
1.0,
);
// Reuse one MediaFile per application (rebuilt only when the path
// changes) so each bell replays the same pipeline instead of
// leaking a fresh one. Assign unconditionally: bellMediaFile frees
// any stale MediaFile and returns the current slot value (possibly
// null if the path is now inaccessible), so priv.bell_media never
// dangles.
priv.bell_media = media.bellMediaFile(priv.bell_media, path, required);
const media_file = priv.bell_media orelse break :audio;
media.playBell(media_file, volume);
}
}
//----------------------------------------------------------------
// Boilerplate/Noise

View File

@@ -36,7 +36,6 @@ const Window = @import("window.zig").Window;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const SplitTree = @import("split_tree.zig").SplitTree;
const i18n = @import("../../../os/i18n.zig");
const media = @import("../media.zig");
const global = @import("../../../global.zig");
const gtk_version = @import("../gtk_version.zig");
@@ -677,12 +676,6 @@ pub const Surface = extern struct {
// false by a parent widget.
bell_ringing: bool = false,
// The audio bell's MediaFile, reused across bells so we don't leak a
// GStreamer pipeline (and its GL threads) on every ring. Built lazily
// on the first audio bell and rebuilt when `bell-audio-path` changes;
// unref'd on dispose. See ringBell and media.zig.
bell_media: ?*gtk.MediaFile = null,
/// True if this surface is in an error state. This is currently
/// a simple boolean with no additional information on WHAT the
/// error state is, because we don't yet need it or use it. For now,
@@ -1880,11 +1873,6 @@ pub const Surface = extern struct {
priv.config = null;
}
if (priv.bell_media) |v| {
v.unref();
priv.bell_media = null;
}
if (priv.vadj_signal_group) |group| {
group.setTarget(null);
group.as(gobject.Object).unref();
@@ -2544,8 +2532,6 @@ pub const Surface = extern struct {
/// Handle bell features that need to happen every time a BEL is received
/// Currently this is audio and system but this could change in the future.
fn ringBell(self: *Self) void {
const priv = self.private();
// Emit the signal
signals.bell.impl.emit(
self,
@@ -2557,33 +2543,7 @@ pub const Surface = extern struct {
// Activate actions if they exist
_ = self.as(gtk.Widget).activateAction("tab.ring-bell", null);
_ = self.as(gtk.Widget).activateAction("win.ring-bell", null);
const config = if (priv.config) |c| c.get() else return;
// Do our sound
if (config.@"bell-features".audio) audio: {
const config_path = config.@"bell-audio-path" orelse break :audio;
const path, const required = switch (config_path) {
.optional => |path| .{ path, false },
.required => |path| .{ path, true },
};
const volume = std.math.clamp(
config.@"bell-audio-volume",
0.0,
1.0,
);
// Reuse one MediaFile per surface (rebuilt only when the path
// changes) so each bell replays the same pipeline instead of
// leaking a fresh one. Assign unconditionally: bellMediaFile frees
// any stale MediaFile and returns the current slot value (possibly
// null if the path is now inaccessible), so priv.bell_media never
// dangles.
priv.bell_media = media.bellMediaFile(priv.bell_media, path, required);
const media_file = priv.bell_media orelse break :audio;
media.playBell(media_file, volume);
}
_ = self.as(gtk.Widget).activateAction("app.ring-bell", null);
}
//---------------------------------------------------------------