stevee

My wayland statusbar
git clone git://gtms.dev/stevee
Log | Files | Refs | README | LICENSE

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 }