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) 2002 Jerome Kuptz
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#include <stdio.h>
22#include <string.h>
23#include <stdlib.h>
24#include "config.h"
25
26#include "system.h"
27#include "file.h"
28#include "lcd.h"
29#include "font.h"
30#include "backlight.h"
31#include "action.h"
32#include "kernel.h"
33#include "filetypes.h"
34#include "settings.h"
35#include "skin_engine/skin_engine.h"
36#include "audio.h"
37#include "usb.h"
38#include "status.h"
39#include "storage.h"
40#include "screens.h"
41#include "playlist.h"
42#include "icons.h"
43#include "lang.h"
44#include "bookmark.h"
45#include "misc.h"
46#include "sound.h"
47#include "onplay.h"
48#include "abrepeat.h"
49#include "playback.h"
50#include "splash.h"
51#include "cuesheet.h"
52#include "ata_idle_notify.h"
53#include "root_menu.h"
54#include "backdrop.h"
55#include "quickscreen.h"
56#include "shortcuts.h"
57#include "pitchscreen.h"
58#include "appevents.h"
59#include "viewport.h"
60#include "pcmbuf.h"
61#include "option_select.h"
62#include "playlist_viewer.h"
63#include "wps.h"
64#include "statusbar-skinned.h"
65#include "skin_engine/wps_internals.h"
66#include "open_plugin.h"
67
68#ifdef USB_ENABLE_AUDIO
69#include "usbstack/usb_audio.h"
70#endif
71
72#define FF_REWIND_MAX_PERCENT 3 /* cap ff/rewind step size at max % of file */
73 /* 3% of 30min file == 54s step size */
74#define MIN_FF_REWIND_STEP 500
75
76static struct wps_state wps_state;
77
78/* initial setup of wps_data */
79static void wps_state_init(void);
80static void track_info_callback(unsigned short id, void *param);
81
82#define WPS_DEFAULTCFG WPS_DIR "/rockbox_default.wps"
83#ifdef HAVE_REMOTE_LCD
84#define RWPS_DEFAULTCFG WPS_DIR "/rockbox_default.rwps"
85#define DEFAULT_WPS(screen) ((screen) == SCREEN_MAIN ? \
86 WPS_DEFAULTCFG:RWPS_DEFAULTCFG)
87#else
88#define DEFAULT_WPS(screen) (WPS_DEFAULTCFG)
89#endif
90
91char* wps_default_skin(enum screen_type screen)
92{
93 static char *skin_buf[NB_SCREENS] = {
94#if LCD_DEPTH > 1
95 "%X(d)\n"
96#endif
97 "%s%?it<%?in<%in. |>%it|%fn>\n"
98 "%s%?ia<%ia|%?d(2)<%d(2)|%(root%)>>\n"
99 "%s%?id<%id|%?d(1)<%d(1)|%(root%)>> %?iy<%(%iy%)|>\n\n"
100 "%al%pc/%pt%ar[%pp:%pe]\n"
101 "%fbkBit %?fv<avg|> %?iv<%(id3v%iv%)|%(no id3%)>\n"
102 "%pb\n%pm\n",
103#ifdef HAVE_REMOTE_LCD
104#if LCD_REMOTE_DEPTH > 1
105 "%X(d)\n"
106#endif
107 "%s%?ia<%ia|%?d(2)<%d(2)|%(root%)>>\n"
108 "%s%?it<%?in<%in. |>%it|%fn>\n"
109 "%al%pc/%pt%ar[%pp:%pe]\n"
110 "%fbkBit %?fv<avg|> %?iv<%(id3v%iv%)|%(no id3%)>\n"
111 "%pb\n",
112#endif
113 };
114 return skin_buf[screen];
115}
116
117static void update_non_static(void)
118{
119 FOR_NB_SCREENS(i)
120 skin_update(WPS, i, SKIN_REFRESH_NON_STATIC);
121}
122
123void wps_do_action(enum wps_do_action_type action, bool updatewps)
124{
125 if (action == WPS_PLAYPAUSE) /* toggle the status */
126 {
127 struct wps_state *state = get_wps_state();
128 if (state->paused)
129 action = WPS_PLAY;
130
131 state->paused = !state->paused;
132 }
133
134 if (action == WPS_PLAY) /* unpause_action */
135 {
136 audio_resume();
137 }
138 else /* WPS_PAUSE pause_action */
139 {
140 audio_pause();
141
142 if (global_settings.pause_rewind) {
143 unsigned long elapsed = audio_current_track()->elapsed;
144 long newpos = elapsed - (global_settings.pause_rewind * 1000);
145
146 audio_pre_ff_rewind();
147 audio_ff_rewind(newpos > 0 ? newpos : 0);
148 }
149 if (action == WPS_PLAYPAUSE)
150 {
151 settings_save();
152 #if !defined(HAVE_SW_POWEROFF)
153 call_storage_idle_notifys(true); /* make sure resume info is saved */
154 #endif
155 }
156 }
157
158 /* Bugfix only do a skin refresh if in one of the below screens */
159 enum current_activity act = get_current_activity();
160
161 bool refresh = (act == ACTIVITY_FM ||
162 act == ACTIVITY_WPS ||
163 act == ACTIVITY_RECORDING);
164
165 if (updatewps && refresh)
166 update_non_static();
167}
168
169#ifdef HAVE_TOUCHSCREEN
170static int skintouch_to_wps(void)
171{
172 int offset = 0;
173 struct wps_state *gstate = get_wps_state();
174 struct gui_wps *gwps = skin_get_gwps(WPS, SCREEN_MAIN);
175 int button = skin_get_touchaction(gwps, &offset);
176 switch (button)
177 {
178 case ACTION_STD_PREV:
179 return ACTION_WPS_SKIPPREV;
180 case ACTION_STD_PREVREPEAT:
181 return ACTION_WPS_SEEKBACK;
182 case ACTION_STD_NEXT:
183 return ACTION_WPS_SKIPNEXT;
184 case ACTION_STD_NEXTREPEAT:
185 return ACTION_WPS_SEEKFWD;
186 case ACTION_STD_MENU:
187 return ACTION_WPS_MENU;
188 case ACTION_STD_CONTEXT:
189 return ACTION_WPS_CONTEXT;
190 case ACTION_STD_QUICKSCREEN:
191 return ACTION_WPS_QUICKSCREEN;
192#ifdef HAVE_HOTKEY
193 case ACTION_STD_HOTKEY:
194 return ACTION_WPS_HOTKEY;
195#endif
196 case ACTION_TOUCH_SCROLLBAR:
197 gstate->id3->elapsed = gstate->id3->length*offset/1000;
198 audio_pre_ff_rewind();
199 audio_ff_rewind(gstate->id3->elapsed);
200 return ACTION_TOUCHSCREEN;
201 case ACTION_TOUCH_VOLUME:
202 {
203 const int min_vol = sound_min(SOUND_VOLUME);
204 const int max_vol = sound_max(SOUND_VOLUME);
205 const int step_vol = sound_steps(SOUND_VOLUME);
206
207 global_status.volume = from_normalized_volume(offset, min_vol, max_vol, 1000);
208 global_status.volume -= (global_status.volume % step_vol);
209 setvol();
210 }
211 return ACTION_TOUCHSCREEN;
212 }
213 return button;
214}
215#endif /* HAVE_TOUCHSCREEN */
216
217static bool ffwd_rew(int button, bool seek_from_end)
218{
219 unsigned int step = 0; /* current ff/rewind step */
220 unsigned int max_step = 0; /* maximum ff/rewind step */
221 int ff_rewind_count = 0; /* current ff/rewind count (in ticks) */
222 int direction = -1; /* forward=1 or backward=-1 */
223 bool exit = false;
224 bool usb = false;
225 bool ff_rewind = false;
226 const long ff_rw_accel = (global_settings.ff_rewind_accel + 3);
227 struct wps_state *gstate = get_wps_state();
228 struct mp3entry *old_id3 = gstate->id3;
229
230 if (button == ACTION_NONE)
231 {
232 status_set_ffmode(0);
233 return usb;
234 }
235 while (!exit)
236 {
237 struct mp3entry *id3 = gstate->id3;
238 if (id3 != old_id3)
239 {
240 ff_rewind = false;
241 ff_rewind_count = 0;
242 old_id3 = id3;
243 }
244 if (id3 && seek_from_end)
245 id3->elapsed = id3->length;
246
247 switch ( button )
248 {
249 case ACTION_WPS_SEEKFWD:
250 direction = 1;
251 /* Fallthrough */
252 case ACTION_WPS_SEEKBACK:
253 if (ff_rewind)
254 {
255 if (direction == 1)
256 {
257 /* fast forwarding, calc max step relative to end */
258 max_step = (id3->length -
259 (id3->elapsed +
260 ff_rewind_count)) *
261 FF_REWIND_MAX_PERCENT / 100;
262 }
263 else
264 {
265 /* rewinding, calc max step relative to start */
266 max_step = (id3->elapsed + ff_rewind_count) *
267 FF_REWIND_MAX_PERCENT / 100;
268 }
269
270 max_step = MAX(max_step, MIN_FF_REWIND_STEP);
271
272 if (step > max_step)
273 step = max_step;
274
275 ff_rewind_count += step * direction;
276
277 /* smooth seeking by multiplying step by: 1 + (2 ^ -accel) */
278 step += step >> ff_rw_accel;
279 }
280 else
281 {
282 if ((audio_status() & AUDIO_STATUS_PLAY) && id3 && id3->length )
283 {
284 audio_pre_ff_rewind();
285 if (direction > 0)
286 status_set_ffmode(STATUS_FASTFORWARD);
287 else
288 status_set_ffmode(STATUS_FASTBACKWARD);
289
290 ff_rewind = true;
291
292 step = 1000 * global_settings.ff_rewind_min_step;
293 }
294 else
295 break;
296 }
297
298 if (direction > 0) {
299 if ((id3->elapsed + ff_rewind_count) > id3->length)
300 ff_rewind_count = id3->length - id3->elapsed;
301 }
302 else {
303 if ((int)(id3->elapsed + ff_rewind_count) < 0)
304 ff_rewind_count = -id3->elapsed;
305 }
306
307 /* set the wps state ff_rewind_count so the progess info
308 displays corectly */
309 gstate->ff_rewind_count = ff_rewind_count;
310
311 FOR_NB_SCREENS(i)
312 {
313 skin_update(WPS, i,
314 SKIN_REFRESH_PLAYER_PROGRESS |
315 SKIN_REFRESH_DYNAMIC);
316 }
317
318 break;
319
320 case ACTION_WPS_STOPSEEK:
321 id3->elapsed = id3->elapsed + ff_rewind_count;
322 audio_ff_rewind(id3->elapsed);
323 gstate->ff_rewind_count = 0;
324 ff_rewind = false;
325 status_set_ffmode(0);
326 exit = true;
327 break;
328
329 default:
330 if(default_event_handler(button) == SYS_USB_CONNECTED) {
331 status_set_ffmode(0);
332 usb = true;
333 exit = true;
334 }
335 break;
336 }
337 if (!exit)
338 {
339 button = get_action(CONTEXT_WPS|ALLOW_SOFTLOCK,TIMEOUT_BLOCK);
340#ifdef HAVE_TOUCHSCREEN
341 if (button == ACTION_TOUCHSCREEN)
342 button = skintouch_to_wps();
343#endif
344 if (button != ACTION_WPS_SEEKFWD
345 && button != ACTION_WPS_SEEKBACK
346 && button != 0 && !IS_SYSEVENT(button))
347 button = ACTION_WPS_STOPSEEK;
348 }
349 }
350 return usb;
351}
352
353static void gwps_caption_backlight(struct wps_state *state)
354{
355#if defined(HAVE_BACKLIGHT) || defined(HAVE_REMOTE_LCD)
356 if (state->id3)
357 {
358#ifdef HAVE_BACKLIGHT
359 if (global_settings.caption_backlight)
360 {
361 /* turn on backlight n seconds before track ends, and turn it off n
362 seconds into the new track. n == backlight_timeout, or 5s */
363 int n = global_settings.backlight_timeout * 1000;
364
365 if ( n < 1000 )
366 n = 5000; /* use 5s if backlight is always on or off */
367
368 if (((state->id3->elapsed < 1000) ||
369 ((state->id3->length - state->id3->elapsed) < (unsigned)n)) &&
370 (state->paused == false))
371 backlight_on();
372 }
373#endif
374#ifdef HAVE_REMOTE_LCD
375 if (global_settings.remote_caption_backlight)
376 {
377 /* turn on remote backlight n seconds before track ends, and turn it
378 off n seconds into the new track. n == remote_backlight_timeout,
379 or 5s */
380 int n = global_settings.remote_backlight_timeout * 1000;
381
382 if ( n < 1000 )
383 n = 5000; /* use 5s if backlight is always on or off */
384
385 if (((state->id3->elapsed < 1000) ||
386 ((state->id3->length - state->id3->elapsed) < (unsigned)n)) &&
387 (state->paused == false))
388 remote_backlight_on();
389 }
390#endif
391 }
392#else
393 (void) state;
394#endif /* def HAVE_BACKLIGHT || def HAVE_REMOTE_LCD */
395}
396
397static void change_dir(int direction)
398{
399 if (global_settings.prevent_skip)
400 return;
401
402 if (direction < 0)
403 audio_prev_dir();
404 else if (direction > 0)
405 audio_next_dir();
406 /* prevent the next dir to immediatly start being ffw'd */
407 action_wait_for_release();
408}
409
410static void prev_track(unsigned long skip_thresh)
411{
412 struct wps_state *state = get_wps_state();
413 if (state->id3->elapsed < skip_thresh)
414 {
415 audio_prev();
416 return;
417 }
418 else
419 {
420 if (state->id3->cuesheet)
421 {
422 curr_cuesheet_skip(state->id3->cuesheet, -1, state->id3->elapsed);
423 return;
424 }
425
426 audio_pre_ff_rewind();
427 audio_ff_rewind(0);
428 }
429}
430
431static void next_track(void)
432{
433 struct wps_state *state = get_wps_state();
434 /* take care of if we're playing a cuesheet */
435 if (state->id3->cuesheet)
436 {
437 if (curr_cuesheet_skip(state->id3->cuesheet, 1, state->id3->elapsed))
438 {
439 /* if the result was false, then we really want
440 to skip to the next track */
441 return;
442 }
443 }
444
445 audio_next();
446}
447
448static void play_hop(int direction)
449{
450 struct wps_state *state = get_wps_state();
451 struct cuesheet *cue = state->id3->cuesheet;
452 long step = global_settings.skip_length*1000;
453 long elapsed = state->id3->elapsed;
454 long remaining = state->id3->length - elapsed;
455
456 /* if cuesheet is active, then we want the current tracks end instead of
457 * the total end */
458 if (cue && (cue->curr_track_idx+1 < cue->track_count))
459 {
460 int next = cue->curr_track_idx+1;
461 struct cue_track_info *t = &cue->tracks[next];
462 remaining = t->offset - elapsed;
463 }
464
465 if (step < 0)
466 {
467 if (direction < 0)
468 {
469 prev_track(DEFAULT_SKIP_THRESH);
470 return;
471 }
472 else if (remaining < DEFAULT_SKIP_THRESH*2)
473 {
474 next_track();
475 return;
476 }
477 else
478 elapsed += (remaining - DEFAULT_SKIP_THRESH*2);
479 }
480 else if (!global_settings.prevent_skip &&
481 (!step ||
482 (direction > 0 && step >= remaining) ||
483 (direction < 0 && elapsed < DEFAULT_SKIP_THRESH)))
484 { /* Do normal track skipping */
485 if (direction > 0)
486 next_track();
487 else if (direction < 0)
488 {
489 if (step > 0 && global_settings.rewind_across_tracks && elapsed < DEFAULT_SKIP_THRESH && playlist_check(-1))
490 {
491 bool audio_paused = (audio_status() & AUDIO_STATUS_PAUSE)?true:false;
492 if (!audio_paused)
493 audio_pause();
494 audio_prev();
495 audio_ff_rewind(-step);
496 if (!audio_paused)
497 audio_resume();
498 return;
499 }
500
501 prev_track(DEFAULT_SKIP_THRESH);
502 }
503 return;
504 }
505 else if (direction == 1 && step >= remaining)
506 {
507 system_sound_play(SOUND_TRACK_NO_MORE);
508 return;
509 }
510 else if (direction == -1 && elapsed < step)
511 {
512 elapsed = 0;
513 }
514 else
515 {
516 elapsed += step * direction;
517 }
518 if(audio_status() & AUDIO_STATUS_PLAY)
519 {
520 audio_pre_ff_rewind();
521 }
522
523 audio_ff_rewind(elapsed);
524}
525
526#if defined(HAVE_LCD_ENABLE) || defined(HAVE_LCD_SLEEP)
527/*
528 * If the user is unable to see the wps, because the display is deactivated,
529 * we suppress updates until the wps is activated again (the lcd driver will
530 * call this hook to issue an instant update)
531 * */
532static void wps_lcd_activation_hook(unsigned short id, void *param)
533{
534 (void)id;
535 (void)param;
536 skin_request_full_update(WPS);
537 /* force timeout in wps main loop, so that the update is instantly */
538 button_queue_post(BUTTON_NONE, 0);
539}
540#endif
541
542static void gwps_leave_wps(bool theme_enabled)
543{
544 FOR_NB_SCREENS(i)
545 {
546 struct gui_wps *gwps = skin_get_gwps(WPS, i);
547 gwps->display->scroll_stop();
548 if (theme_enabled)
549 {
550#ifdef HAVE_BACKDROP_IMAGE
551 skin_backdrop_show(sb_get_backdrop(i));
552
553 /* The following is supposed to erase any traces of %VB
554 viewports drawn by the WPS. May need further thought... */
555 struct wps_data *sbs = skin_get_gwps(CUSTOM_STATUSBAR, i)->data;
556 if (gwps->data->use_extra_framebuffer && sbs->use_extra_framebuffer)
557 skin_update(CUSTOM_STATUSBAR, i, SKIN_REFRESH_ALL);
558#endif
559 viewportmanager_theme_undo(i, skin_has_sbs(gwps));
560 }
561 }
562
563#if defined(HAVE_LCD_ENABLE) || defined(HAVE_LCD_SLEEP)
564 /* Play safe and unregister the hook */
565 remove_event(LCD_EVENT_ACTIVATION, wps_lcd_activation_hook);
566#endif
567 /* unhandle statusbar update delay */
568 sb_skin_set_update_delay(DEFAULT_UPDATE_DELAY);
569#ifdef HAVE_TOUCHSCREEN
570 touchscreen_set_mode(global_settings.touch_mode);
571#endif
572}
573
574static void restore_theme(void)
575{
576 FOR_NB_SCREENS(i)
577 {
578 struct gui_wps *gwps = skin_get_gwps(WPS, i);
579 struct screen *display = gwps->display;
580 display->scroll_stop();
581 viewportmanager_theme_enable(i, skin_has_sbs(gwps), NULL);
582 }
583}
584
585/*
586 * display the wps on entering or restoring */
587static void gwps_enter_wps(bool theme_enabled)
588{
589 struct gui_wps *gwps;
590 struct screen *display;
591 if (theme_enabled)
592 restore_theme();
593 FOR_NB_SCREENS(i)
594 {
595 gwps = skin_get_gwps(WPS, i);
596 display = gwps->display;
597 display->scroll_stop();
598 /* Update the values in the first (default) viewport - in case the user
599 has modified the statusbar or colour settings */
600#if LCD_DEPTH > 1
601 if (display->depth > 1)
602 {
603 struct skin_viewport *svp = skin_find_item(VP_DEFAULT_LABEL_STRING,
604 SKIN_FIND_VP, gwps->data);
605 if (svp)
606 {
607 struct viewport *vp = &svp->vp;
608 vp->fg_pattern = display->get_foreground();
609 vp->bg_pattern = display->get_background();
610 }
611 }
612#endif
613 /* make the backdrop actually take effect */
614#ifdef HAVE_BACKDROP_IMAGE
615 skin_backdrop_show(gwps->data->backdrop_id);
616#endif
617 display->clear_display();
618 skin_update(WPS, i, SKIN_REFRESH_ALL);
619
620 }
621#ifdef HAVE_TOUCHSCREEN
622 gwps = skin_get_gwps(WPS, SCREEN_MAIN);
623 skin_disarm_touchregions(gwps);
624 if (gwps->data->touchregions < 0)
625 touchscreen_set_mode(TOUCHSCREEN_BUTTON);
626#endif
627 /* force statusbar/skin update since we just cleared the whole screen */
628 send_event(GUI_EVENT_ACTIONUPDATE, (void*)1);
629}
630
631static long do_wps_exit(long action, bool bookmark)
632{
633 audio_pause();
634 update_non_static();
635 if (bookmark)
636 bookmark_autobookmark(true);
637 audio_stop();
638
639 ab_reset_markers();
640
641 gwps_leave_wps(true);
642#ifdef HAVE_RECORDING
643 if (action == ACTION_WPS_REC)
644 return GO_TO_RECSCREEN;
645#else
646 (void)action;
647#endif
648 if (global_settings.browse_current)
649 return GO_TO_PREVIOUS_BROWSER;
650 return GO_TO_PREVIOUS;
651}
652
653static long do_party_mode(long action)
654{
655 if (global_settings.party_mode)
656 {
657 switch (action)
658 {
659#ifdef ACTION_WPSAB_SINGLE
660 case ACTION_WPSAB_SINGLE:
661 if (!ab_repeat_mode_enabled())
662 break;
663 /* Note: currently all targets use ACTION_WPS_BROWSE
664 * if mapped to any of below actions this will cause problems */
665#endif
666 case ACTION_WPS_PLAY:
667 case ACTION_WPS_SEEKFWD:
668 case ACTION_WPS_SEEKBACK:
669 case ACTION_WPS_SKIPPREV:
670 case ACTION_WPS_SKIPNEXT:
671 case ACTION_WPS_ABSETB_NEXTDIR:
672 case ACTION_WPS_ABSETA_PREVDIR:
673 case ACTION_WPS_STOP:
674 return ACTION_NONE;
675 break;
676 default:
677 break;
678 }
679 }
680 return action;
681}
682
683static inline int action_wpsab_single(long button)
684{
685/* The iPods/X5/M5 use a single button for the A-B mode markers,
686 defined as ACTION_WPSAB_SINGLE in their config files. */
687#ifdef ACTION_WPSAB_SINGLE
688 static int wps_ab_state = 0;
689 if (button == ACTION_WPSAB_SINGLE && ab_repeat_mode_enabled())
690 {
691 switch (wps_ab_state)
692 {
693 case 0: /* set the A spot */
694 button = ACTION_WPS_ABSETA_PREVDIR;
695 break;
696 case 1: /* set the B spot */
697 button = ACTION_WPS_ABSETB_NEXTDIR;
698 break;
699 case 2:
700 button = ACTION_WPS_ABRESET;
701 break;
702 }
703 wps_ab_state = (wps_ab_state+1) % 3;
704 }
705#endif /* def ACTION_WPSAB_SINGLE */
706 return button;
707}
708
709/* The WPS can be left in two ways:
710 * a) call a function, which draws over the wps. In this case, the wps
711 * will be still active (i.e. the below function didn't return)
712 * b) return with a value evaluated by root_menu.c, in this case the wps
713 * is really left, and root_menu will handle the next screen
714 *
715 * In either way, call gwps_leave_wps(true), in order to restore the correct
716 * "main screen" backdrops and statusbars
717 */
718long gui_wps_show(void)
719{
720/* NOTE: if USBAudio ever gets its own DSP channel, this block can go away! */
721#ifdef USB_ENABLE_AUDIO
722 if (usb_audio_get_active())
723 {
724 splash(HZ*2, ID2P(LANG_USB_DAC_ACTIVE));
725 }
726#endif
727 long button = 0;
728 bool restore = true;
729 bool exit = false;
730 bool bookmark = false;
731 bool update = false;
732 bool theme_enabled = true;
733 long last_left = 0, last_right = 0;
734 struct wps_state *state = get_wps_state();
735
736 ab_reset_markers();
737
738 wps_state_init();
739 while ( 1 )
740 {
741 bool hotkey = false;
742 bool audio_paused = (audio_status() & AUDIO_STATUS_PAUSE)?true:false;
743 /* did someone else (i.e power thread) change audio pause mode? */
744 if (state->paused != audio_paused) {
745 state->paused = audio_paused;
746
747 /* if another thread paused audio, we are probably in car mode,
748 about to shut down. lets save the settings. */
749 if (state->paused) {
750 settings_save();
751#if !defined(HAVE_SW_POWEROFF)
752 call_storage_idle_notifys(true);
753#endif
754 }
755 }
756
757 if (restore)
758 {
759 restore = false;
760#if defined(HAVE_LCD_ENABLE) || defined(HAVE_LCD_SLEEP)
761 add_event(LCD_EVENT_ACTIVATION, wps_lcd_activation_hook);
762#endif
763 /* we remove the update delay since it's not very usable in the wps,
764 * e.g. during volume changing or ffwd/rewind */
765 sb_skin_set_update_delay(0);
766 skin_request_full_update(WPS);
767 update = true;
768 gwps_enter_wps(theme_enabled);
769 theme_enabled = true;
770 }
771 else
772 {
773 gwps_caption_backlight(state);
774
775 FOR_NB_SCREENS(i)
776 {
777#if defined(HAVE_LCD_ENABLE) || defined(HAVE_LCD_SLEEP)
778 /* currently, all remotes are readable without backlight
779 * so still update those */
780 if (lcd_active() || (i != SCREEN_MAIN))
781#endif
782 {
783 bool full_update = skin_do_full_update(WPS, i);
784 if (update || full_update)
785 {
786 skin_update(WPS, i, full_update ?
787 SKIN_REFRESH_ALL : SKIN_REFRESH_NON_STATIC);
788 }
789 }
790 }
791 update = false;
792 }
793
794 if (exit)
795 {
796 return do_wps_exit(button, bookmark);
797 }
798
799 if (button && !IS_SYSEVENT(button) )
800 storage_spin();
801
802 button = skin_wait_for_action(WPS, CONTEXT_WPS|ALLOW_SOFTLOCK, HZ/5);
803
804 /* Exit if audio has stopped playing. This happens e.g. at end of
805 playlist or if using the sleep timer. */
806 if (!(audio_status() & AUDIO_STATUS_PLAY))
807 exit = true;
808#ifdef HAVE_TOUCHSCREEN
809 if (button == ACTION_TOUCHSCREEN)
810 button = skintouch_to_wps();
811#endif
812 button = do_party_mode(button); /* block select actions in party mode */
813
814 button = action_wpsab_single(button); /* iPods/X5/M5 */
815
816 switch(button)
817 {
818#ifdef HAVE_HOTKEY
819 case ACTION_WPS_HOTKEY:
820 {
821 hotkey = true;
822 if (!global_settings.hotkey_wps)
823 break;
824 if (get_hotkey(global_settings.hotkey_wps)->flags & HOTKEY_FLAG_NOSBS)
825 {
826 /* leave WPS without re-enabling theme */
827 theme_enabled = false;
828 gwps_leave_wps(theme_enabled);
829 onplay(state->id3->path,
830 FILE_ATTR_AUDIO, CONTEXT_WPS, hotkey, ONPLAY_NO_CUSTOMACTION);
831 if (!audio_status())
832 {
833 /* re-enable theme since we're returning to SBS */
834 gwps_leave_wps(true);
835 return GO_TO_ROOT;
836 }
837 restore = true;
838 break;
839 }
840 }
841 /* fall through */
842#endif /* def HAVE_HOTKEY */
843 case ACTION_WPS_CONTEXT:
844 {
845 gwps_leave_wps(true);
846 int retval = onplay(state->id3->path,
847 FILE_ATTR_AUDIO, CONTEXT_WPS, hotkey, ONPLAY_NO_CUSTOMACTION);
848 /* if music is stopped in the context menu we want to exit the wps */
849 if (retval == ONPLAY_MAINMENU
850 || !audio_status())
851 return GO_TO_ROOT;
852 else if (retval == ONPLAY_PLAYLIST)
853 return GO_TO_PLAYLIST_VIEWER;
854 else if (retval == ONPLAY_PLUGIN)
855 {
856 restore_theme();
857 theme_enabled = false;
858 open_plugin_run(ID2P(LANG_OPEN_PLUGIN_SET_WPS_CONTEXT_PLUGIN));
859 }
860
861 restore = true;
862 }
863 break;
864
865 case ACTION_WPS_BROWSE:
866 gwps_leave_wps(true);
867 return GO_TO_PREVIOUS_BROWSER;
868 break;
869
870 /* play/pause */
871 case ACTION_WPS_PLAY:
872 wps_do_action(WPS_PLAYPAUSE, true);
873 break;
874
875 case ACTION_WPS_VOLUP: /* fall through */
876 case ACTION_WPS_VOLDOWN:
877 if (button == ACTION_WPS_VOLUP)
878 adjust_volume(1);
879 else
880 adjust_volume(-1);
881
882 setvol();
883 FOR_NB_SCREENS(i)
884 {
885 skin_update(WPS, i, SKIN_REFRESH_NON_STATIC);
886 }
887 update = false;
888 break;
889 /* fast forward
890 OR next dir if this is straight after ACTION_WPS_SKIPNEXT */
891 case ACTION_WPS_SEEKFWD:
892 if (current_tick -last_right < HZ)
893 {
894 if (state->id3->cuesheet && playlist_check(1))
895 {
896 audio_next();
897 }
898 else
899 {
900 change_dir(1);
901 }
902 }
903 else
904 ffwd_rew(ACTION_WPS_SEEKFWD, false);
905 last_right = last_left = 0;
906 break;
907 /* fast rewind
908 OR prev dir if this is straight after ACTION_WPS_SKIPPREV,*/
909 case ACTION_WPS_SEEKBACK:
910 if (current_tick - last_left < HZ)
911 {
912 if (state->id3->cuesheet && playlist_check(-1))
913 {
914 audio_prev();
915 }
916 else
917 {
918 change_dir(-1);
919 }
920 } else if (global_settings.rewind_across_tracks
921 && get_wps_state()->id3->elapsed < DEFAULT_SKIP_THRESH
922 && playlist_check(-1))
923 {
924 if (!audio_paused)
925 audio_pause();
926 audio_prev();
927 ffwd_rew(ACTION_WPS_SEEKBACK, true);
928 if (!audio_paused)
929 audio_resume();
930 }
931 else
932 ffwd_rew(ACTION_WPS_SEEKBACK, false);
933 last_left = last_right = 0;
934 break;
935
936 /* prev / restart */
937 case ACTION_WPS_SKIPPREV:
938 last_left = current_tick;
939
940 /* if we're in A/B repeat mode and the current position
941 is past the A marker, jump back to the A marker... */
942 if ( ab_repeat_mode_enabled() && ab_after_A_marker(state->id3->elapsed) )
943 {
944 ab_jump_to_A_marker();
945 break;
946 }
947 else /* ...otherwise, do it normally */
948 play_hop(-1);
949 break;
950
951 /* next
952 OR if skip length set, hop by predetermined amount. */
953 case ACTION_WPS_SKIPNEXT:
954 last_right = current_tick;
955
956 /* if we're in A/B repeat mode and the current position is
957 before the A marker, jump to the A marker... */
958 if ( ab_repeat_mode_enabled() )
959 {
960 if ( ab_before_A_marker(state->id3->elapsed) )
961 {
962 ab_jump_to_A_marker();
963 break;
964 }
965 }
966 else /* ...otherwise, do it normally */
967 play_hop(1);
968 break;
969 /* next / prev directories */
970 /* and set A-B markers if in a-b mode */
971 case ACTION_WPS_ABSETB_NEXTDIR:
972 if (ab_repeat_mode_enabled())
973 {
974 ab_set_B_marker(state->id3->elapsed);
975 ab_jump_to_A_marker();
976 }
977 else
978 {
979 change_dir(1);
980 }
981 break;
982 case ACTION_WPS_ABSETA_PREVDIR:
983 if (ab_repeat_mode_enabled())
984 ab_set_A_marker(state->id3->elapsed);
985 else
986 {
987 change_dir(-1);
988 }
989 break;
990 /* menu key functions */
991 case ACTION_WPS_MENU:
992 gwps_leave_wps(true);
993 return GO_TO_ROOT;
994 break;
995
996
997#ifdef HAVE_QUICKSCREEN
998 case ACTION_WPS_QUICKSCREEN:
999 {
1000 gwps_leave_wps(true);
1001 bool enter_shortcuts_menu = global_settings.shortcuts_replaces_qs;
1002 if (!enter_shortcuts_menu)
1003 {
1004 int ret = quick_screen_quick(button);
1005 if (ret == QUICKSCREEN_IN_USB)
1006 return GO_TO_ROOT;
1007 else if (ret == QUICKSCREEN_GOTO_SHORTCUTS_MENU)
1008 enter_shortcuts_menu = true;
1009 else
1010 restore = true;
1011 }
1012
1013 if (enter_shortcuts_menu) /* enter_shortcuts_menu */
1014 {
1015 global_status.last_screen = GO_TO_SHORTCUTMENU;
1016 int ret = do_shortcut_menu(NULL);
1017 return (ret == GO_TO_PREVIOUS ? GO_TO_WPS : ret);
1018 }
1019 }
1020 break;
1021#endif /* HAVE_QUICKSCREEN */
1022
1023 /* screen settings */
1024
1025 /* pitch screen */
1026#ifdef HAVE_PITCHCONTROL
1027 case ACTION_WPS_PITCHSCREEN:
1028 {
1029 gwps_leave_wps(true);
1030 if (1 == gui_syncpitchscreen_run())
1031 return GO_TO_ROOT;
1032 restore = true;
1033 }
1034 break;
1035#endif /* HAVE_PITCHCONTROL */
1036
1037 /* reset A&B markers */
1038 case ACTION_WPS_ABRESET:
1039 if (ab_repeat_mode_enabled())
1040 {
1041 ab_reset_markers();
1042 update = true;
1043 }
1044 break;
1045
1046 /* stop and exit wps */
1047 case ACTION_WPS_STOP:
1048 bookmark = true;
1049 exit = true;
1050 break;
1051
1052 case ACTION_WPS_LIST_BOOKMARKS:
1053 gwps_leave_wps(true);
1054 if (bookmark_load_menu() == BOOKMARK_USB_CONNECTED)
1055 {
1056 return GO_TO_ROOT;
1057 }
1058 restore = true;
1059 break;
1060
1061 case ACTION_WPS_CREATE_BOOKMARK:
1062 gwps_leave_wps(true);
1063 bookmark_create_menu();
1064 restore = true;
1065 break;
1066
1067 case ACTION_WPS_ID3SCREEN:
1068 {
1069 gwps_leave_wps(true);
1070 if (browse_id3(audio_current_track(),
1071 playlist_get_display_index(),
1072 playlist_amount(), NULL, 1, NULL))
1073 return GO_TO_ROOT;
1074 restore = true;
1075 }
1076 break;
1077 /* this case is used by the softlock feature
1078 * it requests a full update here */
1079 case ACTION_REDRAW:
1080 skin_request_full_update(WPS);
1081 skin_request_full_update(CUSTOM_STATUSBAR); /* if SBS is used */
1082 break;
1083 case ACTION_NONE: /* Timeout, do a partial update */
1084 update = true;
1085 ffwd_rew(button, false); /* hopefully fix the ffw/rwd bug */
1086 break;
1087#ifdef HAVE_RECORDING
1088 case ACTION_WPS_REC:
1089 exit = true;
1090 break;
1091#endif
1092 case ACTION_WPS_VIEW_PLAYLIST:
1093 gwps_leave_wps(true);
1094 return GO_TO_PLAYLIST_VIEWER;
1095 break;
1096 default:
1097 switch(default_event_handler(button))
1098 { /* music has been stopped by the default handler */
1099 case SYS_USB_CONNECTED:
1100 case SYS_CALL_INCOMING:
1101 case BUTTON_MULTIMEDIA_STOP:
1102 gwps_leave_wps(true);
1103 return GO_TO_ROOT;
1104 }
1105 update = true;
1106 break;
1107 }
1108 }
1109 return GO_TO_ROOT; /* unreachable - just to reduce compiler warnings */
1110}
1111
1112struct wps_state *get_wps_state(void)
1113{
1114 return &wps_state;
1115}
1116
1117/* this is called from the playback thread so NO DRAWING! */
1118static void track_info_callback(unsigned short id, void *param)
1119{
1120 struct wps_state *state = get_wps_state();
1121
1122 if (id == PLAYBACK_EVENT_TRACK_CHANGE || id == PLAYBACK_EVENT_CUR_TRACK_READY)
1123 {
1124 state->id3 = ((struct track_event *)param)->id3;
1125 if (state->id3->cuesheet)
1126 {
1127 cue_find_current_track(state->id3->cuesheet, state->id3->elapsed);
1128 }
1129 }
1130#ifdef AUDIO_FAST_SKIP_PREVIEW
1131 else if (id == PLAYBACK_EVENT_TRACK_SKIP)
1132 {
1133 state->id3 = audio_current_track();
1134 }
1135#endif
1136 if (id == PLAYBACK_EVENT_NEXTTRACKID3_AVAILABLE)
1137 state->nid3 = audio_next_track();
1138 skin_request_full_update(WPS);
1139}
1140
1141static void wps_state_init(void)
1142{
1143 struct wps_state *state = get_wps_state();
1144 state->paused = false;
1145 if(audio_status() & AUDIO_STATUS_PLAY)
1146 {
1147 state->id3 = audio_current_track();
1148 state->nid3 = audio_next_track();
1149 }
1150 else
1151 {
1152 state->id3 = NULL;
1153 state->nid3 = NULL;
1154 }
1155 /* We'll be updating due to restore initialized with true */
1156 skin_request_full_update(WPS);
1157 /* add the WPS track event callbacks */
1158 add_event(PLAYBACK_EVENT_TRACK_CHANGE, track_info_callback);
1159 add_event(PLAYBACK_EVENT_NEXTTRACKID3_AVAILABLE, track_info_callback);
1160 /* Use the same callback as ..._TRACK_CHANGE for when remaining handles have
1161 finished */
1162 add_event(PLAYBACK_EVENT_CUR_TRACK_READY, track_info_callback);
1163#ifdef AUDIO_FAST_SKIP_PREVIEW
1164 add_event(PLAYBACK_EVENT_TRACK_SKIP, track_info_callback);
1165#endif
1166}