Pulse.zig (5973B)
1 const std = @import("std"); 2 3 const pulse = @cImport(@cInclude("pulse/pulseaudio.h")); 4 5 const Module = @import("../Modules.zig").Module; 6 const Event = @import("../Loop.zig").Event; 7 const render = @import("../render.zig"); 8 const utils = @import("../utils.zig"); 9 const Pulse = @This(); 10 11 const state = &@import("root").state; 12 13 fd: std.posix.fd_t, 14 mainloop: *pulse.pa_threaded_mainloop, 15 api: *pulse.pa_mainloop_api, 16 context: *pulse.pa_context, 17 // owned by pulse api 18 sink_name: []const u8, 19 sink_is_running: bool, 20 volume: u8, 21 muted: bool, 22 23 pub fn init() !Pulse { 24 // create descriptor for poll in Loop 25 const efd = efd: { 26 const fd = try std.posix.eventfd(0, std.os.linux.EFD.NONBLOCK); 27 break :efd @as(std.posix.fd_t, @intCast(fd)); 28 }; 29 30 // setup pulseaudio api 31 const mainloop = pulse.pa_threaded_mainloop_new() orelse { 32 return error.InitFailed; 33 }; 34 const api = pulse.pa_threaded_mainloop_get_api(mainloop); 35 const context = pulse.pa_context_new(api, "stevee") orelse { 36 return error.InitFailed; 37 }; 38 const connected = pulse.pa_context_connect(context, null, pulse.PA_CONTEXT_NOFAIL, null); 39 if (connected < 0) return error.InitFailed; 40 41 return Pulse{ 42 .fd = efd, 43 .mainloop = mainloop, 44 .api = api, 45 .context = context, 46 .sink_name = "", 47 .sink_is_running = false, 48 .volume = 0, 49 .muted = false, 50 }; 51 } 52 53 pub fn deinit(self: *Pulse) void { 54 if (self.api.quit) |quit| quit(self.api, 0); 55 pulse.pa_threaded_mainloop_stop(self.mainloop); 56 pulse.pa_threaded_mainloop_free(self.mainloop); 57 } 58 59 pub fn start(self: *Pulse) !void { 60 pulse.pa_context_set_state_callback( 61 self.context, 62 contextStateCallback, 63 @as(*anyopaque, @ptrCast(self)), 64 ); 65 const started = pulse.pa_threaded_mainloop_start(self.mainloop); 66 if (started < 0) return error.StartFailed; 67 } 68 69 pub fn refresh(self: *Pulse) !void { 70 var data = std.mem.zeroes([8]u8); 71 _ = try std.posix.read(self.fd, &data); 72 73 for (state.wayland.monitors.items) |monitor| { 74 if (monitor.bar) |bar| { 75 if (bar.configured) { 76 render.renderClock(bar) catch continue; 77 render.renderModules(bar) catch continue; 78 bar.clock.surface.commit(); 79 bar.modules.surface.commit(); 80 bar.background.surface.commit(); 81 } 82 } 83 } 84 } 85 86 pub fn print(self: *Pulse, writer: anytype) !void { 87 if (self.muted) { 88 try writer.print(" ", .{}); 89 } else { 90 try writer.print(" {d}% ", .{self.volume}); 91 } 92 } 93 94 export fn contextStateCallback( 95 ctx: ?*pulse.pa_context, 96 self_opaque: ?*anyopaque, 97 ) void { 98 const self = utils.cast(Pulse)(self_opaque.?); 99 100 const ctx_state = pulse.pa_context_get_state(ctx); 101 switch (ctx_state) { 102 pulse.PA_CONTEXT_READY => { 103 _ = pulse.pa_context_get_server_info( 104 ctx, 105 serverInfoCallback, 106 self_opaque, 107 ); 108 pulse.pa_context_set_subscribe_callback( 109 ctx, 110 subscribeCallback, 111 self_opaque, 112 ); 113 const mask = pulse.PA_SUBSCRIPTION_MASK_SERVER | 114 pulse.PA_SUBSCRIPTION_MASK_SINK | 115 pulse.PA_SUBSCRIPTION_MASK_SINK_INPUT | 116 pulse.PA_SUBSCRIPTION_MASK_SOURCE | 117 pulse.PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT; 118 _ = pulse.pa_context_subscribe(ctx, mask, null, null); 119 }, 120 pulse.PA_CONTEXT_TERMINATED, pulse.PA_CONTEXT_FAILED => { 121 std.log.info("pulse: restarting", .{}); 122 self.deinit(); 123 self.* = Pulse.init() catch return; 124 std.log.info("pulse: restarted", .{}); 125 }, 126 else => {}, 127 } 128 } 129 130 export fn serverInfoCallback( 131 ctx: ?*pulse.pa_context, 132 info: ?*const pulse.pa_server_info, 133 self_opaque: ?*anyopaque, 134 ) void { 135 const self = utils.cast(Pulse)(self_opaque.?); 136 137 self.sink_name = std.mem.span(info.?.default_sink_name); 138 self.sink_is_running = true; 139 std.log.info("pulse: sink set to {s}", .{self.sink_name}); 140 141 _ = pulse.pa_context_get_sink_info_list(ctx, sinkInfoCallback, self_opaque); 142 } 143 144 export fn subscribeCallback( 145 ctx: ?*pulse.pa_context, 146 event_type: pulse.pa_subscription_event_type_t, 147 index: u32, 148 self_opaque: ?*anyopaque, 149 ) void { 150 const operation = event_type & pulse.PA_SUBSCRIPTION_EVENT_TYPE_MASK; 151 if (operation != pulse.PA_SUBSCRIPTION_EVENT_CHANGE) return; 152 153 const facility = event_type & pulse.PA_SUBSCRIPTION_EVENT_FACILITY_MASK; 154 if (facility == pulse.PA_SUBSCRIPTION_EVENT_SINK) { 155 _ = pulse.pa_context_get_sink_info_by_index( 156 ctx, 157 index, 158 sinkInfoCallback, 159 self_opaque, 160 ); 161 } 162 } 163 164 export fn sinkInfoCallback( 165 _: ?*pulse.pa_context, 166 maybe_info: ?*const pulse.pa_sink_info, 167 _: c_int, 168 self_opaque: ?*anyopaque, 169 ) void { 170 const self = utils.cast(Pulse)(self_opaque.?); 171 const info = maybe_info orelse return; 172 173 const sink_name = std.mem.span(info.name); 174 const is_current = std.mem.eql(u8, self.sink_name, sink_name); 175 const is_running = info.state == pulse.PA_SINK_RUNNING; 176 177 if (is_current) self.sink_is_running = is_running; 178 179 if (!self.sink_is_running and is_running) { 180 self.sink_name = sink_name; 181 self.sink_is_running = true; 182 std.log.info("pulse: sink set to {s}", .{sink_name}); 183 } 184 185 self.volume = volume: { 186 const avg = pulse.pa_cvolume_avg(&info.volume); 187 const norm = @as(f64, @floatFromInt(pulse.PA_VOLUME_NORM)); 188 const ratio = 100 * @as(f64, @floatFromInt(avg)) / norm; 189 break :volume @as(u8, @intFromFloat(@round(ratio))); 190 }; 191 self.muted = info.mute != 0; 192 193 const increment = std.mem.asBytes(&@as(u64, 1)); 194 _ = std.posix.write(self.fd, increment) catch return; 195 }