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

AudioSource v2 API #1317

Merged
merged 82 commits into from
Nov 25, 2017
Merged

AudioSource v2 API #1317

merged 82 commits into from
Nov 25, 2017

Conversation

uklotzde
Copy link
Contributor

@uklotzde uklotzde commented Jul 22, 2017

The new and simplified API for AudioSources. New sources only need to implement a single function for decoding audio data!! The index and range calculations in various implementations can still be tweaked and optimized, but at least the API is clean and hopefully stable.

All existing sources have been migrated and tested extensively. With only a single exception: SoundSourceCoreAudio. This one still uses an adapter that I have written while gradually migrating all implementations from the v1 API. Until Apple decides to provide cross-platform development VMs instead of forcing everyone to buy their hardware, I'm not able to finish this task myself. But the adapter works flawlessly, so we don't need to migrate this remaining implementation now.

Please also note that the new seekBoundaries() tests I added cause failures for SoundSourceFFmpeg and is therefore disabled for this source -> OpenAudioSourceMode::DisableFFmpeg. I'm not familiar with the internals of the implementation and gave up to repair it ;)

I also found and fixed a flaw in the CachingReader framework: When skipping randomly through the track the next frames might not have been decoded when a read request is queued. Instead of silently delivering a buffer filled with silence as before it now returns an empty buffer to signal that no data is available, yet. The caller is able to decide when to retry or to continue instantly without any data. You will notice some debug logs whenever this happens, just skip through some track while playing it.

The original idea and motivation has been sketched here: https://blueprints.launchpad.net/mixxx/+spec/audiosourcev2

[Update 2017-10-04] I was able to streamline the API even further. There is no need to explicitly "skip" through an audio stream in the public API. This functionality is only useful for testing. During tests for sample accurate decoding you need to explicitly control this behavior yourself instead of relying on some hidden implementation that might decide to seek whenever it thinks to do so! Otherwise you are comparing apples with apples ;)

Empty,
Forward,
Backward,
};
Copy link
Member

Choose a reason for hiding this comment

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

I am not entirely through with my reading...
I just wonder if we need a range class that supports forward and backward in the same time. Will the using code simpler or not save if we have dedicated classes or sub classes only supporting one direction?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have actually written an unidirectional version first, but changed it to a symmetric, bidirectional version that is more consistent and universally usable.

I did not find anything suitable in some library, otherwise I would have used it.

// Splits this range into two adjacent parts by slicing off
// and returning a range of given length and same direction
// from the head side. The given length must not exceed
// the length of this range.
Copy link
Member

Choose a reason for hiding this comment

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

this comment is not clear enough. What will be returned what will remain in the class? Would it be better to just return both parts, and leave the original unchanged?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those mutable operations sacrifice safety for usability. In languages like Rust or Scala that support pattern matching for variable bindings I would have done it the way you proposed.


