stephen / airsonos

:musical_note: AirPlay to Sonos
MIT License
2.1k stars 252 forks source link

send PCM instead of MP3 stream #26

Open rainforest1155 opened 10 years ago

rainforest1155 commented 10 years ago

When Airsonos is streaming, it needs pretty much 100% CPU on my server (BeagleBoneBlack) and since I'm using the server for other things as well, uninterrupted audio streaming is not possible. I would imagine that the mp3 encoding takes up the most cpu cycles and I'm wondering if it would be possible to disable the lame encoder and stream as PCM instead.

I'm not concerned about bandwidth if it would make the streaming possible.

stephen commented 10 years ago

Unfortunately, I haven't found a way to get Sonos devices to support direct PCM streams, as AirSonos currently takes advantages of Sonos' mp3 radio stream support to work.

rainforest1155 commented 10 years ago

It looks like Sonos does accept WAV if it's coming from a UPNP server like this Foobar plugin (http://www.foobar2000.org/components/view/foo_upnp). Capturing a UPNP packet, it looks like this:

Origin: OpenSource.UPnP.HTTPSession [13092880]
Time: 31.07.2014 02:38:15

NOTIFY /RINCON_xx_MR/urn:upnp-org:serviceId:AVTransport HTTP/1.1
HOST:  serverIP:53702
CONNECTION:  close
CONTENT-TYPE:  text/xml
SEQ:  24
NTS:  upnp:propchange
SID:  uuid:RINCON_xx_sub0000000009
NT:  upnp:event
Content-Length: 2195

<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"><e:property><LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/"><InstanceID val="0"><TransportState val="TRANSITIONING"/><CurrentPlayMode val="NORMAL"/><CurrentCrossfadeMode val="0"/><NumberOfTracks val="1"/><CurrentTrack val="1"/><CurrentSection val="0"/><CurrentTrackURI val="http://serverUUID.x-udn/content/6955e868e77566245c13b19f2fd49146.wav?profile_id=0&convert=wav"/><CurrentTrackDuration val="0:00:00"/><CurrentTrackMetaData val="<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="-1" parentID="-1" restricted="true"><res protocolInfo="http-get:*:audio/wav:*">http://serverUUID.x-udn/content/6955e868e77566245c13b19f2fd49146.wav?profile_id=0&amp;convert=wav</res><r:streamContent></r:streamContent><r:radioShowMd></r:radioShowMd><dc:title>6955e868e77566245c13b19f2fd49146.wav?profile_id=0&amp;convert=wav</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class></item></DIDL-Lite>"/><r:NextTrackURI val=""/><r:NextTrackMetaData val=""/><r:EnqueuedTransportURI val="http://serverUUID.x-udn/content/6955e868e77566245c13b19f2fd49146.wav?profile_id=0&convert=wav"/><r:EnqueuedTransportURIMetaData val=""/></InstanceID></Event></LastChange></e:property></e:propertyset>

I did a bit of trial and error hacking and managed to replace the Lame encoder in nicercast with the wav writer (https://www.npmjs.org/package/wav). To show you, I forked nicercast here: https://github.com/rainforest1155/nicercast/blob/master/index.js

and in Airsonos, I changed this section: device.play({ uri: 'x-rincon-mp3radio://' + ip.address() + ':' + port + '/listen.m3u',

to: device.play({ uri: 'http://' + ip.address() + ':' + port + '/listen.m3u',

While playback is not without interruptions, Sonos does recognize and play back the stream.

I was now thinking of trying to utilize the AVTransport.prototype.SetAVTransportURI function to see if that would allow for a better (uninterrupted) stream. I suspect that the Foobar UPNP server might be utilizing that as well to serve up the wav stream.

Note that I'm not versed at using Github or coding in general, so I'm not sure if I did the forking properly or if I would still need to update any of the package.json info to make it proper, like in terms of attribution. The code is also just a trial and error attempt at replacing existing code with something else. So it's not following any coding guidelines nor did I do proper testing.

stephen commented 10 years ago

Ah, amazing. This is awesome!

The trick for the interruptions is the mp3 metadata interleaving. You can remove this by changing 8192 to 0 https://github.com/rainforest1155/nicercast/blob/master/index.js#L55-L61.

One problem is it does seem like there are more audio frames being dropped via PCM, assumably because there's more PCM data to send over the wire.

Possible solution could be to add some buffering mechanisms to nicercast.. I'll investigate further when I have some time. :)

rainforest1155 commented 10 years ago

Thank you for the pointer. Though, setting those values to 0 seemed to make the matter worse. Therefore, I simply commented the 2 metadata related sections out for now. That seems to improve things.

stephen commented 10 years ago

Hmm, interesting. The values are basically the offsets at which the stream attempts to inject metadata for the Sonos controller to read; removing the metadata-related streaming would be sufficient to stop the (corrupted stream data) related problem.

jishi commented 9 years ago

I just wanted to share my experience with this.

Sonos identifies stream-type using url suffixes when you set a AVTransportURI directly, and disregards any content-type passed by http headers. Because of this, a raw PCM stream, need to generate a url which simulates a file with the ending .wav, like this:

http://10.0.0.1/stream.wav

I'm not sure how it handles m3u files (the poster claimed that it worked? did the embedded url end with .wav?)

Regarding buffering and dropouts, the optimal buffering method would be having the Sonos player buffer and delay playback accordingly. This is however not possible to control, since it is expected to be able to read the complete file in advance (since we are emulating a UPnP Media server). My solution to this was to generate silent data and send it initially before the actual stream starts, that way the player is busy rendering silent data until it will consume the actual stream.

Sonos players are also especially dumb when it comes to handling buffer underruns. It will actually drop samples, when it can not keep up, making the situation even worse. I'm not sure about the reasoning for that, but very annoying in this case.

I have made some tests, and I seem to get a pretty stable streaming using raw PCM when initially sending 150 000 samples of silence (4 bytes per sample). This translates to roughly 3 seconds delay.

Using a wired connected between both computer and the Sonos player, I could actually manage as low as 100ms delay with a small enough silent buffer, but this is really a best case scenario. Also, the quality of your WiFi and SonosNet will affect which buffering size that is appropriate.