[alsa-devel] AudioArduino - an ALSA soundcard driver for FTDI-based Arduinos
Hi all,
(cross-posting to alsa-devel@alsa-project.org and gmane.linux.usb.general/linux-usb@vger.kernel.org; apologies if it's inappropriate)
I'd just like to inform that the AudioArduino project has been posted online:
http://imi.aau.dk/~sd/phd/index.php?title=AudioArduino
It contains links to open-source Arduino microcontroller code, and corresponding ALSA driver source code - which can demonstrate mono, full-duplex, 8-bit / 44.1 kHz soundcard operation on an FTDI-based Arduino.
There is also a related post on the (new) Arduino Forum: http://arduino.cc/forum/index.php/topic,62582.0.html
The driver essentially represents an ALSA extension to ftdi-sio (which is why I thought it would be relevant to both alsa-devel, and linux-usb). Many thanks to the community for critical help, like in:
ftdi_sio, and usb_serial->interface->dev->driver_data being null http://comments.gmane.org/gmane.linux.usb.general/30455
Help with dummy.c (where/how to write?) http://comments.gmane.org/gmane.linux.alsa.devel/74487
Looking forward to any feedback regarding this project,
Cheers!
On Mon, May 30, 2011 at 06:41:08AM +0200, sdaau wrote:
Hi all,
(cross-posting to alsa-devel@alsa-project.org and gmane.linux.usb.general/linux-usb@vger.kernel.org; apologies if it's inappropriate)
I'd just like to inform that the AudioArduino project has been posted online:
http://imi.aau.dk/~sd/phd/index.php?title=AudioArduino
It contains links to open-source Arduino microcontroller code, and corresponding ALSA driver source code - which can demonstrate mono, full-duplex, 8-bit / 44.1 kHz soundcard operation on an FTDI-based Arduino.
There is also a related post on the (new) Arduino Forum: http://arduino.cc/forum/index.php/topic,62582.0.html
Very cool stuff, nice job!
Any thoughts about submitting back your kernel driver changes so we can add them to the main kernel tree so that people don't have to build out-of-tree kernel drivers to use this?
It could just be an option to the existing ftdi_sio driver that is enabled somehow for a specific device, right?
thanks,
greg k-h
Hi Greg K-H,
Very cool stuff, nice job!
Any thoughts about submitting back your kernel driver changes so we can add them to the main kernel tree so that people don't have to build out-of-tree kernel drivers to use this?
Thanks for the encouraging and kind feedback - and of course, for all your (and everyone else's) work, that forms the backbone of the project!
I would, personally, be very happy to have a contribution in the kernel :) One reservation that I'd have, is that often times I read on mailing lists that the kernel is 'bloated'. And since this is a driver for a device not (necessarily) intended to be a soundcard - essentially, a (basic) academic exercise in providing an example of a working, yet fully open soundcard, spanning both software and hardware - I'm fearing that some users may consider it a 'bloat'; but maybe that is irrelevant?
However, the bigger concern for me is how one would do it technically:
It could just be an option to the existing ftdi_sio driver that is enabled somehow for a specific device, right?
Well, I wish it was so - but unfortunately, it is not that way currently.
The thing is, I'm not really an expert in programming, and even less in kernel drivers. So, for the time that I had at my disposal, the only thing I could have done is try to understand ALSA driver architecture a bit better - while the USB part is completely "abstracted".
What I mean by that, is that the main driver file (ftdi_sio-audard.c) is first and foremost a copy of ftdi_sio.c from 2.6.32 kernel; then it includes a 'snd_ftdi_audard.h' file, which is here not a traditional header; it simply contains additional functions which 'add' the ALSA interface to ftdi_sio.c.
Finally, the two architectures are connected, such that the ftdi's callbacks also call the ALSA related callbacks, in particular:
* ftdi_sio's ftdi_process_packet (raised on incoming USB data) calls ALSA-related audard_xfer_buf function (and so the audio capture direction is handled) * ALSA related snd_card_audard_pcm_timer_function calls ftdi_sio's ftdi_write function (which controls outgoing USB data - and so the audio playback direction is handled).
I've just posted a diff on the SVN repository between the current ftdi_sio-audard.c and the original ftdi_sio.c from kernel 2.6.32:
http://sdaaubckp.svn.sourceforge.net/viewvc/sdaaubckp/audard/snd_ftdi_audard...
... the link should show syntax coloring, so it maybe gets easier to see what's going on there.
In essence, this driver keeps everything ftdi_sio has (so you still get a /dev/ttyUSB0; you can still 'cat' from or 'echo' to this device node as usual) - except that, additionally, data is passed also to the ALSA-related memory structures (i.e. the dma_area of a particular ALSA substream).
Initially, I really wanted to essentially keep ftdi_sio as is, so that I wouldn't have to worry about changes to its code; but as the ALSA interface requires new functions, I couldn't find a way how to selectively #include these functions based on a driver option. One could use #defines, maybe, but that's still compile-time handling, not run-time handling. And so, I decided to focus instead on problems that I had a chance of solving (such as wrapping data for the ALSA ring buffers). And just today I discovered that the driver needs a couple of changes, that were implemented in ftdi_sio.c since 2.6.32, so that it would compile for 2.6.38 (I have now added comments related to this is the source file up on SVN).
However, if there is some sort of a tutorial/guide (or even a specific existing driver file example) that I could refer to, so as to turn the current architecture into an option-based one, such that I avoid messing with ftdi_sio.c directly -- I'd definitely love to try and do that; so please let me know if there is some sort of a recommended reading for this.
Thanks again for the feedback, Cheers!
On 2011-05-30 09:42, Greg KH wrote:
On Mon, May 30, 2011 at 06:41:08AM +0200, sdaau wrote:
Hi all,
(cross-posting to alsa-devel@alsa-project.org and gmane.linux.usb.general/linux-usb@vger.kernel.org; apologies if it's inappropriate)
I'd just like to inform that the AudioArduino project has been posted online:
http://imi.aau.dk/~sd/phd/index.php?title=AudioArduino
It contains links to open-source Arduino microcontroller code, and corresponding ALSA driver source code - which can demonstrate mono, full-duplex, 8-bit / 44.1 kHz soundcard operation on an FTDI-based Arduino.
There is also a related post on the (new) Arduino Forum: http://arduino.cc/forum/index.php/topic,62582.0.html
thanks,
greg k-h
On Mon, May 30, 2011 at 05:28:52PM +0200, sdaau wrote:
It could just be an option to the existing ftdi_sio driver that is enabled somehow for a specific device, right?
Well, I wish it was so - but unfortunately, it is not that way currently.
The thing is, I'm not really an expert in programming, and even less in kernel drivers. So, for the time that I had at my disposal, the only thing I could have done is try to understand ALSA driver architecture a bit better - while the USB part is completely "abstracted".
What I mean by that, is that the main driver file (ftdi_sio-audard.c) is first and foremost a copy of ftdi_sio.c from 2.6.32 kernel; then it includes a 'snd_ftdi_audard.h' file, which is here not a traditional header; it simply contains additional functions which 'add' the ALSA interface to ftdi_sio.c.
Finally, the two architectures are connected, such that the ftdi's callbacks also call the ALSA related callbacks, in particular:
- ftdi_sio's ftdi_process_packet (raised on incoming USB data) calls
ALSA-related audard_xfer_buf function (and so the audio capture direction is handled)
- ALSA related snd_card_audard_pcm_timer_function calls ftdi_sio's
ftdi_write function (which controls outgoing USB data - and so the audio playback direction is handled).
I've just posted a diff on the SVN repository between the current ftdi_sio-audard.c and the original ftdi_sio.c from kernel 2.6.32:
http://sdaaubckp.svn.sourceforge.net/viewvc/sdaaubckp/audard/snd_ftdi_audard...
... the link should show syntax coloring, so it maybe gets easier to see what's going on there.
Care to email it? That way we can review it here and see how to structure it.
In essence, this driver keeps everything ftdi_sio has (so you still get a /dev/ttyUSB0; you can still 'cat' from or 'echo' to this device node as usual) - except that, additionally, data is passed also to the ALSA-related memory structures (i.e. the dma_area of a particular ALSA substream).
Initially, I really wanted to essentially keep ftdi_sio as is, so that I wouldn't have to worry about changes to its code; but as the ALSA interface requires new functions, I couldn't find a way how to selectively #include these functions based on a driver option. One could use #defines, maybe, but that's still compile-time handling, not run-time handling. And so, I decided to focus instead on problems that I had a chance of solving (such as wrapping data for the ALSA ring buffers). And just today I discovered that the driver needs a couple of changes, that were implemented in ftdi_sio.c since 2.6.32, so that it would compile for 2.6.38 (I have now added comments related to this is the source file up on SVN).
However, if there is some sort of a tutorial/guide (or even a specific existing driver file example) that I could refer to, so as to turn the current architecture into an option-based one, such that I avoid messing with ftdi_sio.c directly -- I'd definitely love to try and do that; so please let me know if there is some sort of a recommended reading for this.
I don't think there is a "tutorial" for something like this, but we would be glad to help you out here to do this.
So, care to post the code here?
thanks,
greg k-h
Hi all,
Missed a couple of replies, so I'll try to answer everything now... We may be going slightly off topic as far as both linux-usb and alsa-devel are concerned - I hope the respective communities won't mind :)
On 2011-05-30 23:15, Sid Boyce wrote:
Thanks, just wondered. With many SDR (Software Define Radio) project we are constantly on the lookout for higher performance cards that are not bandwidth restricted.
There are many interesting projects out there using arduino, good luck. I've got a Teensy++ 2.0 on the way to have a play with. Regards Sid.
Hah - I noticed the signature, but I didn't really see the connection, until you clarified about SDR! :) Thanks for the note, and the reference to Teensy (have been hearing about that lately, but haven't had the time to look deeper)...
On 2011-06-03 07:37, Greg KH wrote:
[snip]
I've just posted a diff on the SVN repository between the current ftdi_sio-audard.c and the original ftdi_sio.c from kernel 2.6.32:
http://sdaaubckp.svn.sourceforge.net/viewvc/sdaaubckp/audard/snd_ftdi_audard...
... the link should show syntax coloring, so it maybe gets easier to see what's going on there.
Care to email it? That way we can review it here and see how to structure it.
Of course! I am aware that people exchange patches on mailing lists, but unfortunately I don't have much experience with it. So I just attached the above-mentioned diff file here...
If needed, I will attach all other relevant files in plain text - let me know.
[snip] However, if there is some sort of a tutorial/guide (or even a specific existing driver file example) that I could refer to, so as to turn the current architecture into an option-based one, such that I avoid messing with ftdi_sio.c directly -- I'd definitely love to try and do that; so please let me know if there is some sort of a recommended reading for this.
I don't think there is a "tutorial" for something like this, but we would be glad to help you out here to do this.
So, care to post the code here?
Sure - and just in case, I've attached the other relevant file, snd_ftdi_audard.h (which becomes included by the ftdi_sio == ftdi_sio-audard.c after its patched); let me know if I should attach the other files too (there is the code for the Arduino too, and also ftdi_sio.h and ftdi_sio_ids.h, but those are used verbatim - and a Makefile too).
On 2011-05-30 23:51, Caleb Crome wrote:
FYI, if you can raise the PWM rate on the arduino output, you can use modulation tricks to increase the resolution.
On the Arduino Duemillanove , there is 16 MHz clock and ATmega328 - as far as I could gather, the highest possible PWM frequency there is 62.5 kHz (in Fast PWM mode - achievable by setting the correct prescalers).
There are a lot of limitations here, but there is probably some degree of improvement possible. Virtually all modern codecs do this to the extreme -- they sample really fast, like 12 MHz, but only use 1 bit of resolution. Modern 24-bit codecs are really only 1 bit on the business end, but really fast. Search for delta-sigma or sigma-delta modulators if you're not familiar.
Thanks for the reference - I've studied delta-sigma waay back, and forgotten most of that; however, even then I haven't quite made the connection to what is referred to as 1-bit; so thanks for this, certainly something to read up on! As a side note, the ATmega328 on the sampling side, if I recall correctly, has a successive-approximation ADC built in (plus a multiplexer in front of it, to provide for multiple analog channels)...
If you can transmit, say, 10 bits and run your sample rate at ~4x your intended rate, you can probably get about 10-bits of resolution on the output.
Interesting - I'm not sure if I understand that statement correctly: in this case, an output compare (OC) unit of an 8-bit timer/counter of the Arduino's ATMega328 is used to produce PWM output; and since I by default can only write 8-bit values there in the microcontroller code, I have a hard time seeing how I could squeeze out 10 bits on output in that context; but maybe you had a different context in mind?
It requires a lowpass (antialiasing) filter though -- which gets really nasty at sample rates below 40khz -- because then they need to be sharp so you don't hear the aliasing.
Heh, this is where I got surprised :)
I too expected that at least I would hear some high-frequency "buzz" due to the 'hard edges' of the PWM signal - which would assume that I don't have a low pass filter which is sharp enough, as you state. But the surprising bit for me was that I could just stick this 62.5 kHz (carrying a 44.1 kHz data) PWM signal raw in a loudspeaker - and I can *not* hear a high-frequency "buzz" that I expected :) Obviously, the loudspeaker itself performs low pass filtering - I just thought originally it would not be sharp enough :)
Modern codecs manage this by shoving all the aliasing noise up at a few MHz, so a simple RC filter is enough to eliminate aliasing issues. And not even necessary if driving speakers/headphones directly.
Thanks for mentioning this too - I have often heard about the technique of "pushing aliasing noise up to MHz", however, still do not understand it practically yet. And interesting that you state it is "not necessary" for speakers/headphones - I'd agree with that (based on my above experience), but I wander if that is mainly due to the mechanical inertia of the loudspeaker membrane - or because of presence of coil/inductor?
Anyway, 9-bits is going to be much better than 8, so if you can run the arduino at 88.2k, you could probably get roughly 9 bits of resolution. You'd probably do the upsampling on the linux side -- since the AVR is not exactly bursting with MIPS.
I could send a data stream at 88.2 kHz, and there would be available data bandwidth too - however, since I have only an 8-bit timer/counter OC as a PWM generator, I still cannot see how I could get 9-bit resolution out of that. Maybe you imply some technique - like, I don't know - padding the 9-bit value to 16-bit, and then reproduce the MSB and LSB of this 16-bit value in rapid succession on the 8-bit PWM (though I'm just saying this off top of my head, this rapid succession thing would probably not work at all)?
Hmmm, or another idea: what if you use 2 PWM channels and mix the outputs together with an op-amp? You could potentially get much improved resolution that way. Basically, use a summing node on your op-amp with one signal at unity, and the other signal at 1/256*unity.
Heh, that's exactly what I was thinking too :) (I think its mentioned very briefly in the end of the paper) - basically, one 8-bit PWM reproduces the MSB of the 16-bit sample; and the other PWM reproduces the LSB of this 16-bit simple, and they are appropriately mixed in the analog domain. However, as you say:
I doubt that you'd ever get near 96 dB SNR (the ideal for 16-bits), but you'd probably do better than 1 channel PWM at 8-bits could do (theoretical best is 48 dB).
... yes, I too was asking myself whether the gains of reproducing 16-bit would not be offset by noise, added in the process of analog mixing of 8-bit MSB and LSB analog values separately (which is, I believe, the same that you are saying; however, I have not yet the context to learn working with dB as measure of SNR properly).
Of course, if you're going through the trouble to add an op-amp, you might as well skip the arduino all together and put a USB<-> audio chip in. :-)
Heh, indeed :) However, the point of this whole exercise was to have a complete overview of a sound card as system (including the PC).
For instance, before this, I've always had the misconception that when, say, Audacity plays a file, it sends the value for each sample (say, 16-bit) one-by-one to the soundcard, and so the soundcard plays back the samples one-by-one. And that is not what happens: ALSA does its own chunking, then USB does its own chunking - which means that, at least for playback, as a code for the device (soundcard) you better be storing those incoming bursts of data in an array somewhere, and *then* reproduce sample-by-sample (from the stored data) at the requested reproduction rate.
Of course, in such a system, one needs to think about the analog domain as well - I tried to document a simple board, that would take the AudioArduino as is, and try to provide an analog line in/out interface to it with the simplest approaches (e.g. a discrete ramp-and-hold circuit, to convert the PWM signal to analog voltage levels); however, all I could conclude was that simple approaches actually produce slightly worse results (than just having the AudioArduino PWM out going directly into a speaker).
Cheers!
On Fri, Jun 3, 2011 at 11:22 AM, sdaau sd@imi.aau.dk wrote:
On 2011-05-30 23:51, Caleb Crome wrote:
FYI, if you can raise the PWM rate on the arduino output, you can use
modulation tricks to increase the resolution.
On the Arduino Duemillanove , there is 16 MHz clock and ATmega328 - as far as I could gather, the highest possible PWM frequency there is 62.5 kHz (in Fast PWM mode - achievable by setting the correct prescalers).
Ah, so that's not going to let you get too far in terms of oversampling. You could set your audio bandwidth to 62.5 / 2 / 2 ~= 15kHz and try for ~9-bit resolution. Here's the basic procedure:
On your host, resample the audio file to 62.5 kHz, lowpass filter it, and convert it to 8-bits. You can do this all as preprocessing steps until you get it into your application. It looks something like this:
sox input.wav -r 31250 i1.wav # This resamples to 31250 Hz. This
includes a built-in antialiasing filter, so will limit bandwidth to somewhere below 15kHz
sox i1.wav -r 62500 i2.wav # This resamples to 62500 Hz. It
creates samples between all the 31250Hz samples that are interpolated. This is the important bit!
sox i2.wav --bits 8 i3.wav # This converts to 8-bit samples.
(these sox command not tested ;-)
Now, you have a band-limited signal, with 8-bit samples. However, the sample rate is double what it would need to be for the limited bandwidth (<15kHz).
Now, just play this i3.wav file out to your arduino at 62.5 kHz. This *should* give you somewhat improved SNR on your signal. Theoretical best would be .... hmmm I don't recall, either 3 dB or 6 dB, or maybe 4.2 dB :-) Somewhere between 3 and 6 for sure.
If you can transmit, say, 10 bits and run your sample rate at ~4x your
intended rate, you can probably get about 10-bits of resolution on the output.
Interesting - I'm not sure if I understand that statement correctly: in this case, an output compare (OC) unit of an 8-bit timer/counter of the Arduino's ATMega328 is used to produce PWM output; and since I by default can only write 8-bit values there in the microcontroller code, I have a hard time seeing how I could squeeze out 10 bits on output in that context; but maybe you had a different context in mind?
Sorry, your PWM will still be 8-bits. The point is that *somebody* needs to upsample and lowpass filter the data. Forget this idea, and look to the sox example above. You could try different varients of that to get better resoltuion in the band of interest.
It requires a lowpass (antialiasing) filter though -- which gets
really nasty at sample rates below 40khz -- because then they need to be sharp so you don't hear the aliasing.
Heh, this is where I got surprised :)
I too expected that at least I would hear some high-frequency "buzz" due to the 'hard edges' of the PWM signal - which would assume that I don't have a low pass filter which is sharp enough, as you state. But the surprising bit for me was that I could just stick this 62.5 kHz (carrying a 44.1 kHz data) PWM signal raw in a loudspeaker - and I can *not* hear a high-frequency "buzz" that I expected :) Obviously, the loudspeaker itself performs low pass filtering - I just thought originally it would not be sharp enough :)
Yep, the loudspeaker does a descent job of lowpass filtering. Also, if your sample rate is above ~40kHz, any aliasing is inaudible (because you only hear to 20kHz at best). Unfortunately, if you take the output and drive an amplifier with it, the amplifier will be slew-rate limited on the hard PWM edges, and it'll start distorting. That's why you need an antialiasing filter if running into an opamp, but don't necessarily need one if going directly to a speaker or headphone.
Modern codecs manage this by shoving
all the aliasing noise up at a few MHz, so a simple RC filter is enough to eliminate aliasing issues. And not even necessary if driving speakers/headphones directly.
Thanks for mentioning this too - I have often heard about the technique of "pushing aliasing noise up to MHz", however, still do not understand it practically yet. And interesting that you state it is "not necessary" for speakers/headphones - I'd agree with that (based on my above experience), but I wander if that is mainly due to the mechanical inertia of the loudspeaker membrane - or because of presence of coil/inductor?
As long as the aliased signals are above the audible range, it's okay even if your loudspeaker/headphone *does* reproduce them. Who cares if your speaker also produces ultrasonic noise at 1 MHz? Reality is that speakers do act as lowpass filters (even good tweeters ;-), so it's really not something that you need to worry about.
But, you do need to worry about it if going to another amplifier because of the slew rate limitations above.
Anyway, 9-bits is going to be much better than 8, so if you can run the
arduino at 88.2k, you could probably get roughly 9 bits of resolution. You'd probably do the upsampling on the linux side -- since the AVR is not exactly bursting with MIPS.
I could send a data stream at 88.2 kHz, and there would be available data bandwidth too - however, since I have only an 8-bit timer/counter OC as a PWM generator, I still cannot see how I could get 9-bit resolution out of that.
It's the oversampling technique I demonstrated above. By running at a faster rate, the lowpass filter (i.e. speaker) smooths the 8-bit samples out to get better-than-8-bit resoltuion. It's a type of averaging.
Think of it this way: at 88.2kHz, you have 2 samples for every 1 time slice that you care about. (you care about only 20kHz audio bandwidth or 1/40kHz slices) If sample 1 is 34 and sample 2 is 35 (both 8-bit values), the AVERAGE sample, for that 1/40kHz time slice is (34+35) /2 = 34.5. This is how oversampling helps you. Lowpass filters *are* averagers.
Hmmm, or another idea: what if you use 2 PWM channels and mix the outputs
together with an op-amp? You could potentially get much improved resolution that way. Basically, use a summing node on your op-amp with one signal at unity, and the other signal at 1/256*unity.
Heh, that's exactly what I was thinking too :) (I think its mentioned very briefly in the end of the paper) - basically, one 8-bit PWM reproduces the MSB of the 16-bit sample; and the other PWM reproduces the LSB of this 16-bit simple, and they are appropriately mixed in the analog domain. However, as you say:
BTW, another limitation is the cleanlieness of your 5V power supply. It's probably extremely dirty, which will translate directly into noise on the output because your HIGH levels can only be as clean as the VCC rail. Low levels will be pretty clean (limited by the Ron of the AVR's output stage).
For instance, before this, I've always had the misconception that when, say, Audacity plays a file, it sends the value for each sample (say, 16-bit) one-by-one to the soundcard, and so the soundcard plays back the samples one-by-one. And that is not what happens: ALSA does its own chunking, then USB does its own chunking - which means that, at least for playback, as a code for the device (soundcard) you better be storing those incoming bursts of data in an array somewhere, and *then* reproduce sample-by-sample (from the stored data) at the requested reproduction rate.
:-) Yep. It would be really inefficient to have a 1 sample block size :-)
I believe that USB is fixed on 1mS block size, so the block size depends on your sample rate. I could be mistaken here.
Of course, in such a system, one needs to think about the analog domain as well - I tried to document a simple board, that would take the AudioArduino as is, and try to provide an analog line in/out interface to it with the simplest approaches (e.g. a discrete ramp-and-hold circuit, to convert the PWM signal to analog voltage levels); however, all I could conclude was that simple approaches actually produce slightly worse results (than just having the AudioArduino PWM out going directly into a speaker)
I think that the project is pretty neat, and a good demonstration project.
Take care,
-Caleb
participants (3)
-
Caleb Crome
-
Greg KH
-
sdaau