Search This Blog

Sunday, March 11, 2012

Android tone generator app

Finally published my audio app today, this turned out to be be a long and painful road.   I leaned a lot about Android audio, but the app took so long it was no longer fun.  In the end I published a scale down app just so I could hold my head up and not have to declare failure.  But more about that later.  I'm still polishing my skills and entertaining myself, so I'm developing simple Android apps in preparation for some killer app ideas down the road.     I need to master playing mathematically generated audio files.

Made this icon for the signal generator

First I decided to expand the audio tone generator functions in my starter app to make a publishable signal generator and audio test app.  I thought this would go well with my hobby electronics bent.    The flashlight strobe app had the issue that many phones use different hardware, and I got a lot of complaints that it didn't work on certain phones.  This app sticks to commonly compatible hardware.

I'm making a point not to search for apps like the one I want to develop. That is way too depressing because everything imaginable has already been written.  The way to learn is to do, so I'm making apps that I feel would be useful.  It is much more exciting to maintain the illusion of being creative.  Plus I don't want to be accused of copying anything.

I'll use the background thread asynctask function from the light blinker app to generate the tone.  The previous app just made a tone for a short duration and locked up the UI while it did so.  I want to be able to make a continuous tone, and modulate it with the slider bar and have it running in a background thread.

One big help is that suddenly the tone generator works in the emulator.  Previously I could play tones from my phone, but the emulator was mute.   My PC has been rebooted and maybe the soundcard was just out to lunch.  But it works now, I can hear the tones from the PC.  It does seem that the loading in the emulator leads to more interruptions in the sound playback than on a real device.

I started with these functions to generate a tone

// http://stackoverflow.com/questions/2413426/playing-an-arbitrary-tone-with-android
    // functions for tone generation
    void genTone(double freqOfTone){
        // 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) {
            // scale to maximum amplitude
            short val = (short) ((dVal * 32767));
            // in 16 bit wav PCM, first byte is the low order byte
            generatedSnd[idx++] = (byte) (val & 0x00ff);
            generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
        }
    }
    void playSound(){
        final AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
                AudioFormat.ENCODING_PCM_16BIT, numSamples,
                AudioTrack.MODE_STATIC);
        audioTrack.write(generatedSnd, 0, generatedSnd.length);
        audioTrack.play();
    }
 
           
In order to make the tone continuous, and not a series of chopped tones, I need to use STREAM not STATIC mode.  Need to write continuously to the audio track, turns out the buffer is blocking so you just keep writing.  This mod made a nice continuous tone:

void playSound(){
    final AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
            sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
            AudioFormat.ENCODING_PCM_16BIT, numSamples,
            AudioTrack.MODE_STREAM);
    audioTrack.play();
    while (blinking==true){
    audioTrack.write(generatedSnd, 0, generatedSnd.length); 
    }
}

Awesome, I have the soul of the app.  I copied over my light blinking app and pasted this function into the Asynctask, and it worked great.   that is why it says "blinking" .  That variable is controlled by the GUI and breaks the loop when the user pushes the button.   The tone turns on and off with the toggle switch and the background thread is working great, the UI doesn't lock up.

With a few minutes hacking I replaced the light blinking with the tone generation in my previous app.  Now the radio buttons play different tones and the slider sets the tone frequency.    Just proof of concept, the buttons all say the wrong thing, etc.

The second half of the proof of concept that the slider bar can control  the tone while it plays.  What will actually happen is the slider bar will change the variables for tone gen function that will calculate the data, and then that will get queued into the audio playback.   Hopefully the lag won't be so great the user gets annoyed.   I put a new tone generation step in the loop.
void playSound(){
    final AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
            sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
            AudioFormat.ENCODING_PCM_16BIT, numSamples,
            AudioTrack.MODE_STREAM);
    audioTrack.play();
    while (blinking==true){
    audioTrack.write(generatedSnd, 0, generatedSnd.length);
    genTone(infreqOfTone); 
    }
}

