-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add opus custom support #18
base: main
Are you sure you want to change the base?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change means that if you pass an empty data packet, opus decoder gets the nil it needs to signify a dropped packet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -22,6 +23,27 @@ public extension Opus { | |||
} | |||
} | |||
|
|||
public init(customOpus: OpaquePointer, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
That is the Opus.Custom.swift file.
@@ -5,9 +5,10 @@ public extension Opus { | |||
class Decoder { | |||
let format: AVAudioFormat | |||
let decoder: OpaquePointer | |||
let customFrameSize: Int32? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've implemented this a different way; using the optional presence of a custom frame size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The size can be multiples of the underlying frame size
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does it need the packetSize
argument? Can that be inferred from input.count
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, now I've had my coffee...
Compressed packet size != frame size.
Both are needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, great. Then can that be taken from the customFrameSize
instance variable?
public init(format: AVAudioFormat, application _: Application = .audio) throws { | ||
customFrameSize = nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is made non-optional, this can be customFrameSize = 0
, with customMode = nil
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.