Search This Blog

Saturday, December 31, 2011

Android app continued v: time, tones, and GUI

As stated in the previous couple of posts, I want to take advantage of the android phone/tablet platform to do a lot of the cool widgets I've build with Arduino, etc.   I've been making the first baby steps to creating an application and learning to interact with the phone hardware.   So far I've installed all the software, drivers, etc and successfully uploaded an app to my phone.  (see previous posts)  Then I upgraded my app to control the camera flash LED in response to a button press.  Now the goal is to learn about the Android system clock and what precise time keeping functions are available.  To do much useful with the Android I need to be able to perform functions like the Arduino delay() and pwm timers.  Once that is licked I can use the LED to send a serial stream of data which is my eventual goal.

I'm going to start with the app in my previous post and try to add timebase functions.  I'll put that on the scope and see how accurate it is.    I'll also try generating precise tones and measuring them.

Found this useful page on android system clock
http://developer.android.com/reference/android/os/SystemClock.html

This is a little trickier than it might seem, because I need to use a handler rather than tying up the phone 100% in a timing loop waiting for an event.  But for now we need to get a clock running.

http://stackoverflow.com/questions/526524/android-get-time-of-chronometer-widget
This page had an example of accessing a chronometer that I was able to integrate to make a time base
However I did not need all the functionality in this example.  just mChronometer.start, stop and setBase.   Added buttons to start, stop and clear the timer.    I get an output readout of ms, which is useful, but limits my maximum frequency to 1/2ms = 500Hz for a string of 101010 at this rate.  Boo,  Not good enough for most serial transmission applications.

Went off to try audio tone generation.  Found some examples of playing tones, this was the best, incorporated this code that used the audio sample rate to set a tone frequency
http://stackoverflow.com/questions/2413426/playing-an-arbitrary-tone-with-android
I thought this might lead me to a faster timebase to use, but it turns out this sets a sample rate on the audio player as the timebase, it doesn't use the system clock. I'll put this on the scope and see how accurate it is.

Having an annoying problem that the emulator sound doesn't play, but my phone's does!
Went to Run->configurations and added -useaudio in the command line options field.
Didn't work.  I'm not going to figure out this issue right now.  I'll have to use the real phone for development for the moment.   The computer audio is windows-fied.


Took a side trip and polished my app for a couple hours, working around making the image change when buttons were pressed so that I could learn to control what was on the screen and make GUIs.
http://developer.android.com/guide/topics/graphics/2d-graphics.html  was a good example.  Now I have a GUI with buttons, checkboxes, a picture that changes in response to button pushes, a timer, data entry, the camera LED turns on and off and a beep is played if a box is checked.  Basically a starting point for many apps.  I piled in every function I could think of.




Using an audio jack cable connected to the phone, I measured the frequency of the beep on the oscilloscope, it was about 1 / 2.3ms = 434.78Hz.   Probably was really the 440Hz that was programmed by private final double freqOfTone = 440; // hz.   I just couldn't measure it more accurately.

Next I set the frequency to 5000 Hz, knowing that the audio sample rate is 8000 Hz, so I am above Nyquist and the waveform is going to be distorted and aliased.  The frequency is wrong because of the aliasing.  Here is what it looked like:

I'll need to see if I can up the sample rate to 16000.  Wow, that sounds a lot better.  Much purer tone.  Here is 5Khz at 16KHz sample.
Now here is 5kHz at 32kHz sample rate.  Now we are in business.  This tone sounds pure and the amplitude isn't getting modulated by much as it beats against the sample frequency.


So where are we?   I'm up and comfortable with creating apps, having fun, although I'm sure I'd make a programmer cringe with my code.    The system clock is in milliseconds, and is too slow to be very useful for applications other than human interface.    The audio playback is pretty good up to 5kHz, I didn't push the audio sample rate up higher, but I imagine it won't go much past 44KHz (CD quality).    Next I'm going to have to dive deeper in the hardware to realize my goal of high frequency LED modulation.