Unfortunately the tone is interrupted when the genTone function runs, I guess it is taking too long and the buffer empties.  So this math needs to be done in the UI thread and passed to the audio playing thread.  I pasted the genTone() function into the slider and toggle listeners, and ba da bing!   The UI is a tad bit laggy and I'll work on that, but now the slider changes the tone as it plays, and the tone is continuous with no clicks or gaps.

The basics are working, next I need to build the polished app.  Here goes the PCR list!   This is my working list I'm using to built the app.

Side note ...Found a neat tool for making icons and such.  Didn't try it yet but it looks like it could save time on the artwork.
http://android-ui-utils.googlecode.com/hg/asset-studio/dist/index.html

I started in on building the GUI, and found I had way too many functions to put in.  The GUI will be a mile long list.  I'd like to organize it into tabs, pages, something.

I good description of the sliding drawer and some xml that actually worked, however this isn't what I want.
http://android-er.blogspot.com/2011/03/slidingdrawer.html
This hides buttons and slides them onto the screen.  Cool but not right for this app.

Found a page on tabhost.
http://dewful.com/?p=15
Turns out the double quote " symbol I copied out of this example was the wrong double quote ", and I spent an hour chasing the weirdest bugs you ever saw.  The symbol was not a real quote and looked like one.  That is really a way to screw up your compiler.  After that it worked and I made a UI with multiple tab pages.

Figured out how to make multiple tabs with different check boxes on each using the above example.   Added content1, content2, content3 to the tab setup

tabs.setup();
        TabSpec tspec1 = tabs.newTabSpec("First Tab");
        tspec1.setIndicator("One");
        tspec1.setContent(R.id.content1);
        tabs.addTab(tspec1);
        TabSpec tspec2 = tabs.newTabSpec("Second Tab");
        tspec2.setIndicator("Two");
        tspec2.setContent(R.id.content2);
        tabs.addTab(tspec2);
        TabSpec tspec3 = tabs.newTabSpec("Third Tab");
        tspec3.setIndicator("Three");
        tspec3.setContent(R.id.content3);
        tabs.addTab(tspec3);

Also added three content1, content2, content3 linear layouts under the tab widget in the main.xml

Now to draw up the GUI.....

  • Develop the feature list
    • Generate arbitrary tone
    • Adjust sample rate for maximum purity
    • Add harmonics (Striking this feature for the first rev)
    • sine, square, triangle ramp
    • Option to constrain output to musical notes only
    • Display frequency in Hz
    • Respond live to slider bars
    • On/off switch
    • graph of the tone being played, maybe this is an enhancement.
    • slider for trise, tfall, tduty
    • White noise, Pink noise
    • Audio test sounds


Found an issue with tone generation, when I put the waveform on the scope.  The tone sounds fine but occasionally the phase jumps.  I thought this was due my wave file not starting and stopping at zero. and mismatch between the frequency and the sample rate and number of samples chosen.   I rewrote the audio generation to avoid all these issues.  However it looks like the problem persists.   It seems the audio buffer empties occasionally.   I rewrote things again to put in a big file to start, and then feed it with small chunks after that.   I've played with this a lot and I don't know if the problem is the buffer emptying and restarting, or the sample rate jumping in some way to make it come out even.

I found the equations for generating musical notes.  I'll make that a mode.
http://www.phy.mtu.edu/~suits/NoteFreqCalcs.html

In order to make the tones more pure, I decided to dynamically set the sample rate to be a multiple of the tone frequency.  This sounded good, but hosed up things.  Previously I was continuously writing data into an audio stream.  I"d simply change the math and the calculated wave would change and the tone would change.  Now that I want to also change sample rate, I have to kill the AudioTrack and restart it every time the frequency changes.  The end result is a less smooth interface, but the goal here is to have a quality audio output.

The app is getting a bit out of hand, too many functions.  As a result it is taking too long to get rev 1.0 published.   I'm going to pare back some of the bells and whistles so I can get something published.

