A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 318 lines 10 kB view raw
1/*************************************************************************** 2 * __________ __ ___. 3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 * \/ \/ \/ \/ \/ 8 * $Id$ 9 * 10 * Copyright (C) 2010 Thomas Martitz 11 * 12 * This program is free software; you can redistribute it and/or 13 * modify it under the terms of the GNU General Public License 14 * as published by the Free Software Foundation; either version 2 15 * of the License, or (at your option) any later version. 16 * 17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 18 * KIND, either express or implied. 19 * 20 ****************************************************************************/ 21 22package org.rockbox; 23 24import java.util.Arrays; 25import org.rockbox.Helper.Logger; 26import android.content.BroadcastReceiver; 27import android.content.Context; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.media.AudioFormat; 31import android.media.AudioManager; 32import android.media.AudioTrack; 33import android.os.Handler; 34import android.os.HandlerThread; 35import android.os.Process; 36 37public class RockboxPCM extends AudioTrack 38{ 39 private static final int streamtype = AudioManager.STREAM_MUSIC; 40 private static final int samplerate = 44100; 41 /* should be CHANNEL_OUT_STEREO in 2.0 and above */ 42 private static final int channels = 43 AudioFormat.CHANNEL_OUT_STEREO; 44 private static final int encoding = 45 AudioFormat.ENCODING_PCM_16BIT; 46 private AudioManager audiomanager; 47 private RockboxService rbservice; 48 private byte[] raw_data; 49 50 private int refillmark; 51 private int maxstreamvolume; 52 private int setstreamvolume = -1; 53 private float minpcmvolume; 54 private float curpcmvolume = 0; 55 private float pcmrange; 56 57 /* 8k is plenty, but some devices may have a higher minimum. 58 * 8k represents 125ms of audio */ 59 private static final int chunkSize = 60 Math.max(8<<10, getMinBufferSize(samplerate, channels, encoding)); 61 Streamer streamer; 62 63 public RockboxPCM() 64 { 65 super(streamtype, samplerate, channels, encoding, 66 chunkSize, AudioTrack.MODE_STREAM); 67 68 streamer = new Streamer(chunkSize); 69 streamer.start(); 70 raw_data = new byte[chunkSize]; /* in shorts */ 71 Arrays.fill(raw_data, (byte) 0); 72 73 /* find cleaner way to get context? */ 74 rbservice = RockboxService.getInstance(); 75 audiomanager = 76 (AudioManager) rbservice.getSystemService(Context.AUDIO_SERVICE); 77 maxstreamvolume = audiomanager.getStreamMaxVolume(streamtype); 78 79 minpcmvolume = getMinVolume(); 80 pcmrange = getMaxVolume() - minpcmvolume; 81 82 setupVolumeHandler(); 83 postVolume(audiomanager.getStreamVolume(streamtype)); 84 } 85 86 /** 87 * This class does the actual playback work. Its run() method 88 * continuously writes data to the AudioTrack. This operation blocks 89 * and should therefore be run on its own thread. 90 */ 91 private class Streamer extends Thread 92 { 93 byte[] buffer; 94 private boolean quit = false; 95 96 Streamer(int bufsize) 97 { 98 super("audio thread"); 99 buffer = new byte[bufsize]; 100 } 101 102 @Override 103 public void run() 104 { 105 /* THREAD_PRIORITY_URGENT_AUDIO can only be specified via 106 * setThreadPriority(), and not via thread.setPriority(). This is 107 * also how the android's HandlerThread class implements it */ 108 Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); 109 while (!quit) 110 { 111 switch(getPlayState()) 112 { 113 case PLAYSTATE_PLAYING: 114 nativeWrite(buffer, buffer.length); 115 break; 116 case PLAYSTATE_PAUSED: 117 case PLAYSTATE_STOPPED: 118 { 119 synchronized (this) 120 { 121 try 122 { 123 wait(); 124 } 125 catch (InterruptedException e) { e.printStackTrace(); } 126 break; 127 } 128 } 129 } 130 } 131 } 132 133 synchronized void quit() 134 { 135 quit = true; 136 notify(); 137 } 138 139 synchronized void kick() 140 { 141 notify(); 142 } 143 144 void quitAndJoin() 145 { 146 while(true) 147 { 148 try 149 { 150 quit(); 151 join(); 152 return; 153 } 154 catch (InterruptedException e) { } 155 } 156 } 157 } 158 159 private native void postVolumeChangedEvent(int volume); 160 161 private void postVolume(int volume) 162 { 163 int rbvolume = ((maxstreamvolume - volume) * -99) / 164 maxstreamvolume; 165 Logger.d("java:postVolumeChangedEvent, avol "+volume+" rbvol "+rbvolume); 166 postVolumeChangedEvent(rbvolume); 167 } 168 169 private void setupVolumeHandler() 170 { 171 BroadcastReceiver broadcastReceiver = new BroadcastReceiver() 172 { 173 @Override 174 public void onReceive(Context context, Intent intent) 175 { 176 int streamType = intent.getIntExtra( 177 "android.media.EXTRA_VOLUME_STREAM_TYPE", -1); 178 int volume = intent.getIntExtra( 179 "android.media.EXTRA_VOLUME_STREAM_VALUE", -1); 180 181 if (streamType == RockboxPCM.streamtype && 182 volume != -1 && 183 volume != setstreamvolume && 184 rbservice.isRockboxRunning()) 185 { 186 postVolume(volume); 187 } 188 } 189 }; 190 191 /* at startup, change the internal rockbox volume to what the global 192 android music stream volume is */ 193 int volume = audiomanager.getStreamVolume(streamtype); 194 int rbvolume = ((maxstreamvolume - volume) * -99) / maxstreamvolume; 195 postVolumeChangedEvent(rbvolume); 196 197 /* We're relying on internal API's here, 198 this can break in the future! */ 199 rbservice.registerReceiver( 200 broadcastReceiver, 201 new IntentFilter("android.media.VOLUME_CHANGED_ACTION")); 202 } 203 204 private int bytes2frames(int bytes) 205 { 206 /* 1 sample is 2 bytes, 2 samples are 1 frame */ 207 return (bytes/4); 208 } 209 210 private int frames2bytes(int frames) 211 { 212 /* 1 frame is 2 samples, 1 sample is 2 bytes */ 213 return (frames*4); 214 } 215 216 private void play_pause(boolean pause) 217 { 218 RockboxService service = RockboxService.getInstance(); 219 if (pause) 220 { 221 Intent widgetUpdate = new Intent("org.rockbox.UpdateState"); 222 widgetUpdate.putExtra("state", "pause"); 223 service.sendBroadcast(widgetUpdate); 224 service.stopForeground(); 225 pause(); 226 } 227 else 228 { 229 Intent widgetUpdate = new Intent("org.rockbox.UpdateState"); 230 widgetUpdate.putExtra("state", "play"); 231 service.sendBroadcast(widgetUpdate); 232 service.startForeground(); 233 if (getPlayState() == AudioTrack.PLAYSTATE_STOPPED) 234 { 235 /* need to fill with silence before starting playback */ 236 write(raw_data, 0, raw_data.length); 237 } 238 play(); 239 } 240 } 241 242 @Override 243 public void play() throws IllegalStateException 244 { 245 super.play(); 246 /* when stopped or paused the streamer is in a wait() state. need 247 * it to wake it up */ 248 streamer.kick(); 249 } 250 251 @Override 252 public synchronized void stop() throws IllegalStateException 253 { 254 /* flush pending data, but turn the volume off so it cannot be heard. 255 * This is so that we don't hear old data if music is resumed very 256 * quickly after (e.g. when seeking). 257 */ 258 float old_vol = curpcmvolume; 259 try { 260 setStereoVolume(0, 0); 261 flush(); 262 super.stop(); 263 } catch (IllegalStateException e) { 264 throw new IllegalStateException(e); 265 } finally { 266 setStereoVolume(old_vol, old_vol); 267 } 268 269 Intent widgetUpdate = new Intent("org.rockbox.UpdateState"); 270 widgetUpdate.putExtra("state", "stop"); 271 RockboxService.getInstance().sendBroadcast(widgetUpdate); 272 RockboxService.getInstance().stopForeground(); 273 } 274 275 @Override 276 public void release() 277 { 278 super.release(); 279 /* stop streamer if this AudioTrack is destroyed by whomever */ 280 streamer.quitAndJoin(); 281 } 282 283 public int setStereoVolume(float leftVolume, float rightVolume) 284 { 285 curpcmvolume = leftVolume; 286 return super.setStereoVolume(leftVolume, rightVolume); 287 } 288 289 private void set_volume(int volume) 290 { 291 Logger.d("java:set_volume("+volume+")"); 292 /* Rockbox 'volume' is 0..-990 deci-dB attenuation. 293 Android streams have rather low resolution volume control, 294 typically 8 or 15 steps. 295 Therefore we use the pcm volume to add finer steps between 296 every android stream volume step. 297 It's not "real" dB, but it gives us 100 volume steps. 298 */ 299 300 float fraction = 1 - (volume / -990.0f); 301 int streamvolume = (int)Math.ceil(maxstreamvolume * fraction); 302 if (streamvolume > 0) { 303 float streamfraction = (float)streamvolume / maxstreamvolume; 304 float pcmvolume = 305 (fraction / streamfraction) * pcmrange + minpcmvolume; 306 setStereoVolume(pcmvolume, pcmvolume); 307 } 308 309 int oldstreamvolume = audiomanager.getStreamVolume(streamtype); 310 if (streamvolume != oldstreamvolume) { 311 Logger.d("java:setStreamVolume("+streamvolume+")"); 312 setstreamvolume = streamvolume; 313 audiomanager.setStreamVolume(streamtype, streamvolume, 0); 314 } 315 } 316 317 public native int nativeWrite(byte[] temp, int len); 318}