Аудио библиотека PPM для Android

Мне нужно реализовать аудио PPM (Pulse Position Modulation) на Android

Ссылка: http://en.wikipedia.org/wiki/Pulse-position_modulation

Я хочу вывести PPM из аудиовыхода смартфона. Последняя задача - создать джойстик для радиоуправления. но эта библиотека может иметь много будущих целей (следуй за мной, lightbridge и т. д.). Радиостанции обычно имеют выход PPM. Передатчики (и компьютерные симуляторы полета) обычно имеют вход PPM. Моя задача - заменить радио на устройство Android. Я хотел бы знать, есть ли какой-то кусок кода, готовый к использованию, или я должен начать с нуля?

РЕДАКТИРОВАТЬ: я нашел несколько точек, с чего начать

1) smartpropplus - это программное обеспечение для Windows, которое получает аудио PPM и декодирует его http://sourceforge.net/p/smartpropoplus/code/HEAD/tree/SPP4/

2) структура PPM: http://www.aerodesign.de/peter/2000/PCM/PCM_PPM_eng.html

3) это простое изображение, объясняющее, как устроен сигнал: http://www.aerodesign.de/peter/2000/PCM/frame_ppm.gif

Я рассчитал, что выборки аудиосигнала на частоте 22000 Гц будет достаточно для достижения хорошего разрешения для каждого канала (22 шага для каждого канала)

Примечание: если вы заинтересованы в получении аудиосигнала ppm, вам необходим класс ppm-декодера android, который вы можете найти здесь: аудио-библиотека android PPM decoder

1 ответ

Решение

Я сделал рабочий пример для класса кодировщика ppm.

Вот как я это проверил:

1) запись сгенерированного звука на ПК. Я вижу в "редакторе волновой панели" форму волны, и она соответствует тому, что нам нужно.

2) записывая аудиовыход смартфона с ПК и анализируя аудиосигнал с помощью программного обеспечения "smartpropoplus" и его утилит отладки, я могу правильно управлять каналами PPM с помощью своего приложения для Android.

3) Я подключил телефон к приемнику PPM (DJI Lightbridge), но сигнал не был принят правильно. Я подозреваю, что уровень сигнала не тот, который ожидается от устройства DJI. Я буду ждать ваших комментариев, но до этого момента, я подозреваю, что я сделал лучшее, что мы можем сделать с Android.


ПРИМЕЧАНИЕ: если вы хотите использовать мой полный пример, вам нужно использовать файл joystickView.jar для управления каналами с помощью графического джойстика. вот как это использовать:

1) скачайте файл jar по этой ссылке: https://github.com/downloads/zerokol/JoystickView/joystickview.jar

2) Создайте папку с именем "libs" в корне вашего проекта и поместите файл JAR в эту папку.


Теперь вы можете проверить мое приложение.

Это файлы моего тестового приложения:

файл 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>

файл 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>

файл 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();
    }
}
 }

файл PPMEncoder.java (это класс, запрошенный для исходного вопроса)

 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;
    }
}
 }

Конечные заметки:

1) как вы можете видеть, 2 джойстика перемещаются только по 4 каналам, но ясно, что вы можете добавить еще 2 джойстика для управления деятельностью, чтобы переместить все 8 каналов.

2) Кредит идет на этот сайт, где вы можете увидеть, как это сделано, джойстик управления http://www.zerokol.com/2012/03/joystickview-custom-android-view-to.html на тот случай, если вы захотите его настроить. Я хочу сделать это, но у меня не было времени. сегодня я потратил целый день, чтобы написать этот пост.

Другие вопросы по тегам