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. 149
      android/src/com/levien/synthesizer/android/service/SynthesizerService.java
  3. 168
      android/src/com/levien/synthesizer/android/ui/PianoActivity2.java

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

@ -1,12 +1,12 @@
/*
* Copyright 2010 Google Inc.
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -18,20 +18,23 @@ package com.levien.synthesizer.android.service;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.ArrayList;
import java.util.List;
import android.annotation.TargetApi;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
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.soundfont.SoundFontReader;
/**
* An Android Service that plays audio from a synthesizer.
@ -47,51 +50,47 @@ public class SynthesizerService extends Service {
/**
* Gets the underlying synthesizer powering this service.
*
* Obsolete, to be deleted.
*/
public MultiChannelSynthesizer getSynthesizer() {
return SynthesizerService.this.synthesizer_;
return null;
}
}
public SynthesizerService() {
logger_ = Logger.getLogger(getClass().getName());
}
/**
* Run when the Service is first created.
*/
@Override
public void onCreate() {
super.onCreate();
// Get the native audio settings.
int sampleRateInHz = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
// For now, cap the sample rate to reduce cpu requirements.
sampleRateInHz = Math.min(sampleRateInHz, 11025);
SoundFontReader sampleLibrary = null;
//InputStream sampleLibraryFile = getResources().openRawResource(R.raw.drums);
//try {
// sampleLibrary = new SoundFontReader(sampleLibraryFile);
//} catch (IOException e) {
// logger_.log(Level.SEVERE, "Unable to load sample library.", e);
// sampleLibrary = null;
//}
synthesizer_ = new MultiChannelSynthesizer(CHANNELS, FINGERS, sampleRateInHz, sampleLibrary);
// Load the presets from a file.
InputStream presetsFile = getResources().openRawResource(R.raw.presets);
try {
synthesizer_.loadLibraryFromText(presetsFile);
} catch (IOException e) {
Log.e(getClass().getName(), "Unable to load presets from raw file.", e);
Log.d("synth", "service onCreate");
if (androidGlue_ == null) {
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);
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) {
Log.e(getClass().getName(), "loading patches failed");
}
}
// Start a synthsizer thread playing the data.
synthesizerThread_ = new SynthesizerThread(synthesizer_, sampleRateInHz);
synthesizerThread_.play();
// No Activities are yet bound to this Service.
referenceCount_ = 0;
androidGlue_.setPlayState(true);
}
/**
@ -99,12 +98,7 @@ public class SynthesizerService extends Service {
*/
@Override
public void onDestroy() {
super.onDestroy();
// Free up the underlying data structures.
synthesizerThread_.stop();
synthesizerThread_ = null;
synthesizer_ = null;
androidGlue_.setPlayState(false);
}
/**
@ -112,39 +106,54 @@ public class SynthesizerService extends Service {
*/
@Override
public IBinder onBind(Intent intent) {
++referenceCount_;
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 boolean onUnbind(Intent intent) {
if (--referenceCount_ == 0) {
// No more Activities are using this Service, so kill it.
stopSelf();
}
return super.onUnbind(intent);
public void sendRawMidi(byte[] buf) {
androidGlue_.sendMidi(buf);
}
// Binder to use for Activities in this process.
private final IBinder binder_ = new LocalBinder();
// The module that provides the sampled audio data.
private MultiChannelSynthesizer synthesizer_;
public List<String> getPatchNames() {
return patchNames_;
}
// The thread that actually does the work of playing the audio data.
private SynthesizerThread synthesizerThread_;
class AudioParams {
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.
private int referenceCount_;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
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.
private static final int CHANNELS = 8;
// Binder to use for Activities in this process.
private final IBinder binder_ = new LocalBinder();
// The number of fingers the synthesizer supports.
private static final int FINGERS = 5;
private static AndroidGlue androidGlue_;
private Logger logger_;
private static List<String> patchNames_;
}

@ -1,12 +1,12 @@
/*
* Copyright 2012 Google Inc.
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -16,40 +16,35 @@
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.List;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.TextView;
import com.levien.synthesizer.R;
import com.levien.synthesizer.android.AndroidGlue;
import com.levien.synthesizer.android.stats.JitterStats;
import com.levien.synthesizer.android.service.SynthesizerService;
import com.levien.synthesizer.android.usb.UsbMidiDevice;
import com.levien.synthesizer.android.widgets.knob.KnobListener;
import com.levien.synthesizer.android.widgets.knob.KnobView;
@ -75,109 +70,35 @@ public class PianoActivity2 extends Activity {
overdriveKnob_ = (KnobView)findViewById(R.id.overdriveKnob);
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() {
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) {
}
});
cutoffKnob_.setKnobListener(new KnobListener() {
public void onKnobChanged(double newValue) {
int value = (int)Math.round(newValue * 127);
androidGlue_.onController(0, 1, value);
synthesizerService_.getMidiListener().onController(0, 1, value);
}
});
resonanceKnob_.setKnobListener(new KnobListener() {
public void onKnobChanged(double newValue) {
int value = (int)Math.round(newValue * 127);
androidGlue_.onController(0, 2, value);
synthesizerService_.getMidiListener().onController(0, 2, value);
}
});
overdriveKnob_.setKnobListener(new KnobListener() {
public void onKnobChanged(double newValue) {
int value = (int)Math.round(newValue * 127);
androidGlue_.onController(0, 3, value);
synthesizerService_.getMidiListener().onController(0, 3, value);
}
});
piano_.bindTo(androidGlue_);
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());
}
});
//piano_.bindTo(synthesizerService_.getMidiListener());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
setupUsbMidi(getIntent());
@ -187,17 +108,15 @@ public class PianoActivity2 extends Activity {
@Override
protected void onDestroy() {
Log.d("synth", "activity onDestroy");
androidGlue_.shutdown();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
unregisterReceiver(usbReceiver_);
}
super.onDestroy();
}
@Override
protected void onPause() {
Log.d("synth", "activity onPause");
androidGlue_.setPlayState(false);
setMidiInterface(null, null);
super.onPause();
}
@ -205,13 +124,25 @@ public class PianoActivity2 extends Activity {
@Override
protected void onResume() {
Log.d("synth", "activity onResume " + getIntent());
androidGlue_.setPlayState(true);
if (usbDevice_ != null && usbMidiConnection_ == null) {
connectUsbMidi(usbDevice_);
}
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
protected void onNewIntent(Intent intent) {
Log.d("synth", "activity onNewIntent " + intent);
@ -356,43 +287,32 @@ public class PianoActivity2 extends Activity {
public void sendMidiBytes(byte[] buf) {
// TODO: in future we'll want to reflect MIDI to UI (knobs turn, keys press)
androidGlue_.sendMidi(buf);
synthesizerService_.sendRawMidi(buf);
}
class AudioParams {
AudioParams(int sr, int bs) {
confident = false;
sampleRate = sr;
bufferSize = bs;
private ServiceConnection synthesizerConnection_ = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
SynthesizerService.LocalBinder binder = (SynthesizerService.LocalBinder)service;
synthesizerService_ = binder.getService();
piano_.bindTo(synthesizerService_.getMidiListener());
List<String> patchNames = synthesizerService_.getPatchNames();
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
PianoActivity2.this, android.R.layout.simple_spinner_item, patchNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
presetSpinner_.setAdapter(adapter);
}
public String toString() {
return "sampleRate=" + sampleRate + " bufferSize=" + bufferSize;
public void onServiceDisconnected(ComponentName className) {
synthesizerService_ = null;
}
boolean confident;
int sampleRate;
int bufferSize;
}
};
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
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 SynthesizerService synthesizerService_;
private AndroidGlue androidGlue_;
private PianoView piano_;
private KnobView cutoffKnob_;
private KnobView resonanceKnob_;
private KnobView overdriveKnob_;
private Spinner presetSpinner_;
private Handler statusHandler_;
private Runnable statusRunnable_;
private JitterStats jitterStats_;
private UsbDevice usbDevice_;
private UsbDeviceConnection usbMidiConnection_;
private UsbMidiDevice usbMidiDevice_;

Loading…
Cancel
Save