Skip to content
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

New namespace, constexprs and scoped enums #18

Merged
merged 11 commits into from Dec 1, 2017
Merged

New namespace, constexprs and scoped enums #18

merged 11 commits into from Dec 1, 2017

Conversation

dturner
Copy link
Collaborator

@dturner dturner commented Nov 27, 2017

!!!BREAKING CHANGE!!!

Summary

  1. New oboe:: namespace
  2. Macro constants replaced with constexpr and scoped enums
  3. Oboe_convert*Text methods replaced with single convertToText method

Detailed changelog

1) oboe namespace used for all classes and functions

Why?
Having a namespace provides better encapsulation, avoids polluting the global namespace and also avoids having to prefix all Oboe classes and methods with the word "Oboe".

What code changes do I need to make?
If you're using any classes which start with "Oboe" you'll need to change them to use oboe::. For example:

OboeStreamBuilder builder;

becomes

oboe::AudioStreamBuilder builder;

or you can use the oboe namespace without qualification:

using namespace oboe;
AudioStreamBuilder builder;

2) Constants defined by macros are replaced by constexpr and scoped enums

Why?
C++ Core Guidelines ES.31: Don’t use macros for constants or “functions” http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-macros2.
Effective Modern C++ (Scott Meyer) Item 15 - Use constexpr wherever possible.
Scoped enums allows better type checking at compile time and eliminate the need for long #defines in the global namespace.

What code changes do I need to make?
Anywhere you were using a value starting with OBOE_ you'll need to change it either to the corresponding value starting with oboe::k. For example OBOE_NANOS_PER_MICROSECOND becomes oboe::kNanosPerMicrosecond. Or you'll need to change it to the corresponding scoped enum:

OBOE_AUDIO_FORMAT_PCM_FLOAT becomes oboe::AudioFormat::Float

3) Oboe_convert*Text methods replaced with single convertToText method. **

Why?
A single method is easier to remember and to use.

What code changes do I need to make?
If you are using a Oboe_convert*Text method then you'll need to change it to use convertToText. For example:

Oboe_convertResultToText(result)

becomes

oboe::convertToText(result)

@@ -48,24 +48,24 @@ Include the Oboe header:

#include <oboe/Oboe.h>

Streams are built using an `OboeStreamBuilder`. Create one like this:
Streams are built using an `StreamBuilder`. Create one like this:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an -> a

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring doing the an/a changes until we have made a decision on Stream vs AudioStream naming


Define an `OboeStreamCallback` class to receive callbacks whenever the stream requires new data.
Define an `StreamCallback` class to receive callbacks whenever the stream requires new data.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an -> a

OboeStream *stream;
oboe_result_t result = builder.openStream(&stream);
oboe::Stream *stream;
oboe::Result result = builder.openStream(&stream);

