1

I need to implement audio PPM (Pulse Position Modulation) on android

Reference: http://en.wikipedia.org/wiki/Pulse-position_modulation

I want to receive PPM using the audio microphone input of the smartphone.

The radios for radiocontrols, drones, etc, commonly have a PPM output. Transmitters (and pc flight simulators) commonly have PPM input.

I wish to know if you can help me in this task.

As you can read here i still wrote the ppm encoder class: android PPM encoder audio library

These are some starting documentation and tools:

1) smartpropplus is a windows software that receives PPM audio and decodes it http://sourceforge.net/p/smartpropoplus/code/HEAD/tree/SPP4/

2) this is how PPM is structured: http://www.aerodesign.de/peter/2000/PCM/PCM_PPM_eng.html#Anker144123

3) this is a easy image that explains how the signal is structured: http://www.aerodesign.de/peter/2000/PCM/frame_ppm.gif

4) ppm signal measurements with oscilloscope: http://www.andrewhazelden.com/blog/2011/08/analyzing-rc-radio-ppm-signals/

EDIT: While waiting your support, i started writing the sample app:

this is the class PPMdecoder.java where it is missing the signal decoding, and the calibration phase.... but it allows to correctly aquire the signal from the microphone

NOTE: the "//To Be Done" comments in the code identify the missing code to solve this question.

class PPMdecoder.java

 package com.tr3ma.PPMtestProject;

 import java.util.ArrayList;
 import android.content.Context;
 import android.media.AudioFormat;
 import android.media.AudioManager;
 import android.media.AudioRecord;
 import android.media.MediaRecorder;
 import android.os.AsyncTask;
 import android.util.Log;



 public class PPMDecoder {

public int SAMPLE_RATE = 44100;
public int ppmFrameBufferSize = (int)(SAMPLE_RATE * 0.0225); // 22KHz * 22,5ms that it is the duration of a frame ppm
public int audioBufferSize;
int calibrationStatus=0;

private ArrayList<Float> channelValues;

AudioManager audioManager;
RecordAudio receivePPMSignalTask;

private boolean started;

long elapsedTimeSinceLastPublish;
long lastPublishMilliseconds;

IAsyncFetchListener fetchListener = null;

 public void setListener(IAsyncFetchListener listener) {
        this.fetchListener = listener;
    }

public PPMDecoder(Context context)
{
    audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);





    channelValues = new ArrayList<Float>(8);
    for (int i = 0; i < 8; i++) {
        channelValues.add(null);
    }

}

public int startDecoding()
{
    try {


        audioBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT)*2;

        if (audioBufferSize<=0 ) return -2;





        started = true;

        receivePPMSignalTask = new RecordAudio();
        receivePPMSignalTask.execute();
        return 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return -1;
}

public int stopDecoding()
{
    try {
        started = false;

        receivePPMSignalTask.cancel(true);
        receivePPMSignalTask = null;
        return 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return -1;
}

private float samplesToTime(int samples)
{
    return (((float)samples/( (float)SAMPLE_RATE)) * (float)1000); //time is expressed in milliseconds
}

public int getChannelValue(int channel)
{
    float tmpVal = channelValues.get(channel-1); //get the value
     tmpVal = (((tmpVal-(float)0.68181818) * (float)255 ) / (float)1.0); //convert to value between 0 and 255
     if (tmpVal<0) return 0;
     if (tmpVal>255) return 255;
     return (int)Math.round( tmpVal);
}

public int setSamplingRate(int freq) {
    //we can change the sampling frequency in case the default one is not supported
    try {
        SAMPLE_RATE=freq;

        ppmFrameBufferSize = (int)(SAMPLE_RATE* 0.0225); // 22KHz * 22,5ms

        audioBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE,
                AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT)*2;

        if (audioBufferSize<=0 ) return -2;

        started=false;
        stopDecoding();
        startDecoding();

        //frame=new byte[streamBufferSize];
        return 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return -1;
}

public class RecordAudio extends AsyncTask<Void, Void, Throwable> {

    @Override
    protected Throwable doInBackground(Void... arg0) {

        try {

            AudioRecord audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, SAMPLE_RATE, 
                    AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, audioBufferSize); 

            short[] buffer = new short[audioBufferSize];

            //final double[] fftData = new double[audioBufferSize];
            //RealDoubleFFT fft = new RealDoubleFFT(audioBufferSize);

            audioRecord.startRecording();

            lastPublishMilliseconds=System.currentTimeMillis();

            while (started) {
                //Log.e("audioRecord Time1 before read", "" + System.currentTimeMillis() );
                int bufferReadResult = audioRecord.read(buffer, 0,audioBufferSize);
                //Log.e("audioRecord Time2 after read", "" + System.currentTimeMillis() );
                //Log.e("audioRecord bufferReadResult", "" + bufferReadResult );

                if(AudioRecord.ERROR_INVALID_OPERATION == bufferReadResult){
                    //Log.e("audioRecord ErrorInvalidOperation", "Error " + System.currentTimeMillis() );

                } else {
                    //process the data
                    if (calibrationStatus>0){
                        //Log.w(TAG, "WE are calibrating");
                        //To Be Done................................ 
                        continue;
                    }


                    //decode
                    //To Be Done.......................

                    //each 100ms publish the channels value
                    elapsedTimeSinceLastPublish=System.currentTimeMillis()-lastPublishMilliseconds;
                    if (elapsedTimeSinceLastPublish>100){
                        publishProgress();
                        lastPublishMilliseconds=System.currentTimeMillis();

                    }

                }
            }

            audioRecord.stop();
            audioRecord.release();
            audioRecord=null;

        } catch (Throwable t) {
            t.printStackTrace();
            Log.e("audioRecord", "Recording Failed");
            return t;
        }
        return null;

    } //fine di doInBackground

    @Override
    protected void onProgressUpdate(Void... arg0) {
        //generate interupt in the father thread
        if (fetchListener != null) fetchListener.update(channelValues);
    }

    protected void onPostExecute(Throwable result){
        if (result==null){
            return;
        }
        //if we arrive here, report error "Sound Recorder Busy" and reset the acquisition
        //To be done... 
    }

} //Fine Classe RecordAudio (AsyncTask)

 }

