Move ownership of native synth to service

This patch moves the ownership of the NDK synthesizer from an activity
(PianoActivity2) to a service, so that the same synthesizer may be
shared by multiple activities.

Note: this change repurposes an existing class that was used by the old
Java synthesizer. Those code paths will no longer work, and should be
aggressively deleted.
master
Raph Levien 11 years ago
parent c1a59d57dd
commit 9f1b753f81
  1. 5
      android/AndroidManifest.xml
  2. 139
      android/src/com/levien/synthesizer/android/service/SynthesizerService.java
  3. 158
      android/src/com/levien/synthesizer/android/ui/PianoActivity2.java

@ -74,12 +74,7 @@
android:name="com.levien.synthesizer.android.ui.KarplusStrongActivity" android:name="com.levien.synthesizer.android.ui.KarplusStrongActivity"
android:label="@string/karplus_strong" android:label="@string/karplus_strong"
android:screenOrientation="landscape" /> android:screenOrientation="landscape" />
<!-- Don't need the service if we're running native sound engine
<service android:name=".android.service.SynthesizerService"> <service android:name=".android.service.SynthesizerService">
<intent-filter>
<action android:name=".android.service.ISynthesizerService" />
</intent-filter>
</service> </service>
-->
</application> </application>
</manifest> </manifest>

@ -18,20 +18,23 @@ package com.levien.synthesizer.android.service;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.logging.Level; import java.util.ArrayList;
import java.util.logging.Logger; import java.util.List;
import android.annotation.TargetApi;
import android.app.Service; import android.app.Service;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Binder; import android.os.Binder;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import com.levien.synthesizer.R; import com.levien.synthesizer.R;
import com.levien.synthesizer.android.AndroidGlue;
import com.levien.synthesizer.core.midi.MidiListener;
import com.levien.synthesizer.core.model.composite.MultiChannelSynthesizer; import com.levien.synthesizer.core.model.composite.MultiChannelSynthesizer;
import com.levien.synthesizer.core.soundfont.SoundFontReader;
/** /**
* An Android Service that plays audio from a synthesizer. * An Android Service that plays audio from a synthesizer.
@ -47,14 +50,12 @@ public class SynthesizerService extends Service {
/** /**
* Gets the underlying synthesizer powering this service. * Gets the underlying synthesizer powering this service.
*
* Obsolete, to be deleted.
*/ */
public MultiChannelSynthesizer getSynthesizer() { public MultiChannelSynthesizer getSynthesizer() {
return SynthesizerService.this.synthesizer_; return null;
}
} }
public SynthesizerService() {
logger_ = Logger.getLogger(getClass().getName());
} }
/** /**
@ -62,36 +63,34 @@ public class SynthesizerService extends Service {
*/ */
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); Log.d("synth", "service onCreate");
if (androidGlue_ == null) {
// Get the native audio settings. AudioParams params = new AudioParams(44100, 64);
int sampleRateInHz = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC); // TODO: for pre-JB-MR1 devices, do some matching against known devices to
// For now, cap the sample rate to reduce cpu requirements. // get best audio parameters.
sampleRateInHz = Math.min(sampleRateInHz, 11025); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
SoundFontReader sampleLibrary = null; getJbMr1Params(params);
//InputStream sampleLibraryFile = getResources().openRawResource(R.raw.drums); }
//try { // Empirical testing shows better performance with small buffer size
// sampleLibrary = new SoundFontReader(sampleLibraryFile); // than actually matching the media server's reported buffer size.
//} catch (IOException e) { params.bufferSize = 64;
// logger_.log(Level.SEVERE, "Unable to load sample library.", e);
// sampleLibrary = null; androidGlue_ = new AndroidGlue();
//} androidGlue_.start(params.sampleRate, params.bufferSize);
synthesizer_ = new MultiChannelSynthesizer(CHANNELS, FINGERS, sampleRateInHz, sampleLibrary); InputStream patchIs = getResources().openRawResource(R.raw.rom1a);
byte[] patchData = new byte[4104];
// Load the presets from a file.
InputStream presetsFile = getResources().openRawResource(R.raw.presets);
try { try {
synthesizer_.loadLibraryFromText(presetsFile); patchIs.read(patchData);
androidGlue_.sendMidi(patchData);
patchNames_ = new ArrayList<String>();
for (int i = 0; i < 32; i++) {
patchNames_.add(new String(patchData, 124 + 128 * i, 10, "ISO-8859-1"));
}
} catch (IOException e) { } catch (IOException e) {
Log.e(getClass().getName(), "Unable to load presets from raw file.", e); Log.e(getClass().getName(), "loading patches failed");
} }
}
// Start a synthsizer thread playing the data. androidGlue_.setPlayState(true);
synthesizerThread_ = new SynthesizerThread(synthesizer_, sampleRateInHz);
synthesizerThread_.play();
// No Activities are yet bound to this Service.
referenceCount_ = 0;
} }
/** /**
@ -99,12 +98,7 @@ public class SynthesizerService extends Service {
*/ */
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); androidGlue_.setPlayState(false);
// Free up the underlying data structures.
synthesizerThread_.stop();
synthesizerThread_ = null;
synthesizer_ = null;
} }
/** /**
@ -112,39 +106,54 @@ public class SynthesizerService extends Service {
*/ */
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
++referenceCount_;
return binder_; return binder_;
} }
public MidiListener getMidiListener() {
return androidGlue_;
}
/** /**
* Run when any Activity unbinds from this Service. * Sends raw MIDI data to the synthesizer.
*
* @param buf MIDI bytes to send
*/ */
@Override public void sendRawMidi(byte[] buf) {
public boolean onUnbind(Intent intent) { androidGlue_.sendMidi(buf);
if (--referenceCount_ == 0) {
// No more Activities are using this Service, so kill it.
stopSelf();
}
return super.onUnbind(intent);
} }
// Binder to use for Activities in this process. public List<String> getPatchNames() {
private final IBinder binder_ = new LocalBinder(); return patchNames_;
}
// The module that provides the sampled audio data.
private MultiChannelSynthesizer synthesizer_;
// The thread that actually does the work of playing the audio data. class AudioParams {
private SynthesizerThread synthesizerThread_; AudioParams(int sr, int bs) {
confident = false;
sampleRate = sr;
bufferSize = bs;
}
public String toString() {
return "sampleRate=" + sampleRate + " bufferSize=" + bufferSize;
}
boolean confident;
int sampleRate;
int bufferSize;
}
// How many Activities are currently bound to this Service. @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private int referenceCount_; void getJbMr1Params(AudioParams params) {
AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
String sr = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
String bs = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
params.confident = true;
params.sampleRate = Integer.parseInt(sr);
params.bufferSize = Integer.parseInt(bs);
}
// The number of channels the synthesizer supports. // Binder to use for Activities in this process.
private static final int CHANNELS = 8; private final IBinder binder_ = new LocalBinder();
// The number of fingers the synthesizer supports. private static AndroidGlue androidGlue_;
private static final int FINGERS = 5;
private Logger logger_; private static List<String> patchNames_;
} }

