Make open() posix compliant api-wise. A few calls (those with O_CREAT) need the addit...
[kugel-rb.git] / apps / plugins / test_codec.c
blob1ecf225529710d4e141b471823b72211f441415c
1 /***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
10 * Copyright (C) 2007 Dave Chapman
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.
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
20 ****************************************************************************/
21 #include "plugin.h"
23 PLUGIN_HEADER
25 /* All swcodec targets have BUTTON_SELECT apart from the H10 and M3 */
27 #if CONFIG_KEYPAD == IRIVER_H10_PAD
28 #define TESTCODEC_EXITBUTTON BUTTON_RIGHT
29 #elif CONFIG_KEYPAD == IAUDIO_M3_PAD
30 #define TESTCODEC_EXITBUTTON BUTTON_RC_PLAY
31 #elif CONFIG_KEYPAD == SAMSUNG_YH_PAD
32 #define TESTCODEC_EXITBUTTON BUTTON_PLAY
33 #elif CONFIG_KEYPAD == COWON_D2_PAD || CONFIG_KEYPAD == ONDAVX747_PAD
34 #define TESTCODEC_EXITBUTTON BUTTON_POWER
35 #elif CONFIG_KEYPAD == PBELL_VIBE500_PAD
36 #define TESTCODEC_EXITBUTTON BUTTON_REC
37 #elif defined(HAVE_TOUCHSCREEN)
38 #define TESTCODEC_EXITBUTTON BUTTON_TOPLEFT
39 #else
40 #define TESTCODEC_EXITBUTTON BUTTON_SELECT
41 #endif
43 /* Log functions copied from test_disk.c */
44 static int line = 0;
45 static int max_line = 0;
46 static int log_fd = -1;
47 static char logfilename[MAX_PATH];
49 static bool log_init(bool use_logfile)
51 int h;
53 rb->lcd_getstringsize("A", NULL, &h);
54 max_line = LCD_HEIGHT / h;
55 line = 0;
56 rb->lcd_clear_display();
57 rb->lcd_update();
59 if (use_logfile) {
60 rb->create_numbered_filename(logfilename, "/", "test_codec_log_", ".txt",
61 2 IF_CNFN_NUM_(, NULL));
62 log_fd = rb->open(logfilename, O_RDWR|O_CREAT|O_TRUNC, 0666);
63 return log_fd >= 0;
66 return true;
69 static void log_text(char *text, bool advance)
71 rb->lcd_puts(0, line, text);
72 rb->lcd_update();
73 if (advance)
75 if (++line >= max_line)
76 line = 0;
77 if (log_fd >= 0)
78 rb->fdprintf(log_fd, "%s\n", text);
82 static void log_close(void)
84 if (log_fd >= 0)
85 rb->close(log_fd);
88 struct wavinfo_t
90 int fd;
91 int samplerate;
92 int channels;
93 int sampledepth;
94 int stereomode;
95 int totalsamples;
98 static void* audiobuf;
99 static void* codec_mallocbuf;
100 static size_t audiosize;
101 static char str[MAX_PATH];
103 /* Our local implementation of the codec API */
104 static struct codec_api ci;
106 struct test_track_info {
107 struct mp3entry id3; /* TAG metadata */
108 size_t filesize; /* File total length */
111 static struct test_track_info track;
112 static bool taginfo_ready = true;
114 static bool use_dsp;
116 static bool checksum;
117 static uint32_t crc32;
119 static volatile unsigned int elapsed;
120 static volatile bool codec_playing;
121 static volatile long endtick;
122 struct wavinfo_t wavinfo;
124 static unsigned char wav_header[44] =
126 'R','I','F','F', // 0 - ChunkID
127 0,0,0,0, // 4 - ChunkSize (filesize-8)
128 'W','A','V','E', // 8 - Format
129 'f','m','t',' ', // 12 - SubChunkID
130 16,0,0,0, // 16 - SubChunk1ID // 16 for PCM
131 1,0, // 20 - AudioFormat (1=16-bit)
132 0,0, // 22 - NumChannels
133 0,0,0,0, // 24 - SampleRate in Hz
134 0,0,0,0, // 28 - Byte Rate (SampleRate*NumChannels*(BitsPerSample/8)
135 0,0, // 32 - BlockAlign (== NumChannels * BitsPerSample/8)
136 16,0, // 34 - BitsPerSample
137 'd','a','t','a', // 36 - Subchunk2ID
138 0,0,0,0 // 40 - Subchunk2Size
141 static inline void int2le32(unsigned char* buf, int32_t x)
143 buf[0] = (x & 0xff);
144 buf[1] = (x & 0xff00) >> 8;
145 buf[2] = (x & 0xff0000) >> 16;
146 buf[3] = (x & 0xff000000) >>24;
149 static inline void int2le24(unsigned char* buf, int32_t x)
151 buf[0] = (x & 0xff);
152 buf[1] = (x & 0xff00) >> 8;
153 buf[2] = (x & 0xff0000) >> 16;
156 static inline void int2le16(unsigned char* buf, int16_t x)
158 buf[0] = (x & 0xff);
159 buf[1] = (x & 0xff00) >> 8;
162 /* 32KB should be enough */
163 static unsigned char wavbuffer[32*1024];
164 static unsigned char dspbuffer[32*1024];
166 void init_wav(char* filename)
168 wavinfo.totalsamples = 0;
170 wavinfo.fd = rb->creat(filename, 0666);
172 if (wavinfo.fd >= 0)
174 /* Write WAV header - we go back and fill in the details at the end */
175 rb->write(wavinfo.fd, wav_header, sizeof(wav_header));
180 void close_wav(void)
182 int filesize = rb->filesize(wavinfo.fd);
183 int channels = (wavinfo.stereomode == STEREO_MONO) ? 1 : 2;
184 int bps = 16; /* TODO */
186 /* We assume 16-bit, Stereo */
188 rb->lseek(wavinfo.fd,0,SEEK_SET);
190 int2le32(wav_header+4, filesize-8); /* ChunkSize */
192 int2le16(wav_header+22, channels);
194 int2le32(wav_header+24, wavinfo.samplerate);
196 int2le32(wav_header+28, wavinfo.samplerate * channels * (bps / 8)); /* ByteRate */
198 int2le16(wav_header+32, channels * (bps / 8));
200 int2le32(wav_header+40, filesize - 44); /* Subchunk2Size */
202 rb->write(wavinfo.fd, wav_header, sizeof(wav_header));
204 rb->close(wavinfo.fd);
207 /* Returns buffer to malloc array. Only codeclib should need this. */
208 static void* codec_get_buffer(size_t *size)
210 DEBUGF("codec_get_buffer(%d)\n",(int)size);
211 *size = CODEC_SIZE;
212 return codec_mallocbuf;
215 static int process_dsp(const void *ch1, const void *ch2, int count)
217 const char *src[2] = { ch1, ch2 };
218 int written_count = 0;
219 char *dest = dspbuffer;
221 while (count > 0)
223 int out_count = rb->dsp_output_count(ci.dsp, count);
225 int inp_count = rb->dsp_input_count(ci.dsp, out_count);
227 if (inp_count <= 0)
228 break;
230 if (inp_count > count)
231 inp_count = count;
233 out_count = rb->dsp_process(ci.dsp, dest, src, inp_count);
235 if (out_count <= 0)
236 break;
238 written_count += out_count;
239 dest += out_count * 4;
241 count -= inp_count;
244 return written_count;
247 static inline int32_t clip_sample(int32_t sample)
249 if ((int16_t)sample != sample)
250 sample = 0x7fff ^ (sample >> 31);
252 return sample;
255 /* Null output */
256 static void pcmbuf_insert_null(const void *ch1, const void *ch2, int count)
258 if (use_dsp)
259 process_dsp(ch1, ch2, count);
261 /* Prevent idle poweroff */
262 rb->reset_poweroff_timer();
265 /* WAV output or calculate crc32 of output*/
266 static void pcmbuf_insert_wav_checksum(const void *ch1, const void *ch2, int count)
268 const int16_t* data1_16;
269 const int16_t* data2_16;
270 const int32_t* data1_32;
271 const int32_t* data2_32;
272 unsigned char* p = wavbuffer;
273 const int scale = wavinfo.sampledepth - 15;
274 const int dc_bias = 1 << (scale - 1);
275 int channels = (wavinfo.stereomode == STEREO_MONO) ? 1 : 2;
277 /* Prevent idle poweroff */
278 rb->reset_poweroff_timer();
280 if (use_dsp) {
281 count = process_dsp(ch1, ch2, count);
282 wavinfo.totalsamples += count;
283 if (channels == 1)
285 unsigned char *s = dspbuffer, *d = dspbuffer;
286 int c = count;
287 while (c-- > 0)
289 *d++ = *s++;
290 *d++ = *s++;
291 s++;
292 s++;
295 if (checksum)
296 crc32 = rb->crc_32(dspbuffer, count * 2 * channels, crc32);
297 else
298 rb->write(wavinfo.fd, dspbuffer, count * 2 * channels);
300 else
302 if (wavinfo.sampledepth <= 16) {
303 data1_16 = ch1;
304 data2_16 = ch2;
306 switch(wavinfo.stereomode)
308 case STEREO_INTERLEAVED:
309 while (count--) {
310 int2le16(p,*data1_16++);
311 p += 2;
312 int2le16(p,*data1_16++);
313 p += 2;
315 break;
317 case STEREO_NONINTERLEAVED:
318 while (count--) {
319 int2le16(p,*data1_16++);
320 p += 2;
321 int2le16(p,*data2_16++);
322 p += 2;
325 break;
327 case STEREO_MONO:
328 while (count--) {
329 int2le16(p,*data1_16++);
330 p += 2;
332 break;
334 } else {
335 data1_32 = ch1;
336 data2_32 = ch2;
338 switch(wavinfo.stereomode)
340 case STEREO_INTERLEAVED:
341 while (count--) {
342 int2le16(p, clip_sample((*data1_32++ + dc_bias) >> scale));
343 p += 2;
344 int2le16(p, clip_sample((*data1_32++ + dc_bias) >> scale));
345 p += 2;
347 break;
349 case STEREO_NONINTERLEAVED:
350 while (count--) {
351 int2le16(p, clip_sample((*data1_32++ + dc_bias) >> scale));
352 p += 2;
353 int2le16(p, clip_sample((*data2_32++ + dc_bias) >> scale));
354 p += 2;
357 break;
359 case STEREO_MONO:
360 while (count--) {
361 int2le16(p, clip_sample((*data1_32++ + dc_bias) >> scale));
362 p += 2;
364 break;
368 wavinfo.totalsamples += count;
369 if (checksum)
370 crc32 = rb->crc_32(wavbuffer, p - wavbuffer, crc32);
371 else
372 rb->write(wavinfo.fd, wavbuffer, p - wavbuffer);
373 } /* else */
376 /* Set song position in WPS (value in ms). */
377 static void set_elapsed(unsigned long value)
379 elapsed = value;
383 /* Read next <size> amount bytes from file buffer to <ptr>.
384 Will return number of bytes read or 0 if end of file. */
385 static size_t read_filebuf(void *ptr, size_t size)
387 if (ci.curpos > (off_t)track.filesize)
389 return 0;
390 } else {
391 /* TODO: Don't read beyond end of buffer */
392 rb->memcpy(ptr, audiobuf + ci.curpos, size);
393 ci.curpos += size;
394 return size;
399 /* Request pointer to file buffer which can be used to read
400 <realsize> amount of data. <reqsize> tells the buffer system
401 how much data it should try to allocate. If <realsize> is 0,
402 end of file is reached. */
403 static void* request_buffer(size_t *realsize, size_t reqsize)
405 *realsize = MIN(track.filesize-ci.curpos,reqsize);
407 return (audiobuf + ci.curpos);
411 /* Advance file buffer position by <amount> amount of bytes. */
412 static void advance_buffer(size_t amount)
414 ci.curpos += amount;
418 /* Advance file buffer to a pointer location inside file buffer. */
419 static void advance_buffer_loc(void *ptr)
421 ci.curpos = ptr - audiobuf;
425 /* Seek file buffer to position <newpos> beginning of file. */
426 static bool seek_buffer(size_t newpos)
428 ci.curpos = newpos;
429 return true;
433 /* Codec should call this function when it has done the seeking. */
434 static void seek_complete(void)
436 /* Do nothing */
439 /* Request file change from file buffer. Returns true is next
440 track is available and changed. If return value is false,
441 codec should exit immediately with PLUGIN_OK status. */
442 static bool request_next_track(void)
444 /* We are only decoding a single track */
445 return false;
449 /* Free the buffer area of the current codec after its loaded */
450 static void discard_codec(void)
452 /* ??? */
456 static void set_offset(size_t value)
458 /* ??? */
459 (void)value;
463 /* Configure different codec buffer parameters. */
464 static void configure(int setting, intptr_t value)
466 if (use_dsp)
467 rb->dsp_configure(ci.dsp, setting, value);
468 switch(setting)
470 case DSP_SWITCH_FREQUENCY:
471 case DSP_SET_FREQUENCY:
472 DEBUGF("samplerate=%d\n",(int)value);
473 wavinfo.samplerate = (int)value;
474 break;
476 case DSP_SET_SAMPLE_DEPTH:
477 DEBUGF("sampledepth = %d\n",(int)value);
478 wavinfo.sampledepth=(int)value;
479 break;
481 case DSP_SET_STEREO_MODE:
482 DEBUGF("Stereo mode = %d\n",(int)value);
483 wavinfo.stereomode=(int)value;
484 break;
489 static void init_ci(void)
491 /* --- Our "fake" implementations of the codec API functions. --- */
493 ci.codec_get_buffer = codec_get_buffer;
495 if (wavinfo.fd >= 0 || checksum) {
496 ci.pcmbuf_insert = pcmbuf_insert_wav_checksum;
497 } else {
498 ci.pcmbuf_insert = pcmbuf_insert_null;
501 ci.set_elapsed = set_elapsed;
502 ci.read_filebuf = read_filebuf;
503 ci.request_buffer = request_buffer;
504 ci.advance_buffer = advance_buffer;
505 ci.advance_buffer_loc = advance_buffer_loc;
506 ci.seek_buffer = seek_buffer;
507 ci.seek_complete = seek_complete;
508 ci.request_next_track = request_next_track;
509 ci.discard_codec = discard_codec;
510 ci.set_offset = set_offset;
511 ci.configure = configure;
512 ci.dsp = (struct dsp_config *)rb->dsp_configure(NULL, DSP_MYDSP,
513 CODEC_IDX_AUDIO);
515 /* --- "Core" functions --- */
517 /* kernel/ system */
518 ci.sleep = rb->sleep;
519 ci.yield = rb->yield;
521 /* strings and memory */
522 ci.strcpy = rb->strcpy;
523 ci.strlen = rb->strlen;
524 ci.strcmp = rb->strcmp;
525 ci.strcat = rb->strcat;
526 ci.memset = rb->memset;
527 ci.memcpy = rb->memcpy;
528 ci.memmove = rb->memmove;
529 ci.memcmp = rb->memcmp;
530 ci.memchr = rb->memchr;
531 ci.strcasestr = rb->strcasestr;
532 #if defined(DEBUG) || defined(SIMULATOR)
533 ci.debugf = rb->debugf;
534 #endif
535 #ifdef ROCKBOX_HAS_LOGF
536 ci.logf = rb->logf;
537 #endif
539 ci.qsort = rb->qsort;
540 ci.global_settings = rb->global_settings;
542 #ifdef RB_PROFILE
543 ci.profile_thread = rb->profile_thread;
544 ci.profstop = rb->profstop;
545 ci.profile_func_enter = rb->profile_func_enter;
546 ci.profile_func_exit = rb->profile_func_exit;
547 #endif
549 #if NUM_CORES > 1
550 ci.cpucache_invalidate = rb->cpucache_invalidate;
551 ci.cpucache_flush = rb->cpucache_flush;
552 #endif
554 #if NUM_CORES > 1
555 ci.create_thread = rb->create_thread;
556 ci.thread_thaw = rb->thread_thaw;
557 ci.thread_wait = rb->thread_wait;
558 ci.semaphore_init = rb->semaphore_init;
559 ci.semaphore_wait = rb->semaphore_wait;
560 ci.semaphore_release = rb->semaphore_release;
561 #endif
563 #ifdef CPU_ARM
564 ci.__div0 = rb->__div0;
565 #endif
568 static void codec_thread(void)
570 const char* codecname;
571 int res;
573 codecname = rb->get_codec_filename(track.id3.codectype);
575 /* Load the codec and start decoding. */
576 res = rb->codec_load_file(codecname,&ci);
578 /* Signal to the main thread that we are done */
579 endtick = *rb->current_tick;
580 codec_playing = false;
583 static enum plugin_status test_track(const char* filename)
585 size_t n;
586 int fd;
587 enum plugin_status res = PLUGIN_ERROR;
588 long starttick;
589 long ticks;
590 unsigned long speed;
591 unsigned long duration;
592 const char* ch;
594 /* Display filename (excluding any path)*/
595 ch = rb->strrchr(filename, '/');
596 if (ch==NULL)
597 ch = filename;
598 else
599 ch++;
601 rb->snprintf(str,sizeof(str),"%s",ch);
602 log_text(str,true);
604 log_text("Loading...",false);
606 fd = rb->open(filename,O_RDONLY);
607 if (fd < 0)
609 log_text("Cannot open file",true);
610 goto exit;
613 track.filesize = rb->filesize(fd);
615 /* Clear the id3 struct */
616 rb->memset(&track.id3, 0, sizeof(struct mp3entry));
618 if (!rb->get_metadata(&(track.id3), fd, filename))
620 log_text("Cannot read metadata",true);
621 goto exit;
624 if (track.filesize > audiosize)
626 log_text("File too large",true);
627 goto exit;
630 n = rb->read(fd, audiobuf, track.filesize);
632 if (n != track.filesize)
634 log_text("Read failed.",true);
635 goto exit;
638 /* Initialise the function pointers in the codec API */
639 init_ci();
641 /* Prepare the codec struct for playing the whole file */
642 ci.filesize = track.filesize;
643 ci.id3 = &track.id3;
644 ci.taginfo_ready = &taginfo_ready;
645 ci.curpos = 0;
646 ci.stop_codec = false;
647 ci.new_track = 0;
648 ci.seek_time = 0;
650 if (use_dsp)
651 rb->dsp_configure(ci.dsp, DSP_RESET, 0);
653 if (checksum)
654 crc32 = 0xffffffff;
656 starttick = *rb->current_tick;
658 codec_playing = true;
660 rb->codec_thread_do_callback(codec_thread, NULL);
662 /* Wait for codec thread to die */
663 while (codec_playing)
665 rb->sleep(HZ);
666 rb->snprintf(str,sizeof(str),"%d of %d",elapsed,(int)track.id3.length);
667 log_text(str,false);
669 ticks = endtick - starttick;
671 /* Be sure it is done */
672 rb->codec_thread_do_callback(NULL, NULL);
674 log_text(str,true);
676 if (checksum)
678 rb->snprintf(str, sizeof(str), "CRC32 - %x", (unsigned)crc32);
679 log_text(str,true);
681 else if (wavinfo.fd < 0)
683 /* Display benchmark information */
684 rb->snprintf(str,sizeof(str),"Decode time - %d.%02ds",(int)ticks/100,(int)ticks%100);
685 log_text(str,true);
687 duration = track.id3.length / 10;
688 rb->snprintf(str,sizeof(str),"File duration - %d.%02ds",(int)duration/100,(int)duration%100);
689 log_text(str,true);
691 if (ticks > 0)
692 speed = duration * 10000 / ticks;
693 else
694 speed = 0;
696 rb->snprintf(str,sizeof(str),"%d.%02d%% realtime",(int)speed/100,(int)speed%100);
697 log_text(str,true);
699 #ifndef SIMULATOR
700 /* show effective clockrate in MHz needed for realtime decoding */
701 if (speed > 0)
703 speed = CPUFREQ_MAX / speed;
704 rb->snprintf(str,sizeof(str),"%d.%02dMHz needed for realtime",
705 (int)speed/100,(int)speed%100);
706 log_text(str,true);
708 #endif
711 res = PLUGIN_OK;
713 exit:
714 rb->backlight_on();
716 if (fd >= 0)
718 rb->close(fd);
721 return res;
724 /* plugin entry point */
725 enum plugin_status plugin_start(const void* parameter)
727 int result, selection = 0;
728 enum plugin_status res = PLUGIN_OK;
729 int scandir;
730 struct dirent *entry;
731 DIR* dir;
732 char* ch;
733 char dirpath[MAX_PATH];
734 char filename[MAX_PATH];
736 if (parameter == NULL)
738 rb->splash(HZ*2, "No File");
739 return PLUGIN_ERROR;
742 codec_mallocbuf = rb->plugin_get_audio_buffer(&audiosize);
743 audiobuf = SKIPBYTES(codec_mallocbuf, CODEC_SIZE);
744 audiosize -= CODEC_SIZE;
746 #ifdef HAVE_ADJUSTABLE_CPU_FREQ
747 rb->cpu_boost(true);
748 #endif
749 rb->lcd_clear_display();
750 rb->lcd_update();
752 enum
754 SPEED_TEST = 0,
755 SPEED_TEST_DIR,
756 WRITE_WAV,
757 SPEED_TEST_WITH_DSP,
758 SPEED_TEST_DIR_WITH_DSP,
759 WRITE_WAV_WITH_DSP,
760 CHECKSUM,
761 CHECKSUM_DIR,
762 QUIT,
765 MENUITEM_STRINGLIST(
766 menu, "test_codec", NULL,
767 "Speed test",
768 "Speed test folder",
769 "Write WAV",
770 "Speed test with DSP",
771 "Speed test folder with DSP",
772 "Write WAV with DSP",
773 "Checksum",
774 "Checksum folder",
775 "Quit",
778 show_menu:
779 rb->lcd_clear_display();
781 result = rb->do_menu(&menu, &selection, NULL, false);
783 if (result == QUIT)
785 res = PLUGIN_OK;
786 goto exit;
789 scandir = 0;
791 if ((checksum = (result == CHECKSUM || result == CHECKSUM_DIR)))
792 result -= 6;
794 if ((use_dsp = ((result >= SPEED_TEST_WITH_DSP)
795 && (result <= WRITE_WAV_WITH_DSP)))) {
796 result -= 3;
798 if (result == SPEED_TEST) {
799 wavinfo.fd = -1;
800 log_init(false);
801 } else if (result == SPEED_TEST_DIR) {
802 wavinfo.fd = -1;
803 scandir = 1;
805 /* Only create a log file when we are testing a folder */
806 if (!log_init(true)) {
807 rb->splash(HZ*2, "Cannot create logfile");
808 res = PLUGIN_ERROR;
809 goto exit;
811 } else if (result == WRITE_WAV) {
812 log_init(false);
813 init_wav("/test.wav");
814 if (wavinfo.fd < 0) {
815 rb->splash(HZ*2, "Cannot create /test.wav");
816 res = PLUGIN_ERROR;
817 goto exit;
819 } else if (result == MENU_ATTACHED_USB) {
820 res = PLUGIN_USB_CONNECTED;
821 goto exit;
822 } else if (result < 0) {
823 res = PLUGIN_OK;
824 goto exit;
827 if (scandir) {
828 /* Test all files in the same directory as the file selected by the
829 user */
831 rb->strlcpy(dirpath,parameter,sizeof(dirpath));
832 ch = rb->strrchr(dirpath,'/');
833 ch[1]=0;
835 DEBUGF("Scanning directory \"%s\"\n",dirpath);
836 dir = rb->opendir(dirpath);
837 if (dir) {
838 entry = rb->readdir(dir);
839 while (entry) {
840 if (!(entry->attribute & ATTR_DIRECTORY)) {
841 rb->snprintf(filename,sizeof(filename),"%s%s",dirpath,entry->d_name);
842 test_track(filename);
843 log_text("", true);
846 /* Read next entry */
847 entry = rb->readdir(dir);
850 rb->closedir(dir);
852 } else {
853 /* Just test the file */
854 res = test_track(parameter);
856 /* Close WAV file (if there was one) */
857 if (wavinfo.fd >= 0) {
858 close_wav();
859 log_text("Wrote /test.wav",true);
861 while (rb->button_get(true) != TESTCODEC_EXITBUTTON);
863 goto show_menu;
865 exit:
866 log_close();
868 #ifdef HAVE_ADJUSTABLE_CPU_FREQ
869 rb->cpu_boost(false);
870 #endif
872 return res;