Skip to content

Commit e1fc390

Browse files
authored
GTK: move audio bell processing to the application (ghostty-org#13657)
This fixes ghostty-org#13647 by using at most one GStreamer thread per application. This was previously addressed in ghostty-org#12815 which used at most one GStreamer thread per surface. Originally discussed in ghostty-org#12808.
2 parents 8eecb8f + f0f3f4d commit e1fc390

2 files changed

Lines changed: 48 additions & 41 deletions

File tree

src/apprt/gtk/class/application.zig

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseCo
4545
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
4646
const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts;
4747
const OpenURI = @import("../portal.zig").OpenURI;
48+
const media = @import("../media.zig");
4849

4950
const log = std.log.scoped(.gtk_ghostty_application);
5051

@@ -223,6 +224,12 @@ pub const Application = extern struct {
223224

224225
open_uri: OpenURI = undefined,
225226

227+
// The audio bell's MediaFile, reused across bells so we don't leak a
228+
// GStreamer pipeline (and its GL threads) on every ring. Built lazily
229+
// on the first audio bell and rebuilt when `bell-audio-path` changes;
230+
// unref'd on dispose. See ringBell and media.zig.
231+
bell_media: ?*gtk.MediaFile = null,
232+
226233
pub var offset: c_int = 0;
227234
};
228235

@@ -1478,6 +1485,7 @@ pub const Application = extern struct {
14781485
.init("quit", actionQuit, null),
14791486
.init("reload-config", actionReloadConfig, null),
14801487
.init("toggle-quick-terminal", actionToggleQuickTerminal, null),
1488+
.init("ring-bell", actionRingBell, null),
14811489
};
14821490

14831491
ext.actions.add(Self, self, &actions);
@@ -1549,6 +1557,11 @@ pub const Application = extern struct {
15491557
priv.signal_source = null;
15501558
}
15511559

1560+
if (priv.bell_media) |v| {
1561+
v.unref();
1562+
priv.bell_media = null;
1563+
}
1564+
15521565
gobject.Object.virtual_methods.dispose.call(
15531566
Class.parent,
15541567
self.as(Parent),
@@ -1948,6 +1961,40 @@ pub const Application = extern struct {
19481961
);
19491962
}
19501963

1964+
pub fn actionRingBell(
1965+
_: *gio.SimpleAction,
1966+
_: ?*glib.Variant,
1967+
self: *Self,
1968+
) callconv(.c) void {
1969+
const priv: *Private = self.private();
1970+
const config = priv.config.get();
1971+
1972+
// Do our sound
1973+
if (config.@"bell-features".audio) audio: {
1974+
const config_path = config.@"bell-audio-path" orelse break :audio;
1975+
const path, const required = switch (config_path) {
1976+
.optional => |path| .{ path, false },
1977+
.required => |path| .{ path, true },
1978+
};
1979+
1980+
const volume = std.math.clamp(
1981+
config.@"bell-audio-volume",
1982+
0.0,
1983+
1.0,
1984+
);
1985+
1986+
// Reuse one MediaFile per application (rebuilt only when the path
1987+
// changes) so each bell replays the same pipeline instead of
1988+
// leaking a fresh one. Assign unconditionally: bellMediaFile frees
1989+
// any stale MediaFile and returns the current slot value (possibly
1990+
// null if the path is now inaccessible), so priv.bell_media never
1991+
// dangles.
1992+
priv.bell_media = media.bellMediaFile(priv.bell_media, path, required);
1993+
const media_file = priv.bell_media orelse break :audio;
1994+
media.playBell(media_file, volume);
1995+
}
1996+
}
1997+
19511998
//----------------------------------------------------------------
19521999
// Boilerplate/Noise
19532000

src/apprt/gtk/class/surface.zig

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const Window = @import("window.zig").Window;
3636
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
3737
const SplitTree = @import("split_tree.zig").SplitTree;
3838
const i18n = @import("../../../os/i18n.zig");
39-
const media = @import("../media.zig");
4039
const global = @import("../../../global.zig");
4140
const gtk_version = @import("../gtk_version.zig");
4241

@@ -677,12 +676,6 @@ pub const Surface = extern struct {
677676
// false by a parent widget.
678677
bell_ringing: bool = false,
679678

680-
// The audio bell's MediaFile, reused across bells so we don't leak a
681-
// GStreamer pipeline (and its GL threads) on every ring. Built lazily
682-
// on the first audio bell and rebuilt when `bell-audio-path` changes;
683-
// unref'd on dispose. See ringBell and media.zig.
684-
bell_media: ?*gtk.MediaFile = null,
685-
686679
/// True if this surface is in an error state. This is currently
687680
/// a simple boolean with no additional information on WHAT the
688681
/// error state is, because we don't yet need it or use it. For now,
@@ -1880,11 +1873,6 @@ pub const Surface = extern struct {
18801873
priv.config = null;
18811874
}
18821875

1883-
if (priv.bell_media) |v| {
1884-
v.unref();
1885-
priv.bell_media = null;
1886-
}
1887-
18881876
if (priv.vadj_signal_group) |group| {
18891877
group.setTarget(null);
18901878
group.as(gobject.Object).unref();
@@ -2544,8 +2532,6 @@ pub const Surface = extern struct {
25442532
/// Handle bell features that need to happen every time a BEL is received
25452533
/// Currently this is audio and system but this could change in the future.
25462534
fn ringBell(self: *Self) void {
2547-
const priv = self.private();
2548-
25492535
// Emit the signal
25502536
signals.bell.impl.emit(
25512537
self,
@@ -2557,33 +2543,7 @@ pub const Surface = extern struct {
25572543
// Activate actions if they exist
25582544
_ = self.as(gtk.Widget).activateAction("tab.ring-bell", null);
25592545
_ = self.as(gtk.Widget).activateAction("win.ring-bell", null);
2560-
2561-
const config = if (priv.config) |c| c.get() else return;
2562-
2563-
// Do our sound
2564-
if (config.@"bell-features".audio) audio: {
2565-
const config_path = config.@"bell-audio-path" orelse break :audio;
2566-
const path, const required = switch (config_path) {
2567-
.optional => |path| .{ path, false },
2568-
.required => |path| .{ path, true },
2569-
};
2570-
2571-
const volume = std.math.clamp(
2572-
config.@"bell-audio-volume",
2573-
0.0,
2574-
1.0,
2575-
);
2576-
2577-
// Reuse one MediaFile per surface (rebuilt only when the path
2578-
// changes) so each bell replays the same pipeline instead of
2579-
// leaking a fresh one. Assign unconditionally: bellMediaFile frees
2580-
// any stale MediaFile and returns the current slot value (possibly
2581-
// null if the path is now inaccessible), so priv.bell_media never
2582-
// dangles.
2583-
priv.bell_media = media.bellMediaFile(priv.bell_media, path, required);
2584-
const media_file = priv.bell_media orelse break :audio;
2585-
media.playBell(media_file, volume);
2586-
}
2546+
_ = self.as(gtk.Widget).activateAction("app.ring-bell", null);
25872547
}
25882548

25892549
//---------------------------------------------------------------

0 commit comments

Comments
 (0)