SINT outputSampleOffset = 0;
if (seekFrameIndex > readableFrames.head()) {
const auto unreadableFrames = readableFrames.splitHead(seekFrameIndex - readableFrames.head());
Copy link
Member

Choose a reason for hiding this comment

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

It was not instantly clear for me, what this line does.
Head is in one case a single position, and in an other case a new range. the auto keyword hides this fact even more.

Can we move to head = range and top and bottom = a single index? O really other better words? Than we have the top of head.
This can than become for example cutOutHead(seekFrameIndex - readableFrames.top())

Copy link
Contributor Author

@uklotzde uklotzde Jul 26, 2017

Choose a reason for hiding this comment

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

I will change head() and tail() to front() and back() like in std::vector not so good idea

I adopted start() and end() from Rust's Range.


SINT outputSampleOffset = 0;
if (seekFrameIndex > readableFrames.head()) {
const auto unreadableFrames = readableFrames.splitHead(seekFrameIndex - readableFrames.head());
Copy link
Member

Choose a reason for hiding this comment

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

or End of Tail ..

// the length of this range.
IndexRange splitHead(SINT headLength);
IndexRange splitFront(SINT frontLength);
Copy link
Member

Choose a reason for hiding this comment

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

This can be read as "split the Front part". Ideas: cutOutFront() .. cutFront() .. sliceOutFront() .. sliceOffFront()

// The opposite boundary (exclusive)
SINT tail() const {
// The next index beyond this range (exclusive)
SINT end() const {
return second;
}

bool empty() const {
Copy link
Member

Choose a reason for hiding this comment

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

isEmpty() empty() can be an imperative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But even Qt is using the simplified naming for boolean getters.

Copy link
Member

Choose a reason for hiding this comment

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

You are right for getValue() vs. just value()
But that does not apply for empty. Here Qt use IsEmpty() http://doc.qt.io/qt-4.8/qbytearray.html#isEmpty

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On the other hand STL uses empty() everywhere and Qt adopts this usage for compatibility in QList for example.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, in that case ;-) ... I will keep quite.

}

// Clamps index by this range including both head() and tail()
// Clamps index by this range including both start() and end()
// boundaries.
SINT clamp(SINT index) const {
Copy link
Member

Choose a reason for hiding this comment

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

it is not clear that the index ins clamped-
clampIndex() .. or .. getNearestInBounds()

return readableFrames;
}
DEBUG_ASSERT(readableFrames.start() >= frameIndexRange.start());
if (pOutputBuffer) {
Copy link
Member

Choose a reason for hiding this comment

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

this is already checked above

// sample values.
//
// On errors only a sub range of the requested frames might be returned.
virtual IndexRange readOrSkipSampleFrames(
Copy link
Member

Choose a reason for hiding this comment

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

After reading the code I understand the function name. But the "OrSkip" part was confusing on the first read.
Now it is obvious that if you pass a null buffer that it skips frames. Isn't the main nature of this function that it seeks to the file and fills the buffer if it can do it?

How about seekAndReadSampleFrames()

//
// Only that part of the output buffer corresponding to the returned
// range is allowed to be modified. All remaining samples in the output
// buffer should stay untouched. The samples in the output buffer need
Copy link
Member

Choose a reason for hiding this comment

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

Why we do not zero out the buffer, if the requested range starts earlier than the readable range of the file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's the responsibility of the caller. We don't want to make any assumptions what the caller is doing with the results. Moreover we would need to add extra code to each of the implementations to fill the unread parts with zero (that maybe are never used by the caller anyway).

Copy link
Member

Choose a reason for hiding this comment

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

ok

SampleBuffer::WritableSlice* pOutputBuffer) {
auto readableFrames =
adjustReadableFrameIndexRangeAndOutputBuffer(
frameIndexRange, pOutputBuffer);
Copy link
Member

Choose a reason for hiding this comment

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

It is confusing, that the output buffer is modified by a pointer but not the frameIndexRange.
Can we modify both by pointer?

Copy link
Member

Choose a reason for hiding this comment

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

the same is also true for readOrSkipSampleFrames. Do we need the original IndexRange passed?

+ (readableFrames.start() / m_framesPerSampleBlock);
DEBUG_ASSERT(isValidSampleBlockId(sampleBlockId));
if ((readableFrames.start() < m_curFrameIndex) || // seeking backwards?
!isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position?
Copy link
Member

Choose a reason for hiding this comment

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

How can that be invalid?

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see, after starting the decoder. Can you change the comment accordingly?

const auto precedingFrames =
IndexRange::between(m_curFrameIndex, readableFrames.start());
if (!precedingFrames.empty()
&& (precedingFrames != skipSampleFrames(precedingFrames))) {
Copy link
Member

Choose a reason for hiding this comment

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

Is this a recursive call? How is guaranteed that we not reed the prefetched part over and over again. Can't we avoid this recursion?

@uklotzde
Copy link
Contributor Author

@daschuer Inspired by your remarks I will prepare an improved and simplified API:

enum class ReadMode {
    Store, // write/copy decoded sample data into buffer
    Skip,  // discard decoded sample data
};

virtual ReadableSampleFrames readSampleFrames(
    ReadMode readMode,
    WritableSampleFrames sampleFrames) = 0;

Readable-/WritableSampleFrames are simple DTOs that contain both an IndexRange of frames and the corresponding slice of a SampleBuffer for transferring/storing sample data. This results in a single function that covers all our use cases.

@uklotzde uklotzde changed the title [WiP] AudioSource v2 API AudioSource v2 API Sep 16, 2017
@uklotzde
Copy link
Contributor Author

The OS X build on Travis works when switching to Xcode 8.3.

@Be-ing
Copy link
Contributor

Be-ing commented Sep 16, 2017

This doesn't introduce any user facing changes, does it? As far as I understand this is just making the code more maintainable. Does it fix identifiable bugs?

@uklotzde
Copy link
Contributor Author

uklotzde commented Sep 16, 2017

No new features, just a consolidated and lean API for plugin developers, more tests and lots of internal code rework. Common functionality like bounds checking and post-processing (downsampling to stereo) has been integrated into Mixxx. The decoding code was already pretty stable and well tested, now it should be even safer with only a minor performance degradation.

There actually were some decoding bugs when seeking near boundaries in some implementations. I'm not sure if I fixed all of them with a previous PR that we have split off this branch.

I wouldn't call the CachingReader flaw a bug, it is just undocumented and untested behavior. At least I didn't notice any audible dropouts, neither before nor after the fix and refactoring. We definitely need more tests for both the CachingReader and the engine itself. But those tests are difficult and brittle, because they have to consider timing. I even can't imagine how such tests should look like.

@uklotzde
Copy link
Contributor Author

Too bad that both CI servers struggle with Windows and OS X builds. Travis aborts with a timeout when switching to Xcode 8.3, the build just takes too long.

@Be-ing
Copy link
Contributor

Be-ing commented Sep 16, 2017

With the free options for AppVeyor and Travis we use, they both time out randomly. I'm not sure if switching to Xcode 8.3 slows it down too much.

@sblaisot
Copy link
Member

there is an issue with appveyor. They updated their NSIS version and our NSIS patching is rejected.
I need to check that and update the patch to restore appveyor CI.

Let me have a look at that.

@sblaisot
Copy link
Member

please cherry-pick 2e9475b from #1344 to fix appveyor build.

@sblaisot
Copy link
Member

You can also cherry-pick ac34d82 from #1345 to fix travis-ci mac build

@uklotzde
Copy link
Contributor Author

Rebased on master once again for a clean commit history. Thanks @sblaisot for your fixes 👍 Looks like we are back on track.

@uklotzde
Copy link
Contributor Author

I've taken the chance to quickly resolve another outstanding TODO issue: Apply a 2-pass strategy when opening files to find the best matching decoder.

@Be-ing
Copy link
Contributor

Be-ing commented Sep 17, 2017

I've taken the chance to quickly resolve another outstanding TODO issue: Apply a 2-pass strategy when opening files to find the best matching decoder.

Does this mean we can support ALAC via FFMPEG?

@uklotzde
Copy link
Contributor Author

@Be-ing Yes, ALAC can be played through SoundSourceFFmpeg. Just checked it again with example files from the Linn page. This should already be possible with the current master. SoundSourceFFmpeg supports only 16-bit (24-bit: Unsupported sample format: s32p) while SoundSourceFLAC supports both 16-bit and 24-bit.

Please also note that SoundSourceFFmpeg still has issues when seeking to boundaries, see the disabled test. And the internal caching of audio data seems to need some rework according to strange log messages that appear while decoding.

@Be-ing
Copy link
Contributor

Be-ing commented Oct 19, 2017

It would be really helpful if you wrote a high level overview of how the different classes interact and how it interacts with the mixing engine. This could be comments in a header file or it might be better to add a page to the Developer Guide on the wiki.

@uklotzde
Copy link
Contributor Author

uklotzde commented Oct 19, 2017

@Be-ing I agree, a short HOWTO write and adding a new SoundSource(Plugin) would be helpful.

I'm not satisfied with the current plugin system which is not properly isolated from the main code base, so a HOWTO about plugins might follow only after fixing this. We should first investigate how other projects handle extensions and plugins and even consider the implications of a future build system like Meson!

Passing sample data to the engine is handled by ReadAheadManager/CachingReader and a different story. These components also need a second round of rework, maybe exchanging some of the brittle hand-written multi-threading code by standard C++ or Boost components ;)

Well, now I ended up with 3 follow-up tasks.

// have actually been decoded
m_sampleBuffer.readFifo(decodeBufferCapacity - numberOfSamplesDecoded);
m_sampleBuffer.readFromTail(decodeBufferCapacity - numberOfSamplesDecoded);
Copy link
Member

Choose a reason for hiding this comment

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

Did you consider to rename this function to shrinkTail or something? The return value is never used.

@uklotzde
Copy link
Contributor Author

uklotzde commented Nov 22, 2017

Regarding Readable-/WritableSlice: The code would not get safer by using 2 individual values (pointer + size) as before instead of wrapping them into a shallow wrapper class. The wrapper classes at least prevent that the values are modified. Even worse, individual values can be modified independently of each other! The term slice should indicate that the memory is borrowed and not owned. Well, not in a safe ways as in Rust ;) The different types clearly indicate their purpose, i.e. read from or write into.

@daschuer
Copy link
Member

Yes, I agree, this approach is nice.

// Shrink the size of the buffer to the samples that have
// actually been decoded, i.e. dropping unneeded samples
// from the back of the buffer.
m_sampleBuffer.dropFromTail(decodeBufferCapacity - numberOfSamplesDecoded);
Copy link
Member

Choose a reason for hiding this comment

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

Lets name the function to what it does here. It adjusts the readable range to the just written samples
m_sampleBuffer.shrinkToWritten(numberOfSamplesDecoded);
m_sampleBuffer.confirmSamplesWritten(numberOfSamplesDecoded);

@daschuer
Copy link
Member

OK, I think I am through :-)

writeToTail() grows the readable range. So this function promises that the bytes are readable later.
It would be more logical to grow the radable range after the samples where actually written.
This requires an mandatory extra "confirmWritten()" call or something.

Currently it is silently assumed that we actually write. It is OK for me because it seems the most performance solution, but we should describe this in the writeToTail() comments.

The rest LGTM

@uklotzde
Copy link
Contributor Author

I'm against introducing a confirmWritten(), because ReadAheadSampleBuffer class is NOT intended to be used with concurrent readers and writers as already mentioned in the class comment. I will try to modify the API to avoid this impression. Those restrictions should become obvious even by reading just the code without looking at the comments.

@uklotzde
Copy link
Contributor Author

I was struggeling with the term "shrink" and have to think about how to align the operation names in both SampleBuffer and IndexRange. It should be obvious if we are "shrinking" just the range or actually the buffer. It's confusing if the same term is used for different purposes, so we need to decide which terms to use for ranges and buffers. Those terms should differ.

@uklotzde
Copy link
Contributor Author

ReadAheadSampleBuffer is the combination of both a SampleBuffer and an IndexRange. So we need to carefully choose the corresponding operation names. There is also some confusion around "capacity"/"size"/"length". I will try to disambiguate the usage.

@daschuer
Copy link
Member

That was my final thought. I think we can stop here to overoptimize it and put the our final thoughts to the comments.

@uklotzde
Copy link
Contributor Author

I've improved the documentation and renamed the member functions of ReadAheadSampleBuffer. Should be much more consistent and comprehensible than before.

All changes divided into single commits to illustrate what I actually changed.

@daschuer
Copy link
Member

LGTM Really great now. Thank you very much!

@daschuer daschuer merged commit 80f3ea5 into mixxxdj:master Nov 25, 2017
@JosepMaJAZ
Copy link
Contributor

Now that the audiosource v2 is in master, I found a bug that happens with M4A files under Windows causing a program crash (I should have helped testing it on Windows).

Concretely, a stack overflow happens because SoundSourceMediaFoundation::readSampleFramesClamped always calls SoundSourceMediaFoundation::seekSampleFrame, but the implementation of seekSampleFrame calls readSampleFramesClamped in some cases, causing the aforementioned stack overflow.

The problem happens when loading one track, playing and then seeking to a later part of the track (like clicking with the mouse to go to the half of the track).

```

soundsourcemediafoundation.dll!mixxx::SoundSourceMediaFoundation::readSampleFramesClamped(mixxx::WritableSampleFrames writableSampleFrames) Línea 252 C++
soundsourcemediafoundation.dll!mixxx::SoundSourceMediaFoundation::seekSampleFrame(__int64 frameIndex) Línea 209 C++

soundsourcemediafoundation.dll!mixxx::SoundSourceMediaFoundation::readSampleFramesClamped(mixxx::WritableSampleFrames writableSampleFrames) Línea 252 C++
soundsourcemediafoundation.dll!mixxx::SoundSourceMediaFoundation::seekSampleFrame(__int64 frameIndex) Línea 209 C++
soundsourcemediafoundation.dll!mixxx::SoundSourceMediaFoundation::readSampleFramesClamped(mixxx::WritableSampleFrames writableSampleFrames) Línea 252 C++
mixxx.exe!mixxx::AudioSourceTrackProxy::readSampleFramesClamped(mixxx::WritableSampleFrames sampleFrames) Línea 48 C++
mixxx.exe!mixxx::AudioSourceStereoProxy::readSampleFramesClamped(mixxx::WritableSampleFrames sampleFrames) Línea 60 C++
mixxx.exe!CachingReaderChunk::bufferSampleFrames(const std::shared_ptrmixxx::AudioSource & pAudioSource, mixxx::SampleBuffer::WritableSlice tempOutputBuffer) Línea 70 C++
mixxx.exe!CachingReaderWorker::processReadRequest(const CachingReaderChunkReadRequest & request) Línea 53 C++
mixxx.exe!CachingReaderWorker::run() Línea 110 C++

@uklotzde
Copy link
Contributor Author

uklotzde commented Dec 6, 2017

Thanks for reporting!!! I'll fix this immediately.

@uklotzde
Copy link
Contributor Author

uklotzde commented Dec 6, 2017

Work in progress: #1406

First I want to have a failing regression test in place to prevent this happening again.

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

6 participants