Meanwhile I'll post my relevant code at the end of this post and perhaps look into joining the app store as a distribution method.  It is also posted here:
http://code.google.com/p/arduino-java-xyzcnc/downloads/detail?name=Siliconfish%20Blink.zip&can=2&q=#makechanges

Activity.java


package com.example.android.skeletonapp;

import android.app.Activity;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Chronometer;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import java.lang.reflect.Method;
import java.lang.Object;
import android.content.Context;
import android.util.Log;

/**
 * This class provides a basic demonstration of how to write an Android
 * activity. Inside of its window, it places a single view: an EditText that
 * displays and edits some internal text.
 */
public class SkeletonActivity extends Activity {
 
// sets up menu items for the pop up menu
    static final private int BACK_ID = Menu.FIRST;
    static final private int CLEAR_ID = Menu.FIRST + 1;
    static final private int BEEP_ID = Menu.FIRST + 2;
    static final private int NOFLASH_ID = Menu.FIRST + 3;

    // sets up the objects that are on the screen
    private EditText mEditor;
    private ImageView mImage;
    private TextView mTextView;
    private CheckBox mCheckBox;
    private EditText mFreq;
    Chronometer mChronometer;
 
    // name for the exception debug log
    private static final String TAG = "sifish.blink";
     
    // variables for tone generation
    private final int duration = 3; // seconds
    private final int sampleRate = 32000;
    private final int numSamples = duration * sampleRate;
    private double sample[] = new double[numSamples];
    private double infreqOfTone = 440.0; // hz
    private byte generatedSnd[] = new byte[2 * numSamples];
    Handler handler = new Handler();
 
    public SkeletonActivity() {
    }

    /** Called with the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Inflate our UI from its XML layout description.
        setContentView(R.layout.skeleton_activity);

        // Find the objects inside the screen layout xml, because we
        // want to do various programmatic things with them.
        mEditor = (EditText) findViewById(R.id.editor);
        mImage = (ImageView ) findViewById(R.id.imageView1);
        mChronometer = (Chronometer ) findViewById (R.id.chronometer1);
        mTextView = (TextView) findViewById (R.id.TextView1);
        mCheckBox = (CheckBox) findViewById (R.id.checkBox1);
        mFreq = (EditText) findViewById(R.id.editFreq);
 

        // Hook up button presses to the appropriate event handler.
        ((Button) findViewById(R.id.back)).setOnClickListener(mBackListener);
        ((Button) findViewById(R.id.clear)).setOnClickListener(mClearListener);
        ((Button) findViewById(R.id.flash)).setOnClickListener(mFlashListener);
        ((Button) findViewById(R.id.noflash)).setOnClickListener(mNoFlashListener);
     
        // Do initial setups
        mEditor.setText("Hello from Siliconfish!");
        mImage.setImageDrawable(getWallpaper());
        mFreq.setText(Double.toString(infreqOfTone));
     
        mChronometer.setBase(SystemClock.elapsedRealtime());
        mChronometer.stop();
        mTextView.setText(Long.toString(SystemClock.elapsedRealtime() - mChronometer.getBase()));
        genTone(infreqOfTone);  //make sure the tone exists
    }

    /**
     * Called when the activity is about to start interacting with the user.
     */
    @Override
    protected void onResume() {
        super.onResume();
    }

