8000 Proposed changes to machine.DAC · Issue #4254 · micropython/micropython · GitHub
[go: up one dir, main page]

Skip to content

Proposed changes to machine.DAC #4254

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

Open
MrSurly opened this issue Oct 19, 2018 · 12 comments
Open

Proposed changes to machine.DAC #4254

MrSurly opened this issue Oct 19, 2018 · 12 comments

Comments

@MrSurly
Copy link
Contributor
MrSurly commented Oct 19, 2018

Been thinking about using the DAC to output audio (on the ESP32, but the proposed changes aren't specific to that platform). I'm wondering how to:

  • Output data continuously
  • Output data "in the background" (a la DMA)

DAC.write_timed for the pyb will use DMA under the hood, but there's presently no way to know when to queue new data.

Proposed:

  • DAC.write_timed will now enqueue the data. Doing another DAC.write_timed before the first one finishes enqueues the next bytestream. Enqueuing would only work for DAC.NORMAL, and only if the last item was also DAC.NORMAL. Calling with DAC.CIRCULAR while the queue is active with a DAC.NORMAL will fail.
  • add DAC.queue_size, which returns a quantity of how many items are in the queue, including the currently streamed item.

Examples:

d = machine.DAQ(1)

b = getNewBytes() # bytes object
d.write_timed(b, 1000) # Begin outputting

b2 = getMoreBytes()
d.write_timed(b2, 1000) #append onto the end of the queue

b3 = getEvenMoreBytes()
d.write_timed(b3, 1000, DAC.CIRCULAR) # <-- this fails, queue is _not_ empty

while d.queue_size():
    time.sleep(.1)

d.write_timed(b3, 1000, DAC.CIRCULAR) # <-- this works; queue is now empty
d.write_timed(bytes(), 1000)  # DAC.NORMAL cancels DAC.CIRCULAR, and enqueues data.  In this case, an empty array. 
@dpgeorge
Copy link
Member

Thanks for bringing this up, it would be good to flesh out the DAC class.

At the moment there isn't really a defined machine.DAC. The only port that has such a class is the esp32. Pyboard has pyb.DAC instead. But that is good because it means it can be defined now without too much worry about backwards compatibility.


Writing a stream of values, either in the foreground or background, is definitely a good use case for a DAC. (There are also other entities like SPI and I2S which would also benefit from being able to specify background activity, something to keep in mind.)

The micro:bit has a bit of support for background activity, like playing audio and animating the display. The music module has the following simple API:

music.play(tune, pin=None, wait=True, loop=False)
music.stop()

And the audio module this:

audio.play(source, pin=None, wait=True)
audio.stop()
audio.is_playing()

There are no callbacks in this scheme.

I think it is important to support IRQs/callbacks (eg when the output queue is half empty) and/or ability to poll the DAC with the uselect module. This will allow to write efficient code that doesn't need to have continuous polling, and can fit in with other asynchronous code.

@peterhinch
Copy link
Contributor

I strongly favour background operation. It would be great to be able to run a DAC and an ADC concurrently, with nonblocking methods.

Background operation also facilitates asynchronous coding. If there were a method returning the current free space in the queue you could write (in Python) ioctl and write methods enabling a DAC to be used as a StreamWriter. Then finite and infinite datasets (e.g. files and generators) could be supported in uasyncio applications.

IRQ's and callbacks are needed where speed is critical.

@MrSurly
Copy link
Contributor Author
MrSurly commented Oct 19, 2018

@dpgeorge Are you okay with the queuing scheme? I can move forward on that, and we can figure out the "need more data" bit later. Was going to implement on the ESP32.

@dpgeorge
Copy link
Member

Are you okay with the queuing scheme?

Support for "double buffering" is a good idea. It's just a question of how it's exposed to the user.

From a functionality point of view the following seem necessary (but not necessarily exposed like this):

DAC.set_freq(freq) # set Hz, or set using a Timer object
DAC.set_mode_circular() # repeats single buffer over and over
DAC.set_mode_sequential() # consumes buffer after buffer
DAC.enqueue_data(buf)
DAC.get_queue_size()
DAC.start() # start outputting data
DAC.stop() # will pause the output, it can be started again if needed
DAC.set_callback(cb) # to be called when it gets to the end of a buffer

The DAC.write_timed() method proposed above does set_freq, set_mode_X, enqueue_data and start all in one call. With write_timed the frequency needs to be set each time new data is queued, when in most cases the frequency wouldn't change.

One advantage of separating out the functionality of setting the frequency/timer to a dedicated method is that the pattern generalises to other things like SPI:

SPI.set_mode_circular()
SPI.set_mode_sequential()
SPI.enqueue_data()
...

I can move forward on that

If you can implement the low-level C code for esp32 that would be a place to start. Then connect it up to Python methods when an API is worked out.

@MrSurly
Copy link
Contributor Author
MrSurly commented Oct 23, 2018

Support for "double buffering" is a good idea. It's just a question of how it's exposed to the user.

So ... two?

I'm okay with the API, with one possible change:

DAC.start() # start outputting data
DAC.pause() # will pause the output, it can be started again if needed
DAC.clear() # (or .stop()) clear out any data

The last item where you may have a large amount of data queued, and need to stop it entirely, and possibly enqueue different data.

I'll begin coding soon; I'll keep the buffer enqueuing generic.

@dpgeorge
Copy link
Member

So ... two?

Following how displays work, two buffers (active and hidden) is usually enough to get smooth output. If, due to latency issues, the application needs more buffers then that might be solvable instead by using larger buffers, but just two of them.

But I guess a given port could support more than two. Two would be the minimum.

The last item where you may have a large amount of data queued, and need to stop it entirely, and possibly enqueue different data.

Yes, good point, a "flush buffers" is needed.

MrSurly added a commit to MrSurly/micropython that referenced this issue Oct 25, 2018
@tannewt
Copy link
tannewt commented Oct 29, 2018

FYI in CircuitPython we have a separate audioio module for audio playback. It establishes two data sources RawSample and WaveFile, a Mixer and can be output by an AudioOut or I2SOut. The dma work is handled in the backround. We don't actually support queued continuous samples yet but it'd be easy to add as a sample source.

Just food for thought. Let me know if you have questions.

@MrSurly
Copy link
Contributor Author
MrSurly commented Oct 31, 2018

Got this mostly working, w/o the callback for low buffers, and ... it sounds terrible. You can hear the audio, but there's lots of other noise and distortion.

@peterhinch
Copy link
Contributor

If you have access to an oscilloscope you could examine sinewaves at various frequencies. This would probably demonstrate the cause.

@MrSurly
Copy link
Contributor Author
MrSurly commented Oct 31, 2018

I have. Leaning towards "the dac is not very good" and/or "timer has jitter." I'll analyze more when I have time.

@nalt
Copy link
nalt commented Apr 5, 2020

This repo has routines for tone generation, even playing WAV files: https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo/wiki/dac

tannewt added a commit to tannewt/circuitpython that referenced this issue Mar 4, 2021
@cgreening
Copy link
Contributor

You can output to the DAC using I2S and DMA. It might be worth combining this into the work that is being done on getting the I2S peripheral working? #4170

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

No branches or pull requests

6 participants
0