Android音频之SoundPool

Android提供了两种不同的框架来处理音频,分别是:

  • MediaPlayer / MediaRecoder:处理音频的标准方法,但数据源必须是文件或者基于流的数据。使用这个类处理音频文件的时候须有创建自己的线程运行。本文要说的SoundPool类就是使用了这个框架。
  • AudioTrack / AudioRecoder:该框架支持直接访问原始音频文件。用于在内存中处理音频文件,或者开始播放音频的同时写入缓冲区,或者在其他不需要文件和数据流的场合中使用。运行过程中不需要创建新线程。

SoundPool

和MediaPlayer主要用来播放一些长音频不同,SoundPool一般用来处理一些短的但是带有重叠,回放等特效的音频。这是因为SoundPool带有一个音频缓冲区,可以很方便的支持音频的回放、快放和慢放。

使用SoundPool的基本步骤

  • 初始化
  • 加载资源
  • 播放
  • 释放资源

测试SoundPool

我在测试过程中发现不能播放大于1M的音频文件,所以这里使用一个900K的sound.wav做测试。在点击Play后,音乐将重复播放5次(mySP.play(soundId, 1f, 1f, 1, 5, rate)中的第5个参数指定),每重复点击Play按钮,音乐会降低一半速率播放(mySP.play(soundId, 1f, 1f, 1, 5, rate)中的第6个参数指定)。测试程序同时最大可以叠加10个音频(SoundPool(10, AudioManager.STREAM_MUSIC, 0)中的第1个参数指定)。

public class AudioExamplesSP extends Activity {

	static float rate = 2f;

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

		Button playButton = (Button) findViewById(R.id.play_pause);
		final SoundPool mySP = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
		final int soundId = mySP.load(this, R.raw.sound, 1);

		playButton.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View arg0) {
				rate = rate / 2;
				mySP.play(soundId, 1f, 1f, 1, 5, rate);
			}
		});
	}
}

可能的问题

如果在播放中出现下面的错误:

sample 1 not READY

原因是SoundPool需要自己在后台启动一个线程来做音频回放,当调用SoundPool.load()加载资源后立即调用SoundPool.play()进行播放的时候,有可能出现资源加载未完成的情况,在这种情况下就可能出现上面的错误。解决的方法如下:

  • 把启动播放的事件跟一个用户事件关联起来。如上面的例子。
  • 重复测试Sound.play()的返回值,例如:
int sid = 0;
for(int i = 0; i < 1000; i++) {
    if(sid == 0) {
	try {
	    rate = rate / 2;
	    sid = mySP.play(soundId, 1f, 1f, 1, 5, rate);
	    Thread.sleep(1);
	} catch(InterruptedException e) {
	    e.printStackTrace();
	}
    } else {
	    break;
    }
}

为SoundPool设置OnLoadCompleteListener:这个监听器需要Android2.2以上才能使用。例如:

mySP.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
			
	@Override
	public void onLoadComplete(SoundPool arg0, int arg1, int arg2) {
		mySP.play(soundId, 1f, 1f, 1, 5, rate);
	}
});

ubuntu下编译ImageMagick使支持JPEG

OS : ubuntu 12.04 (32bit)

ImageMagick ver : 6.8.4

ubuntu 12.04默认使用的是JPEG-8,但是ImageMagick-6.8.4需要JPEG-9。使用默认configure编译ImageMagick后,每当需要处理jpeg文件时,IMageMagick报错:

Magick: Wrong JPEG library version: library is 80, caller expects 90

解决方法如下:

  • 下载编译安装JPEG-9,下载地址http://www.ijg.org/ 。编译并安装到默认位置:/usr/local/

  • 使用下面的配置,重新编译ImageMagick-6.8.4,并且安装

    $ ./configure CXXFLAGS=-I/usr/local/include LDFLAGS=-L/usr/local/lib --disable-static --with-quantum-depth=8
    

进入/usr/local/lib/下,查看一下libMagickCore-6.Q8.so的依赖库:

$ ldd libMagickCore-6.Q8.so  | grep -i  jpeg

输出:

	libjpeg.so.9 => /usr/local/lib/libjpeg.so.9 (0xb711f000)
	libjpeg.so.8 => /usr/lib/i386-linux-gnu/libjpeg.so.8 (0xb67cb000)

结果显示ImageMagick已经正确连接到jpeg-9。

Done !

Android使用多点触摸(一)

Android触摸事件分类

事件名 动作
ACTION_DOWN 按下第一个点
ACTION_POINTER_DOWN 按下第二个点
ACTION_MOVE 手势发生移动
ACTION_POINTER_UP 释放第二个点
ACTION_UP 释放第一个点

简单示例

activity_multitouch.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".Multitouch" >

	<ImageView android:id="@+id/imageView"
	    android:contentDescription="@string/DESC"
	    android:layout_width="fill_parent"
	    android:layout_height="fill_parent"
	    android:src="@drawable/aywdhz8u"
	    android:scaleType="matrix" >
	</ImageView>
</RelativeLayout>

Multitouch.java

public class Multitouch extends Activity implements OnTouchListener{

	Matrix matrix = new Matrix();
	Matrix eventMatrix = new Matrix();

	final static int NONE = 0;
	final static int DRAG = 1;
	final static int ZOOM = 2;
	int touchState = NONE;

	final static int MIN_DIST = 50;
	static float eventDistance = 0;
	static float centerX = 0, centerY = 0;

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

