├── LICENSE ├── README.md ├── client.go ├── cmd ├── ding │ └── main.go └── proxy │ └── main.go ├── commands.go ├── frame.go ├── pulse.go ├── sample.go └── types.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joel Jensen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOTE: 2 | Someone nice has rewritten this package into a much better package, with better code, a better API, and more protocol coverage! See https://github.com/jfreymuth/pulse 3 | 4 | # pulse 5 | PulseAudio client implementation in pure Go 6 | 7 | # why? 8 | I saw several wrappers out there of libpulse in Go using CGO, but CGO has many drawbacks. I wanted to try actually implementing the pulseaudio wire protocol in Go instead. The protocol is a very nice binary protocol that can speak over a unix socket. Mostly I figured it out by reading pulseaudio's source code, and by a little debugging unix socket proxy that I've included here. Just fire up the proxy and then run PULSE_SERVER="unix:/tmp/pulsedebug" paplay something.wav 9 | 10 | # status 11 | 12 | Working: 13 | - Basic protocol negotiation 14 | - It will connect to the default audio sink and synthesize a 440hz (A) sine wave 15 | 16 | Not working yet: 17 | - It does not correctly buffer according to the servers requests (It just sends frames on a time schedule and hopes it comes out unbroken). 18 | - shm / memfd support 19 | - The rest of the protocol... 20 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "net" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | //const use_debug_proxy = true 12 | const use_debug_proxy = false 13 | 14 | type Client struct { 15 | conn net.Conn 16 | 17 | requests_mu sync.Mutex 18 | requests_next_id uint32 19 | requests map[uint32]chan *Response 20 | 21 | version uint32 22 | shm bool 23 | memfd bool 24 | 25 | client_index uint32 26 | } 27 | 28 | type Request struct { 29 | Frame *Frame 30 | } 31 | 32 | type Response struct { 33 | Frame *Frame 34 | Err error 35 | } 36 | 37 | func NewClient() (*Client, error) { 38 | addr := fmt.Sprintf("/run/user/%d/pulse/native", os.Getuid()) 39 | if use_debug_proxy { 40 | addr = "/tmp/pulsedebug" 41 | } 42 | 43 | conn, err := net.Dial("unix", addr) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | c := &Client{ 49 | conn: conn, 50 | requests_next_id: 1, 51 | requests: make(map[uint32]chan *Response), 52 | } 53 | 54 | go c.reader() 55 | 56 | return c, nil 57 | } 58 | 59 | func (c *Client) Close() { 60 | c.conn.Close() 61 | } 62 | 63 | func (c *Client) SetNegotiatedVersion(req *CommandAuth, resp *CommandAuthReply) { 64 | c.version = req.Version 65 | if resp.Version < req.Version { 66 | c.version = resp.Version 67 | } 68 | c.shm = req.Shm 69 | if !resp.Shm { 70 | c.shm = false 71 | } 72 | c.memfd = req.Memfd 73 | if !resp.Memfd { 74 | c.memfd = false 75 | } 76 | fmt.Printf("Negotiated native protocol version %d (shm %v memfd %v)\n", c.version, c.shm, c.memfd) 77 | } 78 | 79 | func (c *Client) GetNegotiatedVersion() uint32 { 80 | return c.version 81 | } 82 | 83 | func (c *Client) SetIndex(index uint32) { 84 | c.client_index = index 85 | fmt.Printf("Client index %d\n", c.client_index) 86 | } 87 | 88 | func (c *Client) reader() { 89 | defer c.conn.Close() 90 | 91 | for { 92 | frame, err := ReadFrame(c.conn) 93 | if err != nil { 94 | fmt.Println(err) 95 | return 96 | } 97 | frame.Client = c 98 | 99 | c.requests_mu.Lock() 100 | rc := c.requests[frame.Tag] 101 | if rc != nil { 102 | delete(c.requests, frame.Tag) 103 | } 104 | c.requests_mu.Unlock() 105 | 106 | if rc == nil { 107 | if frame.Cmd == Command_REQUEST { 108 | // Ignore these for now. 109 | } else { 110 | fmt.Println("!Read frame", frame, "cmd") 111 | } 112 | } else { 113 | rc <- &Response{Frame: frame} 114 | close(rc) 115 | } 116 | } 117 | } 118 | 119 | func NewRequest(command Commander) *Request { 120 | return &Request{ 121 | &Frame{ 122 | Command: command, 123 | }, 124 | } 125 | } 126 | 127 | func (c *Client) Request(req *Request) (*Response, error) { 128 | rc := make(chan *Response) 129 | 130 | c.requests_mu.Lock() 131 | for c.requests[c.requests_next_id] != nil { 132 | c.requests_next_id++ 133 | if c.requests_next_id == math.MaxUint32 { 134 | c.requests_next_id = 0 135 | } 136 | } 137 | id := c.requests_next_id 138 | c.requests[id] = rc 139 | c.requests_mu.Unlock() 140 | 141 | req.Frame.Client = c 142 | req.Frame.Length = 0 143 | req.Frame.Channel = 0xffffffff 144 | req.Frame.OffsetHigh = 0 145 | req.Frame.OffsetLow = 0 146 | req.Frame.Flags = 0 147 | req.Frame.Cmd = req.Frame.Command.Cmd() 148 | req.Frame.Tag = id 149 | 150 | err := req.Frame.WriteTo(c.conn) 151 | if err != nil { 152 | // error during write: assume we aren't going 153 | // to get a response back. 154 | c.requests_mu.Lock() 155 | if c.requests[id] == nil { 156 | // whoa, apparently we did get an insanely fast response back. 157 | // the message should come on the channel below. 158 | fmt.Println("Error writing frame:", err, "but we got a response back anyways?!?", req.Frame) 159 | err = nil // don't early out 160 | } else { 161 | delete(c.requests, id) 162 | } 163 | c.requests_mu.Unlock() 164 | 165 | if err != nil { 166 | return nil, err 167 | } 168 | } 169 | 170 | resp := <-rc 171 | 172 | if resp.Err != nil { 173 | return resp, resp.Err 174 | } 175 | 176 | resp.Frame.Origin = req.Frame.Command 177 | err = resp.Frame.ReadCommand() 178 | if err != nil { 179 | return resp, err 180 | } 181 | 182 | fmt.Println(req.Frame.Command, "->", resp.Frame.Command) 183 | 184 | return resp, nil 185 | } 186 | -------------------------------------------------------------------------------- /cmd/ding/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/yobert/pulse" 6 | ) 7 | 8 | func main() { 9 | if err := pulse.Ding(); err != nil { 10 | fmt.Println(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cmd/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "net" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | server, err := net.Listen("unix", "/tmp/pulsedebug") 13 | if err != nil { 14 | fmt.Println(err) 15 | return 16 | } 17 | defer server.Close() 18 | 19 | for { 20 | conn, err := server.Accept() 21 | if err != nil { 22 | fmt.Println(err) 23 | return 24 | } 25 | 26 | go serve(conn) 27 | } 28 | } 29 | 30 | func dbg(buf []byte) { 31 | for i, v := range buf { 32 | fmt.Printf("\t%d\t%08b %02x %03d", i, v, v, v) 33 | if i > 512 { 34 | fmt.Println("...") 35 | return 36 | } 37 | if v > 31 && v < 127 { 38 | fmt.Printf(" '%c'", v) 39 | } 40 | 41 | if v == 'L' && i < len(buf)+4 { 42 | r := bytes.NewReader(buf[i+1:]) 43 | var li uint32 44 | err := binary.Read(r, binary.BigEndian, &li) 45 | if err == nil { 46 | fmt.Printf(" %d", li) 47 | } else { 48 | fmt.Printf(" %v", err) 49 | } 50 | } 51 | if v == 't' { 52 | str := "" 53 | for ii := i + 1; ii < len(buf); ii++ { 54 | if buf[ii] == 0 { 55 | break 56 | } 57 | str += string(buf[ii]) 58 | } 59 | fmt.Printf(" %#v", str) 60 | } 61 | 62 | fmt.Println() 63 | } 64 | } 65 | 66 | func serve(conn net.Conn) { 67 | defer conn.Close() 68 | 69 | proxy, err := net.Dial("unix", fmt.Sprintf("/run/user/%d/pulse/native", os.Getuid())) 70 | if err != nil { 71 | fmt.Println(err) 72 | return 73 | } 74 | 75 | go func() { 76 | defer proxy.Close() 77 | 78 | buf := make([]byte, 1024*1024) 79 | for { 80 | n, err := proxy.Read(buf) 81 | if err != nil { 82 | fmt.Println(err) 83 | return 84 | } 85 | fmt.Printf("server %d\n", n) 86 | dbg(buf[:n]) 87 | 88 | _, err = conn.Write(buf[:n]) 89 | if err != nil { 90 | fmt.Println(err) 91 | return 92 | } 93 | } 94 | }() 95 | 96 | buf := make([]byte, 1024*1024) 97 | for { 98 | n, err := conn.Read(buf) 99 | if err != nil { 100 | fmt.Println(err) 101 | return 102 | } 103 | fmt.Printf("client %d\n", n) 104 | dbg(buf[:n]) 105 | 106 | _, err = proxy.Write(buf[:n]) 107 | if err != nil { 108 | fmt.Println(err) 109 | return 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | var ErrNotImplemented = errors.New("Not implemented") 10 | 11 | const ( 12 | /* Generic commands */ 13 | Command_ERROR uint32 = iota 14 | Command_TIMEOUT 15 | Command_REPLY // 2 16 | 17 | /* CLIENT->SERVER */ 18 | Command_CREATE_PLAYBACK_STREAM // 3 19 | Command_DELETE_PLAYBACK_STREAM 20 | Command_CREATE_RECORD_STREAM 21 | Command_DELETE_RECORD_STREAM 22 | Command_EXIT 23 | Command_AUTH // 8 24 | Command_SET_CLIENT_NAME 25 | Command_LOOKUP_SINK 26 | Command_LOOKUP_SOURCE 27 | Command_DRAIN_PLAYBACK_STREAM 28 | Command_STAT 29 | Command_GET_PLAYBACK_LATENCY 30 | Command_CREATE_UPLOAD_STREAM 31 | Command_DELETE_UPLOAD_STREAM 32 | Command_FINISH_UPLOAD_STREAM 33 | Command_PLAY_SAMPLE 34 | Command_REMOVE_SAMPLE // 19 35 | 36 | Command_GET_SERVER_INFO 37 | Command_GET_SINK_INFO 38 | Command_GET_SINK_INFO_LIST 39 | Command_GET_SOURCE_INFO 40 | Command_GET_SOURCE_INFO_LIST 41 | Command_GET_MODULE_INFO 42 | Command_GET_MODULE_INFO_LIST 43 | Command_GET_CLIENT_INFO 44 | Command_GET_CLIENT_INFO_LIST 45 | Command_GET_SINK_INPUT_INFO 46 | Command_GET_SINK_INPUT_INFO_LIST 47 | Command_GET_SOURCE_OUTPUT_INFO 48 | Command_GET_SOURCE_OUTPUT_INFO_LIST 49 | Command_GET_SAMPLE_INFO 50 | Command_GET_SAMPLE_INFO_LIST 51 | Command_SUBSCRIBE 52 | 53 | Command_SET_SINK_VOLUME 54 | Command_SET_SINK_INPUT_VOLUME 55 | Command_SET_SOURCE_VOLUME 56 | 57 | Command_SET_SINK_MUTE 58 | Command_SET_SOURCE_MUTE // 40 59 | 60 | Command_CORK_PLAYBACK_STREAM 61 | Command_FLUSH_PLAYBACK_STREAM 62 | Command_TRIGGER_PLAYBACK_STREAM // 43 63 | 64 | Command_SET_DEFAULT_SINK 65 | Command_SET_DEFAULT_SOURCE // 45 66 | 67 | Command_SET_PLAYBACK_STREAM_NAME 68 | Command_SET_RECORD_STREAM_NAME // 47 69 | 70 | Command_KILL_CLIENT 71 | Command_KILL_SINK_INPUT 72 | Command_KILL_SOURCE_OUTPUT // 50 73 | 74 | Command_LOAD_MODULE 75 | Command_UNLOAD_MODULE // 52 76 | 77 | Command_ADD_AUTOLOAD___OBSOLETE 78 | Command_REMOVE_AUTOLOAD___OBSOLETE 79 | Command_GET_AUTOLOAD_INFO___OBSOLETE 80 | Command_GET_AUTOLOAD_INFO_LIST___OBSOLETE //56 81 | 82 | Command_GET_RECORD_LATENCY 83 | Command_CORK_RECORD_STREAM 84 | Command_FLUSH_RECORD_STREAM 85 | Command_PREBUF_PLAYBACK_STREAM // 60 86 | 87 | /* SERVER->CLIENT */ 88 | Command_REQUEST // 61 89 | Command_OVERFLOW 90 | Command_UNDERFLOW 91 | Command_PLAYBACK_STREAM_KILLED 92 | Command_RECORD_STREAM_KILLED 93 | Command_SUBSCRIBE_EVENT 94 | 95 | /* A few more client->server commands */ 96 | 97 | Command_MOVE_SINK_INPUT 98 | Command_MOVE_SOURCE_OUTPUT 99 | Command_SET_SINK_INPUT_MUTE 100 | Command_SUSPEND_SINK 101 | Command_SUSPEND_SOURCE 102 | 103 | Command_SET_PLAYBACK_STREAM_BUFFER_ATTR 104 | Command_SET_RECORD_STREAM_BUFFER_ATTR 105 | 106 | Command_UPDATE_PLAYBACK_STREAM_SAMPLE_RATE 107 | Command_UPDATE_RECORD_STREAM_SAMPLE_RATE 108 | 109 | /* SERVER->CLIENT */ 110 | Command_PLAYBACK_STREAM_SUSPENDED 111 | Command_RECORD_STREAM_SUSPENDED 112 | Command_PLAYBACK_STREAM_MOVED 113 | Command_RECORD_STREAM_MOVED 114 | 115 | Command_UPDATE_RECORD_STREAM_PROPLIST 116 | Command_UPDATE_PLAYBACK_STREAM_PROPLIST 117 | Command_UPDATE_CLIENT_PROPLIST 118 | Command_REMOVE_RECORD_STREAM_PROPLIST 119 | Command_REMOVE_PLAYBACK_STREAM_PROPLIST 120 | Command_REMOVE_CLIENT_PROPLIST 121 | 122 | /* SERVER->CLIENT */ 123 | Command_STARTED 124 | 125 | Command_EXTENSION 126 | 127 | Command_GET_CARD_INFO 128 | Command_GET_CARD_INFO_LIST 129 | Command_SET_CARD_PROFILE 130 | 131 | Command_CLIENT_EVENT 132 | Command_PLAYBACK_STREAM_EVENT 133 | Command_RECORD_STREAM_EVENT 134 | 135 | /* SERVER->CLIENT */ 136 | Command_PLAYBACK_BUFFER_ATTR_CHANGED 137 | Command_RECORD_BUFFER_ATTR_CHANGED 138 | 139 | Command_SET_SINK_PORT 140 | Command_SET_SOURCE_PORT 141 | 142 | Command_SET_SOURCE_OUTPUT_VOLUME 143 | Command_SET_SOURCE_OUTPUT_MUTE 144 | 145 | Command_SET_PORT_LATENCY_OFFSET 146 | 147 | /* BOTH DIRECTIONS */ 148 | Command_ENABLE_SRBCHANNEL 149 | Command_DISABLE_SRBCHANNEL 150 | 151 | /* BOTH DIRECTIONS */ 152 | Command_REGISTER_MEMFD_SHMID 153 | 154 | Command_MAX 155 | ) 156 | 157 | const ( 158 | PA_PROTOCOL_FLAG_MASK uint32 = 0xFFFF0000 159 | PA_PROTOCOL_VERSION_MASK uint32 = 0x0000FFFF 160 | 161 | PA_PROTOCOL_FLAG_SHM uint32 = 0x80000000 162 | PA_PROTOCOL_FLAG_MEMFD uint32 = 0x40000000 163 | ) 164 | 165 | type Commander interface { 166 | String() string 167 | Cmd() uint32 168 | WriteTo(io.Writer, uint32) (int, error) 169 | ReadFrom(io.Reader, uint32) error 170 | } 171 | 172 | type CommandAuth struct { 173 | Version uint32 174 | Shm bool 175 | Memfd bool 176 | Cookie [256]byte 177 | } 178 | 179 | func (c *CommandAuth) String() string { 180 | return fmt.Sprintf("AUTH (v%d shm %v memfd %v)", c.Version, c.Shm, c.Memfd) 181 | } 182 | func (c *CommandAuth) Cmd() uint32 { 183 | return Command_AUTH 184 | } 185 | func (c *CommandAuth) WriteTo(w io.Writer, version uint32) (int, error) { 186 | 187 | v := c.Version 188 | 189 | if c.Version >= 13 && c.Shm { 190 | v &= PA_PROTOCOL_FLAG_SHM 191 | } 192 | if c.Version >= 31 && c.Memfd { 193 | v &= PA_PROTOCOL_FLAG_MEMFD 194 | } 195 | 196 | n, err := bwrite(w, Uint32Value, v, ArbitraryValue, uint32(len(c.Cookie))) 197 | if err != nil { 198 | return n, err 199 | } 200 | n2, err := w.Write(c.Cookie[:]) 201 | n += n2 202 | if err != nil { 203 | return n, err 204 | } 205 | return n, nil 206 | } 207 | func (c *CommandAuth) ReadFrom(r io.Reader, version uint32) error { 208 | return ErrNotImplemented 209 | } 210 | 211 | type CommandAuthReply struct { 212 | Version uint32 213 | Shm bool 214 | Memfd bool 215 | } 216 | 217 | func (c *CommandAuthReply) String() string { 218 | return fmt.Sprintf("AUTH REPLY (v%d shm %v memfd %v)", c.Version, c.Shm, c.Memfd) 219 | } 220 | func (c *CommandAuthReply) Cmd() uint32 { 221 | return Command_REPLY 222 | } 223 | func (c *CommandAuthReply) WriteTo(w io.Writer, version uint32) (int, error) { 224 | return 0, ErrNotImplemented 225 | } 226 | func (c *CommandAuthReply) ReadFrom(r io.Reader, version uint32) error { 227 | if err := bread_uint32(r, &c.Version); err != nil { 228 | return err 229 | } 230 | if (c.Version & PA_PROTOCOL_VERSION_MASK) >= 13 { 231 | if c.Version&PA_PROTOCOL_FLAG_SHM > 0 { 232 | c.Shm = true 233 | } 234 | if (c.Version & PA_PROTOCOL_VERSION_MASK) >= 31 { 235 | if c.Version&PA_PROTOCOL_FLAG_MEMFD > 0 { 236 | c.Memfd = true 237 | } 238 | } 239 | c.Version &= PA_PROTOCOL_VERSION_MASK 240 | } 241 | return nil 242 | } 243 | 244 | type CommandSetClientName struct { 245 | Props PropList 246 | } 247 | 248 | func (c *CommandSetClientName) String() string { 249 | return "SET CLIENT NAME (" + c.Props.String() + ")" 250 | } 251 | func (c *CommandSetClientName) Cmd() uint32 { 252 | return Command_SET_CLIENT_NAME 253 | } 254 | func (c *CommandSetClientName) WriteTo(w io.Writer, version uint32) (int, error) { 255 | return c.Props.WriteTo(w) 256 | } 257 | func (c *CommandSetClientName) ReadFrom(r io.Reader, version uint32) error { 258 | return ErrNotImplemented 259 | } 260 | 261 | type CommandSetClientNameReply struct { 262 | ClientIndex uint32 263 | } 264 | 265 | func (c *CommandSetClientNameReply) String() string { 266 | return fmt.Sprintf("SET CLIENT NAME REPLY (client index %d)", c.ClientIndex) 267 | } 268 | func (c *CommandSetClientNameReply) Cmd() uint32 { 269 | return Command_REPLY 270 | } 271 | func (c *CommandSetClientNameReply) WriteTo(w io.Writer, version uint32) (int, error) { 272 | return 0, ErrNotImplemented 273 | } 274 | func (c *CommandSetClientNameReply) ReadFrom(r io.Reader, version uint32) error { 275 | if err := bread_uint32(r, &c.ClientIndex); err != nil { 276 | return err 277 | } 278 | return nil 279 | } 280 | 281 | type CommandCreatePlaybackStream struct { 282 | Format SampleType 283 | Channels byte 284 | Rate uint32 285 | ChannelMap []byte 286 | ChannelVolume []uint32 287 | Props PropList 288 | } 289 | 290 | func (c *CommandCreatePlaybackStream) String() string { 291 | return fmt.Sprintf("CREATE PLAYBACK STREAM") 292 | } 293 | func (c *CommandCreatePlaybackStream) Cmd() uint32 { 294 | return Command_CREATE_PLAYBACK_STREAM 295 | } 296 | func (c *CommandCreatePlaybackStream) WriteTo(w io.Writer, version uint32) (int, error) { 297 | n, err := bwrite(w, 298 | SampleSpecValue, 299 | c.Format, 300 | c.Channels, 301 | c.Rate, 302 | 303 | ChannelMapValue, 304 | byte(len(c.ChannelMap)), 305 | c.ChannelMap, 306 | 307 | Uint32Value, 308 | uint32(0xffffffff), // sink index 309 | StringNullValue, // sink name 310 | 311 | Uint32Value, 312 | uint32(0xffffffff), // buffer max length 313 | FalseValue, // corked 314 | Uint32Value, 315 | uint32(0xffffffff), // buffer target length 316 | Uint32Value, 317 | uint32(0xffffffff), // buffer pre-buffer length 318 | Uint32Value, 319 | uint32(0xffffffff), // buffer minimum request 320 | Uint32Value, 321 | uint32(0), // sync id-- no idea what that does 322 | 323 | CvolumeValue, 324 | byte(len(c.ChannelVolume)), 325 | c.ChannelVolume, 326 | ) 327 | if err != nil { 328 | return n, err 329 | } 330 | 331 | if version >= 12 { 332 | n2, err := bwrite(w, 333 | FalseValue, // no remap 334 | FalseValue, // no remix 335 | FalseValue, // fix format 336 | FalseValue, // fix rate 337 | FalseValue, // fix channels 338 | FalseValue, // no move 339 | FalseValue, // variable rate 340 | ) 341 | n += n2 342 | if err != nil { 343 | return n, err 344 | } 345 | } 346 | 347 | if version >= 13 { 348 | n2, err := bwrite(w, 349 | FalseValue, // muted 350 | FalseValue, // adjust latency 351 | ) 352 | n += n2 353 | if err != nil { 354 | return n, err 355 | } 356 | n2, err = c.Props.WriteTo(w) 357 | n += n2 358 | if err != nil { 359 | return n, err 360 | } 361 | } 362 | 363 | if version >= 14 { 364 | n2, err := bwrite(w, 365 | FalseValue, // volume set 366 | FalseValue, // early requests 367 | ) 368 | n += n2 369 | if err != nil { 370 | return n, err 371 | } 372 | } 373 | 374 | if version >= 15 { 375 | n2, err := bwrite(w, 376 | FalseValue, // muted set 377 | FalseValue, // dont inhibit auto suspend 378 | FalseValue, // fail on suspend 379 | ) 380 | n += n2 381 | if err != nil { 382 | return n, err 383 | } 384 | } 385 | 386 | if version >= 17 { 387 | n2, err := bwrite(w, 388 | FalseValue, // relative volume 389 | ) 390 | n += n2 391 | if err != nil { 392 | return n, err 393 | } 394 | } 395 | 396 | if version >= 18 { 397 | n2, err := bwrite(w, 398 | FalseValue, // passthrough 399 | ) 400 | n += n2 401 | if err != nil { 402 | return n, err 403 | } 404 | } 405 | 406 | if version >= 21 { 407 | n2, err := bwrite(w, 408 | ByteValue, 409 | byte(0), // n_formats 410 | ) 411 | n += n2 412 | if err != nil { 413 | return n, err 414 | } 415 | } 416 | 417 | return n, nil 418 | } 419 | func (c *CommandCreatePlaybackStream) ReadFrom(r io.Reader, version uint32) error { 420 | return ErrNotImplemented 421 | } 422 | 423 | type CommandCreatePlaybackStreamReply struct { 424 | StreamIndex uint32 425 | SinkInputIndex uint32 426 | Missing uint32 427 | 428 | BufferMaxLength uint32 429 | BufferTargetLength uint32 430 | BufferPrebufferLength uint32 431 | BufferMinimumRequest uint32 432 | 433 | Format SampleType 434 | Channels byte 435 | Rate uint32 436 | ChannelMap []byte 437 | 438 | SinkInputSinkIndex uint32 439 | SinkInputSinkName string 440 | SinkInputSinkSuspended bool 441 | 442 | SinkLatency uint64 443 | 444 | Encoding EncodingType 445 | Props PropList 446 | } 447 | 448 | func (c *CommandCreatePlaybackStreamReply) String() string { 449 | return fmt.Sprintf("CREATE PLAYBACK STREAM REPLY (index %d, %d:%#v) %s", c.StreamIndex, c.SinkInputSinkIndex, c.SinkInputSinkName, c.Props.String()) 450 | } 451 | func (c *CommandCreatePlaybackStreamReply) Cmd() uint32 { 452 | return Command_REPLY 453 | } 454 | func (c *CommandCreatePlaybackStreamReply) WriteTo(w io.Writer, version uint32) (int, error) { 455 | return 0, ErrNotImplemented 456 | } 457 | func (c *CommandCreatePlaybackStreamReply) ReadFrom(r io.Reader, version uint32) error { 458 | if err := bread_uint32(r, 459 | &c.StreamIndex, 460 | &c.SinkInputIndex, 461 | &c.Missing, 462 | ); err != nil { 463 | return err 464 | } 465 | 466 | if version >= 9 { 467 | if err := bread_uint32(r, 468 | &c.BufferMaxLength, 469 | &c.BufferTargetLength, 470 | &c.BufferPrebufferLength, 471 | &c.BufferMinimumRequest, 472 | ); err != nil { 473 | return err 474 | } 475 | } 476 | 477 | if version >= 12 { 478 | if err := bread(r, 479 | SampleSpecValue, 480 | &c.Format, 481 | &c.Channels, 482 | &c.Rate, 483 | ); err != nil { 484 | return err 485 | } 486 | 487 | var l byte 488 | if err := bread(r, ChannelMapValue, &l); err != nil { 489 | return err 490 | } 491 | c.ChannelMap = make([]byte, l) 492 | if err := bread(r, &c.ChannelMap); err != nil { 493 | return err 494 | } 495 | 496 | if err := bread(r, 497 | Uint32Value, 498 | &c.SinkInputSinkIndex, 499 | StringValue, 500 | &c.SinkInputSinkName, 501 | &c.SinkInputSinkSuspended, 502 | ); err != nil { 503 | return err 504 | } 505 | } 506 | 507 | if version >= 13 { 508 | if err := bread(r, UsecValue, &c.SinkLatency); err != nil { 509 | return err 510 | } 511 | } 512 | 513 | if version >= 21 { 514 | if err := bread(r, FormatInfoValue, ByteValue, &c.Encoding); err != nil { 515 | return err 516 | } 517 | if err := c.Props.ReadFrom(r); err != nil { 518 | return err 519 | } 520 | } 521 | 522 | return nil 523 | } 524 | -------------------------------------------------------------------------------- /frame.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | const ( 10 | FRAME_SIZE_MAX_ALLOW = 1024 * 1024 * 16 11 | 12 | PA_FLAG_SHMDATA uint32 = 0x80000000 13 | PA_FLAG_SHMDATA_MEMFD_BLOCK uint32 = 0x20000000 14 | PA_FLAG_SHMRELEASE uint32 = 0x40000000 15 | PA_FLAG_SHMREVOKE uint32 = 0xC0000000 16 | PA_FLAG_SHMMASK uint32 = 0xFF000000 17 | PA_FLAG_SEEKMASK uint32 = 0x000000FF 18 | PA_FLAG_SHMWRITABLE uint32 = 0x00800000 19 | ) 20 | 21 | type Frame struct { 22 | Client *Client 23 | Length uint32 24 | Buf *bytes.Buffer 25 | 26 | Channel uint32 27 | OffsetHigh uint32 28 | OffsetLow uint32 29 | Flags uint32 30 | 31 | Cmd uint32 32 | Tag uint32 33 | 34 | Command Commander 35 | Origin Commander 36 | } 37 | 38 | func (f *Frame) String() string { 39 | r := fmt.Sprintf("channel %08x flags %08x offset %08x / %08x cmd %d tag %d (%d bytes)", 40 | f.Channel, f.Flags, f.OffsetHigh, f.OffsetLow, f.Cmd, f.Tag, f.Length) 41 | return r 42 | } 43 | 44 | func ReadFrame(r io.Reader) (*Frame, error) { 45 | f := &Frame{} 46 | f.Buf = &bytes.Buffer{} 47 | 48 | if _, err := io.CopyN(f.Buf, r, 4); err != nil { 49 | return nil, err 50 | } 51 | 52 | err := bread(f.Buf, &f.Length) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | if f.Length > FRAME_SIZE_MAX_ALLOW { 58 | return nil, fmt.Errorf("Frame size %d is too long (only %d allowed)", f.Length, FRAME_SIZE_MAX_ALLOW) 59 | } 60 | 61 | f.Buf.Grow(int(f.Length) + 16) 62 | 63 | _, err = io.CopyN(f.Buf, r, int64(f.Length)+16) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | if err = bread(f.Buf, &f.Channel, &f.OffsetHigh, &f.OffsetLow, &f.Flags); err != nil { 69 | return nil, err 70 | } 71 | 72 | if err = bread_uint32(f.Buf, &f.Cmd, &f.Tag); err != nil { 73 | return nil, err 74 | } 75 | 76 | // Don't decode the command yet. We need to associate a reply with 77 | // it's original request before we can do it easily. 78 | // See Decode() 79 | 80 | return f, nil 81 | } 82 | 83 | func (f *Frame) ReadCommand() error { 84 | var c Commander 85 | 86 | switch f.Cmd { 87 | case Command_REPLY: 88 | if f.Origin != nil { 89 | switch f.Origin.Cmd() { 90 | case Command_AUTH: 91 | c = &CommandAuthReply{} 92 | case Command_SET_CLIENT_NAME: 93 | c = &CommandSetClientNameReply{} 94 | case Command_CREATE_PLAYBACK_STREAM: 95 | c = &CommandCreatePlaybackStreamReply{} 96 | } 97 | } 98 | } 99 | 100 | if c == nil { 101 | if f.Origin == nil { 102 | return fmt.Errorf("Not sure how to read command code %d", f.Cmd) 103 | } else { 104 | return fmt.Errorf("Not sure how to read command code %d (from origin %s)", f.Origin.String()) 105 | } 106 | } 107 | 108 | err := c.ReadFrom(f.Buf, f.Client.GetNegotiatedVersion()) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | // success! 114 | f.Command = c 115 | return nil 116 | } 117 | 118 | func (f *Frame) WriteTo(w io.Writer) error { 119 | // build out the frame's buffer 120 | 121 | f.Buf = &bytes.Buffer{} 122 | 123 | n, err := bwrite(f.Buf, 124 | f.Length, // dummy value-- we'll overwrite at the end when we know our final length 125 | f.Channel, 126 | f.OffsetHigh, 127 | f.OffsetLow, 128 | f.Flags, 129 | ) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | // apparently we don't want to actually count the first 20 bytes. 135 | n = 0 136 | 137 | n2, err := bwrite(f.Buf, 138 | Uint32Value, 139 | f.Cmd, 140 | Uint32Value, 141 | f.Tag, 142 | ) 143 | n += n2 144 | if err != nil { 145 | return err 146 | } 147 | 148 | n2, err = f.Command.WriteTo(f.Buf, f.Client.GetNegotiatedVersion()) 149 | n += n2 150 | if err != nil { 151 | return err 152 | } 153 | 154 | if n > FRAME_SIZE_MAX_ALLOW { 155 | return fmt.Errorf("Frame size %d is too long (only %d allowed)", n, FRAME_SIZE_MAX_ALLOW) 156 | } 157 | f.Length = uint32(n) 158 | 159 | // Rewrite size entry at start of buffer 160 | start := &bytes.Buffer{} 161 | if _, err = bwrite(start, f.Length); err != nil { 162 | return err 163 | } 164 | copy(f.Buf.Bytes(), start.Bytes()) 165 | 166 | // Done! Do the actual write. 167 | wn, err := f.Buf.WriteTo(w) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | fmt.Println("Wrote frame", f, "in", wn, "bytes") 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /pulse.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "math" 10 | "os" 11 | "os/user" 12 | "path" 13 | "time" 14 | ) 15 | 16 | func bwrite(w io.Writer, data ...interface{}) (int, error) { 17 | n := 0 18 | for _, v := range data { 19 | if err := binary.Write(w, binary.BigEndian, v); err != nil { 20 | return n, err 21 | } 22 | n += binary.Size(v) 23 | } 24 | return n, nil 25 | } 26 | func bread(r io.Reader, data ...interface{}) error { 27 | for _, v := range data { 28 | t, ok := v.(ValueType) 29 | if ok { 30 | var tt ValueType 31 | if err := binary.Read(r, binary.BigEndian, &tt); err != nil { 32 | return err 33 | } 34 | if tt != t { 35 | return fmt.Errorf("Protcol error: Got type %s but expected %s", tt, t) 36 | } 37 | continue 38 | } 39 | 40 | sptr, ok := v.(*string) 41 | if ok { 42 | buf := make([]byte, 1024) // max string length i guess. 43 | i := 0 44 | for { 45 | _, err := r.Read(buf[i : i+1]) 46 | if err != nil { 47 | return err 48 | } 49 | if buf[i] == 0 { 50 | *sptr = string(buf[:i]) 51 | break 52 | } else { 53 | if i > len(buf) { 54 | return fmt.Errorf("String is too long (max %d bytes)", len(buf)) 55 | } 56 | i++ 57 | } 58 | } 59 | continue 60 | } 61 | 62 | bptr, ok := v.(*bool) 63 | if ok { 64 | var tt ValueType 65 | if err := binary.Read(r, binary.BigEndian, &tt); err != nil { 66 | return err 67 | } 68 | if tt == TrueValue { 69 | *bptr = true 70 | } else if tt == FalseValue { 71 | *bptr = false 72 | } else { 73 | return fmt.Errorf("Protcol error: Got type %s but expected boolean true or false", tt) 74 | } 75 | continue 76 | } 77 | 78 | if err := binary.Read(r, binary.BigEndian, v); err != nil { 79 | return err 80 | } 81 | } 82 | return nil 83 | } 84 | func bread_uint32(r io.Reader, data ...interface{}) error { 85 | for _, v := range data { 86 | var t ValueType 87 | if err := binary.Read(r, binary.BigEndian, &t); err != nil { 88 | return err 89 | } 90 | if t != Uint32Value { 91 | return fmt.Errorf("Protcol error: Got type %s but expected %s", t.String(), Uint32Value) 92 | } 93 | if err := binary.Read(r, binary.BigEndian, v); err != nil { 94 | return err 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | func Ding() error { 101 | 102 | client, err := NewClient() 103 | if err != nil { 104 | return err 105 | } 106 | defer client.Close() 107 | 108 | auth := &CommandAuth{ 109 | Version: 32, 110 | } 111 | 112 | cookie_path := os.Getenv("HOME") + "/.config/pulse/cookie" 113 | cookie, err := ioutil.ReadFile(cookie_path) 114 | if err != nil { 115 | return err 116 | } 117 | if len(cookie) != len(auth.Cookie) { 118 | return fmt.Errorf("Pulse audio client cookie has incorrect length %d: Expected %d (path %#v)", 119 | len(cookie), len(auth.Cookie), cookie_path) 120 | } 121 | copy(auth.Cookie[:], cookie) 122 | 123 | resp, err := client.Request(NewRequest(auth)) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | fmt.Println(" Read frame", resp.Frame) 129 | 130 | ar, ok := resp.Frame.Command.(*CommandAuthReply) 131 | if !ok { 132 | return fmt.Errorf("Unexpected command %v: Wanted CommandAuthReply", resp.Frame.Command) 133 | } 134 | 135 | client.SetNegotiatedVersion(auth, ar) 136 | 137 | current, err := user.Current() 138 | if err != nil { 139 | return err 140 | } 141 | hostname, err := os.Hostname() 142 | if err != nil { 143 | return err 144 | } 145 | 146 | set_client_name := &CommandSetClientName{ 147 | Props: PropList{ 148 | V: map[string]string{ 149 | "media.format": "WAV (Microsoft)", 150 | "media.name": "Ding!", 151 | "application.name": path.Base(os.Args[0]), 152 | "application.process.id": fmt.Sprintf("%d", os.Getpid()), 153 | "application.process.user": current.Username, 154 | "application.process.host": hostname, 155 | "application.process.binary": os.Args[0], 156 | "application.language": "en_US.UTF-8", 157 | "window.x11.display": os.Getenv("DISPLAY"), 158 | }, 159 | }, 160 | } 161 | 162 | resp, err = client.Request(NewRequest(set_client_name)) 163 | if err != nil { 164 | return err 165 | } 166 | 167 | fmt.Println(" Read frame", resp.Frame) 168 | 169 | sr, ok := resp.Frame.Command.(*CommandSetClientNameReply) 170 | if !ok { 171 | return fmt.Errorf("Unexpected command %v: Wanted CommandSetClientNameReply", resp.Frame.Command) 172 | } 173 | 174 | client.SetIndex(sr.ClientIndex) 175 | 176 | rate := uint32(44100) 177 | 178 | create_playback_stream := &CommandCreatePlaybackStream{ 179 | Format: SampleFloat32LE, 180 | Channels: 1, 181 | Rate: rate, 182 | ChannelMap: []byte{1}, 183 | ChannelVolume: []uint32{256}, 184 | Props: PropList{ 185 | V: map[string]string{ 186 | "media.format": set_client_name.Props.V["media.format"], 187 | "application.name": set_client_name.Props.V["application.name"], 188 | "media.name": set_client_name.Props.V["media.name"], 189 | }, 190 | }, 191 | } 192 | 193 | resp, err = client.Request(NewRequest(create_playback_stream)) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | fmt.Println(" Read frame", resp.Frame) 199 | 200 | _, ok = resp.Frame.Command.(*CommandCreatePlaybackStreamReply) 201 | if !ok { 202 | return fmt.Errorf("Unexpected command %v: Wanted CommandCreatePlaybackStreamReply", resp.Frame.Command) 203 | } 204 | 205 | bytes_per_sample := 4 206 | max_frame_bytes := 64000 207 | samples_per_frame := int(max_frame_bytes / bytes_per_sample) 208 | seconds_per_frame := float64(samples_per_frame) / float64(rate) 209 | 210 | fmt.Printf("samples per frame: %d (%.4f seconds)\n", samples_per_frame, seconds_per_frame) 211 | 212 | t := 0.0 213 | 214 | for { 215 | f := &Frame{} 216 | f.Length = uint32(samples_per_frame * bytes_per_sample) 217 | f.Buf = &bytes.Buffer{} 218 | _, err := bwrite(f.Buf, 219 | f.Length, 220 | f.Channel, 221 | f.OffsetHigh, 222 | f.OffsetLow, 223 | f.Flags, 224 | ) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | for i := 0; i < samples_per_frame; i++ { 230 | 231 | v := math.Sin(t*2*math.Pi*440) * 0.1 232 | 233 | if err := binary.Write(f.Buf, binary.LittleEndian, float32(v)); err != nil { 234 | return err 235 | } 236 | 237 | t += 1.0 / float64(rate) 238 | } 239 | 240 | n, err := f.Buf.WriteTo(client.conn) 241 | if err != nil { 242 | return err 243 | } 244 | fmt.Printf("wrote %d length audio frame (%d bytes)\n", f.Length, n) 245 | 246 | time.Sleep(time.Duration(float64(time.Second) * seconds_per_frame * 0.999)) 247 | } 248 | 249 | fmt.Println("blocking") 250 | select {} 251 | 252 | return nil 253 | } 254 | -------------------------------------------------------------------------------- /sample.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | type SampleType byte 4 | 5 | const ( 6 | SampleU8 SampleType = iota 7 | SampleAlaw 8 | SampleUlaw 9 | SampleS16LE 10 | SampleS16BE 11 | SampleFloat32LE 12 | SampleFloat32BE 13 | SampleS32LE 14 | SampleS32BE 15 | SampleS24LE 16 | SampleS24BE 17 | SampleS24_32LE 18 | SampleS24_32BE 19 | ) 20 | 21 | type EncodingType byte 22 | 23 | const ( 24 | EncodingAny EncodingType = iota 25 | EncodingPCM 26 | EncodingAC3Padded 27 | EncodingEAC3Padded 28 | EncodingMpegPadded // mpeg1 or mpeg2 (part 3, not aac) 29 | EncodingDTSPadded 30 | EncodingMpeg2AACPadded 31 | ) 32 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | ) 8 | 9 | type ValueType byte 10 | 11 | const ( 12 | InvalidValue ValueType = 0 13 | StringValue ValueType = 't' 14 | StringNullValue ValueType = 'N' 15 | Uint32Value ValueType = 'L' 16 | ByteValue ValueType = 'B' 17 | Uint64Value ValueType = 'R' 18 | Int64Value ValueType = 'r' 19 | SampleSpecValue ValueType = 'a' 20 | ArbitraryValue ValueType = 'x' 21 | TrueValue ValueType = '1' 22 | FalseValue ValueType = '0' 23 | TimeValue ValueType = 'T' 24 | UsecValue ValueType = 'U' 25 | ChannelMapValue ValueType = 'm' 26 | CvolumeValue ValueType = 'v' 27 | PropListValue ValueType = 'P' 28 | VolumeValue ValueType = 'V' 29 | FormatInfoValue ValueType = 'f' 30 | ) 31 | 32 | func (t ValueType) String() string { 33 | switch t { 34 | case InvalidValue: 35 | return "InvalidValue" 36 | case StringValue: 37 | return "StringValue" 38 | case StringNullValue: 39 | return "StringNullValue" 40 | case Uint32Value: 41 | return "Uint32Value" 42 | case ByteValue: 43 | return "ByteValue" 44 | case Uint64Value: 45 | return "Uint64Value" 46 | case Int64Value: 47 | return "Int64Value" 48 | case SampleSpecValue: 49 | return "SampleSpecValue" 50 | case ArbitraryValue: 51 | return "ArbitraryValue" 52 | case TrueValue: 53 | return "TrueValue" 54 | case FalseValue: 55 | return "FalseValue" 56 | case TimeValue: 57 | return "TimeValue" 58 | case UsecValue: 59 | return "UsecValue" 60 | case ChannelMapValue: 61 | return "ChannelMapValue" 62 | case CvolumeValue: 63 | return "CvolumeValue" 64 | case PropListValue: 65 | return "PropListValue" 66 | case VolumeValue: 67 | return "VolumeValue" 68 | case FormatInfoValue: 69 | return "FormatInfoValue" 70 | default: 71 | return fmt.Sprintf("UnknownValue(%d)", t) 72 | } 73 | } 74 | 75 | type PropList struct { 76 | V map[string]string 77 | } 78 | 79 | func (p *PropList) String() string { 80 | d := "" 81 | var keys []string 82 | for k := range p.V { 83 | keys = append(keys, k) 84 | } 85 | sort.Strings(keys) 86 | for i, k := range keys { 87 | if i != 0 { 88 | d += ", " 89 | } 90 | d += fmt.Sprintf("%s %#v", k, p.V[k]) 91 | } 92 | if len(d) > 512 { 93 | d = d[:512] + " ..." 94 | } 95 | return d 96 | } 97 | func (p *PropList) WriteTo(w io.Writer) (int, error) { 98 | n, err := bwrite(w, PropListValue) 99 | if err != nil { 100 | return n, err 101 | } 102 | 103 | for k, v := range p.V { 104 | if v == "" { 105 | continue 106 | } 107 | 108 | l := uint32(len(v) + 1) // +1 for null at the end of string 109 | n2, err := bwrite(w, 110 | StringValue, []byte(k), byte(0), 111 | Uint32Value, l, 112 | ArbitraryValue, l, 113 | []byte(v), byte(0), 114 | ) 115 | n += n2 116 | if err != nil { 117 | return n, err 118 | } 119 | } 120 | 121 | n2, err := bwrite(w, StringNullValue) 122 | n += n2 123 | if err != nil { 124 | return n, err 125 | } 126 | 127 | return n, nil 128 | } 129 | func (p *PropList) ReadFrom(r io.Reader) error { 130 | if p.V == nil { 131 | p.V = make(map[string]string) 132 | } 133 | err := bread(r, PropListValue) 134 | if err != nil { 135 | return err 136 | } 137 | for { 138 | var t ValueType 139 | if err = bread(r, &t); err != nil { 140 | return err 141 | } 142 | 143 | if t == StringNullValue { 144 | // end of the proplist. 145 | break 146 | } 147 | if t != StringValue { 148 | return fmt.Errorf("Protcol error: Got type %s but expected %s", t, StringValue) 149 | } 150 | 151 | var k, v string 152 | var l1, l2 uint32 153 | if err = bread(r, 154 | &k, 155 | Uint32Value, &l1, 156 | ArbitraryValue, &l2, 157 | &v, 158 | ); err != nil { 159 | return err 160 | } 161 | if len(v) != int(l1-1) || len(v) != int(l2-1) { 162 | return fmt.Errorf("Protocol error: Proplist value length mismatch (len %d, arb len %d, value len %d)", 163 | l1, l2, len(v)) 164 | } 165 | p.V[k] = v 166 | } 167 | return nil 168 | } 169 | --------------------------------------------------------------------------------