Tuesday, 27 April 2010

Generate And Play A Tone In Android

Update: Thanks to xxv for spotting a bug in my encoding. He posted the updated code on stackoverflow, here is the link and made a comment here to let me know. Basically I was getting the byte order wrong in the encoding and forgetting to factor in the sampleRate into the wav- oops and sorry! I have update the code here.

Original post.

I threw this together the other night just to figure out how to do some very simple sound synthesis. I am not going to go into details about how the code works as it is pretty obvious for the most part.

It basically generate a sine wave at a given frequency then converts this into a 16bit pcm format and finally plays it. I used a thread to make sure my generation does not block.

Sound synthesis is something I would like to spend some more time messing with. I have written some Haskell code that lets me play with some synthesis ideas, nothing elaborate. One day I will get round to porting it to Java and making some synthesis apps for android.

Here is the code.


public class GenerateSound extends Activity {
private final int duration = 3; // seconds
private final int sampleRate = 8000;
private final int numSamples = duration * sampleRate;
private final double sample[] = new double[numSamples];
private final double freqOfTone = 100; // hz

private final byte generatedSnd[] = new byte[2 * numSamples];

Handler handler = new Handler();

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}

@Override
protected void onResume() {
super.onResume();

// Use a new tread as this can take a while
Thread thread = new Thread(new Runnable() {
   public void run() {
     genTone();
     handler.post(new Runnable() {

public void run() {
playSound();
}
});
   }  
 });
 thread.start();
}

void genTone(){
// fill out the array
for (int i = 0; i < numSamples; ++i) {
sample[i] = Math.sin(2 * Math.PI * i / (sampleRate/freqOfTone));
}

// convert to 16 bit pcm sound array
// assumes the sample buffer is normalised.
int idx = 0;
for (double dVal : sample) {
short val = (short) (dVal * 32767);
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
}
}

void playSound(){
AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
8000, AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT, numSamples,
AudioTrack.MODE_STATIC);
audioTrack.write(generatedSnd, 0, numSamples);
audioTrack.play();
}
}

