Commit graph

42 commits

Author SHA1 Message Date
4d4d654677 MP3Decoder: set the nonblocking flag as needed
this makes SSL sockets (which return readable when not yet actually
readable) work better.
2024-06-13 11:11:17 -05:00
6c9eaf1415 MP3Decoder: better handle underflows of mp3 stream
* Don't consume in the case of "indata overflow".
   Doing so leaves us at a bad boundary within the MP3 data
   and can continue decoding from an inappropriate spot
   i.e., one that looks likede mp3 data but is NOT.

   because there are many crashing bugs in the helix mp3 library on
   invalid input data, we must do our best to avoid them, and this
   is one way to do that.

 * clear the output buffer in the case there's not a sync word in the
   buffer. this can also happen when too little data is available.

   this changes more "stuttering" conditions into "silent" conditions.

With these changes, I can get through 3+ plays of "idea.mp3" from a local
http server with long pauses (but not stuttering glitches or safe mode
crashes).

I was also able to play through 10+ minutes of http://ice2.somafm.com/dronezone-128-mp3
without crashing or "end of stream", though again there are pauses
due to packet loss.

I think this is good now, except for the problems that arise when
the socket layer doesn't deliver a fresh packet for a long time.
2024-06-05 14:52:07 -05:00
cb295c57d7 MP3Decoder: Make it possible to start in the middle of an MP3
You can now, e.g.,
```
with open("/whatever.mp3") as mp3_file:
    mp3_file.seek(16000*30)
    decoder.file = mp3_file
    i2s.play(decoder)
```
to start about 30 seconds into a 128kbit/s CBR track.

If a track is looped, the loop will start at the beginning.

