stevee

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

commit 7e2284dde1e4abfd2bbadc44b7c0b83a68e2ad6b
parent f2b74950b7431f6024814ff54cbc0949e29e9a5d
Author: Andrea Feletto <andrea@andreafeletto.com>
Date:   Wed, 27 Apr 2022 13:36:40 +0200

replace alsa with pulseaudio

Diffstat:
MREADME.md | 7++++---
Mbuild.zig | 2+-
Msrc/Modules.zig | 2+-
Msrc/main.zig | 6+++---
Dsrc/modules/Alsa.zig | 150-------------------------------------------------------------------------------
Asrc/modules/Pulse.zig | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 201 insertions(+), 158 deletions(-)

diff --git a/README.md b/README.md @@ -3,12 +3,11 @@ levee is a statusbar for the [river] wayland compositor, written in [zig] without any UI toolkit. It currently provides full support for workspace tags -and displays battery capacity and screen brightness. +and displays pulseaudio volume, battery capacity and screen brightness. Some important things are not implemented yet: * configuration via config file -* volume (alsa/pulseaudio/pipewire) module * cpu module ## Build @@ -24,7 +23,7 @@ zig build -Drelease-safe --prefix ~/.local install Add the following toward the end of `$XDG_CONFIG_HOME/river/init`: ``` -riverctl spawn levee -m backlight -m battery +riverctl spawn levee -m pulse -m backlight -m battery ``` ## Dependencies @@ -33,6 +32,7 @@ riverctl spawn levee -m backlight -m battery * [wayland] 1.20.0 * [pixman] 0.40.0 * [fcft] 3.0.1 +* [libpulse] 15.0 ## Contributing @@ -48,6 +48,7 @@ or learn about it following this excellent [tutorial]. [wayland]: https://wayland.freedesktop.org/ [pixman]: http://pixman.org/ [fcft]: https://codeberg.org/dnkl/fcft/ +[libpulse]: https://www.freedesktop.org/wiki/Software/PulseAudio/ [mailing list]: https://lists.sr.ht/~andreafeletto/public-inbox [issue tracker]: https://todo.sr.ht/~andreafeletto/levee [web interface]: https://git.sr.ht/~andreafeletto/levee/send-email diff --git a/build.zig b/build.zig @@ -49,10 +49,10 @@ pub fn build(b: *std.build.Builder) void { exe.addPackage(wayland); exe.linkLibC(); - exe.linkSystemLibrary("alsa"); exe.linkSystemLibrary("fcft"); exe.linkSystemLibrary("libudev"); exe.linkSystemLibrary("pixman-1"); + exe.linkSystemLibrary("libpulse"); exe.linkSystemLibrary("wayland-client"); exe.install(); diff --git a/src/Modules.zig b/src/Modules.zig @@ -8,9 +8,9 @@ const Modules = @This(); state: *State, modules: ArrayList(Module), -pub const Alsa = @import("modules/Alsa.zig"); pub const Backlight = @import("modules/Backlight.zig"); pub const Battery = @import("modules/Battery.zig"); +pub const Pulse = @import("modules/Pulse.zig"); pub const Module = struct { impl: *anyopaque, diff --git a/src/main.zig b/src/main.zig @@ -49,12 +49,12 @@ pub fn main() anyerror!void { // modules for (args.options("--module")) |module_name| { - if (mem.eql(u8, module_name, "alsa")) { - try state.modules.register(Modules.Alsa); - } else if (mem.eql(u8, module_name, "backlight")) { + if (mem.eql(u8, module_name, "backlight")) { try state.modules.register(Modules.Backlight); } else if (mem.eql(u8, module_name, "battery")) { try state.modules.register(Modules.Battery); + } else if (mem.eql(u8, module_name, "pulse")) { + try state.modules.register(Modules.Pulse); } else { std.log.err("unknown module: {s}", .{ module_name }); os.exit(1); diff --git a/src/modules/Alsa.zig b/src/modules/Alsa.zig @@ -1,150 +0,0 @@ -const std = @import("std"); -const fmt = std.fmt; -const math = std.math; -const mem = std.mem; -const os = std.os; - -const alsa = @cImport(@cInclude("alsa/asoundlib.h")); - -const Module = @import("../Modules.zig").Module; -const Event = @import("../Loop.zig").Event; -const render = @import("../render.zig"); -const State = @import("../main.zig").State; -const utils = @import("../utils.zig"); -const Alsa = @This(); - -state: *State, -devices: DeviceList, - -const Device = struct { - ctl: *alsa.snd_ctl_t, - name: []const u8, -}; - -const DeviceList = std.ArrayList(Device); - -pub fn create(state: *State) !*Alsa { - const self = try state.gpa.create(Alsa); - self.* = .{ - .state = state, - .devices = DeviceList.init(state.gpa), - }; - - var card: i32 = -1; - while(alsa.snd_card_next(&card) >= 0 and card >= 0) { - const name = try fmt.allocPrintZ(state.gpa, "hw:{d}", .{ card }); - - var ctl: ?*alsa.snd_ctl_t = null; - _ = alsa.snd_ctl_open(&ctl, name.ptr, alsa.SND_CTL_READONLY); - _ = alsa.snd_ctl_subscribe_events(ctl, 1); - - try self.devices.append(.{ .ctl = ctl.?, .name = name }); - } - - return self; -} - -pub fn module(self: *Alsa) !Module { - return Module{ - .impl = @ptrCast(*anyopaque, self), - .funcs = .{ - .getEvent = getEvent, - .print = print, - .destroy = destroy, - }, - }; -} - -fn getEvent(self_opaque: *anyopaque) !Event { - const self = utils.cast(Alsa)(self_opaque); - - var fd = mem.zeroes(alsa.pollfd); - const device = &self.devices.items[0]; - _ = alsa.snd_ctl_poll_descriptors(device.ctl, &fd, 1); - - return Event{ - .fd = @bitCast(os.pollfd, fd), - .data = self_opaque, - .callbackIn = callbackIn, - .callbackOut = Event.noop, - }; -} - -fn print(self_opaque: *anyopaque, writer: Module.StringWriter) !void { - const self = utils.cast(Alsa)(self_opaque); - _ = self; - - var handle: ?*alsa.snd_mixer_t = null; - _ = alsa.snd_mixer_open(&handle, 0); - _ = alsa.snd_mixer_attach(handle, "default"); - _ = alsa.snd_mixer_selem_register(handle, null, null); - _ = alsa.snd_mixer_load(handle); - - var sid: ?*alsa.snd_mixer_selem_id_t = null; - _ = alsa.snd_mixer_selem_id_malloc(&sid); - defer alsa.snd_mixer_selem_id_free(sid); - alsa.snd_mixer_selem_id_set_index(sid, 0); - alsa.snd_mixer_selem_id_set_name(sid, "Master"); - const elem = alsa.snd_mixer_find_selem(handle, sid); - - var unmuted: i32 = 0; - _ = alsa.snd_mixer_selem_get_playback_switch( - elem, - alsa.SND_MIXER_SCHN_MONO, - &unmuted, - ); - if (unmuted == 0) { - return writer.print(" 🔇 ", .{}); - } - - var min: i64 = 0; - var max: i64 = 0; - _ = alsa.snd_mixer_selem_get_playback_volume_range(elem, &min, &max); - - var volume: i64 = 0; - _ = alsa.snd_mixer_selem_get_playback_volume( - elem, - alsa.SND_MIXER_SCHN_MONO, - &volume, - ); - - const percent = percent: { - var x = @intToFloat(f64, volume) / @intToFloat(f64, max); - x = math.tanh(math.sqrt(x) * 0.65) * 180.0; - break :percent @floatToInt(u8, @round(x)); - }; - return writer.print("🔊 {d}%", .{ percent }); -} - -fn callbackIn(self_opaque: *anyopaque) error{Terminate}!void { - const self = utils.cast(Alsa)(self_opaque); - - var event: ?*alsa.snd_ctl_event_t = null; - _ = alsa.snd_ctl_event_malloc(&event); - defer alsa.snd_ctl_event_free(event); - - const device = &self.devices.items[0]; - _ = alsa.snd_ctl_read(device.ctl, event); - - for (self.state.wayland.monitors.items) |monitor| { - if (monitor.bar) |bar| { - if (bar.configured) { - render.renderClock(bar) catch continue; - render.renderModules(bar) catch continue; - bar.clock.surface.commit(); - bar.modules.surface.commit(); - bar.background.surface.commit(); - } - } - } -} - -fn destroy(self_opaque: *anyopaque) void { - const self = utils.cast(Alsa)(self_opaque); - - for (self.devices.items) |*device| { - self.state.gpa.free(device.name); - } - self.devices.deinit(); - self.state.gpa.destroy(self); -} diff --git a/src/modules/Pulse.zig b/src/modules/Pulse.zig @@ -0,0 +1,192 @@ +const std = @import("std"); +const mem = std.mem; +const os = std.os; + +const pulse = @cImport(@cInclude("pulse/pulseaudio.h")); + +const Module = @import("../Modules.zig").Module; +const Event = @import("../Loop.zig").Event; +const render = @import("../render.zig"); +const State = @import("../main.zig").State; +const utils = @import("../utils.zig"); +const Pulse = @This(); + +state: *State, +mainloop: *pulse.pa_threaded_mainloop, +api: *pulse.pa_mainloop_api, +context: *pulse.pa_context, +fd: os.fd_t, +sink_name: []const u8, +volume: u8, +muted: bool, + +pub fn create(state: *State) !*Pulse { + const self = try state.gpa.create(Pulse); + self.state = state; + + self.mainloop = pulse.pa_threaded_mainloop_new() orelse { + return error.InitFailed; + }; + self.api = pulse.pa_threaded_mainloop_get_api(self.mainloop); + self.context = pulse.pa_context_new(self.api, "levee") orelse { + return error.InitFailed; + }; + const connected = pulse.pa_context_connect( + self.context, + null, + pulse.PA_CONTEXT_NOFAIL, + null, + ); + if (connected < 0) return error.InitFailed; + pulse.pa_context_set_state_callback( + self.context, + contextStateCallback, + @ptrCast(*anyopaque, self), + ); + const started = pulse.pa_threaded_mainloop_start(self.mainloop); + if (started < 0) return error.InitFailed; + + const fd = try os.eventfd(0, os.linux.EFD.NONBLOCK); + self.fd = @intCast(os.fd_t, fd); + return self; +} + +pub fn module(self: *Pulse) !Module { + return Module{ + .impl = @ptrCast(*anyopaque, self), + .funcs = .{ + .getEvent = getEvent, + .print = print, + .destroy = destroy, + }, + }; +} + +fn getEvent(self_opaque: *anyopaque) !Event { + const self = utils.cast(Pulse)(self_opaque); + + return Event{ + .fd = .{ .fd = self.fd, .events = os.POLL.IN, .revents = undefined }, + .data = self_opaque, + .callbackIn = callbackIn, + .callbackOut = Event.noop, + }; +} + +fn print(self_opaque: *anyopaque, writer: Module.StringWriter) !void { + const self = utils.cast(Pulse)(self_opaque); + + return writer.print("🔊 {d}%", .{ self.volume }); +} + +fn callbackIn(self_opaque: *anyopaque) error{Terminate}!void { + const self = utils.cast(Pulse)(self_opaque); + + for (self.state.wayland.monitors.items) |monitor| { + if (monitor.bar) |bar| { + if (bar.configured) { + render.renderClock(bar) catch continue; + render.renderModules(bar) catch continue; + bar.clock.surface.commit(); + bar.modules.surface.commit(); + bar.background.surface.commit(); + } + } + } +} + +fn destroy(self_opaque: *anyopaque) void { + const self = utils.cast(Pulse)(self_opaque); + + self.api.quit.?(self.api, 0); + pulse.pa_threaded_mainloop_stop(self.mainloop); + pulse.pa_threaded_mainloop_free(self.mainloop); + self.state.gpa.destroy(self); +} + +export fn contextStateCallback( + ctx: ?*pulse.pa_context, + self_opaque: ?*anyopaque, +) void { + const self = utils.cast(Pulse)(self_opaque.?); + + const ctx_state = pulse.pa_context_get_state(ctx); + switch (ctx_state) { + pulse.PA_CONTEXT_READY => { + _ = pulse.pa_context_get_server_info( + ctx, + serverInfoCallback, + self_opaque, + ); + pulse.pa_context_set_subscribe_callback( + ctx, + subscribeCallback, + self_opaque, + ); + const mask = pulse.PA_SUBSCRIPTION_MASK_SERVER | + pulse.PA_SUBSCRIPTION_MASK_SINK | + pulse.PA_SUBSCRIPTION_MASK_SINK_INPUT | + pulse.PA_SUBSCRIPTION_MASK_SOURCE | + pulse.PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT; + _ = pulse.pa_context_subscribe(ctx, mask, null, null); + }, + pulse.PA_CONTEXT_TERMINATED => self.api.quit.?(self.api, 0), + pulse.PA_CONTEXT_FAILED => self.api.quit.?(self.api, 0), + else => {}, + } +} + +export fn serverInfoCallback( + ctx: ?*pulse.pa_context, + info: ?*const pulse.pa_server_info, + self_opaque: ?*anyopaque, +) void { + const self = utils.cast(Pulse)(self_opaque.?); + + self.sink_name = mem.span(info.?.default_sink_name); + _ = pulse.pa_context_get_sink_info_list(ctx, sinkInfoCallback, self_opaque); +} + +export fn subscribeCallback( + ctx: ?*pulse.pa_context, + event_type: pulse.pa_subscription_event_type_t, + index: u32, + self_opaque: ?*anyopaque, +) void { + const operation = event_type & pulse.PA_SUBSCRIPTION_EVENT_TYPE_MASK; + if (operation != pulse.PA_SUBSCRIPTION_EVENT_CHANGE) return; + + const facility = event_type & pulse.PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + if (facility == pulse.PA_SUBSCRIPTION_EVENT_SINK) { + _ = pulse.pa_context_get_sink_info_by_index( + ctx, + index, + sinkInfoCallback, + self_opaque, + ); + } +} + +export fn sinkInfoCallback( + _: ?*pulse.pa_context, + maybe_info: ?*const pulse.pa_sink_info, + _: c_int, + self_opaque: ?*anyopaque, +) void { + const self = utils.cast(Pulse)(self_opaque.?); + const info = maybe_info orelse return; + + const sink_name = mem.span(info.name); + if (!mem.eql(u8, self.sink_name, sink_name)) return; + + self.volume = volume: { + const avg = pulse.pa_cvolume_avg(&info.volume); + const norm = @intToFloat(f64, pulse.PA_VOLUME_NORM); + const ratio = 100 * @intToFloat(f64, avg) / norm; + break :volume @floatToInt(u8, @round(ratio)); + }; + self.muted = info.mute != 0; + + const increment = mem.asBytes(&@as(u64, 1)); + _ = os.write(self.fd, increment) catch return; +}