rockbox/android/src/org/rockbox/RockboxPCM.java

280 lines
9.6 KiB
Java

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2010 Thomas Martitz
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
package org.rockbox;
import java.util.Arrays;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.util.Log;
public class RockboxPCM extends AudioTrack
{
private static final int streamtype = AudioManager.STREAM_MUSIC;
private static final int samplerate = 44100;
/* should be CHANNEL_OUT_STEREO in 2.0 and above */
private static final int channels =
AudioFormat.CHANNEL_CONFIGURATION_STEREO;
private static final int encoding =
AudioFormat.ENCODING_PCM_16BIT;
/* 32k is plenty, but some devices may have a higher minimum */
private static final int buf_len =
Math.max(32<<10, 4*getMinBufferSize(samplerate, channels, encoding));
private AudioManager audiomanager;
private RockboxService rbservice;
private byte[] raw_data;
private int refillmark;
private int maxstreamvolume;
private int setstreamvolume = -1;
private float minpcmvolume;
private float curpcmvolume = 0;
private float pcmrange;
private void LOG(CharSequence text)
{
Log.d("Rockbox", (String) text);
}
public RockboxPCM()
{
super(streamtype, samplerate, channels, encoding,
buf_len, AudioTrack.MODE_STREAM);
HandlerThread ht = new HandlerThread("audio thread",
Process.THREAD_PRIORITY_URGENT_AUDIO);
ht.start();
raw_data = new byte[buf_len]; /* in shorts */
Arrays.fill(raw_data, (byte) 0);
/* find cleaner way to get context? */
rbservice = RockboxService.get_instance();
audiomanager =
(AudioManager) rbservice.getSystemService(Context.AUDIO_SERVICE);
maxstreamvolume = audiomanager.getStreamMaxVolume(streamtype);
minpcmvolume = getMinVolume();
pcmrange = getMaxVolume() - minpcmvolume;
setupVolumeHandler();
postVolume(audiomanager.getStreamVolume(streamtype));
refillmark = buf_len / 4; /* higher values don't work on many devices */
/* getLooper() returns null if thread isn't running */
while(!ht.isAlive()) Thread.yield();
setPlaybackPositionUpdateListener(
new PCMListener(buf_len / 2), new Handler(ht.getLooper()));
refillmark = bytes2frames(refillmark);
}
private native void postVolumeChangedEvent(int volume);
private void postVolume(int volume)
{
int rbvolume = ((maxstreamvolume - volume) * -99) /
maxstreamvolume;
LOG("java:postVolumeChangedEvent, avol "+volume+" rbvol "+rbvolume);
postVolumeChangedEvent(rbvolume);
}
private void setupVolumeHandler()
{
BroadcastReceiver broadcastReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
int streamType = intent.getIntExtra(
"android.media.EXTRA_VOLUME_STREAM_TYPE", -1);
int volume = intent.getIntExtra(
"android.media.EXTRA_VOLUME_STREAM_VALUE", -1);
if (streamType == RockboxPCM.streamtype &&
volume != -1 &&
volume != setstreamvolume &&
rbservice.isRockboxRunning())
{
postVolume(volume);
}
}
};
/* at startup, change the internal rockbox volume to what the global
android music stream volume is */
int volume = audiomanager.getStreamVolume(streamtype);
int rbvolume = ((maxstreamvolume - volume) * -99) / maxstreamvolume;
postVolumeChangedEvent(rbvolume);
/* We're relying on internal API's here,
this can break in the future! */
rbservice.registerReceiver(
broadcastReceiver,
new IntentFilter("android.media.VOLUME_CHANGED_ACTION"));
}
@SuppressWarnings("unused")
private int bytes2frames(int bytes)
{
/* 1 sample is 2 bytes, 2 samples are 1 frame */
return (bytes/4);
}
@SuppressWarnings("unused")
private int frames2bytes(int frames)
{
/* 1 frame is 2 samples, 1 sample is 2 bytes */
return (frames*4);
}
@SuppressWarnings("unused")
private void play_pause(boolean pause)
{
RockboxService service = RockboxService.get_instance();
if (pause)
{
Intent widgetUpdate = new Intent("org.rockbox.UpdateState");
widgetUpdate.putExtra("state", "pause");
service.sendBroadcast(widgetUpdate);
service.stopForeground();
pause();
}
else
{
Intent widgetUpdate = new Intent("org.rockbox.UpdateState");
widgetUpdate.putExtra("state", "play");
service.sendBroadcast(widgetUpdate);
service.startForeground();
if (getPlayState() == AudioTrack.PLAYSTATE_STOPPED)
{
setNotificationMarkerPosition(refillmark);
/* need to fill with silence before starting playback */
write(raw_data, 0, raw_data.length);
}
play();
}
}
@Override
public synchronized void stop() throws IllegalStateException
{
/* flush pending data, but turn the volume off so it cannot be heard.
* This is so that we don't hear old data if music is resumed very
* quickly after (e.g. when seeking).
*/
float old_vol = curpcmvolume;
try {
setStereoVolume(0, 0);
flush();
super.stop();
} catch (IllegalStateException e) {
throw new IllegalStateException(e);
} finally {
setStereoVolume(old_vol, old_vol);
}
Intent widgetUpdate = new Intent("org.rockbox.UpdateState");
widgetUpdate.putExtra("state", "stop");
RockboxService.get_instance().sendBroadcast(widgetUpdate);
RockboxService.get_instance().stopForeground();
}
public int setStereoVolume(float leftVolume, float rightVolume)
{
curpcmvolume = leftVolume;
return super.setStereoVolume(leftVolume, rightVolume);
}
@SuppressWarnings("unused")
private void set_volume(int volume)
{
LOG("java:set_volume("+volume+")");
/* Rockbox 'volume' is 0..-990 deci-dB attenuation.
Android streams have rather low resolution volume control,
typically 8 or 15 steps.
Therefore we use the pcm volume to add finer steps between
every android stream volume step.
It's not "real" dB, but it gives us 100 volume steps.
*/
float fraction = 1 - (volume / -990.0f);
int streamvolume = (int)Math.ceil(maxstreamvolume * fraction);
if (streamvolume > 0) {
float streamfraction = (float)streamvolume / maxstreamvolume;
float pcmvolume =
(fraction / streamfraction) * pcmrange + minpcmvolume;
setStereoVolume(pcmvolume, pcmvolume);
}
int oldstreamvolume = audiomanager.getStreamVolume(streamtype);
if (streamvolume != oldstreamvolume) {
LOG("java:setStreamVolume("+streamvolume+")");
setstreamvolume = streamvolume;
audiomanager.setStreamVolume(streamtype, streamvolume, 0);
}
}
public native int nativeWrite(byte[] temp, int len);
private class PCMListener implements OnPlaybackPositionUpdateListener
{
byte[] pcm_data;
public PCMListener(int _refill_bufsize)
{
pcm_data = new byte[_refill_bufsize];
}
public void onMarkerReached(AudioTrack track)
{
/* push new data to the hardware */
RockboxPCM pcm = (RockboxPCM)track;
int result = -1;
result = pcm.nativeWrite(pcm_data, pcm_data.length);
if (result >= 0)
{
switch(getPlayState())
{
case PLAYSTATE_PLAYING:
case PLAYSTATE_PAUSED:
setNotificationMarkerPosition(pcm.refillmark);
break;
case PLAYSTATE_STOPPED:
LOG("Stopped");
break;
}
}
else /* stop on error */
stop();
}
public void onPeriodicNotification(AudioTrack track)
{
}
}
}