android: Implement emulator audio playback

Former-commit-id: 5d0daee9e7616212aa184e31be360b0110b4c054
This commit is contained in:
Michel Heily 2020-02-28 18:37:59 +02:00
parent 40360f7839
commit b270265694
9 changed files with 110 additions and 36 deletions

View file

@ -45,6 +45,13 @@ public class EmulatorBindings {
*/ */
public static native void runFrame(long ctx, int[] frame_buffer); public static native void runFrame(long ctx, int[] frame_buffer);
/**
* Collect pending audio samples
* @param ctx
* @return sample buffer
*/
public static native short[] collectAudioSamples(long ctx);
/** /**
* @param ctx * @param ctx
* @return The loaded ROM title * @return The loaded ROM title

View file

@ -1,12 +0,0 @@
package com.mrmichel.rustboyadvance;
import java.io.IOException;
import java.io.InputStream;
public class AudioStream extends InputStream {
@Override
public int read(byte[] b) throws IOException {
return super.read(b);
}
}

View file

@ -0,0 +1,47 @@
package com.mrmichel.rustdroid_emu.core;
import android.media.AudioTrack;
public class AudioThread extends Thread {
AudioTrack audioTrack;
Emulator emulator;
boolean enabled;
boolean stopping;
public AudioThread(AudioTrack audioTrack, Emulator emulator) {
super();
this.audioTrack = audioTrack;
this.emulator = emulator;
this.enabled = true;
this.stopping = false;
}
public void setStopping(boolean stopping) {
this.stopping = stopping;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isStopping() {
return stopping;
}
public boolean isEnabled() {
return enabled;
}
@Override
public void run() {
super.run();
while (!stopping) {
if (enabled) {
short[] samples = emulator.collectAudioSamples();
audioTrack.write(samples, 0, samples.length);
}
}
}
}

View file

@ -1,7 +1,5 @@
package com.mrmichel.rustdroid_emu.core; package com.mrmichel.rustdroid_emu.core;
import android.graphics.Bitmap;
import com.mrmichel.rustboyadvance.EmulatorBindings; import com.mrmichel.rustboyadvance.EmulatorBindings;
public class Emulator { public class Emulator {
@ -32,8 +30,12 @@ public class Emulator {
} }
public synchronized void runFrame() { public synchronized void runFrame() {
EmulatorBindings.setKeyState(this.ctx, this.keypad.getKeyState()); EmulatorBindings.setKeyState(ctx, keypad.getKeyState());
EmulatorBindings.runFrame(this.ctx, this.frameBuffer); EmulatorBindings.runFrame(ctx, frameBuffer);
}
public synchronized short[] collectAudioSamples() {
return EmulatorBindings.collectAudioSamples(ctx);
} }
public synchronized void setKeyState(int keyState) { public synchronized void setKeyState(int keyState) {

View file

@ -2,7 +2,6 @@ package com.mrmichel.rustdroid_emu.core;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
public class Snapshot { public class Snapshot {

View file

@ -9,13 +9,10 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.util.Log; import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.sql.Timestamp; import java.sql.Timestamp;

View file

@ -1,10 +1,13 @@
package com.mrmichel.rustdroid_emu.ui; package com.mrmichel.rustdroid_emu.ui;
import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -13,9 +16,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import android.util.Log; import android.util.Log;
import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
@ -25,10 +26,10 @@ import android.widget.Switch;
import android.widget.Toast; import android.widget.Toast;
import com.mrmichel.rustboyadvance.EmulatorBindings; import com.mrmichel.rustboyadvance.EmulatorBindings;
import com.mrmichel.rustdroid_emu.core.AudioThread;
import com.mrmichel.rustdroid_emu.core.Emulator; import com.mrmichel.rustdroid_emu.core.Emulator;
import com.mrmichel.rustdroid_emu.core.Keypad; import com.mrmichel.rustdroid_emu.core.Keypad;
import com.mrmichel.rustdroid_emu.R; import com.mrmichel.rustdroid_emu.R;
import com.mrmichel.rustdroid_emu.core.Snapshot;
import com.mrmichel.rustdroid_emu.core.SnapshotManager; import com.mrmichel.rustdroid_emu.core.SnapshotManager;
import com.mrmichel.rustdroid_emu.ui.snapshots.ChosenSnapshot; import com.mrmichel.rustdroid_emu.ui.snapshots.ChosenSnapshot;
import com.mrmichel.rustdroid_emu.ui.snapshots.SnapshotActivity; import com.mrmichel.rustdroid_emu.ui.snapshots.SnapshotActivity;
@ -39,13 +40,18 @@ import java.io.InputStream;
public class EmulatorActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { public class EmulatorActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
private static final String TAG = "EmulatorActivty"; private static final String TAG = "EmulatorActivty";
private static final int LOAD_ROM_REQUESTCODE = 123; private static final int LOAD_ROM_REQUESTCODE = 123;
private static final int LOAD_SNAPSHOT_REQUESTCODE = 124; private static final int LOAD_SNAPSHOT_REQUESTCODE = 124;
private static int SAMPLE_RATE_HZ = 44100;
private byte[] bios; private byte[] bios;
private Emulator emulator = null; private Emulator emulator = null;
private EmulationRunnable runnable; private EmulationRunnable runnable;
private Thread emulationThread; private Thread emulationThread;
private AudioThread audioThread;
private AudioTrack audioTrack;
private byte[] on_resume_saved_state = null; private byte[] on_resume_saved_state = null;
private ImageView screen; private ImageView screen;
private boolean turboMode = false; private boolean turboMode = false;
@ -169,6 +175,16 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
} }
emulationThread = null; emulationThread = null;
} }
if (audioThread != null) {
audioThread.setStopping(true);
try {
audioThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "audio thread join interrupted");
};
audioThread = null;
}
if (emulator.isOpen()) { if (emulator.isOpen()) {
emulator.close(); emulator.close();
} }
@ -193,6 +209,9 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
runnable = new EmulationRunnable(this.emulator, this); runnable = new EmulationRunnable(this.emulator, this);
emulationThread = new Thread(runnable); emulationThread = new Thread(runnable);
emulationThread.start(); emulationThread.start();
audioThread = new AudioThread(audioTrack, emulator);
audioThread.start();
} }
public void loadRomButton(View v) { public void loadRomButton(View v) {
@ -216,6 +235,31 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
this.bios = getIntent().getByteArrayExtra("bios"); this.bios = getIntent().getByteArrayExtra("bios");
this.screen = findViewById(R.id.gbaMockImageView); this.screen = findViewById(R.id.gbaMockImageView);
this.emulator = new Emulator(); this.emulator = new Emulator();
if (Build.VERSION.SDK_INT >= 23) {
AudioTrack.Builder audioTrackBuilder = new AudioTrack.Builder()
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(SAMPLE_RATE_HZ)
.setChannelMask(AudioFormat.CHANNEL_IN_STEREO | AudioFormat.CHANNEL_OUT_STEREO)
.build()
)
.setBufferSizeInBytes(4096)
.setTransferMode(AudioTrack.MODE_STREAM);
if (Build.VERSION.SDK_INT >= 26) {
audioTrackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
}
this.audioTrack = audioTrackBuilder.build();
} else {
this.audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
SAMPLE_RATE_HZ,
AudioFormat.CHANNEL_IN_STEREO | AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.ENCODING_PCM_16BIT,
4096,
AudioTrack.MODE_STREAM);
}
this.audioTrack.play();
} }
@Override @Override
@ -233,6 +277,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
audioTrack.stop();
if (emulator.isOpen()) { if (emulator.isOpen()) {
if (runnable != null) { if (runnable != null) {
runnable.pauseEmulation(); runnable.pauseEmulation();
@ -261,6 +306,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
runnable.resumeEmulation(); runnable.resumeEmulation();
} }
} }
audioTrack.play();
} }
public void onSaveSnapshot(View v) { public void onSaveSnapshot(View v) {

View file

@ -1,22 +1,13 @@
package com.mrmichel.rustdroid_emu.ui.snapshots; package com.mrmichel.rustdroid_emu.ui.snapshots;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.GridView; import android.widget.GridView;
import android.widget.ListView;
import android.widget.Toast; import android.widget.Toast;
import com.mrmichel.rustdroid_emu.R; import com.mrmichel.rustdroid_emu.R;

View file

@ -5,7 +5,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@ -15,8 +14,6 @@ import androidx.annotation.Nullable;
import com.mrmichel.rustdroid_emu.R; import com.mrmichel.rustdroid_emu.R;
import com.mrmichel.rustdroid_emu.core.Snapshot; import com.mrmichel.rustdroid_emu.core.Snapshot;
import org.w3c.dom.Text;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.ArrayList; import java.util.ArrayList;