forked from len0rd/rockbox
The old way actually mis-used the API (I misunderstood the docs) because it specified the marker position as a "low buffer watermark" but instead of a future playback head position. The replacement is a simple thread that writes the data regardless of the filling level of the buffer (write() will just block) and polls the playback state periodically. Change-Id: If29237cee4ce78dc42f5a8320878bab0cafe78f7 Reviewed-on: http://gerrit.rockbox.org/422 Tested-by: Dominik Riebeling <Dominik.Riebeling@gmail.com> Reviewed-by: Thomas Martitz <kugel@rockbox.org>
318 lines
10 KiB
Java
318 lines
10 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 org.rockbox.Helper.Logger;
|
|
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;
|
|
|
|
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_OUT_STEREO;
|
|
private static final int encoding =
|
|
AudioFormat.ENCODING_PCM_16BIT;
|
|
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;
|
|
|
|
/* 8k is plenty, but some devices may have a higher minimum.
|
|
* 8k represents 125ms of audio */
|
|
private static final int chunkSize =
|
|
Math.max(8<<10, getMinBufferSize(samplerate, channels, encoding));
|
|
Streamer streamer;
|
|
|
|
public RockboxPCM()
|
|
{
|
|
super(streamtype, samplerate, channels, encoding,
|
|
chunkSize, AudioTrack.MODE_STREAM);
|
|
|
|
streamer = new Streamer(chunkSize);
|
|
streamer.start();
|
|
raw_data = new byte[chunkSize]; /* in shorts */
|
|
Arrays.fill(raw_data, (byte) 0);
|
|
|
|
/* find cleaner way to get context? */
|
|
rbservice = RockboxService.getInstance();
|
|
audiomanager =
|
|
(AudioManager) rbservice.getSystemService(Context.AUDIO_SERVICE);
|
|
maxstreamvolume = audiomanager.getStreamMaxVolume(streamtype);
|
|
|
|
minpcmvolume = getMinVolume();
|
|
pcmrange = getMaxVolume() - minpcmvolume;
|
|
|
|
setupVolumeHandler();
|
|
postVolume(audiomanager.getStreamVolume(streamtype));
|
|
}
|
|
|
|
/**
|
|
* This class does the actual playback work. Its run() method
|
|
* continuously writes data to the AudioTrack. This operation blocks
|
|
* and should therefore be run on its own thread.
|
|
*/
|
|
private class Streamer extends Thread
|
|
{
|
|
byte[] buffer;
|
|
private boolean quit = false;
|
|
|
|
Streamer(int bufsize)
|
|
{
|
|
super("audio thread");
|
|
buffer = new byte[bufsize];
|
|
}
|
|
|
|
@Override
|
|
public void run()
|
|
{
|
|
/* THREAD_PRIORITY_URGENT_AUDIO can only be specified via
|
|
* setThreadPriority(), and not via thread.setPriority(). This is
|
|
* also how the android's HandlerThread class implements it */
|
|
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
|
|
while (!quit)
|
|
{
|
|
switch(getPlayState())
|
|
{
|
|
case PLAYSTATE_PLAYING:
|
|
nativeWrite(buffer, buffer.length);
|
|
break;
|
|
case PLAYSTATE_PAUSED:
|
|
case PLAYSTATE_STOPPED:
|
|
{
|
|
synchronized (this)
|
|
{
|
|
try
|
|
{
|
|
wait();
|
|
}
|
|
catch (InterruptedException e) { e.printStackTrace(); }
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
synchronized void quit()
|
|
{
|
|
quit = true;
|
|
notify();
|
|
}
|
|
|
|
synchronized void kick()
|
|
{
|
|
notify();
|
|
}
|
|
|
|
void quitAndJoin()
|
|
{
|
|
while(true)
|
|
{
|
|
try
|
|
{
|
|
quit();
|
|
join();
|
|
return;
|
|
}
|
|
catch (InterruptedException e) { }
|
|
}
|
|
}
|
|
}
|
|
|
|
private native void postVolumeChangedEvent(int volume);
|
|
|
|
private void postVolume(int volume)
|
|
{
|
|
int rbvolume = ((maxstreamvolume - volume) * -99) /
|
|
maxstreamvolume;
|
|
Logger.d("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"));
|
|
}
|
|
|
|
private int bytes2frames(int bytes)
|
|
{
|
|
/* 1 sample is 2 bytes, 2 samples are 1 frame */
|
|
return (bytes/4);
|
|
}
|
|
|
|
private int frames2bytes(int frames)
|
|
{
|
|
/* 1 frame is 2 samples, 1 sample is 2 bytes */
|
|
return (frames*4);
|
|
}
|
|
|
|
private void play_pause(boolean pause)
|
|
{
|
|
RockboxService service = RockboxService.getInstance();
|
|
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)
|
|
{
|
|
/* need to fill with silence before starting playback */
|
|
write(raw_data, 0, raw_data.length);
|
|
}
|
|
play();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void play() throws IllegalStateException
|
|
{
|
|
super.play();
|
|
/* when stopped or paused the streamer is in a wait() state. need
|
|
* it to wake it up */
|
|
streamer.kick();
|
|
}
|
|
|
|
@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.getInstance().sendBroadcast(widgetUpdate);
|
|
RockboxService.getInstance().stopForeground();
|
|
}
|
|
|
|
@Override
|
|
public void release()
|
|
{
|
|
super.release();
|
|
/* stop streamer if this AudioTrack is destroyed by whomever */
|
|
streamer.quitAndJoin();
|
|
}
|
|
|
|
public int setStereoVolume(float leftVolume, float rightVolume)
|
|
{
|
|
curpcmvolume = leftVolume;
|
|
return super.setStereoVolume(leftVolume, rightVolume);
|
|
}
|
|
|
|
private void set_volume(int volume)
|
|
{
|
|
Logger.d("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) {
|
|
Logger.d("java:setStreamVolume("+streamvolume+")");
|
|
setstreamvolume = streamvolume;
|
|
audiomanager.setStreamVolume(streamtype, streamvolume, 0);
|
|
}
|
|
}
|
|
|
|
public native int nativeWrite(byte[] temp, int len);
|
|
}
|