rockbox/apps/plugins/mpegplayer/stream_mgr.c

1087 lines
25 KiB
C
Raw Normal View History

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* AV stream manager implementation
*
* Copyright (c) 2007 Michael Sevakis
*
* 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.
*
****************************************************************************/
#include "plugin.h"
#include "mpegplayer.h"
#include "lib/grey.h"
#include "mpeg_settings.h"
#ifndef HAVE_LCD_COLOR
GREY_INFO_STRUCT_IRAM
#endif
static struct event_queue stream_mgr_queue SHAREDBSS_ATTR;
static struct queue_sender_list stream_mgr_queue_send SHAREDBSS_ATTR;
static uint32_t stream_mgr_thread_stack[DEFAULT_STACK_SIZE*2/sizeof(uint32_t)];
struct stream_mgr stream_mgr SHAREDBSS_ATTR;
/* Forward decs */
static int stream_on_close(void);
struct str_broadcast_data
{
long cmd; /* Command to send to stream */
intptr_t data; /* Data to send with command */
};
static inline void stream_mgr_lock(void)
{
rb->mutex_lock(&stream_mgr.str_mtx);
}
static inline void stream_mgr_unlock(void)
{
rb->mutex_unlock(&stream_mgr.str_mtx);
}
static inline void actl_lock(void)
{
rb->mutex_lock(&stream_mgr.actl_mtx);
}
static inline void actl_unlock(void)
{
rb->mutex_unlock(&stream_mgr.actl_mtx);
}
static inline void stream_mgr_post_msg(long id, intptr_t data)
{
rb->queue_post(stream_mgr.q, id, data);
}
static inline intptr_t stream_mgr_send_msg(long id, intptr_t data)
{
return rb->queue_send(stream_mgr.q, id, data);
}
static inline void stream_mgr_reply_msg(intptr_t retval)
{
rb->queue_reply(stream_mgr.q, retval);
}
int str_next_data_not_ready(struct stream *str)
{
/* Save the current window since it actually might be ready by the time
* the registration is received by buffering. */
off_t win_right = str->hdr.win_right;
if (str->hdr.win_right < disk_buf.filesize - MIN_BUFAHEAD &&
disk_buf.filesize > MIN_BUFAHEAD)
{
/* Set right edge to where probing left off + the minimum margin */
str->hdr.win_right += MIN_BUFAHEAD;
}
else
{
/* Request would be passed the end of the file */
str->hdr.win_right = disk_buf.filesize;
}
switch (disk_buf_send_msg(DISK_BUF_DATA_NOTIFY, (intptr_t)str))
{
case DISK_BUF_NOTIFY_OK:
/* Was ready - restore window and process */
str->hdr.win_right = win_right;
return STREAM_OK;
case DISK_BUF_NOTIFY_ERROR:
/* Error - quit parsing */
str_end_of_stream(str);
return STREAM_DATA_END;
default:
/* Not ready - go wait for notification from buffering. */
str->pkt_flags = 0;
return STREAM_DATA_NOT_READY;
}
}
void str_data_notify_received(struct stream *str)
{
/* Normalize win_right back to the packet length */
if (str->state == SSTATE_END)
return;
if (str->curr_packet == NULL)
{
/* Nothing was yet parsed since init */
str->hdr.win_right = str->hdr.win_left;
}
else
{
/* Restore window based upon current packet */
str->hdr.win_right = str->hdr.win_left +
(str->curr_packet_end - str->curr_packet);
}
}
/* Set stream manager to a "no-file" state */
static void stream_mgr_init_state(void)
{
stream_mgr.filename = NULL;
stream_mgr.resume_time = INVALID_TIMESTAMP;
stream_mgr.seeked = false;
}
/* Add a stream to the playback pool */
void stream_add_stream(struct stream *str)
{
actl_lock();
list_remove_item(&str->l);
list_add_item(&stream_mgr.strl, &str->l);
actl_unlock();
}
/* Callback for various list-moving operations */
static bool strl_enum_callback(struct list_item *item, intptr_t data)
{
actl_lock();
list_remove_item(item);
if (data == 1)
list_add_item(&stream_mgr.actl, item);
actl_unlock();
return true;
}
/* Clear all streams from active and playback pools */
void stream_remove_streams(void)
{
list_enum_items(&stream_mgr.strl, strl_enum_callback, 0);
}
/* Move the playback pool to the active list */
void move_strl_to_actl(void)
{
list_enum_items(&stream_mgr.strl, strl_enum_callback, 1);
}
/* Remove a stream from the active list and return it to the pool */
static bool actl_stream_remove(struct stream *str)
{
if (list_is_member(&stream_mgr.actl, &str->l))
{
actl_lock();
list_remove_item(&str->l);
list_add_item(&stream_mgr.strl, &str->l);
actl_unlock();
return true;
}
return false;
}
/* Broadcast a message to all active streams */
static bool actl_stream_broadcast_callback(struct list_item *item,
struct str_broadcast_data *sbd)
{
struct stream *str = TYPE_FROM_MEMBER(struct stream, item, l);
switch (sbd->cmd)
{
case STREAM_PLAY:
case STREAM_PAUSE:
break;
case STREAM_STOP:
if (sbd->data != 0)
{
actl_lock();
list_remove_item(item);
list_add_item(&stream_mgr.strl, item);
actl_unlock();
sbd->data = 0;
}
break;
default:
return false;
}
str_send_msg(str, sbd->cmd, sbd->data);
return true;
}
static void actl_stream_broadcast(int cmd, intptr_t data)
{
struct str_broadcast_data sbd;
sbd.cmd = cmd;
sbd.data = data;
list_enum_items(&stream_mgr.actl,
(list_enum_callback_t)actl_stream_broadcast_callback,
(intptr_t)&sbd);
}
/* Set the current base clock */
static void set_stream_clock(uint32_t time)
{
/* Fudge: Start clock 100ms early to allow for some filling time */
if (time > 100*TS_SECOND/1000)
time -= 100*TS_SECOND/1000;
else
time = 0;
pcm_output_set_clock(TS_TO_TICKS(time));
}
static void stream_start_playback(uint32_t time, bool fill_buffer)
{
if (stream_mgr.seeked)
{
/* Clear any seeked status */
stream_mgr.seeked = false;
/* Flush old PCM data */
pcm_output_flush();
/* Set the master clock */
set_stream_clock(time);
/* Make sure streams are back in active pool */
move_strl_to_actl();
/* Prepare the parser and associated streams */
parser_prepare_streaming();
}
/* Start buffer which optional force fill */
disk_buf_send_msg(STREAM_PLAY, fill_buffer);
/* Tell each stream to start - may generate end of stream signals
* now - we'll handle this when finished */
actl_stream_broadcast(STREAM_PLAY, 0);
/* Actually start the clock */
pcm_output_play_pause(true);
}
/* Return the play time relative to the specified play time */
static uint32_t time_from_whence(uint32_t time, int whence)
{
int64_t currtime;
uint32_t start;
switch (whence)
{
case SEEK_SET:
/* Set the current time (time = unsigned offset from 0) */
if (time > str_parser.duration)
time = str_parser.duration;
break;
case SEEK_CUR:
/* Seek forward or backward from the current time
* (time = signed offset from current) */
currtime = stream_get_seek_time(&start);
currtime -= start;
currtime += (int32_t)time;
if (currtime < 0)
currtime = 0;
else if ((uint64_t)currtime > str_parser.duration)
currtime = str_parser.duration;
time = (uint32_t)currtime;
break;
case SEEK_END:
/* Seek from the end (time = unsigned offset from end) */
if (time > str_parser.duration)
time = str_parser.duration;
time = str_parser.duration - time;
break;
}
return time;
}
/* Handle seeking details if playing or paused */
static uint32_t stream_seek_intl(uint32_t time, int whence,
int status, bool *was_buffering)
{
if (status != STREAM_STOPPED)
{
bool wb;
/* Place streams in a non-running state - keep them on actl */
actl_stream_broadcast(STREAM_STOP, 0);
/* Stop all buffering or else risk clobbering random-access data */
wb = disk_buf_send_msg(STREAM_STOP, 0);
if (was_buffering != NULL)
*was_buffering = wb;
}
time = time_from_whence(time, whence);
stream_mgr.seeked = true;
return parser_seek_time(time);
}
/* Store the resume time at the last seek/current clock point */
static void stream_remember_resume_time(void)
{
/* Assume invalidity */
stream_mgr.resume_time = 0;
if (stream_can_seek())
{
/* Read the current stream time or the last seeked position */
uint32_t start;
uint32_t time = stream_get_seek_time(&start);
if (time >= str_parser.start_pts && time <= str_parser.end_pts)
{
/* Save the current stream time */
stream_mgr.resume_time = time - start;
}
}
}
/* Handle STREAM_OPEN */
void stream_on_open(const char *filename)
{
int err = STREAM_ERROR;
stream_mgr_lock();
trigger_cpu_boost();
/* Open the video file */
if (disk_buf_open(filename) >= 0)
{
/* Initialize the parser */
err = parser_init_stream();
if (err >= STREAM_OK)
{
/* File ok - save the opened filename */
stream_mgr.filename = filename;
}
}
/* If error - cleanup */
if (err < STREAM_OK)
stream_on_close();
cancel_cpu_boost();
stream_mgr_unlock();
stream_mgr_reply_msg(err);
}
/* Handler STREAM_PLAY */
static void stream_on_play(void)
{
int status = stream_mgr.status;
stream_mgr_lock();
if (status == STREAM_STOPPED)
{
uint32_t start;
/* We just say we're playing now */
stream_mgr.status = STREAM_PLAYING;
/* Reply with previous state */
stream_mgr_reply_msg(status);
trigger_cpu_boost();
/* Seek to initial position and set clock to that time */
/* Save the resume time */
stream_remember_resume_time();
/* Prepare seek to start point */
start = stream_seek_intl(stream_mgr.resume_time, SEEK_SET,
STREAM_STOPPED, NULL);
/* Sync and start - force buffer fill */
stream_start_playback(start, true);
}
else
{
/* Reply with previous state */
stream_mgr_reply_msg(status);
}
stream_mgr_unlock();
}
/* Handle STREAM_PAUSE */
static void stream_on_pause(void)
{
int status = stream_mgr.status;
stream_mgr_lock();
/* Reply with previous state */
stream_mgr_reply_msg(status);
if (status == STREAM_PLAYING)
{
/* Pause the clock */
pcm_output_play_pause(false);
/* Pause each active stream */
actl_stream_broadcast(STREAM_PAUSE, 0);
/* Pause the disk buffer - buffer may continue filling */
disk_buf_send_msg(STREAM_PAUSE, false);
/* Unboost the CPU */
cancel_cpu_boost();
/* Offically paused */
stream_mgr.status = STREAM_PAUSED;
}
stream_mgr_unlock();
}
/* Handle STREAM_RESUME */
static void stream_on_resume(void)
{
int status = stream_mgr.status;
stream_mgr_lock();
/* Reply with previous state */
stream_mgr_reply_msg(status);
if (status == STREAM_PAUSED)
{
/* Boost the CPU */
trigger_cpu_boost();
/* Sync and start - no force buffering */
stream_start_playback(str_parser.last_seek_time, false);
/* Officially playing */
stream_mgr.status = STREAM_PLAYING;
}
stream_mgr_unlock();
}
/* Handle STREAM_STOP */
static void stream_on_stop(bool reply)
{
int status = stream_mgr.status;
stream_mgr_lock();
if (reply)
stream_mgr_reply_msg(status);
if (status != STREAM_STOPPED)
{
/* Pause the clock */
pcm_output_play_pause(false);
/* Update the resume time info */
stream_remember_resume_time();
/* Not stopped = paused or playing */
stream_mgr.seeked = false;
/* Stop buffering */
disk_buf_send_msg(STREAM_STOP, 0);
/* Clear any still-active streams and remove from actl */
actl_stream_broadcast(STREAM_STOP, 1);
/* Stop PCM output (and clock) */
pcm_output_stop();
/* Cancel our processor boost */
cancel_cpu_boost();
stream_mgr.status = STREAM_STOPPED;
}
stream_mgr_unlock();
}
/* Handle STREAM_SEEK */
static void stream_on_seek(struct stream_seek_data *skd)
{
uint32_t time = skd->time;
int whence = skd->whence;
switch (whence)
{
case SEEK_SET:
case SEEK_CUR:
case SEEK_END:
if (stream_mgr.filename == NULL)
break;
/* Keep things spinning if already doing so */
stream_keep_disk_active();
/* Have data - reply in order to acquire lock */
stream_mgr_reply_msg(STREAM_OK);
stream_mgr_lock();
if (stream_can_seek())
{
bool buffer;
if (stream_mgr.status == STREAM_PLAYING)
{
/* Keep clock from advancing while seeking */
pcm_output_play_pause(false);
}
time = stream_seek_intl(time, whence, stream_mgr.status, &buffer);
stream_remember_resume_time();
if (stream_mgr.status == STREAM_PLAYING)
{
/* Sync and restart - no force buffering */
stream_start_playback(time, buffer);
}
}
stream_mgr_unlock();
return;
}
/* Invalid parameter or no file */
stream_mgr_reply_msg(STREAM_ERROR);
}
/* Handle STREAM_CLOSE */
static int stream_on_close(void)
{
int status = STREAM_STOPPED;
stream_mgr_lock();
/* Any open file? */
if (stream_mgr.filename != NULL)
{
/* Yes - hide video */
stream_show_vo(false);
/* Stop any playback */
status = stream_mgr.status;
stream_on_stop(false);
/* Tell parser file is finished */
parser_close_stream();
/* Close file */
disk_buf_close();
/* Reinitialize manager */
stream_mgr_init_state();
}
stream_mgr_unlock();
return status;
}
/* Handle STREAM_EV_COMPLETE */
static void stream_on_ev_complete(struct stream *str)
{
stream_mgr_lock();
/* Stream is active? */
if (actl_stream_remove(str))
{
/* No - remove this stream from the active list */
DEBUGF(" finished: 0x%02x\n", str->id);
if (list_is_empty(&stream_mgr.actl))
{
/* All streams have acked - stop playback */
stream_on_stop(false);
stream_mgr.resume_time = 0; /* Played to end - no resume */
}
else
{
/* Stream is done - stop it and place back in pool */
str_send_msg(str, STREAM_STOP, 1);
}
}
stream_mgr_unlock();
}
/* Callback for stream to notify about events internal to them */
void stream_generate_event(struct stream *str, long id, intptr_t data)
{
if (str == NULL)
return;
switch (id)
{
case STREAM_EV_COMPLETE:
/* The last stream has ended */
stream_mgr_post_msg(STREAM_EV_COMPLETE, (intptr_t)str);
break;
}
(void)data;
}
/* Clear any particular notification for which a stream registered */
void stream_clear_notify(struct stream *str, int for_msg)
{
switch (for_msg)
{
case DISK_BUF_DATA_NOTIFY:
disk_buf_send_msg(DISK_BUF_CLEAR_DATA_NOTIFY, (intptr_t)str);
break;
}
}
/* Show/hide the video output */
bool stream_show_vo(bool show)
{
bool vis;
stream_mgr_lock();
vis = parser_send_video_msg(VIDEO_DISPLAY_SHOW, show);
#ifndef HAVE_LCD_COLOR
grey_show(show);
#endif
stream_mgr_unlock();
return vis;
}
/* Query the visibility of video output */
bool stream_vo_is_visible(void)
{
bool vis;
stream_mgr_lock();
vis = parser_send_video_msg(VIDEO_DISPLAY_IS_VISIBLE, 0);
stream_mgr_unlock();
return vis;
}
/* Return the video dimensions */
bool stream_vo_get_size(struct vo_ext *sz)
{
bool retval = false;
stream_mgr_lock();
if (str_parser.dims.w > 0 && str_parser.dims.h > 0)
{
*sz = str_parser.dims;
retval = true;
}
stream_mgr_unlock();
return retval;
}
void stream_vo_set_clip(const struct vo_rect *rc)
{
stream_mgr_lock();
if (rc)
{
stream_mgr.parms.rc = *rc;
rc = &stream_mgr.parms.rc;
}
parser_send_video_msg(VIDEO_SET_CLIP_RECT, (intptr_t)rc);
stream_mgr_unlock();
}
#ifndef HAVE_LCD_COLOR
/* Show/hide the gray video overlay (independently of vo visibility). */
void stream_gray_show(bool show)
{
stream_mgr_lock();
grey_show(show);
stream_mgr_unlock();
}
#endif /* !HAVE_LCD_COLOR */
/* Display a thumbnail at the last seek point */
bool stream_display_thumb(const struct vo_rect *rc)
{
bool retval;
if (rc == NULL)
return false;
stream_mgr_lock();
stream_mgr.parms.rc = *rc;
retval = parser_send_video_msg(VIDEO_PRINT_THUMBNAIL,
(intptr_t)&stream_mgr.parms.rc);
stream_mgr_unlock();
return retval;
}
bool stream_draw_frame(bool no_prepare)
{
bool retval;
stream_mgr_lock();
retval = parser_send_video_msg(VIDEO_PRINT_FRAME, no_prepare);
stream_mgr_unlock();
return retval;
}
/* Return the time playback should resume if interrupted */
uint32_t stream_get_resume_time(void)
{
uint32_t resume_time;
/* A stop request is async and replies before setting this - must lock */
stream_mgr_lock();
resume_time = stream_mgr.resume_time;
stream_mgr_unlock();
return resume_time;
}
uint32_t stream_get_seek_time(uint32_t *start)
{
uint32_t time;
stream_mgr_lock();
if (stream_mgr.seeked)
{
time = str_parser.last_seek_time;
}
else
{
time = TICKS_TO_TS(pcm_output_get_clock());
/* Clock can be start early so keep in range */
if (time < str_parser.start_pts)
time = str_parser.start_pts;
}
if (start != NULL)
*start = str_parser.start_pts;
stream_mgr_unlock();
return time;
}
/* Wait for a state transistion to complete */
void stream_wait_status(void)
{
stream_mgr_lock();
stream_mgr_unlock();
}
/* Returns the smallest file window that includes all active streams'
* windows */
static bool stream_get_window_callback(struct list_item *item,
struct stream_window *sw)
{
struct stream *str = TYPE_FROM_MEMBER(struct stream, item, l);
off_t swl = str->hdr.win_left;
off_t swr = str->hdr.win_right;
if (swl < sw->left)
sw->left = swl;
if (swr > sw->right)
sw->right = swr;
return true;
}
bool stream_get_window(struct stream_window *sw)
{
if (sw == NULL)
return false;
sw->left = LONG_MAX;
sw->right = LONG_MIN;
actl_lock();
list_enum_items(&stream_mgr.actl,
(list_enum_callback_t)stream_get_window_callback,
(intptr_t)sw);
actl_unlock();
return sw->left <= sw->right;
}
/* Playback control thread */
static void stream_mgr_thread(void)
{
struct queue_event ev;
while (1)
{
rb->queue_wait(stream_mgr.q, &ev);
switch (ev.id)
{
case STREAM_OPEN:
stream_on_open((const char *)ev.data);
break;
case STREAM_CLOSE:
stream_on_close();
break;
case STREAM_PLAY:
stream_on_play();
break;
case STREAM_PAUSE:
if (ev.data)
stream_on_resume();
else
stream_on_pause();
break;
case STREAM_STOP:
stream_on_stop(true);
break;
case STREAM_SEEK:
stream_on_seek((struct stream_seek_data *)ev.data);
break;
case STREAM_EV_COMPLETE:
stream_on_ev_complete((struct stream *)ev.data);
break;
case STREAM_QUIT:
if (stream_mgr.status != STREAM_STOPPED)
stream_on_stop(false);
return;
}
}
}
/* Stream command interface APIs */
/* Opens a new file */
int stream_open(const char *filename)
{
if (stream_mgr.thread != 0)
return stream_mgr_send_msg(STREAM_OPEN, (intptr_t)filename);
return STREAM_ERROR;
}
/* Plays the current file starting at time 'start' */
int stream_play(void)
{
if (stream_mgr.thread != 0)
return stream_mgr_send_msg(STREAM_PLAY, 0);
return STREAM_ERROR;
}
/* Pauses playback if playing */
int stream_pause(void)
{
if (stream_mgr.thread != 0)
return stream_mgr_send_msg(STREAM_PAUSE, false);
return STREAM_ERROR;
}
/* Resumes playback if paused */
int stream_resume(void)
{
if (stream_mgr.thread != 0)
return stream_mgr_send_msg(STREAM_PAUSE, true);
return STREAM_ERROR;
}
/* Stops playback if not stopped */
int stream_stop(void)
{
if (stream_mgr.thread != 0)
return stream_mgr_send_msg(STREAM_STOP, 0);
return STREAM_ERROR;
}
/* Seeks playback time to/by the specified time */
int stream_seek(uint32_t time, int whence)
{
int ret;
if (stream_mgr.thread == 0)
return STREAM_ERROR;
stream_mgr_lock();
stream_mgr.parms.skd.time = time;
stream_mgr.parms.skd.whence = whence;
ret = stream_mgr_send_msg(STREAM_SEEK, (intptr_t)&stream_mgr.parms.skd);
stream_mgr_unlock();
return ret;
}
/* Closes the current file */
int stream_close(void)
{
if (stream_mgr.thread != 0)
return stream_mgr_send_msg(STREAM_CLOSE, 0);
return STREAM_ERROR;
}
/* Initializes the playback engine */
int stream_init(void)
{
void *mem;
size_t memsize;
stream_mgr.status = STREAM_STOPPED;
stream_mgr_init_state();
list_initialize(&stream_mgr.actl);
/* Initialize our window to the outside world first */
rb->mutex_init(&stream_mgr.str_mtx);
rb->mutex_init(&stream_mgr.actl_mtx);
stream_mgr.q = &stream_mgr_queue;
rb->queue_init(stream_mgr.q, false);
/* sets audiosize and returns buffer pointer */
mem = rb->plugin_get_audio_buffer(&memsize);
/* Initialize non-allocator blocks first */
#ifndef HAVE_LCD_COLOR
long greysize;
/* Greylib init handles all necessary cache alignment */
if (!grey_init(mem, memsize, GREY_BUFFERED|GREY_ON_COP,
LCD_WIDTH, LCD_HEIGHT, &greysize))
{
rb->splash(HZ, "greylib init failed!");
return STREAM_ERROR;
}
mem += greysize;
memsize -= greysize;
grey_clear_display();
#endif /* !HAVE_LCD_COLOR */
stream_mgr.thread = rb->create_thread(stream_mgr_thread,
stream_mgr_thread_stack, sizeof(stream_mgr_thread_stack),
0, "mpgstream_mgr" IF_PRIO(, PRIORITY_SYSTEM) IF_COP(, CPU));
rb->queue_enable_queue_send(stream_mgr.q, &stream_mgr_queue_send,
stream_mgr.thread);
if (stream_mgr.thread == 0)
{
rb->splash(HZ, "Could not create stream manager thread!");
return STREAM_ERROR;
}
/* Wait for thread to initialize */
stream_mgr_send_msg(STREAM_NULL, 0);
/* Initialise our malloc buffer */
if (!mpeg_alloc_init(mem, memsize))
{
rb->splash(HZ, "Out of memory in stream_init");
}
/* These inits use the allocator */
else if (!pcm_output_init())
{
rb->splash(HZ, "Could not initialize PCM!");
}
else if (!audio_thread_init())
{
rb->splash(HZ, "Cannot create audio thread!");
}
else if (!video_thread_init())
{
rb->splash(HZ, "Cannot create video thread!");
}
/* Disk buffer takes max allotment of what's left so it must be last */
else if (!disk_buf_init())
{
rb->splash(HZ, "Cannot create buffering thread!");
}
else if (!parser_init())
{
rb->splash(HZ, "Parser init failed!");
}
else
{
return STREAM_OK;
}
return STREAM_ERROR;
}
/* Cleans everything up */
void stream_exit(void)
{
stream_close();
/* Stop the threads and wait for them to terminate */
video_thread_exit();
audio_thread_exit();
disk_buf_exit();
pcm_output_exit();
if (stream_mgr.thread != 0)
{
stream_mgr_post_msg(STREAM_QUIT, 0);
rb->thread_wait(stream_mgr.thread);
stream_mgr.thread = 0;
}
#ifndef HAVE_LCD_COLOR
grey_release();
#endif
}