29 comments:

  1. I tried out your code and fixed a few bugs. The modified code is posted on StackOverflow: http://stackoverflow.com/questions/2413426/playing-an-arbitrary-tone-with-android

    Thanks for the code! Let me know if there are any issues with posting the code there.

    ReplyDelete
  2. Hi xxv,

    Thanks for looking at the code. I had recently used the wav encoding parts in my GWT experiments and spent some time wondering why they sounded terrible. Eventually I tracked it down to the order of the bytes but it had not occurred to me that I had published this code otherwise I would have updated this entry.

    So thanks for taking the time to figure out and fix the bug, it had me pulling my hair out for a couple of evenings while I blamed my newer code.

    I will update the code in my post.

    Paul

    ReplyDelete
  3. There were a couple other items, too. For example, why do you have the "+ 0.5" there? And the sine generation is incorrect, as you need to account to the sample rate.

    ReplyDelete
  4. Hi xxv,

    The +0.5 is to round towards zero the nearest short rather than round down towards zero, but then again I can never remember what happens with negative numbers in this instance so probably best removed.

    Oops on the sampleRate! Updating code yet again! This will teach me to publish code without doing the proper testing.

    Thanks again and updating code yet again

    ReplyDelete
  5. Hey I'm having problems with adding two tones together. I thought it would be just averaging two sine waves together but that doesn't work for some reason.

    ReplyDelete
  6. Hey there,

    When adding sine waves together you need to renormalise them so they don't go outside the range of +/-1. So if you add together two tones divide by 2. You can think of +1 as representing the max volume of the speaker.

    You can also have fun with multipling tones together. Take a look at http://obiwannabe.co.uk/tutorials/html/tutorials_main.html for lots of ideas.

    ReplyDelete
  7. What is to be done with the main file??????????

    ReplyDelete
  8. Hi..Can u help me with something..I was able to successfully implement this much..in this the tone is played for a particular duration which is set in code..I want to modify it such that the tone continues to play till the stop button is pressed..and not for a preset duration..I am unable to implement it..can you please help with this?

    ReplyDelete
    Replies
    1. I think you can use the Audio track for this. It has some functionality to let the set the number of times (or forever) to play using setLoopPoints().

      Although googling indicates you may have a short delay before it restarts each time. If that is the case then using SoundPool or MediaPlayer may be a better option. Although I have no experience with them so don't how easy it will be to get a synthesized sound into them and play it.

      Sorry don't have my android development handy at the moment to try it out.

      Delete
  9. is there code to be put in an xml file to make this work?

    ReplyDelete
    Replies
    1. Sorry I took a quick look and don't have the original project handy. I imagine the xml for the manifest was pretty straight forward as it the code does not need any special permissions.

      That would make it pretty close to the standard manifest file that is created when you start a new project.

      Delete
    2. well,um, is there something needed to be done to eclipse that has to be done to get sound out of simulation?

      Delete
    3. I don't think there is anything to be done in eclipse to get this code to work, well at the time.

      I am not 100% sure if I used the emulator with this code, The moment I got a real device I pretty much never touched the emulator with a few small exceptions. So if you are using the emulator the code the above has not been tested.

      Sorry that my help is not helping much :)

      Delete
    4. i had duration set at 1 instead of 3.
      set duration to 10 and it made a short click.
      duration from 10 to 250 makes the same short click.
      duration 270 is slow.
      duration 300 or more throws out of memory error.
      change freqOfTone from 100 up to 15k, short and very little change frequency.

      Delete
    5. I pulled the code into an android project and on my nexus 7 I had silence as well. To get it to play a sound I had to update the CHANNEL_CONFIGURATION_MONO to CHANNEL_OUT_MONO and then I altered the playback frequency of the audio track to 44100

      After that I get a tone coming out.

      Really I should use the sampleRate (set to 44100) as the playback frequency and yep when I do this I need to put the tone up a bit higher to get an audible sound. I changed the tone freqOfTone to 800 and the duration to 3.

      I hope that helps :)

      Delete
    6. thank you. i will check for that.

      adding


      int x = 0;
      // Montior playback to find when done
      do
      {
      if (audioTrack != null)
      x = audioTrack.getPlaybackHeadPosition();
      else
      x = numSamples;
      }
      while (x<numSamples);

      // Track play done. Release track.
      if (audioTrack != null) audioTrack.release();

      after
      audioTrack.play();
      stops the short clicking after the first time it is run.

      now i have to find out why it is not working when i change the freqOfTone.

      Delete
    7. i guess changing the class from "extends Activity" to being called from an activity causes a few issues.
      it makes sound the first time without changing anything, but only short there after.
      adding in some code to check if sound is finished then deleting allows the object to be used more than one time.

      Delete
  10. Can anyone tell me how to calculate the amplitude? All I need to do is to get the sound pressure of the tone in decibels. I searched and found out that I calculate that from the amplitude.

    I need seriously help, I have to finish my assigned task:S

    ReplyDelete
    Replies
    1. Hi, that is well outside the scope of this example as the actual sound loudness is dependent on the hardware used and the volume settings.

      In the example the numbers are between -1 and 1. Roughly speaking zero means no sound and 1(and -1) means max sound. Well I like to think of a speaker where 0 means rest position of the oscillator and 1, -1 mean the max displacement from the rest position.

      When you think about the numbers as meaning positions of the oscillator in the speaker it becomes obviously why you need a varying signal so the speaker can create a variation in air pressure that we interpret as sound. It also becomes clear why this example can no be used to work out the decibels of the given sound.

      Delete
    2. ahh okay.. Thanks for the explanation:) Can you give me some tips for how to make the system generate tones passing the frequency and the sound pressure in decibel as parameters?

      I know this is out of topic but I need any kind of help.

      Delete
    3. In the genTone() method you could pass in an attenuation factor that could be used to scale the signal. How much you have to scale the signal by for 1dB reduction is complicated as dB is a measure of power not amplitude, plus decibels are a logarithmic scale.

      I would have to research a bit to make sure I got a solution correct. It is not the sort of thing I can remember of the top of my head :) Given how busy I am at the moment I am afraid it is unlikely to happen.

      Good luck with your assignment.



      Delete
    4. Okay i appreciate your time. Thanks a lot :)

      Delete
  11. Could u plz tell me how this ramp up/down is working..
    thanks

    ReplyDelete
  12. How can i store this tone in sd card. Can i write this tone in a file and store into sd card .

    ReplyDelete
  13. How can We store this generated tone as an file in phone or sd card. Please also write the code .

    ReplyDelete
  14. Hello. Thank you for the code sample. For some reason, this code is only working for me when I am in debug mode. When i run in release mode, all i hear is static, like on a tv when there is no signal. Any ideas? Thanks.

    ReplyDelete
    Replies
    1. err, sorry I have no idea why that would be. If you figure it out let me know!

      Delete
    2. I figured out a solution, thought it does not directly solve why the code, as is, will not work in a release build. Nevertheless, this code will work just fine if i use audioRecord.write() with an array of shorts. so for the "//fill out the array" part, i have:

      buff[i] = (short) Math.sin(2 * Math.PI * i / (sampleRate/freqOfTone));
      buff[i] *= 32767;

      The cool part is that this is all the genTone() method has to do, since we do not need to convert data to bytes.

      Also, regarding the initialization of the AudioTrack object:

      AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
      8000, AudioFormat.CHANNEL_OUT_MONO,
      AudioFormat.ENCODING_PCM_16BIT, numSamples*2,
      AudioTrack.MODE_STATIC);

      Notice that I changed numSamples to numSamples*2, because that argument is:

      int bufferSizeInBytes

      And each sample is a short which is two bytes. I believe that the original code should do this as well, as each sample gets converted into two bytes. I noticed that the tone was only last 1/2 the time specified.

      At any rate, I hope this may help someone.

      Delete
  15. Hi,
    Can any one have any link that help me to create a android app with sound filtering and i can add echo sound as background effect like cat sound or gun sound.
    please send me links on my gmail id:abhinav1.rakesh@gmail.com

    Thanks
    Abhinav

    ReplyDelete