this is the interface IAsyncFetchListener.java needed to create a listener on the UI thread in order to allow to the decoder to periodically update the UI with the channels value.

 package com.tr3ma.PPMtestProject;

 import java.util.ArrayList;
 import java.util.EventListener;


 public interface IAsyncFetchListener extends EventListener {
     void update(ArrayList<Float> channelValues );

 }

this is the sample activity class Test.java :

 package com.tr3ma.PPMtestProject;

import java.util.ArrayList;

import android.os.Bundle;
import android.widget.TextView;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;

import com.tr3ma.PPMtestProject.R;

 public class Test extends Activity {

PPMDecoder ppmdecoder;

 ....

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test);


    ppmdecoder=new PPMDecoder(this);
    ppmdecoder.setListener(new IAsyncFetchListener() {

        public void update(ArrayList<Float> channelValues ) {
            // do something with channelValues
            //To Be Done................................

        }


    });

    //start to receive the signal through the microphone
    int result=ppmdecoder.startDecoding();
    if (result!=0){
        //error occoured, something went wrong
        AlertDialog.Builder alert = new AlertDialog.Builder(this);
        alert.setTitle("Error");
        alert.setMessage("Error during audio signal receiving. Error Number " + result);
        alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int whichButton) {
            }
        });
        alert.show();

    }


    .....

}

    @Override
    protected void onDestroy() {
        super.onDestroy();
        int result=ppmdecoder.stopDecoding();
        if (result!=0){
            AlertDialog.Builder alert = new AlertDialog.Builder(this);
            alert.setTitle("Error");
            alert.setMessage("Error while stopping the audio receiving. Error number " + result);
            alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int whichButton) {
                }
            });
            alert.show();
        }

        ....
    }
}

this is the AndroidManifest.xml

 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tr3ma.PPMtestProject"
android:versionCode="1"
android:versionName="1.0" >
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<uses-sdk
    android:minSdkVersion="8"
    android:targetSdkVersion="17" />

<application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
        android:name="com.tr3ma.PPMtestProject.Test"
        android:label="@string/app_name" 
        android:screenOrientation="landscape" 
     >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

 </manifest>
Community
  • 1
  • 1
Gaucho
  • 1,328
  • 1
  • 17
  • 32
  • 1
    Thanks for the links! I record generated by PPMGenerator class audio file and play it from my computer to my turnigy9x and see values changes on channels. So it seems class totally working now and i just need to convert it to 5volts – striker Jan 08 '16 at 22:08
  • @striker i made big modification to decoder class, included the calibration process. It's a big work. It requested one entire day and i still didn't finished – Gaucho Jan 09 '16 at 10:03
  • Did you understand how many channels can go in a ppm stream? On the wikipedia it says 8 but on my last link it says that also more it is possible. But in this case whitch is the pulses duration? Which is the start pulse duration? Which is the ppm frame duration? – Gaucho Jan 09 '16 at 10:07
  • The previous questions are important for the ppm encoder, while for the ppm decoder i use the calibration to find all these parameters with an automatic algorithm – Gaucho Jan 09 '16 at 10:09

0 Answers0