A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
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}