    /**
     * Called when your activity's options menu needs to be created.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        // We are going to create two menus. Note that we assign them
        // unique integer IDs, labels from our string resources, and
        // given them shortcuts.
        menu.add(0, BACK_ID, 0, R.string.back).setShortcut('0', 'b');
        menu.add(0, CLEAR_ID, 0, R.string.clear).setShortcut('1', 'c');
        menu.add(0, BEEP_ID, 0, R.string.beep).setShortcut('2', 'b');
        menu.add(0, NOFLASH_ID, 0, R.string.noflash).setShortcut('3', 'x');

        return true;
    }

    /**
     * Called right before your activity's option menu is displayed.
     */
    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);

        // Before showing the menu, we need to decide whether the clear
        // item is enabled depending on whether there is text to clear.
        menu.findItem(CLEAR_ID).setVisible(mEditor.getText().length() > 0);

        return true;
    }

    /**
     * Called when a menu item is selected.
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case BACK_ID:
            finish();
            return true;
        case CLEAR_ID:
            mEditor.setText("");
            return true;
        case BEEP_ID:
            mEditor.setText("! BEEP !");
                     
         // Play a sound Use a new thread as this can take a while
            final Thread thread = new Thread(new Runnable() {
                public void run() {
                    genTone(infreqOfTone);
                    handler.post(new Runnable() {

                        public void run() {
                            playSound();
                        }
                    });
                }
            });
            thread.start();
         
         
            return true;
        case NOFLASH_ID:
            mEditor.setText("MENU - NO FLASH");
            setOn(false, null);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    /**
     * A call-back for when the user presses the back button.
     */
    OnClickListener mBackListener = new OnClickListener() {
        public void onClick(View v) {
            finish();
        }
    };

    /**
     * A call-back for when the user presses the clear button.
     */
    OnClickListener mClearListener = new OnClickListener() {
        public void onClick(View v) {
        mEditor.setText("");
        mChronometer.setBase(SystemClock.elapsedRealtime());
        mImage.setImageDrawable(getWallpaper());
        mTextView.setText(Long.toString(SystemClock.elapsedRealtime() - mChronometer.getBase()));
        }
    };
 
    /**
     * A call-back for when the user presses the flash button.
     */
    OnClickListener mFlashListener = new OnClickListener() {
        public void onClick(View v) {
        setOn(true, null);
        mChronometer.start();
            mEditor.setText("*FLASH*");  
            mImage.setImageResource(R.drawable.scifiarmy_small);
            mTextView.setText(Long.toString(SystemClock.elapsedRealtime() - mChronometer.getBase()));
            if(mCheckBox.isChecked() ) {
            //get the value entered by the user
            try {
            infreqOfTone = Double.parseDouble(mFreq.getText().toString());
            } catch(NumberFormatException nfe) {
            infreqOfTone = 440.0;
            Log.e(TAG, "bad frequency input", nfe);
            }
            mEditor.setText(Double.toString(infreqOfTone));
           
        // Play a sound Use a new thread as this can take a while
           final Thread thread = new Thread(new Runnable() {
               public void run() {
                   genTone(infreqOfTone);
                   handler.post(new Runnable() {

                       public void run() {
                           try {
                        playSound();
                           }
                           catch (Exception e) {
                            Log.e(TAG, "playSound problem", e);
                         
                           }
                       }
                   });
               }
           });
         
           thread.start();
                     
         
            } //if checked
        }
    };
    /**
     * A call-back for when the user presses the no flash button.
     */
    OnClickListener mNoFlashListener = new OnClickListener() {
        public void onClick(View v) {
        setOn(false, null);
        mChronometer.stop();
            mEditor.setText("It is DARK");
            mImage.setImageResource(R.drawable.thorsmall);
            mTextView.setText(Long.toString(SystemClock.elapsedRealtime() - mChronometer.getBase()));
        }
    };
 
     
    // code largely copied from   http://www.java2s.com/Open-Source/Android/Tools/quick-settings/com/bwx/bequick/flashlight/Droid22Flashlight.java.htm
    // Controls the camera flash LED
 
    private Object mManager;

    public boolean isOn(Context context) {
        try {
            Object manager = getManager();
            if (manager != null) {
                Method getFlashlightEnabledMethod = manager.getClass()
                        .getMethod("getFlashlightEnabled");
                return (Boolean) getFlashlightEnabledMethod
                        .invoke(manager);
            }
        } catch (Exception e) {
            Log.e(TAG, "", e);
        }
        return false;
    }

    public void setOn(boolean on, Context context) {
        try {
            Object manager = getManager();
            if (manager != null) {
                Method setFlashlightEnabledMethod = manager.getClass()
                        .getMethod("setFlashlightEnabled",
                                boolean.class);
                setFlashlightEnabledMethod.invoke(manager, on);
            }
        } catch (Exception e) {
            Log.e(TAG, "", e);
        }
    }

    private Object getManager() {
        if (mManager == null) {
            try {
                Class<?> managerClass = Class
                        .forName("android.os.ServiceManager");
                Method methodGetService = managerClass.getMethod(
                        "getService", String.class);
                IBinder hardwareService = (IBinder) methodGetService
                        .invoke(managerClass, "hardware");

                Class<?> stubClass = Class
                        .forName("android.os.IHardwareService$Stub");
                Method asInterfaceMethod = stubClass.getMethod(
                        "asInterface", IBinder.class);
                mManager = asInterfaceMethod.invoke(stubClass,
                        hardwareService);
            } catch (Exception e) {
                Log.e(TAG, "", e);
            }
        }
        return mManager;
    }
   
    // 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();
    }
 
}