Check the result to make sure the stream was opened successfully. Oboe has many convenience methods for converting its types into human-readable strings, they all start with `Oboe_convert`:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all start with oboe::convert

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -177,16 +175,16 @@ class OboeStream : public OboeStreamBase {
* @param timeoutNanoseconds Maximum number of nanoseconds to wait for completion.
* @return The number of frames actually written or a negative error.
*/
virtual oboe_result_t write(const void *buffer,
virtual int32_t write(const void *buffer,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this method would be better off returning a std::tuple of Result and int32_t. This avoids the return type representing both the frames written (if positive) or an error (if negative).


/**
* Base class for Oboe C++ audio stream.
*/
class OboeStream : public OboeStreamBase {
class Stream : public StreamBase {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to consider calling this AudioStream since Stream is quite a generic term. At some point in the future we might also have a MidiStream

if (--mIdleCountDown <= 0) {
mState = STATE_ACTIVE;
mState = State::Active;
}
mPreviousXRuns = mStream.getXRunCount();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getXRunCount might also be better returning a std::tuple

Copy link
Collaborator

@philburk philburk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@@ -48,24 +48,24 @@ Include the Oboe header:

#include <oboe/Oboe.h>

Streams are built using an `OboeStreamBuilder`. Create one like this:
Streams are built using an `StreamBuilder`. Create one like this:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c/an/a/

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the class names Stream and StreamBuilder too generic? Is there a danger of colliding if developers are using multiple namespaces?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There would only be a danger if they're using non-qualified namespaces e.g.

using namespace oboe; Stream stream;

But yes, I do think Stream is too generic. AudioStream, AudioStreamBuilder etc would be more readable. Also if we ever support MIDI we could have a MidiStream. If you agree I'll rename.

OboeStream *stream;
oboe_result_t result = builder.openStream(&stream);
oboe::Stream *stream;
oboe::Result result = builder.openStream(&stream);

Check the result to make sure the stream was opened successfully. Oboe has many convenience methods for converting its types into human-readable strings, they all start with `Oboe_convert`:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they now start with "oboe::convert"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -171,7 +174,7 @@ class OboeStreamBuilder : public OboeStreamBase {
* @param performanceMode for example, OBOE_PERFORMANCE_MODE_LOW_LATENCY
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

old

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -185,7 +188,7 @@ class OboeStreamBuilder : public OboeStreamBase {
* @param deviceId device identifier or OBOE_DEVICE_UNSPECIFIED
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

old

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

return size;
}

const char *convertResultToText(Result returnCode) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If running with a future version AAudio, there may be new results or new formats. So we should add a default "Unrecognized" return value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for all convert methods


constexpr int32_t kUnspecified = 0;

constexpr int64_t kNanosPerMicrosecond = 1000;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a TODO to consider using std::chrono, as it provides a lot of time unit conversion utilities.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Stop = AAUDIO_CALLBACK_RESULT_STOP,
};

enum class Result : aaudio_result_t {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming this class to "Error", which would allow dropping the "Error" prefix from the enum items.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it normal to have an Error::OK value? We're going to see a lot of:

if (error != Error::OK){

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's perhaps not the best expression. But seeing a lot of "Error" prefixes doesn't make me happy either. Although, I've just checked Vulkan's 'Result' type, and they also have "error" prefixes everywhere. OK, let's not consider changing anything here for now.


enum class Result : aaudio_result_t {
OK,
ErrorBase = AAUDIO_ERROR_BASE,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorBase shouldn't be used as a source of an assignment anywhere. To prevent this, I would suggest introducing a helper function 'isError' which compares the value against AAUDIO_ERROR_BASE.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't I just remove ErrorBase from the list of possible values in the Result enum?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. What I've meant is that if anyone will ever need to check against AAUDIO_ERROR_BASE, you can provide a convenience method.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorBase removed

oboe_audio_format_t mFormat = OBOE_AUDIO_FORMAT_UNSPECIFIED;
oboe_direction_t mDirection = OBOE_DIRECTION_OUTPUT;
oboe_performance_mode_t mPerformanceMode = OBOE_PERFORMANCE_MODE_NONE;
StreamCallback *mStreamCallback = NULL;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nullptr

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

);
mPreviousScheduler = scheduler;
}
if (mStreamCallback == NULL) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nullptr

@@ -199,7 +202,7 @@ class OboeStreamBuilder : public OboeStreamBase {
* @param streamCallback
* @return
*/
OboeStreamBuilder *setCallback(OboeStreamCallback *streamCallback) {
StreamBuilder *setCallback(StreamCallback *streamCallback) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider specifying StreamCallback ownership via use of std::unique_ptr or std::shared_ptr. With a raw pointer, it's easy to run into a trouble of the client code deletes the callback instance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot. I think it makes sense for the client to retain ownership of the callback so that means using a std::shared_ptr<StreamCallback>. I've updated the code to do this. Would you mind taking a look.


void convertPcm16ToFloat(const int16_t *source, float *destination, int32_t numSamples) {
for (int i = 0; i < numSamples; i++) {
destination[i] = source[i] * (1.0f / 32768.0f);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious: why not just "source[i] / 32768.0f"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philburk one for you

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid rounding in opposite directions because of the cast. But Andy has a better technique now. Will change.

Copy link
Collaborator

@mnaganov mnaganov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me. Thanks!

@@ -83,7 +83,7 @@ class LatencyTuner {
// arbitrary number of calls to wait before bumping up the latency
static constexpr int32_t kIdleCount = 8;

Stream &mStream;
AudioStream &mStream;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

realign

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

mInternalCallback = new AudioStreamBufferedCallback(this);
mStreamCallback = mInternalCallback;
LOGD("StreamBuffered(): mInternalCallback = %p", mInternalCallback);
mStreamCallback = std::make_shared<AudioStreamBufferedCallback>(this);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me nervous. Can this object be deleted properly if it holds a reference to itself?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "this" actually an AudioStreamBufferedCallback?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"this" is the argument for AudioStreamBufferedCallback's constructor. Thus, the constructed callback will hold a raw pointer to the stream, while the stream will be holding a refcounted pointer to the callback.

Copy link
Collaborator Author

@dturner dturner Nov 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an AudioStreamBuffered. The call is equivalent to doing new AudioStreamBufferedCallback(this) except that instead of a raw pointer the result is a shared_ptr which will ensure deletion when the refcount drops to zero.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Thanks for the explanation.

@@ -28,11 +28,11 @@ The audio device attached to a stream determines whether the stream is for input

A stream has a sharing mode:

* `OBOE_SHARING_MODE_EXCLUSIVE` (available on API 26+) means the stream has exclusive access to its audio device; the device cannot be used by any other audio stream. If the audio device is already in use, it might not be possible for the stream to have exclusive access. Exclusive streams provide the lowest possible latency, but they are also more likely to get disconnected. You should close exclusive streams as soon as you no longer need them, so that other apps can access the device.
* `OBOE_SHARING_MODE_SHARED` allows Oboe to mix audio. Oboe mixes all the shared streams assigned to the same device.
* `SharingMode::Exclusive` (available on API 26+) means the stream has exclusive access to its audio device; the device cannot be used by any other audio stream. If the audio device is already in use, it might not be possible for the stream to have exclusive access. Exclusive streams provide the lowest possible latency, but they are also more likely to get disconnected. You should close exclusive streams as soon as you no longer need them, so that other apps can access the device.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

audio device

Are we defining the oboe meaning of audio device somewhere ? Because audio device as define by the policy and audio flinger means the actual hardware (speaker, mic...). But exclusive mode allows exclusive access to a front end of this hardware. So

the device cannot be used by any other audio stream

might be confusing for the user as it does not mean the hardware can not be concurrently played to. For example on pixel, the dsp will mix the aaudio exclusive port with the audioflinger low latency port resulting in concurrent playback on the same hardware.

Ignore this comment if this is subtility of the "exclusive" mode is explain in some other part of the doc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very open to a better term than "audio device". Calling it a "front end of this hardware" is still not very clear. Maybe "audio resource"? A phone can have two "front ends" for a device. One is used by AudioTrack and one is available for MMAP.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's fix this doc issue in a later CL. We need to get this big refactoring done.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the concept of an audio device "endpoint" to explain this more clearly in #23.

Stop = AAUDIO_CALLBACK_RESULT_STOP,
};

enum class Result : aaudio_result_t {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are relying on aaudio_result_t beeing int32_t in multiple places. Maybe we should add in some .cpp static_assert(is_same<int32_t, aaudio_result_t>)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Added checks for all AAudio primitive data types.

#include "oboe/AudioStreamCallback.h"
#include "oboe/Definitions.h"

namespace oboe {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may or may not want to add an inline version namespace.
http://en.cppreference.com/w/cpp/language/namespace#Inline_namespaces
For non template library like Oboe, the advantage of inline version namespace is that symbols of different version will have a different name. Thus preventing a user from linking to the wrong version (aka less user error).
It also allows to have 2 oboe version in the same process.
Though I am not sure we want to support this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention was to have an App statically link to Oboe.
I suppose there could be a problem if an app tried to use with a library that had it's own calls to an old version of Oboe. But that seems unlikely. Most audio libraries just pass PCM in and out and leave the audio I/O to the main app..

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Oboe is static linked, inline version namespace have a lot less attractivity. Do not bother with it.

OBOE_CASE_ENUM(Result::ErrorNoService);
OBOE_CASE_ENUM(Result::ErrorInvalidRate);
default:
return "Unrecognized result";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to return the unrecognized value. Eg: "unrecognized result (-50)" to help the developer determined the problem (wrong casting, value is positive (not an error)...). Though, it would mean changing the api...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a TODO for this as it's a non-breaking change and I couldn't figure out exactly how this could be achieved easily without a lot of messing about with stringstream->string->c_str

Copy link

@krocard krocard Dec 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This IS a breaking change. You would have to return a string buffer that the client owned (eg: std::string). instead of returning a static char *

#include "oboe/Definitions.h"
#include "oboe/Utilities.h"

#define OBOE_CASE_ENUM(name) case name: return #name
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually like having both the human readable value and the number. How about

#define STR(x) #x
#define OBOE_CASE_ENUM(name) case name: return #name " (" STR(name) ")"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this and it just outputs the type name e.g. "Result::OK" twice in a row. I think I need to static_cast name to an int32_t then concat with the return string - could be tough to implement in a macro.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, for Kevin's approach to work, "name" must be a preprocessor define, so it can be expanded into a value during macro expansion phase, like described here: https://gcc.gnu.org/onlinedocs/gcc-4.1.2/cpp/Stringification.html. Preprocessor knows nothing about the values of enums.

But actually, the tricky part that involves the use of preprocessor is getting the program symbol name, that is, the name of the enum constant. The value of a enum constant can be obtained at compile- or run-time.

So, for example, in your enum to string function you could use a stringstream, where you first put the value of the enum (unconditionally), and then use the stringification trick in order to add the symbol name of the enum.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I though the name was an AAudio constant. There are solutions to convert the enum value to a compile time string, but it would add too much complexity. Hopefully in C++20, reflection will make all that much easier.
The alternative would be to construct the string at build time which would make it much easier:

template <class Enum, Enum enum>
const char* enumToStr(const char* name) {
    const static str = name
}
#define OBOE_CASE_ENUM(name) case name: return std::string(#name) + " (" + std::to_string(name) + ')';

But that changes the API, as a std::string has to be returned.

}
}

const char *convertSharingModeToText(SharingMode mode) {
Copy link

@krocard krocard Nov 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the input type is already in the signature, you may or may not want to remove it from the function name:

const char *convertToText(AudioFormat format);
const char *convertToText(PerformanceMode mode);
const char *convertToText(SharingMode mode);

but for one it is a style preference secondly some people prefer to be explicit.
Of course there is also the template route that allows explicit and implicit input type specification:

template <class From>
const char * convertToText(From);

template <>
const char* convertToText<AudioFormat>(AudioFormat format);
// ...

// Which can be used explicitly:
auto format = AudioFormat::I16;
cout << convertToText<AudioFormat>(format);
// or implicitly
cout << convertToText(format);

But this is not the current style of the API, plus it is a backward incompatible change that might not be worth it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This CL breaks the API anyway. We warned people it would break. And we are modernizing. So now is the time if we want to do this kind of stuff. Up to you Don.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implicit use of convertToText is really nice. Have implemented.

Copy link
Collaborator

@philburk philburk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks Don.

Copy link
Collaborator

@philburk philburk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK to finish adding the int value in a later CL.

@dturner dturner merged commit 2e10dec into master Dec 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants