使用 Android 播放任意的音调

有没有办法让 Android 发出任意频率的声音(意思是,我不想有预先录制的声音文件) ?

我环顾四周,音调发生器是我能找到的唯一的东西,甚至更接近,但它似乎只能输出标准的 DTMF 音调。

有什么想法吗?

95821 次浏览

There are several programs for this, but they suck. I measured a few:

http://www.endolith.com/wordpress/2009/11/24/android-audio-applications/

So don't do whatever they do. :D

I originally found this example code on a blog, but it had some bugs in it that generated some horrendous sounds. I've fixed the bugs and posted the resulting code here. Seems to work well for me!

public class PlaySound extends Activity {
// originally from http://marblemice.blogspot.com/2010/04/generate-and-play-tone-in-android.html
// and modified by Steve Pomeroy <steve@staticfree.info>
private final int duration = 3; // seconds
private final int sampleRate = 8000;
private final int numSamples = duration * sampleRate;
private final double sample[] = new double[numSamples];
private final double freqOfTone = 440; // hz


private final byte generatedSnd[] = new byte[2 * numSamples];


Handler handler = new Handler();


@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}


@Override
protected void onResume() {
super.onResume();


// Use a new tread as this can take a while
final Thread thread = new Thread(new Runnable() {
public void run() {
genTone();
handler.post(new Runnable() {


public void run() {
playSound();
}
});
}
});
thread.start();
}


void genTone(){
// fill out the array
for (int i = 0; i < numSamples; ++i) {
sample[i] = Math.sin(2 * Math.PI * i / (sampleRate/freqOfTone));
}


// convert to 16 bit pcm sound array
// assumes the sample buffer is normalised.
int idx = 0;
for (final double dVal : sample) {
// scale to maximum amplitude
final short val = (short) ((dVal * 32767));
// in 16 bit wav PCM, first byte is the low order byte
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);


}
}


void playSound(){
final AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, generatedSnd.length,
AudioTrack.MODE_STATIC);
audioTrack.write(generatedSnd, 0, generatedSnd.length);
audioTrack.play();
}
}

Improving on the above code:

Add amplitude ramp up and ramp down to avoid the clicks.

Add code to determine when the tack has finished playing.

double duration = 1;            // seconds
double freqOfTone = 1000;       // hz
int sampleRate = 8000;          // a number


double dnumSamples = duration * sampleRate;
dnumSamples = Math.ceil(dnumSamples);
int numSamples = (int) dnumSamples;
double sample[] = new double[numSamples];
byte generatedSnd[] = new byte[2 * numSamples];




for (int i = 0; i < numSamples; ++i) {    // Fill the sample array
sample[i] = Math.sin(freqOfTone * 2 * Math.PI * i / (sampleRate));
}


// convert to 16 bit pcm sound array
// assumes the sample buffer is normalized.
// convert to 16 bit pcm sound array
// assumes the sample buffer is normalised.
int idx = 0;
int i = 0 ;


int ramp = numSamples / 20 ;                                     // Amplitude ramp as a percent of sample count




for (i = 0; i< ramp; ++i) {                                      // Ramp amplitude up (to avoid clicks)
double dVal = sample[i];
// Ramp up to maximum
final short val = (short) ((dVal * 32767 * i/ramp));
// in 16 bit wav PCM, first byte is the low order byte
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
}




for (i = i; i< numSamples - ramp; ++i) {                         // Max amplitude for most of the samples
double dVal = sample[i];
// scale to maximum amplitude
final short val = (short) ((dVal * 32767));
// in 16 bit wav PCM, first byte is the low order byte
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
}


for (i = i; i< numSamples; ++i) {                                // Ramp amplitude down
double dVal = sample[i];
// Ramp down to zero
final short val = (short) ((dVal * 32767 * (numSamples-i)/ramp ));
// in 16 bit wav PCM, first byte is the low order byte
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
}


AudioTrack audioTrack = null;                                    // Get audio track
try {
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT, (int)numSamples*2,
AudioTrack.MODE_STATIC);
audioTrack.write(generatedSnd, 0, generatedSnd.length);        // Load the track
audioTrack.play();                                             // Play the track
}
catch (Exception e){
RunTimeError("Error: " + e);
return false;
}


int x =0;
do{                                                              // Monitor playback to find when done
if (audioTrack != null)
x = audioTrack.getPlaybackHeadPosition();
else
x = numSamples;
} while (x<numSamples);


if (audioTrack != null) audioTrack.release();                    // Track play done. Release track.
    float synth_frequency = 440;
int minSize = AudioTrack.getMinBufferSize(SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT);
AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
minSize,
AudioTrack.MODE_STREAM);
audioTrack.play();
short[] buffer = new short[minSize];
float angle = 0;
while (true)
{
if (play)
{
for (int i = 0; i < buffer.length; i++)
{
float angular_frequency =
(float)(2*Math.PI) * synth_frequency / SAMPLE_RATE;
buffer[i] = (short)(Short.MAX_VALUE * ((float) Math.sin(angle)));
angle += angular_frequency;
}
audioTrack.write(buffer, 0, buffer.length);
}

// You can add arbitrary value in synth_frequency to get change sound for example you can add random variable to get sound

Here's another blog demoing a simple synth plus some UI

http://audioprograming.wordpress.com/2012/10/18/a-simple-synth-in-android-step-by-step-guide-using-the-java-sdk/

You might also be interested in csound or pdlib (pure data lib) for android.

Since there is a bug in some older android versions that causes a memory leak when using MODE_STATIC, I modified Xarph's answer above to use MODE_STREAM. Hopefully it will help some.

public void playTone(double freqOfTone, double duration) {
//double duration = 1000;                // seconds
//   double freqOfTone = 1000;           // hz
int sampleRate = 8000;              // a number


double dnumSamples = duration * sampleRate;
dnumSamples = Math.ceil(dnumSamples);
int numSamples = (int) dnumSamples;
double sample[] = new double[numSamples];
byte generatedSnd[] = new byte[2 * numSamples];




for (int i = 0; i < numSamples; ++i) {      // Fill the sample array
sample[i] = Math.sin(freqOfTone * 2 * Math.PI * i / (sampleRate));
}


// convert to 16 bit pcm sound array
// assumes the sample buffer is normalized.
// convert to 16 bit pcm sound array
// assumes the sample buffer is normalised.
int idx = 0;
int i = 0 ;


int ramp = numSamples / 20 ;                                    // Amplitude ramp as a percent of sample count




for (i = 0; i< ramp; ++i) {                                     // Ramp amplitude up (to avoid clicks)
double dVal = sample[i];
// Ramp up to maximum
final short val = (short) ((dVal * 32767 * i/ramp));
// in 16 bit wav PCM, first byte is the low order byte
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
}




for (i = i; i< numSamples - ramp; ++i) {                        // Max amplitude for most of the samples
double dVal = sample[i];
// scale to maximum amplitude
final short val = (short) ((dVal * 32767));
// in 16 bit wav PCM, first byte is the low order byte
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
}


for (i = i; i< numSamples; ++i) {                               // Ramp amplitude down
double dVal = sample[i];
// Ramp down to zero
final short val = (short) ((dVal * 32767 * (numSamples-i)/ramp ));
// in 16 bit wav PCM, first byte is the low order byte
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
}


AudioTrack audioTrack = null;                                   // Get audio track
try {
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, bufferSize,
AudioTrack.MODE_STREAM);
audioTrack.play();                                          // Play the track
audioTrack.write(generatedSnd, 0, generatedSnd.length);     // Load the track
}
catch (Exception e){
}
if (audioTrack != null) audioTrack.release();           // Track play done. Release track.
}

Modified Code Based on Singhaks' answer

public class MainActivity extends Activity {
private final int duration = 30; // seconds
private final int sampleRate = 8000;
private final int numSamples = duration * sampleRate;
private final double sample[] = new double[numSamples];
private final double freqOfTone = 440; // hz
private final byte generatedSnd[] = new byte[2 * numSamples];
Handler handler = new Handler();
private AudioTrack audioTrack;
private boolean play = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
8000, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, numSamples,
AudioTrack.MODE_STREAM);
}


@Override
protected void onResume() {
super.onResume();


// Use a new tread as this can take a while
Thread thread = new Thread(new Runnable() {
public void run() {


handler.post(new Runnable() {


public void run() {
playSound();
genTone();
}
});
}
});
thread.start();
}


void genTone(){
// fill out the array
while(play){
for (int i = 0; i < numSamples; ++i) {
//  float angular_frequency =
sample[i] = Math.sin(2 * Math.PI * i / (sampleRate/freqOfTone));
}
int idx = 0;


// convert to 16 bit pcm sound array
// assumes the sample buffer is normalised.
for (double dVal : sample) {
short val = (short) (dVal * 32767);
generatedSnd[idx++] = (byte) (val & 0x00ff);
generatedSnd[idx++] = (byte) ((val & 0xff00) >>> 8);
}
audioTrack.write(generatedSnd, 0, numSamples);
}
}




void playSound(){
play = true;
audioTrack.play();
}
}

I wrapped the above wonderful solutions into a neat little package that's more useable out of the box as a simple configurable buzzer. It runs it in a background thread and has stop and play methods and a handful of options you can set.

It's up on JCenter so you can add it to your dependencies list like this

compile 'net.mabboud:android-tone-player:0.2'

and you use it like this for a continuous buzzer

ContinuousBuzzer tonePlayer = new ContinuousBuzzer();
tonePlayer.play();


// just an example don't actually use Thread.sleep in your app
Thread.sleep(1000);
tonePlayer.stop();

or a buzzer played only once and you can set frequency and volume like this

OneTimeBuzzer buzzer = new OneTimeBuzzer();
buzzer.setDuration(5);


// volume values are from 0-100
buzzer.setVolume(50);
buzzer.setToneFreqInHz(110);

Extended blog post here about it here GitHub here

Do major (16 notes)

 public class MainActivity extends AppCompatActivity {


private double mInterval = 0.125;
private int mSampleRate = 8000;
private byte[] generatedSnd;


private final double mStandardFreq = 440;


Handler handler = new Handler();
private AudioTrack audioTrack;




@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}


@Override
protected void onResume() {
super.onResume();


// Use a new tread as this can take a while
final Thread thread = new Thread(new Runnable() {
public void run() {


byte[] tempByte = new byte[0];
for (int i = 0; i < 16 ; i++ ){
double note = getNoteFrequencies(i);
byte[] tonByteNote = getTone(mInterval, mSampleRate, note);
tempByte = concat(tonByteNote, tempByte);
}
generatedSnd = tempByte;


handler.post(new Runnable() {
public void run() {
playTrack(generatedSnd);
}
});
}
});
thread.start();
}


public byte[] concat(byte[] a, byte[] b) {
int aLen = a.length;
int bLen = b.length;
byte[] c= new byte[aLen+bLen];
System.arraycopy(a, 0, c, 0, aLen);
System.arraycopy(b, 0, c, aLen, bLen);
return c;
}


private double getNoteFrequencies(int index){
return mStandardFreq * Math.pow(2, (double) index/12.0d);
}


private byte[] getTone(double duration, int rate, double frequencies){


int maxLength = (int)(duration * rate);
byte generatedTone[] = new byte[2 * maxLength];


double[] sample = new double[maxLength];
int idx = 0;


for (int x = 0; x < maxLength; x++){
sample[x] = sine(x, frequencies / rate);
}




for (final double dVal : sample) {


final short val = (short) ((dVal * 32767));


// in 16 bit wav PCM, first byte is the low order byte
generatedTone[idx++] = (byte) (val & 0x00ff);
generatedTone[idx++] = (byte) ((val & 0xff00) >>> 8);


}


return generatedTone;
}


private AudioTrack getAudioTrack(int length){


if (audioTrack == null)
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
mSampleRate, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, length,
AudioTrack.MODE_STATIC);


return audioTrack;
}


private double sine(int x, double frequencies){
return Math.sin(  2*Math.PI * x * frequencies);
}


void playTrack(byte[] generatedSnd){
getAudioTrack(generatedSnd.length)
.write(generatedSnd, 0, generatedSnd.length);
audioTrack.play();
}


}

see this helpful library

https://github.com/karlotoy/perfectTune

it's easy to use

add this to your dependencies

 compile 'com.github.karlotoy:perfectTune:1.0.2'

And you use it like this:

PerfectTune perfectTune = new PerfectTune();
perfectTune.setTuneFreq(desire_freq);
perfectTune.playTune();

to stop the tune:

perfectTune.stopTune();