I need to implement audio PPM (Pulse Position Modulation) on android
Reference: http://en.wikipedia.org/wiki/Pulse-position_modulation
I want to output PPM from the audio output of the smartphone. The final scope is to create a joystick for radiocontrol. but this library may have many future purposes (follow me, lightbridge, etc.etc.). The radios commonly have a PPM output. Transmitters (and pc flight simulators) commonly have PPM input. My scope is to replace the radio with an android device. I wish to know if there is some piece of code ready to use or should i start from scratch?
EDIT: I found some points where to start
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
I calculated that sampling the audio signal at 22000Hz will be sufficient to achieve a good resolution for each channel (22 steps for each channel)
Note: if you are interested in receiving ppm audio signal, you need the android ppm decoder class that you can find here: android PPM decoder audio library
I made a working example for ppm encoder class.
This is how i tested it:
1)recording the generated sound with a PC i can see on "wavepad editor" the waveform and it corresponds to what we need.
2) recording the audio output of the smartphone with a pc and analyzing the audio signal with the software "smartpropoplus" and its debug utilities, i can correctly control the PPM channels with my android app.
3) I connected the phone to a PPM receiver (DJI Lightbridge) but the signal is not correctly received. I suspect that the signal level is not the one expected by the dji device. I will wait your feedback comments, but until that moment, i suspect that i did the best that we can do with android.
NOTE: if you want to use my complete example, you need to use the file joystickView.jar in order to control the channels with a graphical joypad. this is how to use it:
1)download the jar file from this link: https://github.com/downloads/zerokol/JoystickView/joystickview.jar
2) Create a folder called "libs" on the root of your project and place the JAR file into this folder.
Now you can test my app.
These are the files of my test app:
file 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>
file activity_test.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:baselineAligned="true"
android:orientation="vertical" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:id="@+id/stick1VerticalLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:text="stick1Vertical" />
<TextView
android:id="@+id/stick1HorizontalLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="#ff0000"
android:ems="10"
android:text="stick1Horizontal" />
<TextView
android:id="@+id/stick2VerticalLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:text="stick2Vertical" />
<TextView
android:id="@+id/stick2HorizontalLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#ff0000"
android:layout_weight="1"
android:ems="10"
android:text="stick2Horizontal" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<com.zerokol.views.JoystickView
android:id="@+id/joystickViewLeft"
android:layout_width="wrap_content"
android:layout_height="fill_parent" />
<com.zerokol.views.JoystickView
android:id="@+id/joystickViewRight"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="end" />
</LinearLayout>
</LinearLayout>
file Test.java
package com.tr3ma.PPMtestProject;
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;
import com.zerokol.views.JoystickView;
import com.zerokol.views.JoystickView.OnJoystickMoveListener;
public class Test extends Activity {
PPMEncoder ppmencoder;
private TextView stick1VerticalLabel;
private TextView stick1HorizontalLabel;
private TextView stick2VerticalLabel;
private TextView stick2HorizontalLabel;
// Importing as others views
private JoystickView joystickLeft;
private JoystickView joystickRight;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
ppmencoder=new PPMEncoder(this);
//start the generation of the signal through the speakers
int result=ppmencoder.startGeneration();
if (result!=0){
//error occoured, something went wrong
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle("Error");
alert.setMessage("Error during audio signal generation. Error Number " + result);
alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
});
alert.show();
}
stick1VerticalLabel = (TextView) findViewById(R.id.stick1VerticalLabel);
stick1HorizontalLabel = (TextView) findViewById(R.id.stick1HorizontalLabel);
stick2VerticalLabel = (TextView) findViewById(R.id.stick2VerticalLabel);
stick2HorizontalLabel = (TextView) findViewById(R.id.stick2HorizontalLabel);
// referring as others views
joystickLeft = (JoystickView) findViewById(R.id.joystickViewLeft);
joystickRight = (JoystickView) findViewById(R.id.joystickViewRight);
// Listener of events, it'll return the angle in graus and power in percents
// return to the direction of the moviment
joystickLeft.setOnJoystickMoveListener(new OnJoystickMoveListener() {
@Override
public void onValueChanged(int angle, int power, int direction) {
//scompose the vector
float stickVertical=(float) Math.sin((Math.PI/180) * angle)*power; //values between +100 and -100
stickVertical=stickVertical+(float)100; //values between 0 and 200
stickVertical=(float)stickVertical*(float)((float)255/(float)200); //values between 0 and 255
float stickHorizontal=(float) Math.cos((Math.PI/180) * angle)*power; //values between +100 and -100
stickHorizontal=stickHorizontal+(float)100; //values between 0 and 200
stickHorizontal=stickHorizontal*(float)((float)255/(float)200); //values between 0 and 255
stick1VerticalLabel.setText("channel1:" + String.valueOf(stickVertical));
stick1HorizontalLabel.setText("channel2:" + String.valueOf(stickHorizontal));
ppmencoder.setChannelValue(1, stickVertical);
ppmencoder.setChannelValue(2, stickHorizontal);
}
}, JoystickView.DEFAULT_LOOP_INTERVAL);
joystickRight.setOnJoystickMoveListener(new OnJoystickMoveListener() {
@Override
public void onValueChanged(int angle, int power, int direction) {
//scompose the vector
//scompose the vector
float stickVertical=(float) Math.sin((Math.PI/180) * angle)*power; //values between +100 and -100
stickVertical=stickVertical+(float)100; //values between 0 and 200
stickVertical=(float)stickVertical*(float)((float)255/(float)200); //values between 0 and 255
float stickHorizontal=(float) Math.cos((Math.PI/180) * angle)*power; //values between +100 and -100
stickHorizontal=stickHorizontal+(float)100; //values between 0 and 200
stickHorizontal=stickHorizontal*(float)((float)255/(float)200); //values between 0 and 255
stick2VerticalLabel.setText("channel3:" + String.valueOf(stickVertical));
stick2HorizontalLabel.setText("channel4:" + String.valueOf(stickHorizontal));
ppmencoder.setChannelValue(3, stickVertical);
ppmencoder.setChannelValue(4, stickHorizontal);
}
}, JoystickView.DEFAULT_LOOP_INTERVAL);
}
@Override
protected void onDestroy() {
super.onDestroy();
int result=ppmencoder.stopGeneration();
if (result!=0){
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle("Error");
alert.setMessage("Error while stopping the audio generation. Error number " + result);
alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
});
alert.show();
}
}
}
file PPMEncoder.java (this is the class requested on the original question)
package com.tr3ma.PPMtestProject;
import java.util.ArrayList;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.AsyncTask;
public class PPMEncoder
{
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;
private ArrayList<Float> channelValues;
AudioManager audioManager;
StreamPPMSignalTask streamPPMSignalTask;
private boolean started;
public PPMEncoder(Context context)
{
audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
//set volume to max
//audioManager=(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
int tmpVol = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, tmpVol, 0);
channelValues = new ArrayList<Float>(8);
for (int i = 0; i < 8; i++) {
channelValues.add((float)0.68181818);
}
}
public int startGeneration()
{
try {
audioBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT)*2;
if (audioBufferSize<=0 ) return -2;
started = true;
streamPPMSignalTask = new StreamPPMSignalTask();
streamPPMSignalTask.execute();
return 0;
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
public int stopGeneration()
{
try {
started = false;
streamPPMSignalTask.cancel(true);
streamPPMSignalTask = null;
return 0;
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
private int timeToSamples(float time)
{
//time is expressed in milliseconds
return (int)Math.round(time * 0.001 * SAMPLE_RATE);
}
public void setChannelValue(int channel, float value)
{
channelValues.set(channel - 1, (float)0.68181818+(float)1.0 * ((float)value/(float)255));
}
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 = AudioTrack.getMinBufferSize(SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT) * 2;
if (audioBufferSize<=0 ) return -2;
started=false;
stopGeneration();
startGeneration();
//frame=new byte[streamBufferSize];
return 0;
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
public class StreamPPMSignalTask extends AsyncTask<Void, Double, Void>
{
@Override
protected Void doInBackground(Void... arg0) {
AudioTrack ppmTrack = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, audioBufferSize, AudioTrack.MODE_STREAM);
//set volume of audioplayer to max
ppmTrack.setStereoVolume((float) 1.0, (float) 1.0);
if (ppmTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
ppmTrack.play();
}
//feed the speakers with our audio, by continuously send the PPM frame
int tempBound;
while (started) {
try {
short[] frame = new short[ppmFrameBufferSize];
int i = 0;
tempBound = i + timeToSamples((float)0.3);
for (;i < tempBound; i += 1) {
frame[i] = Short.MIN_VALUE;
}
for (int channel = 0; channel < 8; channel++) {
tempBound = i + timeToSamples(channelValues.get(channel));
for (;i < tempBound; i += 1) {
frame[i] = Short.MAX_VALUE;
}
tempBound= i + timeToSamples((float)0.3);
for (;i < tempBound; i += 1) {
frame[i] = Short.MIN_VALUE;
}
}
for (;i < frame.length; i += 1) {
frame[i] = Short.MAX_VALUE;
}
//send the frame
ppmTrack.write(frame, 0, frame.length);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
}
End notes:
1) as you can see the 2 joysticks moves only 4 channels, but it is clear that you can add another 2 joystick controls on the activity in order to move all 8 channels.
2) credit goes to this site where you can see how it is made the joystickControl http://www.zerokol.com/2012/03/joystickview-custom-android-view-to.html in case you want to customize it. I wish to do it but i had no time. today i spent the entire day to write this post.
来源:https://stackoverflow.com/questions/29715866/android-ppm-encoder-audio-library