		ImageView view = (ImageView)findViewById(R.id.imageView);
		view.setOnTouchListener(this);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.multitouch, menu);
		return true;
	}

	@Override
	public boolean onTouch(View arg0, MotionEvent arg1) {
		ImageView view = (ImageView)arg0;

		switch(arg1.getAction() & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_DOWN:
			touchState = DRAG;
			centerX = arg1.getX(0);
			centerY = arg1.getY(0);
			eventMatrix.set(matrix);
			break;
		case MotionEvent.ACTION_POINTER_DOWN:
			eventDistance = calcDistance(arg1);
			calcMidpoint(centerX, centerY, arg1);
			if(eventDistance > MIN_DIST) {
				eventMatrix.set(matrix);
				touchState = ZOOM;
			}
			break;
		case MotionEvent.ACTION_MOVE:
			if(touchState == DRAG) {
				matrix.set(eventMatrix);
				matrix.setTranslate(arg1.getX(0) - centerX, arg1.getY(0) - centerY);
			} else if(touchState == ZOOM) {
				float dist = calcDistance(arg1);
				if(dist > MIN_DIST) {
					matrix.set(eventMatrix);
					float scale = dist / eventDistance;
					matrix.postScale(scale, scale, centerX, centerY);
				}
			}
			view.setImageMatrix(matrix);
			break;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_POINTER_UP:
			touchState = NONE;
			break;
		}
		return true;
	}

	private float calcDistance(MotionEvent event) {
		float x = event.getX(0) - event.getX(1);
		float y = event.getY(0) - event.getY(1);
		return (float) Math.sqrt(x * x + y * y);
	}

	private void calcMidpoint(float centerX, float centerY, MotionEvent event) {
		centerX = (event.getX(0) + event.getX(1)) / 2;
		centerY = (event.getY(0) + event.getY(1)) / 2;
	}
}

ref:《Android开发秘籍》

MacOS下编译Android源代码

Mac OS: 10.7.5

在 MacOS 上编译 Android 源码的前提是你所使用的文件系统必须是 大小写敏感 的。下面的内容旨在说明如何在不破坏原有文件系统的基础上建立一个供编译 Android 使用的文件系统。

检查你现有的文件系统

依次打开 GO —-> Utilities —-> Disk Utility.app查看当前磁盘的格式,如下图: alt none

在左侧栏中选中你希望在上面编译android的那块磁盘,查看右侧栏中的“Format”一项,如果format格式中带有“Case-sensitive”字样,那么恭喜你,你不需要再做任何操作即可开始编译android源码了,否则,继续往下操作。

确定磁盘空间

当然最方便的方法是把整个磁盘都重新格式成大小写敏感的文件系统,然后重新安装系统。但那样做的成本比较高并且很浪费时间。这里要谈的是不破坏原有系统的前提下,创建一块只供编译android的文件系统。首先需要确保你磁盘的剩余空间在30G以上,编译整个android大概会使用掉25G空间(Google官方显示至少25G)。

创建磁盘镜像

打开一个Terminal,输入如下命令:

$ hdiutil create -type SPARSE -fs 'Case-sensitive Journaled HFS+' -size 60g   YOU_FOLDER/android.dmg

这条命令在你所指定的文件夹中(YOU_FOLDER)创建了一个大小写敏感的可以自增长的最大为60GB的镜像。查看你的文件夹出现了新文件android.img.sparseimage。接下来把这个镜像挂载到系统:

$ hdiutil  attach ./android.dmg.sparseimage  -mountpoint /Volumes/android

这条命令把上一步中创建的android.dmg挂载到了/Volumes/android下,以后这里就是我们编译android的地方。如果你希望每次开机的时候系统自动挂载这个镜像,你可以把下面的命令放到你的~/.profile中。

$ hdiutil attach YOU_FOLDER/android.dmg.sparseimage -mountpoint /Volumes/android

接下来就是把down下来的android源码拷贝到/Volumes/android目录下进行编译。

Android 使用 CountDownTimer

在onResume()方法中构造CountDownTimer的好处是,onResume()是在onRestoreInstanceState()之后执行的, 这样就可以在onRestoreInstanceState()中恢复上次倒计时后的剩余时间,这样在手机屏幕旋转后,倒计时不会重置。

MainActivity.java

package com.example.countdowntimeexample;

import android.os.Bundle;
import android.os.CountDownTimer;
import android.app.Activity;
import android.view.Menu;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends Activity {

	private TextView mTimeLabel;
	CountDownTimer myTimer;
	long myTime = 30000;

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

		mTimeLabel = (TextView) findViewById(R.id.text);	
	}

	@Override
	protected void onResume() {
	       
		myTimer = new CountDownTimer(myTime, 1000) {

			@Override
			public void onTick(long arg0) {
				mTimeLabel.setText("second remaining: " + arg0 / 1000);
				myTime = arg0;
			}

			@Override
			public void onFinish() {
				mTimeLabel.setText("done!");
				myTime = 0;
			}
		}.start();
		super.onResume();
	}

	@Override
	protected void onStop() {
		myTimer.cancel(); // 取消计时器 
		super.onStop();
	}

	@Override
	protected void onSaveInstanceState(Bundle outState) {
   // 保存本次倒计时余下的时间,以便在acitivity再次激活的时候能从上次的断点继续倒计时 
		outState.putLong("time", myTime); 
		super.onSaveInstanceState(outState);
	}

	@Override
	protected void onRestoreInstanceState(Bundle savedInstanceState) {
    // 恢复上次倒计时断点,onResume函数将使用这个值来构造CountDownTimer
		myTime = savedInstanceState.getLong("time");
		super.onRestoreInstanceState(savedInstanceState);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}
}

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>