.. based on some tasks I found that caused stuttering:
# Test SD and printing
while True: os.listdir('.')
# Test bulk I/O
while True: len(open('somefile.wav', 'rb').read())
Each of these tasks *WAS* worse and I am improving them in a separate
PR by adding RUN_BACKGROUND_TASKS to them.
This enables the highest level of debug symbols, and all optimizations
except lto that do NOT interfere with debugging, in the view of the gcc
maintainers.
Testing performed: I used a Particle Xenon with a HDA1334 I2S DAC.
I played a variety of mono 16-bit samples at 11025 and 22050Hz nominal
bit rates. With this setup, all the 11025Hz samples sound good.
I tested play, pause, and loop functionality.
During some runs with 22050Hz samples, there were glitches. However,
these may have only occurred during runs where I had set breakpoints
and watchpoints in gdb.
I also tested with a MAX98357A I2S amplifier. On this device, everything
sounded "scratchy". I was powering it from 5V and the 5V rail seemed
steady, so I don't have an explanation for this. However, I haven't
tried it with a SAMD board.
Previously, we depended on allocated channels to always be
"dma_channel_enabled". However, (A) sometimes, many operations
would take place between find_free_audio_dma_channel and
audio_dma_enable_channel, and (B) some debugging I did led me to believe
that "dma_channel_enabled" would become false when the hardware ended
a scheduled DMA transaction, but while a CP object would still think it
owned the DMA channel.
((B) is not documented in the datasheet and I am not 100% convinced that
my debugging session was not simply missing where we were disabling the
channel, but in either case, it shows a need to directly track allocated
separately from enabled)
Therefore,
* Add audio_dma_{allocate,free}_channel.
* audio_dma_free_channel implies audio_dma_disable_channel
* track via a new array audio_dma_allocated[]
* clear all allocated flags on soft-reboot
* Convert find_free_audio_dma_channel to audio_dma_allocate_channel
* use audio_dma_allocated[] instead of dma_channel_enabled() to check
availability
* remove find_free_audio_dma_channel
* For each one, find a matching audio_dma_disable_channel to convert
to audio_dma_free_channel
Closes: #2058
.. otherwise, a sequence like
>>> a = audioio.AudioOut(board.A0)
>>> a.play(sample, loop=True)
>>> a.deinit()
would potentially leave related DMA channel(s) active.
So far, this supports only 16kHz and 16-bit samples with a fixed gain.
This is enough to support the basic functionality of e.g., sensing
ambient audio levels.
Some ports which actually don't have audioio or audiobusio were still
calling into audio_dma_background(). This wasn't an error until
the assignment to audio_dma_state in audio_dma_stop was added, though
it's not clear why.
audio_dma_stop can be reached twice in normal usage of AudioOut.
This may bear further investigation, but stop it here, by making the
function check for a previously freed channel number. This also prevents
the event channel from being disabled twice.
The first stop location is from audio_dma_get_playing, when the buffers
are exhausted; the second is from common_hal_audioio_audioout_stop when
checking the 'playing' flag.
As identified in #1908, when both AudioOut and PDMIn are used, hard
locks can occur. Because audio_dma_stop didn't clear audio_dma_state[],
a future call to audio_dma_load_next_block could occur using a DMA
object which belongs to PDMIn.
I believe that this Closes: #1908 though perhaps it is still not the full
story.
Testing performed: Loaded a sketch similar to the one on #1908 that
tends to reproduce the bug within ~30s. Ran for >300s without hard
lock. HOWEVER, while my cpx is no longer hard locking, it occasionally
(<1 / 200s) announces
Code done running. Waiting for reload.
(and does so), even though my main loop is surrounded by a 'while True:'
condition, so there are still gremlins nearby.
The original formulation was because I saw the need to avoid a transition
from playing to stopped exactly when a resume was taking place. However,
@tannewt was concerned about this pause causing trouble, because it could
be relatively lengthy (several ms even in a typical case).
After reflection, I've convinced myself that updating the registers
in this order in resume avoids a window where a "stopped" event can
be missed as long as the shortcut is updated first.
Testing re-performed: pause/resume testing of looped RawSample and
WaveFile audio sources.
Testing performed: installed freshly built .uf2 on a Particle Xenon.
Checked that circuitpython still starts.
Checked that the size of all .uf2 files for nrf builds are plausible.
Aside from memory savings, the performance of Python code (pystone)
increased by about +14%.
However, this adds about 12-16 seconds to each nrf build.
Timings & Sizes (build system: i5-3320M, -j5 parallelism on 4 threads):
Before:
$ make -j5 BOARD=particle_xenon
765004 bytes free in flash out of 1048576 bytes ( 1024.0 kb ).
232076 bytes free in ram for stack out of 245760 bytes ( 240.0 kb ).
68.54user 11.83system 0:34.34elapsed 234%CPU
pystones before: 570
After:
$ make -j5 BOARD=particle_xenon
804284 bytes free in flash out of 1048576 bytes ( 1024.0 kb ).
232072 bytes free in ram for stack out of 245760 bytes ( 240.0 kb ).
71.06user 11.77system 0:46.91elapsed 176%CPU
pystones after: 650
Timings on travis:
Before:
Build feather_nrf52840_express for pl took 55.79s and succeeded
Build feather_nrf52840_express for zh_Latn_pinyin took 3.18s and succeeded
After:
Build feather_nrf52840_express for pl took 62.72s and succeeded
Build feather_nrf52840_express for zh_Latn_pinyin took 19.10s
Closes: #1396