@ -16,40 +16,35 @@
package com.levien.synthesizer.android.ui; package com.levien.synthesizer.android.ui;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface; import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager; import android.hardware.usb.UsbManager;
import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener; import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView;
import com.levien.synthesizer.R; import com.levien.synthesizer.R;
import com.levien.synthesizer.android.AndroidGlue; import com.levien.synthesizer.android.service.SynthesizerService;
import com.levien.synthesizer.android.stats.JitterStats;
import com.levien.synthesizer.android.usb.UsbMidiDevice; import com.levien.synthesizer.android.usb.UsbMidiDevice;
import com.levien.synthesizer.android.widgets.knob.KnobListener; import com.levien.synthesizer.android.widgets.knob.KnobListener;
import com.levien.synthesizer.android.widgets.knob.KnobView; import com.levien.synthesizer.android.widgets.knob.KnobView;
@ -75,38 +70,10 @@ public class PianoActivity2 extends Activity {
overdriveKnob_ = (KnobView)findViewById(R.id.overdriveKnob); overdriveKnob_ = (KnobView)findViewById(R.id.overdriveKnob);
presetSpinner_ = (Spinner)findViewById(R.id.presetSpinner); presetSpinner_ = (Spinner)findViewById(R.id.presetSpinner);
AudioParams params = new AudioParams(44100, 64);
// TODO: for pre-JB-MR1 devices, do some matching against known devices to
// get best audio parameters.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
getJbMr1Params(params);
}
// Empirical testing shows better performance with small buffer size
// than actually matching the media server's reported buffer size.
params.bufferSize = 64;
androidGlue_ = new AndroidGlue();
androidGlue_.start(params.sampleRate, params.bufferSize);
InputStream patchIs = getResources().openRawResource(R.raw.rom1a);
byte[] patchData = new byte[4104];
try {
patchIs.read(patchData);
androidGlue_.sendMidi(patchData);
ArrayList<String> patchNames = new ArrayList<String>();
for (int i = 0; i < 32; i++) {
patchNames.add(new String(patchData, 124 + 128 * i, 10, "ISO-8859-1"));
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
this, android.R.layout.simple_spinner_item, patchNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
presetSpinner_.setAdapter(adapter);
} catch (IOException e) {
Log.e(getClass().getName(), "loading patches failed");
}
presetSpinner_.setOnItemSelectedListener(new OnItemSelectedListener() { presetSpinner_.setOnItemSelectedListener(new OnItemSelectedListener() {
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
androidGlue_.sendMidi(new byte[] {(byte)0xc0, (byte)position}); synthesizerService_.getMidiListener().onProgramChange(0, position);
sendMidiBytes(new byte[] {(byte)0xc0, (byte)position});
} }
public void onNothingSelected(AdapterView<?> parent) { public void onNothingSelected(AdapterView<?> parent) {
} }
@ -115,69 +82,23 @@ public class PianoActivity2 extends Activity {
cutoffKnob_.setKnobListener(new KnobListener() { cutoffKnob_.setKnobListener(new KnobListener() {
public void onKnobChanged(double newValue) { public void onKnobChanged(double newValue) {
int value = (int)Math.round(newValue * 127); int value = (int)Math.round(newValue * 127);
androidGlue_.onController(0, 1, value); synthesizerService_.getMidiListener().onController(0, 1, value);
} }
}); });
resonanceKnob_.setKnobListener(new KnobListener() { resonanceKnob_.setKnobListener(new KnobListener() {
public void onKnobChanged(double newValue) { public void onKnobChanged(double newValue) {
int value = (int)Math.round(newValue * 127); int value = (int)Math.round(newValue * 127);
androidGlue_.onController(0, 2, value); synthesizerService_.getMidiListener().onController(0, 2, value);
} }
}); });
overdriveKnob_.setKnobListener(new KnobListener() { overdriveKnob_.setKnobListener(new KnobListener() {
public void onKnobChanged(double newValue) { public void onKnobChanged(double newValue) {
int value = (int)Math.round(newValue * 127); int value = (int)Math.round(newValue * 127);
androidGlue_.onController(0, 3, value); synthesizerService_.getMidiListener().onController(0, 3, value);
} }
}); });
piano_.bindTo(androidGlue_); //piano_.bindTo(synthesizerService_.getMidiListener());
final boolean doStats = false;
if (doStats) {
jitterStats_ = new JitterStats();
jitterStats_.setNominalCb(params.bufferSize / (double)params.sampleRate);
statusHandler_ = new Handler();
statusRunnable_ = new Runnable() {
public void run() {
int n = androidGlue_.statsBytesAvailable();
if (n > 0) {
byte[] buf = new byte[n];
androidGlue_.readStatsBytes(buf, 0, n);
jitterStats_.aggregate(buf);
TextView statusTextView = (TextView)findViewById(R.id.status);
statusTextView.setText(jitterStats_.report());
}
statusHandler_.postDelayed(statusRunnable_, 100);
}
};
statusRunnable_.run();
}
// Create burst of load -- test code to be removed. Ultimately we'll
// be able to get this kind of functionality by hooking up the sequencer
if (false) {
new Handler().postDelayed(new Runnable() {
public void run() {
int n = 110;
byte[] midi = new byte[n * 3];
for (int i = 0; i < n; i++) {
midi[i * 3] = (byte)0x90;
midi[i * 3 + 1] = (byte)(1 + i);
midi[i * 3 + 2] = 64;
}
androidGlue_.sendMidi(midi);
}
}, 10000);
}
Button captureButton = (Button) findViewById(R.id.capture);
captureButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
TextView stats = (TextView) findViewById(R.id.stats);
stats.setText(jitterStats_.reportLong());
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
setupUsbMidi(getIntent()); setupUsbMidi(getIntent());
@ -187,7 +108,6 @@ public class PianoActivity2 extends Activity {
@Override @Override
protected void onDestroy() { protected void onDestroy() {
Log.d("synth", "activity onDestroy"); Log.d("synth", "activity onDestroy");
androidGlue_.shutdown();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
unregisterReceiver(usbReceiver_); unregisterReceiver(usbReceiver_);
} }
@ -197,7 +117,6 @@ public class PianoActivity2 extends Activity {
@Override @Override
protected void onPause() { protected void onPause() {
Log.d("synth", "activity onPause"); Log.d("synth", "activity onPause");
androidGlue_.setPlayState(false);
setMidiInterface(null, null); setMidiInterface(null, null);
super.onPause(); super.onPause();
} }
@ -205,13 +124,25 @@ public class PianoActivity2 extends Activity {
@Override @Override
protected void onResume() { protected void onResume() {
Log.d("synth", "activity onResume " + getIntent()); Log.d("synth", "activity onResume " + getIntent());
androidGlue_.setPlayState(true);
if (usbDevice_ != null && usbMidiConnection_ == null) { if (usbDevice_ != null && usbMidiConnection_ == null) {
connectUsbMidi(usbDevice_); connectUsbMidi(usbDevice_);
} }
super.onResume(); super.onResume();
} }
@Override
protected void onStart() {
super.onStart();
bindService(new Intent(this, SynthesizerService.class),
synthesizerConnection_, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
unbindService(synthesizerConnection_);
}
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
Log.d("synth", "activity onNewIntent " + intent); Log.d("synth", "activity onNewIntent " + intent);
@ -356,43 +287,32 @@ public class PianoActivity2 extends Activity {
public void sendMidiBytes(byte[] buf) { public void sendMidiBytes(byte[] buf) {
// TODO: in future we'll want to reflect MIDI to UI (knobs turn, keys press) // TODO: in future we'll want to reflect MIDI to UI (knobs turn, keys press)
androidGlue_.sendMidi(buf); synthesizerService_.sendRawMidi(buf);
} }
class AudioParams { private ServiceConnection synthesizerConnection_ = new ServiceConnection() {
AudioParams(int sr, int bs) { public void onServiceConnected(ComponentName className, IBinder service) {
confident = false; SynthesizerService.LocalBinder binder = (SynthesizerService.LocalBinder)service;
sampleRate = sr; synthesizerService_ = binder.getService();
bufferSize = bs; piano_.bindTo(synthesizerService_.getMidiListener());
} List<String> patchNames = synthesizerService_.getPatchNames();
public String toString() { ArrayAdapter<String> adapter = new ArrayAdapter<String>(
return "sampleRate=" + sampleRate + " bufferSize=" + bufferSize; PianoActivity2.this, android.R.layout.simple_spinner_item, patchNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
presetSpinner_.setAdapter(adapter);
} }
boolean confident; public void onServiceDisconnected(ComponentName className) {
int sampleRate; synthesizerService_ = null;
int bufferSize;
} }
};
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private SynthesizerService synthesizerService_;
void getJbMr1Params(AudioParams params) {
AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
String sr = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
String bs = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
params.confident = true;
params.sampleRate = Integer.parseInt(sr);
params.bufferSize = Integer.parseInt(bs);
//log("from platform: " + params);
}
private AndroidGlue androidGlue_;
private PianoView piano_; private PianoView piano_;
private KnobView cutoffKnob_; private KnobView cutoffKnob_;
private KnobView resonanceKnob_; private KnobView resonanceKnob_;
private KnobView overdriveKnob_; private KnobView overdriveKnob_;
private Spinner presetSpinner_; private Spinner presetSpinner_;
private Handler statusHandler_;
private Runnable statusRunnable_;
private JitterStats jitterStats_;
private UsbDevice usbDevice_; private UsbDevice usbDevice_;
private UsbDeviceConnection usbMidiConnection_; private UsbDeviceConnection usbMidiConnection_;
private UsbMidiDevice usbMidiDevice_; private UsbMidiDevice usbMidiDevice_;

Loading…
Cancel
Save