activity.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2007 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at
  
          http://www.apache.org/licenses/LICENSE-2.0
  
     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<!-- This file describes the layout of the main SkeletonApp activity
     user interface.
 -->

<!-- The top view is a layout manager that places its child views into
     a row, here set to be vertical (so the first is at the top) -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <!--
         First view is a text editor.  We want it to use all available
         horizontal space, and stretch to fill whatever vertical space
         is available to it.  Note the use of the "id" attribute, which
         allows us to find this object from the Java code.
    -->


    <!--
         Next view is another linear layout manager, now horizontal.  We
         give it a custom background; see colors.xml for the definition
         of drawable/semi_black
    -->

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="match_parent"
        android:layout_height="162dp"
        android:src="@drawable/thorsmall" />

    <DigitalClock
        android:id="@+id/digitalClock1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="DigitalClock" />

   <LinearLayout
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_vertical"
       android:background="@drawable/semi_black"
       android:gravity="center_horizontal"
       android:orientation="horizontal" >
   
   <TextView
       android:id="@+id/textView2"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="Elapsed Time   "
       android:textAppearance="?android:attr/textAppearanceSmall" />
   <Chronometer
       android:id="@+id/chronometer1"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="Chronometer" />
</LinearLayout>
<LinearLayout
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_vertical"
       android:background="@drawable/semi_black"
       android:gravity="center_horizontal"
       android:orientation="horizontal" >
   <TextView
       android:id="@+id/TextView1"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" 
       android:text="0"/>
   <TextView
       android:id="@+id/textView7"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text=" ms"
       android:textAppearance="?android:attr/textAppearanceSmall" />
    </LinearLayout>

<TextView
   android:id="@+id/textView4"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="Text Entry and Display"
   android:textAppearance="?android:attr/textAppearanceSmall" />

    <EditText
        android:id="@+id/editor"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:autoText="true"
        android:capitalize="sentences"
        android:freezesText="true" >

        <requestFocus />
    </EditText>

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Beep Frequency (Hz) "
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <EditText
        android:id="@+id/editFreq"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="numberDecimal" />

    <CheckBox
        android:id="@+id/checkBox1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Beep" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:background="@drawable/semi_black"
        android:gravity="center_horizontal"
        android:orientation="horizontal" >

        <!--
             On the left: the "back" button.  See styles.xml for the
             definition of style/ActionButton, which we use to hold
             common attributes that are used for both this and the
             clear button.  See strings.xml for the definition of
             string/back.
        -->

        <Button
            android:id="@+id/back"
            style="@style/ActionButton"
            android:text="@string/back" />

        <!--
            On the right: another button, this time with its text color
             changed to red.  Again, see colors.xml for the definition.
        -->

        <Button
            android:id="@+id/clear"
            style="@style/ActionButton"
            android:text="@string/clear"
            android:textColor="@color/red" />

        <Button
            android:id="@+id/flash"
            style="@style/ActionButton"
            android:text="@string/flash"
            android:textColor="@color/yellow" />

        <Button
            android:id="@+id/noflash"
            style="@style/ActionButton"
            android:text="@string/noflash" />

</LinearLayout>
  </LinearLayout>









No comments:

Post a Comment