Implementing the square wave functions.   I'll use the same functions as the sine wave, but calculate a square instead.  Used an excel spreadsheet to come up with this function
=-1+(B4<$H$4)*2+(B4<$H$5)*(2*B4/$H$5-2)-((B4>=$H$4)*(B4<($H$4+$H$6)))*(-2+2*((B4-$H$4)/$H$6))
where $H$4=duty, $H$5=rise, $H$6=fall, based on 100 samples
The sliders callback limits the legal values of rise and fall to be less than the appropriate part of the duty cycle

I imported all the audio test files, and now I'm having trouble with soundpool.   All my files are big, ~882K.   If I switch from sound to sound, I get and error that AudioFlinger could not create track, status: -12
I think this is due to running out of memory in soundpool.   Randomly tracks won't play, the app crashes.   Getting discouraged.   Taken so long it ceases to be fun, and I'm doing this for fun.

In the process of debugging the soundPool issue I stumbled across all the audio signal generator apps already on the market.  I had been trying not to see this because it becomes too depressing to develop anything if you see it's already been done.  In this case I saw many many very nice apps already out there and my app is full of bugs.  Time for a rethink.

I decided to make a musical Tone generator using the note generation section I had above.
http://www.phy.mtu.edu/~suits/NoteFreqCalcs.html   I found that kind of cool, I never really knew the formulas behind the musical scale.

44100 sample rate for high quality sine waves
Calculate and display the frequency
Display the musical note and octave
Live slider for note - later canned this because of clicking as it slid, made unpleasant sounds.
Pitch control of the A440 tune so other scale tunings can be made

There is lots of room to expand to play scales and intervals and chords.  However I kept a lid on it.  This project is way out of hand and the ideas for other apps have backed up in my head to the point it might explode.

Had to rewite the tone generation because it's difficult to maintain a constant sample rate, and make the tones start and stop at zero so they can loop without a click or phase jump. Made a spreadsheet to work it out.


// Based on but modified and improved from
 // http://stackoverflow.com/questions/2413426/playing-an-arbitrary-tone-with-android
 // functions for tone generation
 void genTone(double freqOfTone){

//clean out the arrays
for (int i = 0; i < targetSamples * 2; ++i) {
        sample[i] = 0;
     }
for (int i = 0; i < targetSamples * 2 * 2; ++i) {
         generatedSnd[i] = (byte) 0x0000;
}

// calculate adjustments to make the sample start and stop evenly
numCycles = (int) (0.5 +  freqOfTone * targetSamples/sampleRate);
numSamples = (int) (0.5 + numCycles * sampleRate/freqOfTone);

     // 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 normalized.
     int idx = 0;
     for (double dVal : sample) {
    // scale loudness by frequency
    double amplitude = (double) (32767 * 5/(Math.log(freqOfTone)));
    if (amplitude > 32767) amplitude = 32767;
    // scale signal to amplitude
         short val = (short) (dVal * amplitude);
         // in 16 bit wav PCM, first byte is the low order byte
         generatedSnd[idx++] = (byte) (val & 0x00ff);
         generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
     }
 }
 void playSound(){
     final AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
             sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
             AudioFormat.ENCODING_PCM_16BIT, numSamples*2,
             AudioTrack.MODE_STREAM);
     audioTrack.write(generatedSnd, 0, numSamples*2);
     audioTrack.play(); 
     while (running==true){
      audioTrack.write(generatedSnd, 0, numSamples*2);
     }
     audioTrack.stop();
     running = false;

 }



