Skip to content

samd: audio_dma: Shift responsibility for two-dma-channel audio #2533

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

Closed
wants to merge 8 commits into from

Conversation

jepler
Copy link

@jepler jepler commented Jan 20, 2020

While working on audio playback, I noticed that there were problems affecting only the right channel, and only on samd dac; not on i2s or on nrf.

These problems affected both the mp3 and mixer audio sources, and stem from the necessary handling of the "single_channel" flag to the "get_buffer" method being complicated and not (as far as I could see) documented.

Rather than fix the similar bug at least twice, I think it is preferable to shift the responsibility to the port that requires it, so that it can be gotten right once.

(Furthermore, I suspect that after a DMA overrun, there's simply no way to get the right data back in each channel, because in this case they may have been advanced independently and the series of "almost exactly alternating" calls to get_buffer per channel was broken)

So:

  • For samd51, add the concept of a "sibling" DMA channel. When playing stereo on DAC, the left channel is made the "first" sibling (so its sibling channel number is the right channel number) and the right channel is made the second channel.
  • To facilitate this, the step of preloading audio blocks has to be split out, so that the channels can be linked at the right moment
  • Some details around stopping channels together

This touches a lot of stuff, so I'm leaving it in draft state until I get through this self-made testing checklist. Also, I ended up writing it cumulative with #2526 because otherwise there would be conflicts between the two PRs; After that PR is merged this one should be rebased before its own merge.

Testing checklist (pygamer, DAC):

  • Listen to the whole bartlebeats album in JEplayer without an audio glitch
  • Run through the RawSample test cases
  • Run through the WaveFile + AudioMixer test cases

Testing checlist (Metro M4 Express + I2S):

  • Run through the RawSample test cases
  • Run through the WaveFile + AudioMixer test cases

Testing checklist (Trinket M0 / Pyruler):

  • Run through the mono RawSample test cases

This removes downscaling (halving-add) when multiple voices are
being mixed.  To avoid clipping, and get similar behavior to before,
set the "level" of each voice to (1/voice_count).

Slow paths that were applicable to only M0 chips were removed.

As a side effect, the internal volume representation is now 0 ..
0x8000 (inclusive), which additionally makes a level of exactly 0.5
representable.

Testing performed, on PyGamer: For all 4 data cases, for stereo and
mono, for 1 and 2 voices, play pure sign waves represented as
RawSamples and view the result on a scope and through headphones.
Also, scope the amount of time spent in background tasks.

Code size: growth of +272 bytes

Performance (time in background task when mixing 2 stereo 16-bit voices):
76us per down from 135us (once per ~2.9ms long term average)
(Decrease from 4.7% to 2.4% of all CPU time)
What I didn't understand was that initially, the sequence of channel
numbers is "0011", while in normal operation it should consistently
be "0101".  Just keeping track of the "other channel" works during the
steady state, but at startup it causes the right channel to be shifted
by 1.

This cures the small glitches that could be seen clearly when playing
stereo sine wave mp3s.
While working on audio playback, I noticed that there were problems
affecting only the right channel, and only on samd dac; not on i2s or
on nrf.

These problems affected both the mp3 and mixer audio sources, and stem from
the necessary handling of the "single_channel" flag to the "get_buffer"
method being complicated and not (as far as I could see) documented.

Rather than fix the similar bug at least twice, I think it is preferable
to shift the responsibility to the port that requires it, so that it can
be gotten right once.

(Furthermore, I *suspect* that after a DMA overrun, there's simply no way
to get the right data back in each channel, because in this case they may
have been advanced independently and the series of "almost exactly
alternating" calls to get_buffer per channel was broken)

So:
 * For samd51, add the concept of a "sibling" DMA channel.  When playing
   stereo on DAC, the left channel is made the "first" sibling (so its
   sibling channel number is the right channel number) and the right channel
   is made the second channel.
 * To facilitate this, the step of preloading audio blocks has to be
   split out, so that the channels can be linked at the right moment
 * Some details around stopping channels together

Testing checklist (pygamer + jeplayer, DAC):
 * [X] Listen to the whole bartlebeats album without an audio glitch
 * [ ] Run through the RawSample test cases
 * [ ] Run through the WaveFile + AudioMixer test cases

Testing checlist (Metro M4 Express + I2S):
 * [ ] Run through the RawSample test cases
 * [ ] Run through the WaveFile + AudioMixer test cases

Testing checklist (Trinket M0 / Pyruler):
 * [ ] Run through the mono RawSample test cases
These are no longer needed, as the implementation that ever passed
'true' to single_channel is gone.

This also allows removal of some state from audio sample sources,
such as the "channel read count" of WaveFile.

This change reindents code and as such may be better to view with
a whitespace-ignoring diff such as "git show -w"
.. this allows type confusion for all parameters (and even parameter
counts), rather than just for the type of the first parameter.
.. spacing is Just Wrong(TM)
This out-parameter was always being set to 1 for all implementations
@jepler
Copy link
Author

jepler commented Jan 22, 2020

@tannewt It's not review-ready yet, but can I get your comment on the general idea of this change before I spend more time on my testing plan?

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

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

No major concerns from me. I like how the get_buffer calls are simplified.

I would rather not see as many void* and use mp_obj_t (which I think is a typedef for void*) because it implies that type info is in the first word.

I did these gymnastics on SAMD51 to try an minimize memory bus and DMA traffic by doing the one 32bit write instead of two 16bit ones.

@jepler
Copy link
Author

jepler commented Jan 23, 2020

Thanks for the quick feedback. This won't resurface as a priority for awhile, so consider the ball firmly in my court.

@jepler
Copy link
Author

jepler commented Jan 27, 2020

Let's close this one up too. It needs to be heavily reworked with #2552 in, as outlined in my comments there.

@jepler jepler closed this Jan 27, 2020
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.

2 participants