├── .travis.yml ├── LICENSE ├── README.md ├── doc.go ├── example_test.go ├── pulse.go └── pulse_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.11 5 | - 1.10.x 6 | - 1.9 7 | - 1.8 8 | 9 | addons: 10 | apt: 11 | packages: 12 | - pulseaudio-utils 13 | 14 | before_install: 15 | - go get -u -t -v ./... 16 | 17 | script: 18 | - go test -race -coverprofile=coverage.txt -covermode=atomic 19 | 20 | after_success: 21 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2018 SQP 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pulseaudio: native pulseaudio client for go through dbus. 2 | 3 | [![Build Status](https://travis-ci.org/sqp/pulseaudio.svg?branch=master)](https://travis-ci.org/sqp/pulseaudio) 4 | [![codecov](https://codecov.io/gh/sqp/pulseaudio/branch/master/graph/badge.svg)](https://codecov.io/gh/sqp/pulseaudio) 5 | [![golangci](https://golangci.com/badges/github.com/sqp/pulseaudio.svg)](https://golangci.com/r/github.com/sqp/pulseaudio) 6 | [![Go Report Card](https://goreportcard.com/badge/sqp/pulseaudio)](https://goreportcard.com/report/sqp/pulseaudio) 7 | 8 | [![License](https://img.shields.io/:license-ISC-brightgreen.svg)](https://raw.githubusercontent.com/sqp/pulseaudio/master/LICENSE) 9 | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/sqp/pulseaudio) 10 | 11 | 12 | pulseaudio is a simple library that controls a pulseaudio server through its D-Bus interface. 13 | 14 | ### Features 15 | 16 | * Control your audio cards and streams in pure go. 17 | * Native implementation of the pulseaudio D-Bus protocol. 18 | * Small lib pretty close to the DBus API rather than trying to abstract everything. 19 | * Splitted interface to allow clients to implement only what they need. 20 | * Test coverage 86%, missing 9 lines in errors paths harder to test. 21 | * Only one dependency, the dbus library: github.com/godbus/dbus 22 | * [Permissive software licence](https://en.wikipedia.org/wiki/Permissive_software_licence): [ISC](https://raw.githubusercontent.com/sqp/pulseaudio/master/LICENSE) 23 | 24 | ### Installation 25 | 26 | This packages requires Go 1.7 (for the dbus lib). If you installed it and set up your GOPATH, just run: 27 | 28 | ``` 29 | go get -u github.com/sqp/pulseaudio 30 | ``` 31 | 32 | ### Usage 33 | 34 | The complete package documentation is available at [godoc.org](http://godoc.org/github.com/sqp/pulseaudio). 35 | See also: 36 | * [The package example](https://godoc.org/github.com/sqp/pulseaudio/#example_) with a short overview of the basic usage. 37 | * A real use in [a cairo-dock applet](https://github.com/sqp/godock/blob/master/services/Audio/audio.go). 38 | 39 | ### Note 40 | 41 | You will have to enable the dbus module of your pulseaudio server. 42 | This can now be done with ```pulseaudio.LoadModule()``` function (requires the pacmd command, in package ```pulseaudio-utils``` on debian). 43 | 44 | or as a permanent config by adding this line in ```/etc/pulse/default.pa``` 45 | ``` 46 | load-module module-dbus-protocol 47 | ``` 48 | If system-wide daemon mode is used, the file to edit is ```/etc/pulse/system.pa``` 49 | 50 | ### Evolutions 51 | 52 | * The base API has been stable for years and there's no plan to improve it for now. 53 | * A higher level API could be designed to cover simple frequent needs. 54 | Open an issue to discuss it if you want. 55 | * The lib may at some point move to a community repo. This could be an 56 | opportunity to change a little the API, so we'll need some feedback. 57 | 58 | ### Feedback 59 | 60 | Please [open an issue](https://github.com/sqp/pulseaudio/issues) or submit a pull request if: 61 | * You tried or use this library, let us know if you saw things to improve, especially in the doc if you're a native English speaker. 62 | * You want your code to be listed as example. -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package pulseaudio controls a pulseaudio server through its Dbus interface. 3 | 4 | This is a pure go binding for the pulseaudio Dbus interface. 5 | 6 | Note that you will have to enable the dbus module of your pulseaudio server. 7 | This can now be done with the LoadModule function. 8 | 9 | or by adding this line in /etc/pulse/default.pa 10 | load-module module-dbus-protocol 11 | 12 | (if system-wide daemon is used, instead edit /etc/pulse/system.pa ) 13 | 14 | 15 | Registering methods to listen to signals 16 | 17 | Create a type that declares any methods matching the pulseaudio interface. 18 | Methods will be detected when you register the object: Register(myobject) 19 | Methods will start to receive events when you start the loop: Listen() 20 | 21 | Instead of declaring a big interface and forcing clients to provide every method 22 | they don't need, this API use a flexible interface system. 23 | The callback interface is provided by a list of single method interfaces and 24 | only those needed will have to be implemented. 25 | 26 | You can register multiple clients at any time on the same pulseaudio session. 27 | This allow you to split the callback logic of your program and have some parts 28 | (un)loadable like a client GUI. 29 | 30 | See types On... for the list of callback methods that can be used. 31 | 32 | 33 | Create a client object with some callback methods to register: 34 | type Client struct { 35 | // The reference to the object isn't needed for the callbacks to work, 36 | // but it will certainly be useful at some point in your code. 37 | *pulseaudio.Client 38 | } 39 | 40 | func (cl *Client) NewPlaybackStream(path dbus.ObjectPath) { 41 | log.Println("NewPlaybackStream", path) 42 | } 43 | 44 | func (cl *Client) PlaybackStreamRemoved(path dbus.ObjectPath) { 45 | log.Println("PlaybackStreamRemoved", path) 46 | } 47 | 48 | func (cl *Client) DeviceVolumeUpdated(path dbus.ObjectPath, values []uint32) { 49 | log.Println("device volume", path, values) 50 | } 51 | 52 | func (cl *Client) StreamVolumeUpdated(path dbus.ObjectPath, values []uint32) { 53 | log.Println("stream volume", path, values) 54 | } 55 | 56 | 57 | Register your object and start the listening loop: 58 | pulse, e := pulseaudio.New() 59 | if e != nil { 60 | log.Panicln("connect", e) 61 | } 62 | 63 | client := &Client{pulse} 64 | pulse.Register(client) 65 | 66 | pulse.Listen() 67 | 68 | Get properties 69 | 70 | There are way too many properties to have a dedicated method for each of them. 71 | Object Object implementing the property you need. 72 | Core 73 | Device 74 | Stream 75 | Name Name of the property 76 | Type Type of the property. 77 | bool Bool 78 | uint32 Uint32 79 | uint64 Uint64 80 | string String 81 | ObjectPath Path 82 | []uint32 ListUint32 83 | []string ListString 84 | []ObjectPath ListPath 85 | 86 | First you need to get the object implementing the property you need. 87 | Then you will have to call the method matching the type of returned data for the 88 | property you want to get. See the example. 89 | 90 | Set properties 91 | 92 | Properties with the tag RW can also be set. 93 | 94 | Pulseaudio Dbus documentation 95 | 96 | http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/DBus/ 97 | 98 | Dbus documentation was copied to provide some useful informations here. 99 | Still valid in august 2018. 100 | 101 | Check the upstream source for updates or more informations. 102 | 103 | */ 104 | package pulseaudio 105 | 106 | import "github.com/godbus/dbus" 107 | 108 | // Core controls the pulseaudio core. 109 | // 110 | // Properties list: 111 | // Boolean 112 | // !IsLocal This per-client property can be used to find out whether the client is connected to a local server. 113 | // 114 | // Uint32 115 | // !InterfaceRevision The "major" version of the main D-Bus interface is 116 | // embedded in the interface name: org.PulseAudio.Core1. 117 | // When changes are made that break compatibility between 118 | // old clients and new servers the major version is 119 | // incremented. This property tells the "minor" version, 120 | // that is, when new features are added to the interface, 121 | // this version number is incremented so that new clients 122 | // can check if the server they talk to supports the new 123 | // features. This documentation defines revision 0. 124 | // Extensions are versioned separately (i.e. they have 125 | // their own major and minor version numbers). 126 | // !DefaultSampleFormat RW The default sample format that is used when 127 | // initializing a device and the configuration information 128 | // doesn't specify the desired sample format. 129 | // !DefaultSampleRate RW The default sample rate that is used when initializing 130 | // a device and the configuration information doesn't 131 | // specify the desired sample rate. 132 | // 133 | // String 134 | // Name The server name. At the time of writing no competing implementations 135 | // have appeared, so the expected name is "pulseaudio". 136 | // Version The server version string, for example "0.9.17". 137 | // Username The username that the server is running under. 138 | // Hostname The hostname of the machine the server is running on. 139 | // 140 | // ObjectPath 141 | // !FallbackSink RW When a new playback stream is created and there is no other 142 | // policy about to which sink the stream should be connected, 143 | // the fallback sink is selected. This property doesn't exist 144 | // if there's no sink selected as the fallback sink. 145 | // !FallbackSource RW When a new record stream is created and there is no other 146 | // policy about to which source the stream should be connected, 147 | // the fallback source is selected. This property doesn't 148 | // exist if there's no source selected as the fallback source. 149 | // !MyClient This property has a different value for each client: it 150 | // tells the reading client the client object that is 151 | // assigned to its connection. 152 | // 153 | // ListUint32 154 | // !DefaultChannels RW The default channel map that is used when initializing a 155 | // device and the configuration information doesn't specify 156 | // the desired channel map. The default channel count can be 157 | // inferred from this. The channel map is expressed as a 158 | // list of channel positions, 159 | // 160 | // ListString 161 | // Extensions All available server extension interfaces. Each extension interface 162 | // defines an unique string that clients can search from this array. 163 | // The string should contain a version part so that if backward 164 | // compatibility breaking changes are made to the interface, 165 | // old clients don't detect the new interface at all, 166 | // or both old and new interfaces can be provided. 167 | // The string is specific to the D-Bus interface of the extension, 168 | // so if an extension module offers access through both the C API 169 | // and D-Bus, the interfaces can be updated independently. 170 | // The strings are intended to follow the structure and restrictions 171 | // of D-Bus interface names, but that is not enforced. 172 | // The clients should treat the strings as opaque identifiers. 173 | // 174 | // ListPath 175 | // Cards All currently available cards. 176 | // Sinks All currently available sinks. 177 | // Sources All currently available sources. 178 | // !PlaybackStreams All current playback streams. 179 | // !RecordStreams All current record streams. 180 | // Samples All currently loaded samples. 181 | // Modules All currently loaded modules. 182 | // Clients All currently connected clients. 183 | // 184 | func (pulse *Client) Core() *Object { 185 | return NewObject(pulse.conn, DbusInterface, DbusPath) 186 | } 187 | 188 | // Device controls a pulseaudio device. 189 | // 190 | // Methods list: 191 | // Suspend Suspends or unsuspends the device. 192 | // bool: True to suspend, false to unsuspend. 193 | // !GetPortByName Find the device port with the given name. 194 | // string: Port name 195 | // out: ObjectPath: Device port object 196 | // 197 | // Properties list: 198 | // Boolean 199 | // !HasFlatVolume Whether or not the device is configured to use the 200 | // "flat volume" logic, that is, the device volume follows 201 | // the maximum volume of all connected streams. 202 | // Currently this is not implemented for sources, so for 203 | // them this property is always false. 204 | // !HasConvertibleToDecibelVolume If this is true, the volume values of the Volume property 205 | // can be converted to decibels with pa_sw_volume_to_dB(). 206 | // If you want to avoid the C API, the function does 207 | // the conversion as follows: 208 | // If input = 0, then output = -200.0 209 | // Otherwise output = 20 * log10((input / 65536)3) 210 | // Mute RW Whether or not the device is currently muted. 211 | // !HasHardwareVolume Whether or not the device volume controls the hardware volume. 212 | // !HasHardwareMute Whether or not muting the device controls the hardware mute state. 213 | // !HasDynamicLatency Whether or not the device latency can be adjusted 214 | // according to the needs of the connected streams. 215 | // !IsHardwareDevice Whether or not this device is a hardware device. 216 | // !IsNetworkDevice Whether or not this device is a network device. 217 | // 218 | // Uint32 219 | // Index The device index. Sink and source indices are separate, so it's perfectly 220 | // normal to have two devices with the same index: the other device is a 221 | // sink and the other is a source. 222 | // !SampleFormat The sample format of the device. 223 | // !SampleRate The sample rate of the device. 224 | // !BaseVolume The volume level at which the device doesn't perform any 225 | // amplification or attenuation. 226 | // !VolumeSteps If the device doesn't support arbitrary volume values, this property 227 | // tells the number of possible volume values. 228 | // Otherwise this property has value 65537. 229 | // State The current state of the device. 230 | // 231 | // Uint64 232 | // !ConfiguredLatency The latency in microseconds that device has been configured to. 233 | // Latency The length of queued audio in the device buffer. Not all devices 234 | // support latency querying; in those cases this property does not exist. 235 | // 236 | // String 237 | // Name The device name. 238 | // Driver The driver that implements the device object. 239 | // This is usually expressed as a source code file name, for example "module-alsa-card.c". 240 | // 241 | // ObjectPath 242 | // !OwnerModule The module that owns this device. It's not guaranteed that any module 243 | // claims ownership; in such case this property does not exist. 244 | // Card The card that this device belongs to. Not all devices are part of cards; 245 | // in those cases this property does not exist. 246 | // !ActivePort RW The currently active device port. 247 | // This property doesn't exist if the device does not have any ports. 248 | // 249 | // ListUint32 250 | // Channels The channel map of the device. The channel count can be inferred from this. 251 | // The channel map is expressed as a list of channel positions. 252 | // Volume RW The volume of the device. The array is matched against the Channels property: 253 | // the first array element is the volume of the first channel in the Channels 254 | // property, and so on. There are two ways to adjust the volume: 255 | // You can either adjust the overall volume by giving a single-value array, 256 | // or you can precisely control the individual channels by passing an array 257 | // containing a value for each channel. 258 | // 259 | // ListPath 260 | // Ports All available device ports. May be empty. 261 | // 262 | // 263 | // MapString 264 | // !PropertyList The device's property list. 265 | // 266 | func (pulse *Client) Device(sink dbus.ObjectPath) *Object { 267 | return NewObject(pulse.conn, DbusInterface+".Device", sink) 268 | } 269 | 270 | // Stream controls a pulseaudio stream. 271 | // 272 | // Methods list: 273 | // Kill Kills the stream. 274 | // Move Moves the stream to another device. 275 | // ObjectPath: The device to move to. 276 | // 277 | // Properties list: 278 | // 279 | // Boolean 280 | // !VolumeWritable Whether or not the Volume property can be set. Note that read-only volumes can still change, clients just can't control them. 281 | // Mute RW Whether or not the stream is currently muted. Record streams don't currently support muting, so this property exists for playback streams only for now. 282 | // 283 | // Uint32 284 | // Index The stream index. Playback and record stream indices are separate, so it's perfectly normal to have two streams with the same index: the other stream is a playback stream and the other is a record stream. 285 | // !SampleFormat The sample format of the stream. See [[Software/PulseAudio/Documentation/Developer/Clients/DBus/Enumerations]] for the list of possible values. 286 | // !SampleRate The sample rate of the stream. 287 | // 288 | // Uint64 289 | // !BufferLatency The length of buffered audio in microseconds that is not at the device yet/anymore. 290 | // !DeviceLatency The length of buffered audio in microseconds at the device. 291 | // 292 | // String 293 | // Driver The driver that implements the stream object. This is usually expressed as a source code file name, for example "protocol-native.c". 294 | // !ResampleMethod The resampling algorithm that is used to convert the stream audio data to/from the device's sample rate. 295 | // 296 | // ObjectPath 297 | // !OwnerModule The module that owns this stream. It's not guaranteed that any module claims ownership; in such case this property does not exist. 298 | // Client The client whose stream this is. Not all streams are created by clients, in those cases this property does not exist. 299 | // Device The device this stream is connected to. 300 | // 301 | // ListUint32 302 | // Channels The channel map of the stream. The channel count can be inferred from this. The channel map is expressed as a list of channel positions, see [[Software/PulseAudio/Documentation/Developer/Clients/DBus/Enumerations]] for the list of possible channel position values. 303 | // Volume RW The volume of the stream. The array is matched against the Channels property: the first array element is the volume of the first channel in the Channels property, and so on. 304 | // There are two ways to adjust the volume. You can either adjust the overall volume by giving a single-value array, or you can precisely control the individual channels by passing an array containing a value for each channel. 305 | // The volume can only be written if VolumeWritable is true. 306 | // 307 | // MapString 308 | // !PropertyList The stream's property list. 309 | // 310 | func (pulse *Client) Stream(sink dbus.ObjectPath) *Object { 311 | return NewObject(pulse.conn, DbusInterface+".Stream", sink) 312 | } 313 | 314 | // Client controls a pulseaudio client. 315 | // 316 | func (pulse *Client) Client(sink dbus.ObjectPath) *Object { 317 | return NewObject(pulse.conn, DbusInterface+".Client", sink) 318 | } 319 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package pulseaudio_test 2 | 3 | import ( 4 | "github.com/godbus/dbus" 5 | "github.com/sqp/pulseaudio" 6 | 7 | "fmt" 8 | "log" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // 14 | //--------------------------------------------------------------------[ MAIN ]-- 15 | 16 | // Create a pulse dbus service with 2 clients, listen to events, 17 | // then use some properties. 18 | // 19 | func Example() { 20 | // Load pulseaudio DBus module if needed. This module is mandatory, but it 21 | // can also be configured in system files. See package doc. 22 | isLoaded, e := pulseaudio.ModuleIsLoaded() 23 | testFatal(e, "test pulse dbus module is loaded") 24 | if !isLoaded { 25 | e = pulseaudio.LoadModule() 26 | testFatal(e, "load pulse dbus module") 27 | 28 | defer pulseaudio.UnloadModule() // has error to test 29 | } 30 | 31 | // Connect to the pulseaudio dbus service. 32 | pulse, e := pulseaudio.New() 33 | testFatal(e, "connect to the pulse service") 34 | defer pulse.Close() // has error to test 35 | 36 | // Create and register a first client. 37 | app := &AppPulse{} 38 | pulse.Register(app) 39 | defer pulse.Unregister(app) // has errors to test 40 | 41 | // Create and register a second client (optional). 42 | two := &ClientTwo{pulse} 43 | pulse.Register(two) 44 | defer pulse.Unregister(two) // has errors to test 45 | 46 | // Listen to registered events. 47 | go pulse.Listen() 48 | defer pulse.StopListening() 49 | 50 | // Use some properties. 51 | GetProps(pulse) 52 | SetProps(pulse) 53 | 54 | // Output: 55 | // two: device mute updated /org/pulseaudio/core1/sink0 true 56 | // sink muted 57 | // two: device mute updated /org/pulseaudio/core1/sink0 false 58 | // sink restored 59 | } 60 | 61 | // 62 | //--------------------------------------------------------------[ CLIENT ONE ]-- 63 | 64 | // AppPulse is a client that connects 6 callbacks. 65 | // 66 | type AppPulse struct{} 67 | 68 | // NewSink is called when a sink is added. 69 | // 70 | func (ap *AppPulse) NewSink(path dbus.ObjectPath) { 71 | log.Println("one: new sink", path) 72 | } 73 | 74 | // SinkRemoved is called when a sink is removed. 75 | // 76 | func (ap *AppPulse) SinkRemoved(path dbus.ObjectPath) { 77 | log.Println("one: sink removed", path) 78 | } 79 | 80 | // NewPlaybackStream is called when a playback stream is added. 81 | // 82 | func (ap *AppPulse) NewPlaybackStream(path dbus.ObjectPath) { 83 | log.Println("one: new playback stream", path) 84 | } 85 | 86 | // PlaybackStreamRemoved is called when a playback stream is removed. 87 | // 88 | func (ap *AppPulse) PlaybackStreamRemoved(path dbus.ObjectPath) { 89 | log.Println("one: playback stream removed", path) 90 | } 91 | 92 | // DeviceVolumeUpdated is called when the volume has changed on a device. 93 | // 94 | func (ap *AppPulse) DeviceVolumeUpdated(path dbus.ObjectPath, values []uint32) { 95 | log.Println("one: device volume updated", path, values) 96 | } 97 | 98 | // DeviceActiveCardUpdated is called when active card has changed on a device. 99 | // i.e. headphones injected. 100 | func (ap *AppPulse) DeviceActiveCardUpdated(path dbus.ObjectPath, port dbus.ObjectPath) { 101 | log.Println("one: device active card updated", path, port) 102 | } 103 | 104 | // StreamVolumeUpdated is called when the volume has changed on a stream. 105 | // 106 | func (ap *AppPulse) StreamVolumeUpdated(path dbus.ObjectPath, values []uint32) { 107 | log.Println("one: stream volume", path, values) 108 | } 109 | 110 | // 111 | //--------------------------------------------------------------[ CLIENT TWO ]-- 112 | 113 | // ClientTwo is a client that also connects some callbacks (3). 114 | // 115 | type ClientTwo struct { 116 | *pulseaudio.Client 117 | } 118 | 119 | // DeviceVolumeUpdated is called when the volume has changed on a device. 120 | // 121 | func (two *ClientTwo) DeviceVolumeUpdated(path dbus.ObjectPath, values []uint32) { 122 | log.Println("two: device volume updated", path, values) 123 | } 124 | 125 | // DeviceMuteUpdated is called when the output has been (un)muted. 126 | // 127 | func (two *ClientTwo) DeviceMuteUpdated(path dbus.ObjectPath, state bool) { 128 | fmt.Println("two: device mute updated", path, state) 129 | } 130 | 131 | // DeviceActivePortUpdated is called when the port has changed on a device. 132 | // Like a cable connected. 133 | // 134 | func (two *ClientTwo) DeviceActivePortUpdated(path, path2 dbus.ObjectPath) { 135 | log.Println("two: device port updated", path, path2) 136 | } 137 | 138 | // 139 | //----------------------------------------------[ GET OBJECTS AND PROPERTIES ]-- 140 | 141 | // GetProps is an example to show how to get properties. 142 | func GetProps(client *pulseaudio.Client) { 143 | // Get the list of streams from the Core and show some informations about them. 144 | // You better handle errors that were not checked here for code clarity. 145 | 146 | // Get the list of playback streams from the core. 147 | streams, _ := client.Core().ListPath("PlaybackStreams") // []ObjectPath 148 | for _, stream := range streams { 149 | 150 | // Get the device to query properties for the stream referenced by his path. 151 | dev := client.Stream(stream) 152 | 153 | // Get some informations about this stream. 154 | mute, _ := dev.Bool("Mute") // bool 155 | vols, _ := dev.ListUint32("Volume") // []uint32 156 | latency, _ := dev.Uint64("Latency") // uint64 157 | sampleRate, _ := dev.Uint32("SampleRate") // uint32 158 | log.Println("stream", volumeText(mute, vols), "latency", latency, "sampleRate", sampleRate) 159 | 160 | props, e := dev.MapString("PropertyList") // map[string]string 161 | testFatal(e, "get device PropertyList") 162 | log.Println(props) 163 | 164 | // Get the client associated with the stream. 165 | devcltpath, _ := dev.ObjectPath("Client") // ObjectPath 166 | devclt := client.Client(devcltpath) 167 | devcltdrv, _ := devclt.String("Driver") // string 168 | log.Println("device client driver", devcltdrv) 169 | } 170 | } 171 | 172 | // SetProps is an example to show how to set properties. 173 | // Toggles twice the mute state of the first sink device. 174 | func SetProps(client *pulseaudio.Client) { 175 | sinks, e := client.Core().ListPath("Sinks") 176 | testFatal(e, "get list of sinks") 177 | 178 | if len(sinks) == 0 { 179 | fmt.Println("no sinks to test") 180 | return 181 | } 182 | 183 | dev := client.Device(sinks[0]) // Only use the first sink for the test. 184 | 185 | var muted bool 186 | e = dev.Get("Mute", &muted) // Get is a generic method to get properties. 187 | testFatal(e, "get sink muted state") 188 | 189 | e = dev.Set("Mute", !muted) 190 | testFatal(e, "set sink muted state") 191 | 192 | <-time.After(time.Millisecond * 100) 193 | fmt.Println("sink muted") 194 | 195 | e = dev.Set("Mute", muted) // For properties tagged RW in the doc. 196 | testFatal(e, "set sink muted state") 197 | 198 | <-time.After(time.Millisecond * 100) 199 | fmt.Println("sink restored") 200 | } 201 | 202 | // 203 | //------------------------------------------------------------------[ COMMON ]-- 204 | 205 | func volumeText(mute bool, vals []uint32) string { 206 | if mute { 207 | return "muted" 208 | } 209 | vol := int(volumeAverage(vals)) * 100 / 65535 210 | return " " + strconv.Itoa(vol) + "% " 211 | } 212 | 213 | func volumeAverage(vals []uint32) uint32 { 214 | var vol uint32 215 | if len(vals) > 0 { 216 | for _, cur := range vals { 217 | vol += cur 218 | } 219 | vol /= uint32(len(vals)) 220 | } 221 | return vol 222 | } 223 | 224 | func testFatal(e error, msg string) { 225 | if e != nil { 226 | log.Fatalln(msg+":", e) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /pulse.go: -------------------------------------------------------------------------------- 1 | package pulseaudio 2 | 3 | import ( 4 | "github.com/godbus/dbus" 5 | 6 | "fmt" 7 | "os/exec" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | // Dbus objects paths. 13 | const ( 14 | DbusInterface = "org.PulseAudio.Core1" 15 | DbusPath = "/org/pulseaudio/core1" 16 | ) 17 | 18 | // Client manages a pulseaudio Dbus client session. 19 | // 20 | type Client struct { 21 | conn *dbus.Conn 22 | hooker *Hooker 23 | ch chan *dbus.Signal 24 | unknownSignal func(*dbus.Signal) 25 | } 26 | 27 | // New creates a new pulseaudio Dbus client session. 28 | // 29 | func New() (*Client, error) { // chan *dbus.Signal 30 | addr, e := serverLookup() 31 | if e != nil { 32 | return nil, e 33 | } 34 | 35 | conn, e := dbus.Dial(addr) 36 | if e != nil { 37 | return nil, e 38 | } 39 | 40 | e = conn.Auth(nil) 41 | if e != nil { 42 | conn.Close() 43 | return nil, e 44 | } 45 | 46 | pulse := &Client{ 47 | conn: conn, 48 | hooker: NewHooker(), 49 | unknownSignal: func(s *dbus.Signal) { fmt.Println("unknown signal", s.Name, s.Path) }, 50 | } 51 | 52 | pulse.hooker.AddCalls(PulseCalls) 53 | pulse.hooker.AddTypes(PulseTypes) 54 | 55 | return pulse, nil 56 | } 57 | 58 | // Close closes the DBus connection. The client can't be reused after. 59 | // 60 | func (pulse *Client) Close() error { 61 | return pulse.conn.Close() 62 | } 63 | 64 | // Register connects an object to the pulseaudio events hooks it implements. 65 | // If the object declares any of the method in the On... interfaces list, it 66 | // will be registered to receive those events. 67 | // 68 | func (pulse *Client) Register(obj interface{}) (errs []error) { 69 | tolisten := pulse.hooker.Register(obj) 70 | for _, name := range tolisten { 71 | e := pulse.ListenForSignal(name) 72 | if e != nil { 73 | errs = append(errs, e) 74 | } 75 | } 76 | return errs 77 | } 78 | 79 | // Unregister disconnects an object from the pulseaudio events hooks. 80 | // 81 | func (pulse *Client) Unregister(obj interface{}) (errs []error) { 82 | tounlisten := pulse.hooker.Unregister(obj) 83 | for _, name := range tounlisten { 84 | e := pulse.StopListeningForSignal(name) 85 | if e != nil { 86 | errs = append(errs, e) 87 | } 88 | } 89 | return errs 90 | } 91 | 92 | // Listen awaits for pulseaudio messages and dispatch events to registered clients. 93 | // 94 | func (pulse *Client) Listen() { 95 | pulse.ch = make(chan *dbus.Signal, 10) 96 | pulse.conn.Signal(pulse.ch) 97 | 98 | for s := range pulse.ch { 99 | pulse.DispatchSignal(s) 100 | } 101 | } 102 | 103 | // StopListening unregisters an listened event. 104 | // 105 | func (pulse *Client) StopListening() { 106 | pulse.conn.RemoveSignal(pulse.ch) 107 | close(pulse.ch) 108 | } 109 | 110 | // DispatchSignal forwards a signal event to the registered clients. 111 | // 112 | func (pulse *Client) DispatchSignal(s *dbus.Signal) { 113 | name := strings.TrimPrefix(string(s.Name), DbusInterface+".") 114 | if name != s.Name { // dbus interface matched. 115 | if pulse.hooker.Call(name, s) { 116 | return // signal was defined (even if no clients are connected). 117 | } 118 | } 119 | pulse.unknownSignal(s) 120 | } 121 | 122 | // SetOnUnknownSignal sets the unknown signal logger callback. Optional 123 | // 124 | func (pulse *Client) SetOnUnknownSignal(call func(s *dbus.Signal)) { 125 | pulse.unknownSignal = call 126 | } 127 | 128 | // 129 | //------------------------------------------------------------[ DBUS METHODS ]-- 130 | 131 | // ListenForSignal registers a new event to listen. 132 | // 133 | func (pulse *Client) ListenForSignal(name string, paths ...dbus.ObjectPath) error { 134 | args := []interface{}{DbusInterface + "." + name, paths} 135 | return pulse.Core().Call("ListenForSignal", 0, args...).Err 136 | } 137 | 138 | // StopListeningForSignal unregisters an listened event. 139 | // 140 | func (pulse *Client) StopListeningForSignal(name string) error { 141 | return pulse.Core().Call("StopListeningForSignal", 0, DbusInterface+"."+name).Err 142 | } 143 | 144 | // 145 | //-----------------------------------------------------[ CALLBACK INTERFACES ]-- 146 | 147 | // OnFallbackSinkUpdated is an interface to the FallbackSinkUpdated method. 148 | type OnFallbackSinkUpdated interface { 149 | FallbackSinkUpdated(dbus.ObjectPath) 150 | } 151 | 152 | // OnFallbackSinkUnset is an interface to the FallbackSinkUnset method. 153 | type OnFallbackSinkUnset interface { 154 | FallbackSinkUnset() 155 | } 156 | 157 | // OnNewSink is an interface to the NewSink method. 158 | type OnNewSink interface { 159 | NewSink(dbus.ObjectPath) 160 | } 161 | 162 | // OnSinkRemoved is an interface to the SinkRemoved method. 163 | type OnSinkRemoved interface { 164 | SinkRemoved(dbus.ObjectPath) 165 | } 166 | 167 | // OnNewPlaybackStream is an interface to the NewPlaybackStream method. 168 | type OnNewPlaybackStream interface { 169 | NewPlaybackStream(dbus.ObjectPath) 170 | } 171 | 172 | // OnPlaybackStreamRemoved is an interface to the PlaybackStreamRemoved method. 173 | type OnPlaybackStreamRemoved interface { 174 | PlaybackStreamRemoved(dbus.ObjectPath) 175 | } 176 | 177 | // OnDeviceVolumeUpdated is an interface to the DeviceVolumeUpdated method. 178 | type OnDeviceVolumeUpdated interface { 179 | DeviceVolumeUpdated(dbus.ObjectPath, []uint32) 180 | } 181 | 182 | // OnDeviceMuteUpdated is an interface to the DeviceMuteUpdated method. 183 | type OnDeviceMuteUpdated interface { 184 | DeviceMuteUpdated(dbus.ObjectPath, bool) 185 | } 186 | 187 | // OnStreamVolumeUpdated is an interface to the StreamVolumeUpdated method. 188 | type OnStreamVolumeUpdated interface { 189 | StreamVolumeUpdated(dbus.ObjectPath, []uint32) 190 | } 191 | 192 | // OnStreamMuteUpdated is an interface to the StreamMuteUpdated method. 193 | type OnStreamMuteUpdated interface { 194 | StreamMuteUpdated(dbus.ObjectPath, bool) 195 | } 196 | 197 | // OnDeviceActivePortUpdated is an interface to the DeviceActivePortUpdated method. 198 | type OnDeviceActivePortUpdated interface { 199 | DeviceActivePortUpdated(dbus.ObjectPath, dbus.ObjectPath) 200 | } 201 | 202 | // 203 | //--------------------------------------------------------[ CALLBACK METHODS ]-- 204 | 205 | // PulseCalls defines callbacks methods to call the matching object method with 206 | // type-asserted arguments. 207 | // Public so it can be hacked before the first Register. 208 | // 209 | var PulseCalls = Calls{ 210 | "FallbackSinkUpdated": func(m Msg) { m.O.(OnFallbackSinkUpdated).FallbackSinkUpdated(m.P) }, 211 | "FallbackSinkUnset": func(m Msg) { m.O.(OnFallbackSinkUnset).FallbackSinkUnset() }, 212 | "NewSink": func(m Msg) { m.O.(OnNewSink).NewSink(m.P) }, 213 | "SinkRemoved": func(m Msg) { m.O.(OnSinkRemoved).SinkRemoved(m.P) }, 214 | "NewPlaybackStream": func(m Msg) { m.O.(OnNewPlaybackStream).NewPlaybackStream(m.D[0].(dbus.ObjectPath)) }, 215 | "PlaybackStreamRemoved": func(m Msg) { m.O.(OnPlaybackStreamRemoved).PlaybackStreamRemoved(m.D[0].(dbus.ObjectPath)) }, 216 | "Device.VolumeUpdated": func(m Msg) { m.O.(OnDeviceVolumeUpdated).DeviceVolumeUpdated(m.P, m.D[0].([]uint32)) }, 217 | "Device.MuteUpdated": func(m Msg) { m.O.(OnDeviceMuteUpdated).DeviceMuteUpdated(m.P, m.D[0].(bool)) }, 218 | "Device.ActivePortUpdated": func(m Msg) { m.O.(OnDeviceActivePortUpdated).DeviceActivePortUpdated(m.P, m.D[0].(dbus.ObjectPath)) }, 219 | "Stream.VolumeUpdated": func(m Msg) { m.O.(OnStreamVolumeUpdated).StreamVolumeUpdated(m.P, m.D[0].([]uint32)) }, 220 | "Stream.MuteUpdated": func(m Msg) { m.O.(OnStreamMuteUpdated).StreamMuteUpdated(m.P, m.D[0].(bool)) }, 221 | } 222 | 223 | // PulseTypes defines interface types for events to register. 224 | // Public so it can be hacked before the first Register. 225 | // 226 | var PulseTypes = map[string]reflect.Type{ 227 | "FallbackSinkUpdated": reflect.TypeOf((*OnFallbackSinkUpdated)(nil)).Elem(), 228 | "FallbackSinkUnset": reflect.TypeOf((*OnFallbackSinkUnset)(nil)).Elem(), 229 | "NewSink": reflect.TypeOf((*OnNewSink)(nil)).Elem(), 230 | "SinkRemoved": reflect.TypeOf((*OnSinkRemoved)(nil)).Elem(), 231 | "NewPlaybackStream": reflect.TypeOf((*OnNewPlaybackStream)(nil)).Elem(), 232 | "PlaybackStreamRemoved": reflect.TypeOf((*OnPlaybackStreamRemoved)(nil)).Elem(), 233 | "Device.VolumeUpdated": reflect.TypeOf((*OnDeviceVolumeUpdated)(nil)).Elem(), 234 | "Device.MuteUpdated": reflect.TypeOf((*OnDeviceMuteUpdated)(nil)).Elem(), 235 | "Device.ActivePortUpdated": reflect.TypeOf((*OnDeviceActivePortUpdated)(nil)).Elem(), 236 | "Stream.VolumeUpdated": reflect.TypeOf((*OnStreamVolumeUpdated)(nil)).Elem(), 237 | "Stream.MuteUpdated": reflect.TypeOf((*OnStreamMuteUpdated)(nil)).Elem(), 238 | } 239 | 240 | // 241 | //------------------------------------------------------------------[ COMMON ]-- 242 | 243 | // serverLookup asks the main service for the location of the real service. 244 | // It's the only thing the pulseaudio service do on the main session dbus. 245 | // On my system, it returns "unix:path=/run/user/1000/pulse/dbus-socket" 246 | // 247 | func serverLookup() (string, error) { 248 | conn, e := dbus.SessionBus() 249 | if e != nil { 250 | return "", e 251 | } 252 | srv := NewObject(conn, "org.PulseAudio1", "/org/pulseaudio/server_lookup1") 253 | addr, ep := srv.GetProperty("org.PulseAudio.ServerLookup1.Address") 254 | if ep != nil { 255 | return "", ep 256 | } 257 | return addr.Value().(string), nil 258 | } 259 | 260 | // 261 | 262 | // The next part could (should) be moved as another package^w lib) 263 | 264 | // 265 | //-------------------------------------------------------------[ FACTO PROPS ]-- 266 | 267 | // Object extends the dbus Object with properties access methods. 268 | // 269 | type Object struct { 270 | dbus.BusObject 271 | prefix string 272 | } 273 | 274 | // NewObject creates a dbus Object with properties access methods. 275 | // 276 | func NewObject(conn *dbus.Conn, interf string, path dbus.ObjectPath) *Object { 277 | return &Object{conn.Object(interf, path), interf} 278 | } 279 | 280 | // Get queries an object property and set its value to dest. 281 | // dest must be a pointer to the type of data returned by the method. 282 | // 283 | func (dev *Object) Get(property string, dest interface{}) error { 284 | v, e := dev.GetProperty(dev.prefix + "." + property) 285 | if e != nil { 286 | return e 287 | } 288 | 289 | switch val := v.Value().(type) { 290 | case bool, uint32, uint64, string, dbus.ObjectPath, 291 | []uint32, []string, []dbus.ObjectPath: 292 | 293 | reflect.ValueOf(dest).Elem().Set(reflect.ValueOf(val)) 294 | return nil 295 | 296 | case map[string][]byte: 297 | tmp := make(map[string]string) 298 | for k, v := range val { 299 | if len(v) > 0 { 300 | tmp[k] = string(v[:len(v)-1]) // remove \0 at end. 301 | } 302 | } 303 | reflect.ValueOf(dest).Elem().Set(reflect.ValueOf(tmp)) 304 | return nil 305 | } 306 | return fmt.Errorf("unknown type, %T to %T", v.Value(), dest) 307 | } 308 | 309 | // Set updates the given object property with value. 310 | // 311 | func (dev *Object) Set(property string, value interface{}) error { 312 | return dev.SetProperty(dev.prefix+"."+property, value) 313 | } 314 | 315 | // 316 | //---------------------------------------------------[ GET CASTED PROPERTIES ]-- 317 | 318 | // Bool queries an object property and return it as bool. 319 | // 320 | func (dev *Object) Bool(name string) (val bool, e error) { 321 | e = dev.Get(name, &val) 322 | return val, e 323 | } 324 | 325 | // Uint32 queries an object property and return it as uint32. 326 | // 327 | func (dev *Object) Uint32(name string) (val uint32, e error) { 328 | e = dev.Get(name, &val) 329 | return val, e 330 | } 331 | 332 | // Uint64 queries an object property and return it as uint64. 333 | // 334 | func (dev *Object) Uint64(name string) (val uint64, e error) { 335 | e = dev.Get(name, &val) 336 | return val, e 337 | } 338 | 339 | // String queries an object property and return it as string. 340 | // 341 | func (dev *Object) String(name string) (val string, e error) { 342 | e = dev.Get(name, &val) 343 | return val, e 344 | } 345 | 346 | // ObjectPath queries an object property and return it as string. 347 | // 348 | func (dev *Object) ObjectPath(name string) (val dbus.ObjectPath, e error) { 349 | e = dev.Get(name, &val) 350 | return val, e 351 | } 352 | 353 | // ListUint32 queries an object property and return it as []uint32. 354 | // 355 | func (dev *Object) ListUint32(name string) (val []uint32, e error) { 356 | e = dev.Get(name, &val) 357 | return val, e 358 | } 359 | 360 | // ListString queries an object property and return it as []string. 361 | // 362 | func (dev *Object) ListString(name string) (val []string, e error) { 363 | e = dev.Get(name, &val) 364 | return val, e 365 | } 366 | 367 | // ListPath queries an object property and return it as []dbus.ObjectPath. 368 | // 369 | func (dev *Object) ListPath(name string) (val []dbus.ObjectPath, e error) { 370 | e = dev.Get(name, &val) 371 | return val, e 372 | } 373 | 374 | // MapString queries an object property and return it as map[string]string. 375 | // 376 | func (dev *Object) MapString(name string) (val map[string]string, e error) { 377 | e = dev.Get(name, &val) 378 | return val, e 379 | } 380 | 381 | // SetProperty calls org.freedesktop.DBus.Properties.Set on the given object. 382 | // The property name must be given in interface.member notation. 383 | // 384 | // TODO: Should be moved to the dbus api. 385 | // 386 | func (dev *Object) SetProperty(p string, val interface{}) error { 387 | idx := strings.LastIndex(p, ".") 388 | if idx == -1 || idx+1 == len(p) { 389 | return fmt.Errorf("dbus: invalid property %s", p) 390 | } 391 | 392 | iface := p[:idx] 393 | prop := p[idx+1:] 394 | v := dbus.MakeVariant(val) 395 | return dev.Call("org.freedesktop.DBus.Properties.Set", 0, iface, prop, v).Err 396 | } 397 | 398 | // 399 | //-------------------------------------------------------------------[ HOOKS ]-- 400 | 401 | // Msg defines an dbus signal event message. 402 | // 403 | type Msg struct { 404 | O interface{} // client object. 405 | P dbus.ObjectPath // signal path. 406 | D []interface{} // signal data. 407 | } 408 | 409 | // Calls defines a list of event callback methods indexed by dbus method name. 410 | // 411 | type Calls map[string]func(Msg) 412 | 413 | // Types defines a list of interfaces types indexed by dbus method name. 414 | // 415 | type Types map[string]reflect.Type 416 | 417 | // Hooker defines a list of objects indexed by the methods they implement. 418 | // An object can be referenced multiple times. 419 | // If an object declares all methods, it will be referenced in every field. 420 | // hooker:= NewHooker() 421 | // hooker.AddCalls(myCalls) 422 | // hooker.AddTypes(myTypes) 423 | // 424 | // // create a type with some of your callback methods and register it. 425 | // tolisten := hooker.Register(obj) // tolisten is the list of events you may have to listen. 426 | // 427 | // // add the signal forwarder in your events listening loop. 428 | // matched := Call(signalName, dbusSignal) 429 | // 430 | type Hooker struct { 431 | Hooks map[string][]interface{} 432 | Calls Calls 433 | Types Types 434 | } 435 | 436 | // NewHooker handles a loosely coupled hook interface to forward dbus signals 437 | // to registered clients. 438 | // 439 | func NewHooker() *Hooker { 440 | return &Hooker{ 441 | Hooks: make(map[string][]interface{}), 442 | Calls: make(Calls), 443 | Types: make(Types), 444 | } 445 | } 446 | 447 | // Call forwards a Dbus event to registered clients for this event. 448 | // 449 | func (hook Hooker) Call(name string, s *dbus.Signal) bool { 450 | call, ok := hook.Calls[name] 451 | if !ok { // Signal name not defined. 452 | return false 453 | } 454 | if list, ok := hook.Hooks[name]; ok { // Hook clients found. 455 | for _, obj := range list { 456 | call(Msg{obj, s.Path, s.Body}) 457 | } 458 | } 459 | return true 460 | } 461 | 462 | // Register connects an object to the events hooks it implements. 463 | // If the object implements any of the interfaces types declared, it will be 464 | // registered to receive the matching events. 465 | // // 466 | func (hook Hooker) Register(obj interface{}) (tolisten []string) { 467 | t := reflect.ValueOf(obj).Type() 468 | for name, modelType := range hook.Types { 469 | if t.Implements(modelType) { 470 | hook.Hooks[name] = append(hook.Hooks[name], obj) 471 | if len(hook.Hooks[name]) == 1 { // First client registered for this event. need to listen. 472 | tolisten = append(tolisten, name) 473 | } 474 | } 475 | } 476 | return tolisten 477 | } 478 | 479 | // Unregister disconnects an object from the events hooks. 480 | // 481 | func (hook Hooker) Unregister(obj interface{}) (tounlisten []string) { 482 | for name, list := range hook.Hooks { 483 | hook.Hooks[name] = hook.remove(list, obj) 484 | if len(hook.Hooks[name]) == 0 { 485 | delete(hook.Hooks, name) 486 | tounlisten = append(tounlisten, name) // No more clients, need to unlisten. 487 | } 488 | } 489 | return tounlisten 490 | } 491 | 492 | // AddCalls registers a list of callback methods. 493 | // 494 | func (hook Hooker) AddCalls(calls Calls) { 495 | for name, call := range calls { 496 | hook.Calls[name] = call 497 | } 498 | } 499 | 500 | // AddTypes registers a list of interfaces types. 501 | // 502 | func (hook Hooker) AddTypes(tests Types) { 503 | for name, test := range tests { 504 | hook.Types[name] = test 505 | } 506 | } 507 | 508 | // remove removes an object from the list if found. 509 | // 510 | func (hook Hooker) remove(list []interface{}, obj interface{}) []interface{} { 511 | for i, test := range list { 512 | if obj == test { 513 | return append(list[:i], list[i+1:]...) 514 | } 515 | } 516 | return list 517 | } 518 | 519 | // 520 | //-------------------------------------------------------[ PULSE DBUS MODULE ]-- 521 | 522 | // LoadModule loads the PulseAudio DBus module. 523 | // 524 | func LoadModule() error { 525 | return exec.Command("pacmd", "load-module", "module-dbus-protocol").Run() 526 | } 527 | 528 | // UnloadModule unloads the PulseAudio DBus module. 529 | // 530 | func UnloadModule() error { 531 | return exec.Command("pacmd", "unload-module", "module-dbus-protocol").Run() 532 | } 533 | 534 | // ModuleIsLoaded tests if the PulseAudio DBus module is loaded. 535 | // 536 | func ModuleIsLoaded() (bool, error) { 537 | out, e := exec.Command("pacmd", "list-modules").CombinedOutput() 538 | return strings.Contains(string(out), ""), e 539 | } 540 | -------------------------------------------------------------------------------- /pulse_test.go: -------------------------------------------------------------------------------- 1 | package pulseaudio_test 2 | 3 | import ( 4 | "github.com/godbus/dbus" 5 | 6 | "github.com/sqp/pulseaudio" 7 | 8 | "fmt" 9 | "log" 10 | "testing" 11 | ) 12 | 13 | func TestLoadModule(t *testing.T) { 14 | isLoaded, e := pulseaudio.ModuleIsLoaded() 15 | testFatal(e, "test pulse dbus module is loaded") 16 | 17 | if isLoaded { 18 | coverFailPath(t) 19 | 20 | e = pulseaudio.UnloadModule() 21 | testFatal(e, "unload pulse dbus module") 22 | 23 | e = pulseaudio.LoadModule() 24 | testFatal(e, "load pulse dbus module") 25 | return 26 | } 27 | 28 | e = pulseaudio.LoadModule() 29 | testFatal(e, "load pulse dbus module") 30 | 31 | coverFailPath(t) 32 | 33 | e = pulseaudio.UnloadModule() 34 | testFatal(e, "unload pulse dbus module") 35 | } 36 | 37 | func coverFailPath(t *testing.T) { 38 | pulse, e := pulseaudio.New() 39 | testFatal(e, "new pulse") 40 | defer pulse.Close() 41 | 42 | pulse.DispatchSignal(&dbus.Signal{Name: pulseaudio.DbusInterface + ".Invalid"}) 43 | pulse.DispatchSignal(&dbus.Signal{Name: "Invalid"}) 44 | 45 | pulse.SetOnUnknownSignal(func(s *dbus.Signal) { log.Println("unknown signal", s.Name, s.Path) }) 46 | pulse.DispatchSignal(&dbus.Signal{Name: pulseaudio.DbusInterface + ".Invalid"}) 47 | pulse.DispatchSignal(&dbus.Signal{Name: "Invalid"}) 48 | 49 | version, e := pulse.Core().String("Version") 50 | testFatal(e, "get core version") 51 | log.Println("CoreVersion", version) 52 | 53 | exts, e := pulse.Core().ListString("Extensions") 54 | testFatal(e, "get core extensions") 55 | log.Println("CoreExtensions", exts) 56 | 57 | sinks, e := pulse.Core().ListPath("Sinks") 58 | testFatal(e, "get list of sinks") 59 | 60 | if len(sinks) == 0 { 61 | fmt.Println("no sinks to test") 62 | return 63 | } 64 | 65 | dev := pulse.Device(sinks[0]) 66 | dev.SetProperty("willfail", nil) 67 | } 68 | --------------------------------------------------------------------------------