This also changes the behavior if the track is started & stopped: it will
continue from where it left off, except if it had prevously run to
completion. To get other behavior, you can seek the file and then re-assign
the file property.
2024-06-05 10:33:40 -05:00
6f6b680c18 MP3Decoder: improve some comments 2024-06-04 12:24:34 -05:00
f81a3aed68 MP3Decoder: avoid all blocking reads in background
This gets MP3 playback of a soma.fm stream working for up to a minute
at a time, though it's still vulnerable to network glitches.

 * the buffer can empty, in which case a single block of audio
   plays repeatedly in max headroom stutter fashion
 * the server eventually (after 1 to 5 5 minutes) stops getting packets
   at all. At this point stream playback stops, with the internal error
   indicating a problem MP3 decoding (which doesn't quite make sense):
   -9, ERR_MP3_INVALID_HUFFCODES.
 * other combinations of audiomixer buffer & mp3 buffer might give
   different results
```py
import time

import adafruit_connection_manager
import adafruit_requests
import audiobusio
import audiomixer
import audiomp3
import board
import wifi

pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)

# todo: parse PLS files like https://somafm.com/nossl/dronezone.pls
# todo: figure out why https URLs don't work at all (missing select?)
# STREAMING_URL = "http://ice2.somafm.com/dronezone-128-mp3"
STREAMING_URL = "http://ice4.somafm.com/tikitime-128-mp3"


def get_mp3_stream():
    if STREAMING_URL.startswith("http:") or STREAMING_URL.startswith("https:"):
        return requests.get(STREAMING_URL, headers={"connection": "close"}).socket
    return open(STREAMING_URL, "rb")


mixer_buffer_size = 1152 * 2
mp3_buffer = bytearray(32768)

with audiobusio.I2SOut(
    bit_clock=board.D12, word_select=board.D13, data=board.D11
) as i2s, get_mp3_stream() as stream, audiomp3.MP3Decoder(
    stream, mp3_buffer
) as sample, audiomixer.Mixer(
    channel_count=2, sample_rate=44100, buffer_size=mixer_buffer_size
) as m:

    v = m.voice[0]
    print(sample)

    i2s.play(m)

    v.play(sample, loop=False)
    while v.playing:
        time.sleep(0.1)
```
2024-06-03 15:49:14 -05:00
a5c4a86349 MP3Decoder: ssize_t type is not portable, don't use it 2024-05-30 16:45:35 -05:00
dffc55ee17 MP3Decoder: STATIC -> static 2024-05-30 14:32:48 -05:00
9bc89f5fba MP3Decoder: better handle INDATA_UNDERFLOW & MAINDATA_UNDERFLOW
This allows playback of some 128k http streams from somafm, though
glitches occur with some regularity.

```py
import time

import adafruit_connection_manager
import adafruit_requests
import audiobusio
import audiomixer
import audiomp3
import board
import wifi

pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)

# todo: parse PLS files like https://somafm.com/nossl/dronezone.pls
# todo: figure out why https URLs don't work at all (missing select?)
# STREAMING_URL = "http://ice2.somafm.com/dronezone-128-mp3"
STREAMING_URL = "http://ice4.somafm.com/tikitime-128-mp3"


def get_mp3_stream():
    if STREAMING_URL.startswith("http:") or STREAMING_URL.startswith("https:"):
        return requests.get(STREAMING_URL, headers={"connection": "close"}).socket
    return open(STREAMING_URL, "rb")


mixer_buffer_size = 1152 * 16
mp3_buffer = bytearray(16384)

with audiobusio.I2SOut(
    bit_clock=board.D12, word_select=board.D13, data=board.D11
) as i2s, get_mp3_stream() as stream, audiomp3.MP3Decoder(
    stream, mp3_buffer
) as sample, audiomixer.Mixer(
    channel_count=2, sample_rate=44100, buffer_size=mixer_buffer_size
) as m:

    v = m.voice[0]
    print(sample)

    i2s.play(m)

    v.play(sample, loop=False)
    while v.playing:
        time.sleep(0.1)
```
2024-05-30 14:22:51 -05:00
974b21a079 MP3Decoder: do better at keeping stream buffer full 2024-05-30 14:22:51 -05:00
a80311d4c0 MP3Decoder: make testable in coverage build
An mp3 decoder (note that this needs `audiocore.get_buffer`, not
enabled on devices):
```py
import sys
import audiomp3
import audiocore

GET_BUFFER_DONE, GET_BUFFER_MORE_DATA, GET_BUFFER_ERROR = range(3)

with audiomp3.MP3Decoder(sys.argv[1]) as decoder, open(sys.argv[2], "wb") as target:

    while True:
        res, samples = audiocore.get_buffer(decoder)
        if res != GET_BUFFER_ERROR:
            target.write(samples)
        if res != GET_BUFFER_MORE_DATA:
            break
```

this doesn't actually add any tests though
2024-05-30 14:22:39 -05:00
d6ddd55462 MP3Decoder: Allow passing any stream-like object
This can sort-of play MP3s from a http request, but the buffering is
not good enough to play glitch-free. A new kind of buffer that can
read ahead further without blocking is needed.
2024-05-30 14:21:34 -05:00
Dan Halbert
3f4d9310ff CircuitPython files: replace STATIC with static 2024-05-20 11:02:17 -04:00
Dan Halbert
950b5d09d2 guard2once -s ... 2024-05-19 20:40:44 -04:00
Dan Halbert
747b7619ea update headers of most CircuitPython-only files 2024-05-17 14:56:28 -04:00
7af2a13245 MP3Decoder: Allow port to override the allocator
.. espressif will be able to put the mp3 data in faster RAM this way.
2024-05-01 14:20:39 -05:00
08a440d19b espressif: Don't hold interrupts disabled a long time
.. just to prevent background tasks from running
2024-05-01 10:07:13 -05:00
774f6ac6ab
Switch to using MP_ERROR_TEXT instead of translate, globally 2023-10-30 09:49:06 +01:00
Dan Halbert
2c0fa0f7dc initial merge from v1.20.0; just satisifying conflicts 2023-09-19 11:10:12 -04:00
Dan Halbert
fe0e2f13bc wip; fix qstr processing 2023-08-10 20:06:32 -04:00
Scott Shawcroft
9d10a3da66
Conditionalize LTO 2022-05-27 12:59:54 -07:00
Dan Halbert
a01dec1df9 message consolidation and more use of validators 2022-05-19 15:38:37 -04:00
df5f7bc765
MP3Decoder: better handle indicating end of mp3 audio data to caller
The old formulation
 * wouldn't work if there were ID3 tags at the end
 * would choose whether to background-refill the inbuf
   based on a check before skipping to the next sync word, which
   could be incorrect.

I think it was aspect "B" that ended up triggering the erroneous EOF
problem fixed in the prior commit. This would depend on specific data
sizes and offsets occuring in the file such that a read would be
scheduled but then the buffer would be filled and left 100% full by
find_sync_word(). It's just lucky(?) that a particular person produced
such a file, and/or many files produced by Audacity have those
characteristics.
2022-04-22 14:11:02 -05:00
1841af90e2
Don't erroneously set EOF flag if there was room to read 0 bytes 2022-04-22 14:11:01 -05:00
683ece76db
MP3Decoder: Accurately inform when no more data
Some audio implementations, notably samd, really don't like it when
you return 0 samples of data. This was the case when reaching the
end of an MP3 file.

Now, we read forward in an MP3 file to the next sync word during
"get_buffer", so that we can accurately return GET_BUFFER_DONE when the
NEXT call WOULD HAVE resulted in 0 samples.

Tested with @gamblor21's "laugh.mp3" file on a Trellis M4 Express.
2022-04-04 09:16:27 -05:00
Ben Combee
131b94540e audiomp3: reset decoded_samples when file resets
In testing, I saw that the decoded_samples value kept increasing when I
stopped and restarted playback, as I'd missed setting it back to zero
during the reset operation.
2022-01-02 15:19:25 -06:00
Ben Combee
98b0029a29 audiomp3: add decoded_samples property
In my testing, there is no way to accurately know how far into a MP3 file
you're currently playing. You can use monotonic time, but that can have
drift versus the audio playback system, which may not be running at exactly
the expected sample rate.

To allow syncing animation with timestamps in a MP3 file, this presents a
new property, decoded_samples, that records the number of audio samples
sent out of the decoder. While this may not be a completely accurate time,
due to mixer delays, it's much better position that the monotonic clock
difference.

Implementation is keeping track of this value in the mp3file structure and
adding to it whenever data is sent out of the decoder. The property
implementation was a copy/paste from current properties in the audiomp3
files.
2022-01-01 23:28:43 -06:00
621953c960
Additional missing-prototypes fixes
I think this correctly enables missing-prototypes in atmel-samd
and raspberrypi ports.
2021-11-10 10:55:53 -06:00
Jeff Epler
d67acb8a64 MP3Decoder: Fix playback stopping issue
Closes: #5164
2021-08-17 10:03:47 -05:00
Dan Halbert
33bbb8b1f4 RP2040 PWMAudioOut: Release DMA channels after play has finished. 2021-07-01 17:36:29 -04:00
microDev
a52eb88031
run code formatting script 2021-03-15 19:27:36 +05:30
bdab6c12d4 MP3Decoder: take advantage of background callback
Before this, the mp3 file would be read into the in-memory buffer
only when new samples were actually needed.  This meant that the time
to read mp3 content always counted against the ~22ms audio buffer length.

Now, when there's at least 1 full disk block of free space in the input
buffer, we can request that the buffer be filled _after_ returning from
audiomp3_mp3file_get_buffer and actually filling the DMA pointers.  In
this way, the time taken for reading MP3 data from flash/SD is less
likely to cause an underrun of audio DMA.

The existing calls to fill the inbuf remain, but in most cases during
streaming these become no-ops because the buffer will be over half full.
2020-07-15 09:26:47 -05:00
dd6010a17e Fix more build problems 2020-01-06 13:35:43 -06:00
dc729718eb audiomp3: rename to MP3Decoder 2020-01-06 07:51:41 -06:00
ec22520992 MP3File: Add rms_level property
This lets a music player show it vu-meter style
2020-01-02 15:23:42 -06:00
97bb46c047 MP3File: tweak buffer handling
After adding the ability to change files in an existing MP3File object,
it became apparent that at the beginning of a track some part of an
existing buffer was playing first.

I noticed that in get_buffer, the just-populated buffer wasn't being
returned, but the other one was.  But still after fixing this, I heard
wrong audio at the beginning of a track, so I took the heavy duty approach
and zeroed the buffers out.  That means there's a remaining bug to chase,
which is merely hidden by the memset()s.
2019-12-24 09:43:15 -06:00
00628a7ddd MP3File: whitespace 2019-12-23 09:36:50 -06:00
02154caf24 MP3File: Add a settable ".file" property
This enables jeplayer to allocate just one MP3File at startup, rather
than have to make repeated large allocations while the application is
running.

The buffers have to be allocated their theoretical maximum, but that
doesn't matter much as all the real-life MP3 files I checked needed
that much allocation anyway.
2019-12-23 09:36:46 -06:00
91a1706160 MP3: look harder for frame info
Apparently sometimes, a proper "frame info" block is not found after
a "sync word".  Keep looking for one as needed, instead of giving up
after one try.

This was one reason that the "bartlebeats" mp3s would not play.
2019-12-13 18:31:12 -06:00
bf9b7e7ece MP3: skip ID3V2 metadata
This was one reason that the "bartlebeats" mp3s would not play.
2019-12-13 18:31:12 -06:00
1cdac3f16a MP3File: Fix stereo playback on samd AudioOut
There were several problems with the way this worked -- the read_count
approach was too complicated and I made a mistake "simplifying" it from
WaveFile.  And when the right channel was returned, it was off by 1 byte,
making it into static.

Instead, directly track which is the "other" channel that has data
available, and by using the right data type make the "+ channel"
arithmetic give the right result.

This requires a double cast (int16_t*)(void*) due to an alignment warning;
the alignment is now ensured manually, but the compiler doesn't make the
necessary inference that the low address bit must be clear.
2019-12-12 08:44:15 -06:00
8c841af946 MP3File: Avoid crash in get_buffer when deinitted
When a playing mp3 is deinitted, it's possible to reach get_buffer,
but all the internal pointers are NULL.  This would lead to a hard fault.
Avoid it by returning GET_BUFFER_ERROR instead.
2019-12-11 22:04:10 -06:00
a08d9e6d8e audiocore: Add MP3File using Adafruit_MP3 library 2019-12-10 14:03:06 -06:00