Add opus custom support - #18
Conversation
…es; add these to the custom implementation
|
Big PR, thanks! Some initial feedback that I think should be addressed before I can do a thorough review:
Thanks! |
I forgot to update the submodule; done To your first points around breaking those out further, I originally started down this path but stopped as it didn't really help the module be more usable from an API perspective. The underlying calls are different enough that to wrapping them up together was clearer and didn't need to change the existing code at all. |
As I understand it, Opus custom encoders/decoders exist primarily (or only) to support nonstandard frame sizes. I don’t think that justifies a new, somewhat duplicative expansion of this package’s public API. How about this: could you propose a minimal API change that enables nonstandard frame sizes? We can start right here in the PR comments. Once we have a solid API proposal, then implement it. |
If there's a preferred API you'd like to make, I'm all up for that. I agree the almost duplication of some of the helper functions to expand the pointers is not ideal, but as I said - I did not want to disturb the other parts of the code here. Speaking of the existing code, it looks like it's not handling dropped packets. My understanding is that if you have a dropped packet you should feed nil into the decoder to cover that case. The decode method in Opus.Decoder isn't able to handle that: |
|
Ok, I found some time :) I've merged the coding / decoding changes in with the existing classes. |
| } else { | ||
| decodedCount = opus_decode( | ||
| decoder, | ||
| input.isEmpty ? nil : input.baseAddress, |
There was a problem hiding this comment.
This change means that if you pass an empty data packet, opus decoder gets the nil it needs to signify a dropped packet.
ydnar
left a comment
There was a problem hiding this comment.
I think this can be simplified further. Key detail is hiding the existence and ownership of the OpusCustomMode within Encoder and Decoder.
| @@ -0,0 +1,95 @@ | |||
| import AVFoundation | |||
There was a problem hiding this comment.
What’s the purpose of this file? Can it be deleted?
(I think it should be, if Opus.Encoder and Opus.Decoder can support nonstandard frame sizes.
| } | ||
| } | ||
|
|
||
| public init(customOpus: OpaquePointer, |
There was a problem hiding this comment.
Given the encoder and decoder typically exist on different machines, I think Decoder should own the OpusCustomMode and create it here.
It can be freed in deinit.
There was a problem hiding this comment.
I disagree - custom encoders and decoders need the custom object; you need to have a custom object in order to use either.
It maybe in your use case you have encoding on one machine and decoding on another, but you have to have the same custom instance for both if you're using them together.
There was a problem hiding this comment.
The constructor for an OpusCustomMode takes two arguments: a sample rate and a target frame size in number of samples, which must be 64-1024 plus some additional constraints on prime factors.
The sample rate can be derived from the AVAudioFormat in the constructor along with a customFrameSize parameter to the custom constructor for an Encoder and Decoder.
It’s probably useful to have common helper code to aid in the creation of a valid OpusCustomMode that throws an error, but isn’t part of this package’s public API.
There was a problem hiding this comment.
It’s probably useful to have common helper code to aid in the creation of a valid
OpusCustomModethat throws an error, but isn’t part of this package’s public API.
That is the Opus.Custom.swift file.
| class Decoder { | ||
| let format: AVAudioFormat | ||
| let decoder: OpaquePointer | ||
| let customFrameSize: Int32? |
There was a problem hiding this comment.
This can probably be a non-optional int that defaults to 0.
We’ll also need to add a let customMode: OpaquePointer here (see below).
There was a problem hiding this comment.
Actually no, this is a way to avoid having a duplicate un-needed pointer.
If you have a custom frame size, then your decoder is a custom one.
There was a problem hiding this comment.
There’s no function in the Opus library to extract the frame size used to initialize an OpusCustomMode, hence storing it here.
There was a problem hiding this comment.
I don't follow your point here; can you clarify?
| decodedCount = opus_custom_decode( | ||
| decoder, | ||
| input.isEmpty ? nil : input.baseAddress, | ||
| size, |
There was a problem hiding this comment.
Why is packetSize passed as an argument here (and other decode methods) when it should just be equal to Int32(input.count)?
There was a problem hiding this comment.
packet size is needed for the custom decoder to know how much data it has.
In the case of a dropped packet, input is nil, but the encoder still needs to know how much data got dropped
There was a problem hiding this comment.
See comment at the top-level of this PR regarding dropped packets.
If the decoder was initialized with a custom frame size, then either use that, or have decodeDroppedPacket accept an (optional?) frame size argument.
There was a problem hiding this comment.
Custom frame size and compressed packet size are not the same thing. Custom encode / decode need to know both in order to work.
| packetSize: Int32?) throws -> Int | ||
| { | ||
| var decodedCount: Int32 = 0 | ||
| if let size = packetSize { |
There was a problem hiding this comment.
If possible, this should branch on the presence of a non-nil customMode instance variable, which implies the decoder was initialized with a custom frame size.
There was a problem hiding this comment.
I've implemented this a different way; using the optional presence of a custom frame size.
There was a problem hiding this comment.
When decoding a stream of packets, is the frame size consistent, or does it vary? If it’s consistent, then this parameter shouldn’t exist.
There was a problem hiding this comment.
The size can be multiples of the underlying frame size
There was a problem hiding this comment.
Why does it need the packetSize argument? Can that be inferred from input.count?
There was a problem hiding this comment.
OK, now I've had my coffee...
Compressed packet size != frame size.
Both are needed.
There was a problem hiding this comment.
OK, great. Then can that be taken from the customFrameSize instance variable?
|
|
||
| // TODO: throw an error if format is unsupported | ||
| public init(format: AVAudioFormat, application _: Application = .audio) throws { | ||
| customFrameSize = nil |
There was a problem hiding this comment.
If this is made non-optional, this can be customFrameSize = 0, with customMode = nil.
There was a problem hiding this comment.
And then you need 2 new vars; customMode and customFrameSize.
Using an optional for customFrameSize means you don't need a separate variable.
Good observation. I’d be game to add support for this in a separate PR. My instinct says that a |
I've implemented a fix in the PR; no need to make a different method to handle the case of a pretty regular occurrence. Will make the API kinda strange to have to call a different decode API for nil. |
But from the caller’s perspective, a dropped packet isn’t |
If you look at how you'd be using this, it would be from a jitter buffer. Typically that will just have the read method called periodically as the audio system requests more data. |
Right now, there are two public Adding support for nil packets to both of these methods doesn’t seem great, and adding two additional methods to decode a dropped packet also doesn’t seem great. |
Both of those methods already have support in my PR. No changes needed; just using an empty data signifies a nil, which is then passed into the decode method. |
|
@emlynmac thanks for your efforts here, I appreciate it. I think we should split out decoding dropped packets into a separate thread of work (probably a new PR), which is relevant for both custom and non-custom modes. I think this could help improve/inform the public API design for custom mode support. |
You're welcome, I needed it to get jamulus working on swift, so I figured I'd pass it back. If you want to refactor the dropping into another method, then that's def another PR. I need it to work for now, but that's why we have forks :) |
|
I want this to land. Supporting dropped packets and supporting custom modes are two distinct and valuable features, and should be treated as such. Given that dropped packet support is relevant for all users of this package, I think it’s a good candidate to extract and land first. Then we can streamline and simplify the discussion around the design for support for custom modes. |
From my perspective, the cleanest way would be to let the public decode methods take optionals. |
I've been working on an implementation of Jamulus' audio protocol, which uses a custom Opus setting.
The PR contains changes to get the custom mode up and running.