blob: e3f190e69c1fb3062699f20b4c3609f1045015e9 [file] [log] [blame]
Jeffrey Goode9a4420b2009-10-31 19:17:36 +00001/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 * Copyright (C) 2005-2007 Miika Pekkarinen
11 * Copyright (C) 2007-2008 Nicolas Pennequin
12 *
13 * This program is free software; you can redistribute it and/or
14 * modify it under the terms of the GNU General Public License
15 * as published by the Free Software Foundation; either version 2
16 * of the License, or (at your option) any later version.
17 *
18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19 * KIND, either express or implied.
20 *
21 ****************************************************************************/
22
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000023#include "playback.h"
24#include "codec_thread.h"
25#include "system.h"
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000026#include "kernel.h"
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000027#include "codecs.h"
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000028#include "buffering.h"
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000029#include "pcmbuf.h"
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000030#include "dsp.h"
31#include "abrepeat.h"
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000032#include "metadata.h"
33#include "splash.h"
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000034
35/* Define LOGF_ENABLE to enable logf output in this file */
36/*#define LOGF_ENABLE*/
37#include "logf.h"
38
39/* macros to enable logf for queues
40 logging on SYS_TIMEOUT can be disabled */
41#ifdef SIMULATOR
42/* Define this for logf output of all queuing except SYS_TIMEOUT */
43#define PLAYBACK_LOGQUEUES
44/* Define this to logf SYS_TIMEOUT messages */
45/*#define PLAYBACK_LOGQUEUES_SYS_TIMEOUT*/
46#endif
47
48#ifdef PLAYBACK_LOGQUEUES
49#define LOGFQUEUE logf
50#else
51#define LOGFQUEUE(...)
52#endif
53
54#ifdef PLAYBACK_LOGQUEUES_SYS_TIMEOUT
55#define LOGFQUEUE_SYS_TIMEOUT logf
56#else
57#define LOGFQUEUE_SYS_TIMEOUT(...)
58#endif
59
60
Jeffrey Goodee8eefe92009-11-01 19:39:23 +000061/* Variables are commented with the threads that use them:
62 * A=audio, C=codec, V=voice. A suffix of - indicates that
63 * the variable is read but not updated on that thread.
64
65 * Unless otherwise noted, the extern variables are located
66 * in playback.c.
67 */
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000068
69/* Main state control */
Jeffrey Goodee8eefe92009-11-01 19:39:23 +000070volatile bool audio_codec_loaded SHAREDBSS_ATTR = false; /* Codec loaded? (C/A-) */
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000071
72extern struct mp3entry *thistrack_id3, /* the currently playing track */
73 *othertrack_id3; /* prev track during track-change-transition, or end of playlist,
74 * next track otherwise */
75
76/* Track change controls */
77extern bool automatic_skip; /* Who initiated in-progress skip? (C/A-) */
78
79/* Set to true if the codec thread should send an audio stop request
80 * (typically because the end of the playlist has been reached).
81 */
82static bool codec_requested_stop = false;
83
84extern struct event_queue audio_queue;
85extern struct event_queue codec_queue;
Jeffrey Goodee8eefe92009-11-01 19:39:23 +000086
87extern struct codec_api ci; /* from codecs.c */
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000088
89/* Codec thread */
Jeffrey Goodee8eefe92009-11-01 19:39:23 +000090unsigned int codec_thread_id; /* For modifying thread priority later.
91 Used by playback.c and pcmbuf.c */
Jeffrey Goode9a4420b2009-10-31 19:17:36 +000092static struct queue_sender_list codec_queue_sender_list;
93static long codec_stack[(DEFAULT_STACK_SIZE + 0x2000)/sizeof(long)]
94IBSS_ATTR;
95static const char codec_thread_name[] = "codec";
96
Jeffrey Goodee8eefe92009-11-01 19:39:23 +000097/* function prototypes */
98static bool codec_load_next_track(void);
99
100
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000101/**************************************/
102
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000103/** misc external functions */
104
105int get_codec_base_type(int type)
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000106{
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000107 switch (type) {
108 case AFMT_MPA_L1:
109 case AFMT_MPA_L2:
110 case AFMT_MPA_L3:
111 return AFMT_MPA_L3;
112 }
113
114 return type;
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000115}
116
117const char *get_codec_filename(int cod_spec)
118{
119 const char *fname;
120
121#ifdef HAVE_RECORDING
122 /* Can choose decoder or encoder if one available */
123 int type = cod_spec & CODEC_TYPE_MASK;
124 int afmt = cod_spec & CODEC_AFMT_MASK;
125
126 if ((unsigned)afmt >= AFMT_NUM_CODECS)
127 type = AFMT_UNKNOWN | (type & CODEC_TYPE_MASK);
128
129 fname = (type == CODEC_TYPE_ENCODER) ?
130 audio_formats[afmt].codec_enc_root_fn :
131 audio_formats[afmt].codec_root_fn;
132
133 logf("%s: %d - %s",
134 (type == CODEC_TYPE_ENCODER) ? "Encoder" : "Decoder",
135 afmt, fname ? fname : "<unknown>");
136#else /* !HAVE_RECORDING */
137 /* Always decoder */
138 if ((unsigned)cod_spec >= AFMT_NUM_CODECS)
139 cod_spec = AFMT_UNKNOWN;
140 fname = audio_formats[cod_spec].codec_root_fn;
141 logf("Codec: %d - %s", cod_spec, fname ? fname : "<unknown>");
142#endif /* HAVE_RECORDING */
143
144 return fname;
145} /* get_codec_filename */
146
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000147/* Borrow the codec thread and return the ID */
148void codec_thread_do_callback(void (*fn)(void), unsigned int *id)
149{
150 /* Set id before telling thread to call something; it may be
151 * needed before this function returns. */
152 if (id != NULL)
153 *id = codec_thread_id;
154
155 /* Codec thread will signal just before entering callback */
156 LOGFQUEUE("codec >| Q_CODEC_DO_CALLBACK");
157 queue_send(&codec_queue, Q_CODEC_DO_CALLBACK, (intptr_t)fn);
158}
159
160
161/** codec API callbacks */
162
163static void* codec_get_buffer(size_t *size)
164{
165 if (codec_size >= CODEC_SIZE)
166 return NULL;
167 *size = CODEC_SIZE - codec_size;
168 return &codecbuf[codec_size];
169}
170
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000171static bool codec_pcmbuf_insert_callback(
172 const void *ch1, const void *ch2, int count)
173{
174 const char *src[2] = { ch1, ch2 };
175
176 while (count > 0)
177 {
178 int out_count = dsp_output_count(ci.dsp, count);
179 int inp_count;
180 char *dest;
181
182 /* Prevent audio from a previous track from playing */
183 if (ci.new_track || ci.stop_codec)
184 return true;
185
186 while ((dest = pcmbuf_request_buffer(&out_count)) == NULL)
187 {
188 cancel_cpu_boost();
189 sleep(1);
190 if (ci.seek_time || ci.new_track || ci.stop_codec)
191 return true;
192 }
193
194 /* Get the real input_size for output_size bytes, guarding
195 * against resampling buffer overflows. */
196 inp_count = dsp_input_count(ci.dsp, out_count);
197
198 if (inp_count <= 0)
199 return true;
200
201 /* Input size has grown, no error, just don't write more than length */
202 if (inp_count > count)
203 inp_count = count;
204
205 out_count = dsp_process(ci.dsp, dest, src, inp_count);
206
207 if (out_count <= 0)
208 return true;
209
210 pcmbuf_write_complete(out_count);
211
212 count -= inp_count;
213 }
214
215 return true;
216} /* codec_pcmbuf_insert_callback */
217
Jeffrey Goode013fe352009-11-05 17:32:32 +0000218static void codec_set_elapsed_callback(unsigned long value)
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000219{
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000220 if (ci.seek_time)
221 return;
222
223#ifdef AB_REPEAT_ENABLE
224 ab_position_report(value);
225#endif
226
Jeffrey Goode013fe352009-11-05 17:32:32 +0000227 unsigned long latency = pcmbuf_get_latency();
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000228 if (value < latency)
229 thistrack_id3->elapsed = 0;
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000230 else
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000231 {
Jeffrey Goode013fe352009-11-05 17:32:32 +0000232 unsigned long elapsed = value - latency;
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000233 if (elapsed > thistrack_id3->elapsed ||
234 elapsed < thistrack_id3->elapsed - 2)
235 {
236 thistrack_id3->elapsed = elapsed;
237 }
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000238 }
239}
240
241static void codec_set_offset_callback(size_t value)
242{
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000243 if (ci.seek_time)
244 return;
245
Jeffrey Goode013fe352009-11-05 17:32:32 +0000246 unsigned long latency = pcmbuf_get_latency() * thistrack_id3->bitrate / 8;
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000247 if (value < latency)
248 thistrack_id3->offset = 0;
249 else
250 thistrack_id3->offset = value - latency;
251}
252
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000253/* helper function, not a callback */
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000254static void codec_advance_buffer_counters(size_t amount)
255{
256 bufadvance(get_audio_hid(), amount);
257 ci.curpos += amount;
258}
259
260/* copy up-to size bytes into ptr and return the actual size copied */
261static size_t codec_filebuf_callback(void *ptr, size_t size)
262{
263 ssize_t copy_n;
264
265 if (ci.stop_codec || !audio_is_playing())
266 return 0;
267
268 copy_n = bufread(get_audio_hid(), size, ptr);
269
270 /* Nothing requested OR nothing left */
271 if (copy_n == 0)
272 return 0;
273
274 /* Update read and other position pointers */
275 codec_advance_buffer_counters(copy_n);
276
277 /* Return the actual amount of data copied to the buffer */
278 return copy_n;
279} /* codec_filebuf_callback */
280
281static void* codec_request_buffer_callback(size_t *realsize, size_t reqsize)
282{
283 size_t copy_n = reqsize;
284 ssize_t ret;
285 void *ptr;
286
287 if (!audio_is_playing())
288 {
289 *realsize = 0;
290 return NULL;
291 }
292
293 ret = bufgetdata(get_audio_hid(), reqsize, &ptr);
294 if (ret >= 0)
295 copy_n = MIN((size_t)ret, reqsize);
296
297 if (copy_n == 0)
298 {
299 *realsize = 0;
300 return NULL;
301 }
302
303 *realsize = copy_n;
304
305 return ptr;
306} /* codec_request_buffer_callback */
307
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000308static void codec_advance_buffer_callback(size_t amount)
309{
310 codec_advance_buffer_counters(amount);
311 codec_set_offset_callback(ci.curpos);
312}
313
314static void codec_advance_buffer_loc_callback(void *ptr)
315{
316 size_t amount = buf_get_offset(get_audio_hid(), ptr);
317 codec_advance_buffer_callback(amount);
318}
319
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000320static bool codec_seek_buffer_callback(size_t newpos)
321{
322 logf("codec_seek_buffer_callback");
323
324 int ret = bufseek(get_audio_hid(), newpos);
325 if (ret == 0) {
326 ci.curpos = newpos;
327 return true;
328 }
329 else {
330 return false;
331 }
332}
333
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000334static void codec_seek_complete_callback(void)
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000335{
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000336 logf("seek_complete");
337 /* If seeking-while-playing, pcm_is_paused() is true.
338 * If seeking-while-paused, audio_is_paused() is true.
339 * A seamless seek skips this section. */
340 if (pcm_is_paused() || audio_is_paused())
341 {
342 /* Clear the buffer */
343 pcmbuf_play_stop();
344 dsp_configure(ci.dsp, DSP_FLUSH, 0);
345
346 /* If seeking-while-playing, resume pcm playback */
347 if (!audio_is_paused())
348 pcmbuf_pause(false);
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000349 }
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000350 ci.seek_time = 0;
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000351}
352
353static void codec_discard_codec_callback(void)
354{
355 int *codec_hid = get_codec_hid();
356 if (*codec_hid >= 0)
357 {
358 bufclose(*codec_hid);
359 *codec_hid = -1;
360 }
361}
362
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000363static bool codec_request_next_track_callback(void)
364{
365 int prev_codectype;
366
367 if (ci.stop_codec || !audio_is_playing())
368 return false;
369
370 prev_codectype = get_codec_base_type(thistrack_id3->codectype);
371 if (!codec_load_next_track())
372 return false;
373
374 /* Seek to the beginning of the new track because if the struct
375 mp3entry was buffered, "elapsed" might not be zero (if the track has
376 been played already but not unbuffered) */
377 codec_seek_buffer_callback(thistrack_id3->first_frame_offset);
378 /* Check if the next codec is the same file. */
379 if (prev_codectype == get_codec_base_type(thistrack_id3->codectype))
380 {
381 logf("New track loaded");
382 codec_discard_codec_callback();
383 return true;
384 }
385 else
386 {
387 logf("New codec:%d/%d", thistrack_id3->codectype, prev_codectype);
388 return false;
389 }
390}
391
392static void codec_configure_callback(int setting, intptr_t value)
393{
394 if (!dsp_configure(ci.dsp, setting, value))
395 { logf("Illegal key:%d", setting); }
396}
397
398/* Initialize codec API */
399void codec_init_codec_api(void)
400{
401 ci.dsp = (struct dsp_config *)dsp_configure(NULL, DSP_MYDSP,
402 CODEC_IDX_AUDIO);
403 ci.codec_get_buffer = codec_get_buffer;
404 ci.pcmbuf_insert = codec_pcmbuf_insert_callback;
405 ci.set_elapsed = codec_set_elapsed_callback;
406 ci.read_filebuf = codec_filebuf_callback;
407 ci.request_buffer = codec_request_buffer_callback;
408 ci.advance_buffer = codec_advance_buffer_callback;
409 ci.advance_buffer_loc = codec_advance_buffer_loc_callback;
410 ci.seek_buffer = codec_seek_buffer_callback;
411 ci.seek_complete = codec_seek_complete_callback;
412 ci.request_next_track = codec_request_next_track_callback;
413 ci.discard_codec = codec_discard_codec_callback;
414 ci.set_offset = codec_set_offset_callback;
415 ci.configure = codec_configure_callback;
416}
417
418
Jeffrey Goode04b01e12009-11-05 21:59:36 +0000419/* track change */
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000420
421static bool codec_load_next_track(void)
422{
423 intptr_t result = Q_CODEC_REQUEST_FAILED;
424
425 audio_set_prev_elapsed(thistrack_id3->elapsed);
426
427#ifdef AB_REPEAT_ENABLE
428 ab_end_of_track_report();
429#endif
430
431 logf("Request new track");
432
433 if (ci.new_track == 0)
434 {
435 ci.new_track++;
436 automatic_skip = true;
437 }
438
439 if (!ci.stop_codec)
440 {
441 trigger_cpu_boost();
442 LOGFQUEUE("codec >| audio Q_AUDIO_CHECK_NEW_TRACK");
443 result = queue_send(&audio_queue, Q_AUDIO_CHECK_NEW_TRACK, 0);
444 }
445
446 switch (result)
447 {
448 case Q_CODEC_REQUEST_COMPLETE:
449 LOGFQUEUE("codec |< Q_CODEC_REQUEST_COMPLETE");
Jeffrey Goode04b01e12009-11-05 21:59:36 +0000450 pcmbuf_start_track_change(!automatic_skip);
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000451 return true;
452
453 case Q_CODEC_REQUEST_FAILED:
454 LOGFQUEUE("codec |< Q_CODEC_REQUEST_FAILED");
455 ci.new_track = 0;
456 ci.stop_codec = true;
457 codec_requested_stop = true;
458 return false;
459
460 default:
461 LOGFQUEUE("codec |< default");
462 ci.stop_codec = true;
463 codec_requested_stop = true;
464 return false;
465 }
466}
467
Jeffrey Goodee8eefe92009-11-01 19:39:23 +0000468/** CODEC THREAD */
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000469static void codec_thread(void)
470{
471 struct queue_event ev;
472 int status;
473
474 while (1) {
475 status = 0;
476
477 if (!pcmbuf_is_crossfade_active()) {
478 cancel_cpu_boost();
479 }
480
481 queue_wait(&codec_queue, &ev);
482 codec_requested_stop = false;
483
484 switch (ev.id) {
485 case Q_CODEC_LOAD_DISK:
486 LOGFQUEUE("codec < Q_CODEC_LOAD_DISK");
487 queue_reply(&codec_queue, 1);
488 audio_codec_loaded = true;
489 ci.stop_codec = false;
490 status = codec_load_file((const char *)ev.data, &ci);
491 LOGFQUEUE("codec_load_file %s %d\n", (const char *)ev.data, status);
492 break;
493
494 case Q_CODEC_LOAD:
495 LOGFQUEUE("codec < Q_CODEC_LOAD");
496 if (*get_codec_hid() < 0) {
497 logf("Codec slot is empty!");
498 /* Wait for the pcm buffer to go empty */
499 while (pcm_is_playing())
500 yield();
501 /* This must be set to prevent an infinite loop */
502 ci.stop_codec = true;
503 LOGFQUEUE("codec > codec Q_AUDIO_PLAY");
504 queue_post(&codec_queue, Q_AUDIO_PLAY, 0);
505 break;
506 }
507
508 audio_codec_loaded = true;
509 ci.stop_codec = false;
510 status = codec_load_buf(*get_codec_hid(), &ci);
511 LOGFQUEUE("codec_load_buf %d\n", status);
512 break;
513
514 case Q_CODEC_DO_CALLBACK:
515 LOGFQUEUE("codec < Q_CODEC_DO_CALLBACK");
516 queue_reply(&codec_queue, 1);
517 if ((void*)ev.data != NULL)
518 {
519 cpucache_invalidate();
520 ((void (*)(void))ev.data)();
521 cpucache_flush();
522 }
523 break;
524
525#ifdef AUDIO_HAVE_RECORDING
526 case Q_ENCODER_LOAD_DISK:
527 LOGFQUEUE("codec < Q_ENCODER_LOAD_DISK");
528 audio_codec_loaded = false; /* Not audio codec! */
529 logf("loading encoder");
530 ci.stop_encoder = false;
531 status = codec_load_file((const char *)ev.data, &ci);
532 logf("encoder stopped");
533 break;
534#endif /* AUDIO_HAVE_RECORDING */
535
536 default:
537 LOGFQUEUE("codec < default");
538 }
539
540 if (audio_codec_loaded)
541 {
542 if (ci.stop_codec)
543 {
544 status = CODEC_OK;
545 if (!audio_is_playing())
546 pcmbuf_play_stop();
547
548 }
549 audio_codec_loaded = false;
550 }
551
552 switch (ev.id) {
553 case Q_CODEC_LOAD_DISK:
554 case Q_CODEC_LOAD:
555 LOGFQUEUE("codec < Q_CODEC_LOAD");
556 if (audio_is_playing())
557 {
558 if (ci.new_track || status != CODEC_OK)
559 {
560 if (!ci.new_track)
561 {
562 logf("Codec failure, %d %d", ci.new_track, status);
563 splash(HZ*2, "Codec failure");
564 }
565
566 if (!codec_load_next_track())
567 {
568 LOGFQUEUE("codec > audio Q_AUDIO_STOP");
569 /* End of playlist */
570 queue_post(&audio_queue, Q_AUDIO_STOP, 0);
571 break;
572 }
573 }
574 else
575 {
576 logf("Codec finished");
577 if (ci.stop_codec)
578 {
579 /* Wait for the audio to stop playing before
580 * triggering the WPS exit */
581 while(pcm_is_playing())
582 {
583 /* There has been one too many struct pointer swaps by now
584 * so even though it says othertrack_id3, its the correct one! */
585 othertrack_id3->elapsed =
586 othertrack_id3->length - pcmbuf_get_latency();
587 sleep(1);
588 }
589
590 if (codec_requested_stop)
591 {
592 LOGFQUEUE("codec > audio Q_AUDIO_STOP");
593 queue_post(&audio_queue, Q_AUDIO_STOP, 0);
594 }
595 break;
596 }
597 }
598
599 if (*get_codec_hid() >= 0)
600 {
601 LOGFQUEUE("codec > codec Q_CODEC_LOAD");
602 queue_post(&codec_queue, Q_CODEC_LOAD, 0);
603 }
604 else
605 {
606 const char *codec_fn =
607 get_codec_filename(thistrack_id3->codectype);
608 if (codec_fn)
609 {
610 LOGFQUEUE("codec > codec Q_CODEC_LOAD_DISK");
611 queue_post(&codec_queue, Q_CODEC_LOAD_DISK,
612 (intptr_t)codec_fn);
613 }
614 }
615 }
616 break;
617
618#ifdef AUDIO_HAVE_RECORDING
619 case Q_ENCODER_LOAD_DISK:
620 LOGFQUEUE("codec < Q_ENCODER_LOAD_DISK");
621
622 if (status == CODEC_OK)
623 break;
624
625 logf("Encoder failure");
626 splash(HZ*2, "Encoder failure");
627
628 if (ci.enc_codec_loaded < 0)
629 break;
630
631 logf("Encoder failed to load");
632 ci.enc_codec_loaded = -1;
633 break;
634#endif /* AUDIO_HAVE_RECORDING */
635
636 default:
637 LOGFQUEUE("codec < default");
638
639 } /* end switch */
640 }
641}
642
Jeffrey Goode9a4420b2009-10-31 19:17:36 +0000643void make_codec_thread(void)
644{
645 codec_thread_id = create_thread(
646 codec_thread, codec_stack, sizeof(codec_stack),
647 CREATE_THREAD_FROZEN,
648 codec_thread_name IF_PRIO(, PRIORITY_PLAYBACK)
649 IF_COP(, CPU));
650 queue_enable_queue_send(&codec_queue, &codec_queue_sender_list,
651 codec_thread_id);
652}