10BC0 services/oss: add freebsd sound module by charlesrocket · Pull Request #474 · quickshell-mirror/quickshell · GitHub
[go: up one dir, main page]

Skip to content

Conversation

@charlesrocket
Copy link
@charlesrocket charlesrocket commented Jan 11, 2026

This adds OSS support on FBSD targets. Tested with Realtek ALC257 (Analog 2.0+HP/2.0)> on hdaa0 (play/rec) (default), T480 (FreeBSD 15.0-RELEASE-p1 GENERIC).

// audio
RowLayout {
    spacing: 8

    // mic
    Audio {
        mic: true
    }

    // speaker
    Audio {}
}

Example component:

// Audio
import Quickshell.Services.OSS

import QtQuick
import QtQuick.Layouts

Item {
    id: root
    Layout.alignment: Qt.AlignVCenter

    property bool mic: false
    property int deviceId: -1
    property int slideDuration: 250
    property int barLen: 80
    property int iconSize: 16
    property var colNormal: "#b0b4bc"
    property var colMuted: "#4e4e4e"

    implicitWidth: iconText.width + (hoverDetector.containsMouse ? volumeSliderContainer.width + 8 : 0)
    implicitHeight: iconText.height

    Behavior on implicitWidth {
        NumberAnimation {
            duration: root.slideDuration
            easing.type: Easing.OutCubic
        }
    }

    readonly property var device: {
        if (!OSS.devices)
            return null;

        if (root.deviceId >= 0) {
            for (var i = 0; i < OSS.devices.length; i++) {
                if (OSS.devices[i].deviceId === root.deviceId) {
                    return OSS.devices[i];
                }
            }

            return null;
        }

        return OSS.defaultDevice;
    }

    property var control: {
        if (!device || !device.controls)
            return null;

        if (root.mic) {
            for (var i = 0; i < device.controls.length; i++) {
                var devCtl = device.controls[i];
                var trimmedName = devCtl.name.trim().toLowerCase();

                if (trimmedName === "rec")
                    return devCtl;
            }

            for (var i = 0; i < device.controls.length; i++) {
                var devCtl = device.controls[i];
                var trimmedName = devCtl.name.trim().toLowerCase();

                if (trimmedName === "mic")
                    return devCtl;
            }

            return null;
        }

        return device.master;
    }

    readonly property int volume: control ? control.left : 0
    readonly property bool muted: control ? control.muted : false

    function getVolumeIcon(vol, isMuted) {
        if (!mic) {
            if (isMuted || vol === 0)
                return "󰝟";

            return "󰕾";
        } else {
            if (isMuted || vol === 0)
                return "󰍭";

            return "󰍬";
        }
    }

    Timer {
        interval: 200
        running: hoverDetector.containsMouse
        repeat: true

        onTriggered: {
            if (root.device) {
                OSS.refresh();
            }
        }
    }

    Item {
        id: iconContainer
        anchors.left: parent.left
        anchors.verticalCenter: parent.verticalCenter
        width: iconText.width
        height: iconText.height

        Text {
            id: iconText
            anchors.centerIn: parent
            text: root.getVolumeIcon(root.volume, root.muted)
            color: root.muted ? root.colMuted : root.colNormal
            font.family: "Symbols Nerd Font"
            font.pixelSize: root.iconSize
            font.bold: true

            Behavior on color {
                ColorAnimation {
                    duration: 150
                    easing.type: Easing.OutCubic
                }
            }

            Behavior on opacity {
                NumberAnimation {
                    duration: 100
                    easing.type: Easing.InOutQuad
                }
            }

            Behavior on scale {
                NumberAnimation {
                    duration: 100
                    easing.type: Easing.InOutQuad
                }
            }

            onTextChanged: {
                scaleAnimation.restart();
            }

            SequentialAnimation {
                id: scaleAnimation

                NumberAnimation {
                    target: iconText
                    property: "scale"
                    to: 0.8
                    duration: 75
                    easing.type: Easing.InQuad
                }

                NumberAnimation {
                    target: iconText
                    property: "scale"
                    to: 1.0
                    duration: 75
                    easing.type: Easing.OutQuad
                }
            }
        }
    }

    Item {
        id: volumeSliderContainer
        anchors.left: iconContainer.right
        anchors.leftMargin: 8
        anchors.verticalCenter: parent.verticalCenter
        width: root.barLen
        height: iconText.height
        opacity: hoverDetector.containsMouse ? 1 : 0
        scale: hoverDetector.containsMouse ? 1 : 0
        transformOrigin: Item.Left
        visible: opacity > 0

        Behavior on opacity {
            NumberAnimation {
                duration: root.slideDuration
                easing.type: Easing.OutCubic
            }
        }

        Behavior on scale {
            NumberAnimation {
                duration: root.slideDuration
                easing.type: Easing.OutCubic
            }
        }

        Rectangle {
            anchors.fill: parent
            color: "transparent"

            Rectangle {
                id: sliderTrack
                anchors.verticalCenter: parent.verticalCenter
                anchors.left: parent.left
                anchors.right: parent.right
                height: 4
                radius: 6
                color: "#4e4e4e"

                Rectangle {
                    anchors.left: parent.left
                    anchors.verticalCenter: parent.verticalCenter
                    width: parent.width * (root.volume / 100)
                    height: parent.height
                    radius: parent.radius
                    color: root.muted ? root.colMuted : root.colNormal

                    Behavior on width {
                        NumberAnimation {
                            duration: 50
                            easing.type: Easing.OutQuad
                        }
                    }
                }
            }

            Rectangle {
                id: sliderHandle
                anchors.verticalCenter: parent.verticalCenter
                x: (sliderTrack.width - width) * (root.volume / 100)
                width: 12
                height: 12
                radius: 6
                color: root.muted ? root.colMuted : root.colNormal
                border.width: 2
                border.color: root.colNormal

                Behavior on x {
                    NumberAnimation {
                        duration: 50
                        easing.type: Easing.OutQuad
                    }
                }
            }

            MouseArea {
                id: sliderMouseArea
                anchors.fill: parent
                hoverEnabled: true

                function updateVolume(mouseX) {
                    if (root.control) {
                        var newVolume = Math.max(0, Math.min(100, Math.round((mouseX / width) * 100)));

                        root.control.left = newVolume;
                        root.control.right = newVolume;
                    }
                }

                onPressed: function (mouse) {
                    updateVolume(mouse.x);
                }

                onPositionChanged: function (mouse) {
                    if (pressed)
                        updateVolume(mouse.x);
                }
            }
        }
    }

    MouseArea {
        id: hoverDetector
        anchors.fill: parent
        hoverEnabled: true
        propagateComposedEvents: true

        onPressed: function (mouse) {
            mouse.accepted = false;
        }
    }

    MouseArea {
        anchors.left: parent.left
        anchors.top: parent.top
        anchors.bottom: parent.bottom
        width: iconText.width
        hoverEnabled: false

        onClicked: {
            if (root.control) {
                root.control.muted = !root.control.muted;
            }
        }
    }
}

@charlesrocket charlesrocket force-pushed the fbsd-sound branch 13 times, most recently from cb6d9a9 to 364a004 Compare January 12, 2026 15:02
@charlesrocket
Copy link
Author
charlesrocket commented Jan 12, 2026

rebased

@charlesrocket charlesrocket force-pushed the fbsd-sound branch 2 times, most recently from 6c66f91 to 5fa045d Compare January 14, 2026 15:44
@charlesrocket
Copy link
Author

dropped polling and switched to devd

@charlesrocket charlesrocket force-pushed the fbsd-sound branch 2 times, most recently from 7bd2f76 to 73e97de Compare January 16, 2026 19:58
@charlesrocket
Copy link
Author
charlesrocket commented Jan 18, 2026

Could not figure out the way to listen for mixer levels, so I just bind OSS.refresh() to volume keys. A Timer is another option. Tho maybe I can listen for headphones/jack.

@charlesrocket charlesrocket changed the title service/oss: add freebsd sound module services/oss: add freebsd sound module Jan 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0