GUI should show the musical note calculation.   I need to use relative layout, which took a lot of time to move and tweak and set the values and anchor points.   There must be an easier way, but I worked through it slowly bouncing between the graphical view, the code, and the emulator until it did what i wanted.

    <RelativeLayout
            android:id="@+id/relativeLayout2"
            android:layout_width="fill_parent"
            android:layout_height="108dp" >
            <TextView
                android:id="@+id/textViewHz"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:layout_centerVertical="true"
                android:maxLines="1"
                android:maxWidth="80dp"
                android:minWidth="80dp"
                android:text="440.0000000000"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:textColor="@color/red" android:layout_marginLeft="10dp" android:textStyle="normal|bold" android:textSize="22dp"/>
            <TextView
                android:id="@+id/textView5"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/textViewHz"
                android:layout_alignBottom="@+id/textViewHz"
                android:layout_toRightOf="@+id/textViewHz"
                android:text="Hz = "
                android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="@color/red" android:textSize="22dp" android:textStyle="normal"/>
            <TextView
                android:id="@+id/textViewFreq"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/textView5"
                android:layout_alignBottom="@+id/textView5"
                android:layout_toRightOf="@+id/textView5"
                android:maxLines="1"
                android:maxWidth="80dp"
                android:text="440.000000000000"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:textColor="@color/blue" android:textStyle="bold" android:minWidth="80dp" android:textSize="22dp"/>
            <TextView
                android:id="@+id/textViewForm"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_toRightOf="@+id/textViewFreq"
                android:text="·2"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:textColor="@color/black"
                android:textSize="32dp" android:textStyle="bold"/>
            <TextView
                android:id="@+id/textViewParen"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_above="@+id/textViewFreq"
                android:layout_toRightOf="@+id/textViewForm"
                android:text="("
                android:textAppearance="?android:attr/textAppearanceLarge"
                android:textSize="32dp" android:textColor="@color/black"/>

            <TextView
                android:id="@+id/textViewN"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignRight="@+id/TextView01"
                android:layout_alignTop="@+id/textViewParen"
                android:layout_marginLeft="5dp"
                android:layout_toRightOf="@+id/textViewParen"
                android:text="9"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:textColor="@color/purple"
                android:textStyle="bold" />
            <TextView
                android:id="@+id/textView6"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/textViewN"
                android:layout_alignBottom="@+id/textViewN"
                android:layout_toRightOf="@+id/textViewParen"
                android:text="____"
                android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="@color/black"/>
            <TextView
                android:id="@+id/textView3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBottom="@+id/textViewFreq"
                android:layout_alignLeft="@id/textViewN"
                android:layout_below="@+id/textViewN"
                android:text="12"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:textColor="@color/black" />
            <TextView
                android:id="@+id/TextView01"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignTop="@+id/textView6"
                android:layout_marginLeft="3dp"
                android:layout_toRightOf="@+id/textView6"
                android:text=")"
                android:textAppearance="?android:attr/textAppearanceLarge"
                android:textSize="32dp" android:textColor="@color/black"/>
        </RelativeLayout>
Used this page to get the note names and octaves correct.
http://en.wikipedia.org/wiki/Scientific_pitch_notation

Went with a virtual turning fork motif.    Used a background image that vibrates and detect a shake or strike to start.



Want to detect a shake to start the tuning fork vibrating.  Copied code from here and it worked awesome on the first try.  I ended up adding a 1 second timer to debounce the shaking.  During a shake it would turn on and off a couple times which would have annoyed the user.
http://stackoverflow.com/questions/2317428/android-i-want-to-shake-it

Finally I published it.  Not at all the app I started out with, but now I can get back to new ideas and declare victory.  W00T!  I'm not super proud, and you can still get the audio to click sometimes.  I've scoped the output until I'm blue in the face, and tweaked the code, but I can't totally eliminate it.

Switched the icon to a tuning fork.

Download the app here:
https://play.google.com/store/search?q=pub:Siliconfish

This link lets you make a link like the ones below
http://developer.android.com/guide/publishing/publishing.html#BuildaButton


Get it on Google Play

Android app on Google Play

4 comments:

  1. Brilliant! Have been puttering at making that tone generator - originally from marblemice I think. Have found it difficult, but had not realized how difficult it might be.

    ReplyDelete
  2. Hi..what does 'targetSamples' in your genTone() function stand for?

    ReplyDelete
  3. Sorry I cut off the header defining targetSamples. targetSamples is the approximate number of samples you want your waveform to have. I think I used 5000 or 10000. The reason it is a target is that the actual number needs to be a multiple of the frequency repeat so it gets adjusted to come out right. The only effect of changing this number is that the loop time of the audio gets longer and the tone gen takes longer to turn off. Too short and you can't reproduce low frequencies accurately.

    ReplyDelete