|
|
@@ -1,5 +1,6 @@ |
|
|
/* |
|
|
/* |
|
|
Copyright (C) 2001 Paul Davis |
|
|
Copyright (C) 2001 Paul Davis |
|
|
|
|
|
Copyright (C) 2003 Jack O'Quin |
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify |
|
|
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 |
|
|
it under the terms of the GNU General Public License as published by |
|
|
@@ -16,6 +17,7 @@ |
|
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
|
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
|
|
|
|
|
|
|
|
* 2002/08/23 - modify for libsndfile 1.0.0 <andy@alsaplayer.org> |
|
|
* 2002/08/23 - modify for libsndfile 1.0.0 <andy@alsaplayer.org> |
|
|
|
|
|
* 2003/05/26 - use ringbuffers - joq |
|
|
|
|
|
|
|
|
$Id$ |
|
|
$Id$ |
|
|
*/ |
|
|
*/ |
|
|
@@ -28,206 +30,131 @@ |
|
|
#include <sndfile.h> |
|
|
#include <sndfile.h> |
|
|
#include <pthread.h> |
|
|
#include <pthread.h> |
|
|
#include <getopt.h> |
|
|
#include <getopt.h> |
|
|
|
|
|
|
|
|
#include <jack/jack.h> |
|
|
#include <jack/jack.h> |
|
|
#include <jack/jslist.h> |
|
|
|
|
|
|
|
|
#include "ringbuffer.h" |
|
|
|
|
|
|
|
|
typedef struct _thread_info { |
|
|
typedef struct _thread_info { |
|
|
pthread_t thread_id; |
|
|
pthread_t thread_id; |
|
|
SNDFILE *sf; |
|
|
SNDFILE *sf; |
|
|
jack_nframes_t duration; |
|
|
jack_nframes_t duration; |
|
|
|
|
|
jack_nframes_t rb_size; |
|
|
jack_client_t *client; |
|
|
jack_client_t *client; |
|
|
unsigned int channels; |
|
|
unsigned int channels; |
|
|
int bitdepth; |
|
|
int bitdepth; |
|
|
int can_capture; |
|
|
|
|
|
char *path; |
|
|
char *path; |
|
|
int status; |
|
|
|
|
|
int can_process; |
|
|
|
|
|
|
|
|
volatile int can_capture; |
|
|
|
|
|
volatile int can_process; |
|
|
|
|
|
volatile int status; |
|
|
} thread_info_t; |
|
|
} thread_info_t; |
|
|
|
|
|
|
|
|
|
|
|
/* JACK data */ |
|
|
unsigned int nports; |
|
|
unsigned int nports; |
|
|
jack_port_t **ports; |
|
|
jack_port_t **ports; |
|
|
|
|
|
|
|
|
pthread_mutex_t buffer_lock = PTHREAD_MUTEX_INITIALIZER; |
|
|
|
|
|
|
|
|
jack_default_audio_sample_t **in; |
|
|
|
|
|
jack_nframes_t nframes; |
|
|
|
|
|
const size_t sample_size = sizeof(jack_default_audio_sample_t); |
|
|
|
|
|
|
|
|
|
|
|
/* Synchronization between process thread and disk thread. */ |
|
|
|
|
|
#define DEFAULT_RB_SIZE 16384 /* ringbuffer size in frames */ |
|
|
|
|
|
ringbuffer_t *rb; |
|
|
|
|
|
pthread_mutex_t disk_thread_lock = PTHREAD_MUTEX_INITIALIZER; |
|
|
pthread_cond_t data_ready = PTHREAD_COND_INITIALIZER; |
|
|
pthread_cond_t data_ready = PTHREAD_COND_INITIALIZER; |
|
|
|
|
|
long overruns = 0; |
|
|
|
|
|
|
|
|
typedef struct _sample_buffer { |
|
|
|
|
|
jack_nframes_t nframes; |
|
|
|
|
|
jack_default_audio_sample_t **data; |
|
|
|
|
|
} sample_buffer_t; |
|
|
|
|
|
|
|
|
|
|
|
sample_buffer_t * |
|
|
|
|
|
sample_buffer_new (jack_nframes_t nframes, unsigned int nchans) |
|
|
|
|
|
{ |
|
|
|
|
|
sample_buffer_t *buf; |
|
|
|
|
|
unsigned int i; |
|
|
|
|
|
|
|
|
|
|
|
buf = (sample_buffer_t *) malloc (sizeof (sample_buffer_t)); |
|
|
|
|
|
buf->nframes = nframes; |
|
|
|
|
|
buf->data = (jack_default_audio_sample_t **) malloc (sizeof (jack_default_audio_sample_t *) * nchans); |
|
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < nchans; i++) { |
|
|
|
|
|
buf->data[i] = (jack_default_audio_sample_t *) malloc (sizeof (jack_default_audio_sample_t) * nframes); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return buf; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
JSList *pending_writes = NULL; |
|
|
|
|
|
JSList *free_buffers = NULL; |
|
|
|
|
|
|
|
|
|
|
|
sample_buffer_t * |
|
|
|
|
|
get_free_buffer (jack_nframes_t nframes, unsigned int nchans) |
|
|
|
|
|
{ |
|
|
|
|
|
sample_buffer_t *buf; |
|
|
|
|
|
|
|
|
|
|
|
if (free_buffers == NULL) { |
|
|
|
|
|
buf = sample_buffer_new (nframes, nchans); |
|
|
|
|
|
} else { |
|
|
|
|
|
buf = (sample_buffer_t *) free_buffers->data; |
|
|
|
|
|
free_buffers = jack_slist_next (free_buffers); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return buf; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sample_buffer_t * |
|
|
|
|
|
get_write_buffer () |
|
|
|
|
|
{ |
|
|
|
|
|
sample_buffer_t *buf; |
|
|
|
|
|
|
|
|
|
|
|
if (pending_writes == NULL) { |
|
|
|
|
|
return NULL; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
buf = (sample_buffer_t *) pending_writes->data; |
|
|
|
|
|
pending_writes = jack_slist_next (pending_writes); |
|
|
|
|
|
|
|
|
|
|
|
return buf; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void |
|
|
|
|
|
put_write_buffer (sample_buffer_t *buf) |
|
|
|
|
|
{ |
|
|
|
|
|
pending_writes = jack_slist_append (pending_writes, buf); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void |
|
|
|
|
|
put_free_buffer (sample_buffer_t *buf) |
|
|
|
|
|
{ |
|
|
|
|
|
free_buffers = jack_slist_prepend (free_buffers, buf); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void * |
|
|
void * |
|
|
disk_thread (void *arg) |
|
|
disk_thread (void *arg) |
|
|
{ |
|
|
{ |
|
|
sample_buffer_t *buf; |
|
|
|
|
|
thread_info_t *info = (thread_info_t *) arg; |
|
|
thread_info_t *info = (thread_info_t *) arg; |
|
|
unsigned int i; |
|
|
|
|
|
unsigned int chn; |
|
|
|
|
|
jack_nframes_t total_captured = 0; |
|
|
|
|
|
int done = 0; |
|
|
|
|
|
float *fbuf; |
|
|
|
|
|
|
|
|
static jack_nframes_t total_captured = 0; |
|
|
|
|
|
jack_nframes_t samples_per_frame = info->channels; |
|
|
|
|
|
size_t bytes_per_frame = samples_per_frame * sample_size; |
|
|
|
|
|
void *framebuf = malloc (bytes_per_frame); |
|
|
|
|
|
|
|
|
pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL); |
|
|
pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL); |
|
|
pthread_mutex_lock (&buffer_lock); |
|
|
|
|
|
|
|
|
|
|
|
/* preload the buffer cache */ |
|
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < 8; i++) { |
|
|
|
|
|
buf = sample_buffer_new (jack_get_buffer_size (info->client), info->channels); |
|
|
|
|
|
put_free_buffer (buf); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
pthread_mutex_lock (&disk_thread_lock); |
|
|
|
|
|
|
|
|
info->status = 0; |
|
|
info->status = 0; |
|
|
|
|
|
|
|
|
while (!done) { |
|
|
|
|
|
pthread_cond_wait (&data_ready, &buffer_lock); |
|
|
|
|
|
|
|
|
while (1) { |
|
|
|
|
|
|
|
|
while ((buf = get_write_buffer ()) != 0) { |
|
|
|
|
|
pthread_mutex_unlock (&buffer_lock); |
|
|
|
|
|
|
|
|
/* Write the data one frame at a time. This is |
|
|
|
|
|
* inefficient, but makes things simpler. */ |
|
|
|
|
|
while (info->can_capture && |
|
|
|
|
|
(ringbuffer_read_space (rb) >= bytes_per_frame)) { |
|
|
|
|
|
|
|
|
/* libsndfile requires interleaved data */ |
|
|
|
|
|
|
|
|
|
|
|
if (info->can_capture) { |
|
|
|
|
|
|
|
|
ringbuffer_read (rb, framebuf, bytes_per_frame); |
|
|
|
|
|
|
|
|
fbuf = (float *) malloc (sizeof (float) * buf->nframes * info->channels); |
|
|
|
|
|
|
|
|
|
|
|
for (chn = 0; chn < info->channels; chn++) { |
|
|
|
|
|
for (i = 0; i < buf->nframes; i++) { |
|
|
|
|
|
fbuf[chn+(i*info->channels)] = buf->data[chn][i]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (sf_writef_float (info->sf, fbuf, buf->nframes) != (sf_count_t)buf->nframes) { |
|
|
|
|
|
char errstr[256]; |
|
|
|
|
|
sf_error_str (0, errstr, sizeof (errstr) - 1); |
|
|
|
|
|
fprintf (stderr, "cannot write data to sndfile (%s)\n", errstr); |
|
|
|
|
|
info->status = -1; |
|
|
|
|
|
done = 1; |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (sf_writef_float (info->sf, framebuf, 1) != 1) { |
|
|
|
|
|
char errstr[256]; |
|
|
|
|
|
sf_error_str (0, errstr, sizeof (errstr) - 1); |
|
|
|
|
|
fprintf (stderr, |
|
|
|
|
|
"cannot write sndfile (%s)\n", |
|
|
|
|
|
errstr); |
|
|
|
|
|
info->status = EIO; /* write failed */ |
|
|
|
|
|
goto done; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
free (fbuf); |
|
|
|
|
|
total_captured += buf->nframes; |
|
|
|
|
|
|
|
|
|
|
|
if (total_captured >= info->duration) { |
|
|
|
|
|
printf ("disk thread finished\n"); |
|
|
|
|
|
done = 1; |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (++total_captured >= info->duration) { |
|
|
|
|
|
printf ("disk thread finished\n"); |
|
|
|
|
|
goto done; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
pthread_mutex_lock (&buffer_lock); |
|
|
|
|
|
put_free_buffer (buf); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* wait until process() signals more data */ |
|
|
|
|
|
pthread_cond_wait (&data_ready, &disk_thread_lock); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
pthread_mutex_unlock (&buffer_lock); |
|
|
|
|
|
|
|
|
done: |
|
|
|
|
|
pthread_mutex_unlock (&disk_thread_lock); |
|
|
|
|
|
free (framebuf); |
|
|
return 0; |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
int |
|
|
int |
|
|
process (jack_nframes_t nframes, void *arg) |
|
|
process (jack_nframes_t nframes, void *arg) |
|
|
|
|
|
|
|
|
{ |
|
|
{ |
|
|
|
|
|
int chn; |
|
|
|
|
|
size_t i; |
|
|
thread_info_t *info = (thread_info_t *) arg; |
|
|
thread_info_t *info = (thread_info_t *) arg; |
|
|
jack_default_audio_sample_t *in; |
|
|
|
|
|
sample_buffer_t *buf; |
|
|
|
|
|
unsigned int i; |
|
|
|
|
|
|
|
|
|
|
|
if (!info->can_process) { |
|
|
|
|
|
|
|
|
/* Do nothing until we're ready to begin. */ |
|
|
|
|
|
if ((!info->can_process) || (!info->can_capture)) |
|
|
return 0; |
|
|
return 0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* we don't like taking locks, but until we have a lock |
|
|
|
|
|
free ringbuffer written in C, this is what has to be done |
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
pthread_mutex_lock (&buffer_lock); |
|
|
|
|
|
|
|
|
|
|
|
buf = get_free_buffer (nframes, nports); |
|
|
|
|
|
|
|
|
|
|
|
for (i = 0; i < nports; i++) { |
|
|
|
|
|
in = (jack_default_audio_sample_t *) jack_port_get_buffer (ports[i], nframes); |
|
|
|
|
|
memcpy (buf->data[i], in, sizeof (jack_default_audio_sample_t) * nframes); |
|
|
|
|
|
|
|
|
for (chn = 0; chn < nports; chn++) |
|
|
|
|
|
in[chn] = jack_port_get_buffer (ports[chn], nframes); |
|
|
|
|
|
|
|
|
|
|
|
/* Sndfile requires interleaved data. It is simpler here to |
|
|
|
|
|
* just queue interleaved samples to a single ringbuffer. */ |
|
|
|
|
|
for (i = 0; i < nframes; i++) { |
|
|
|
|
|
for (chn = 0; chn < nports; chn++) { |
|
|
|
|
|
if (ringbuffer_write (rb, (void *) (in[chn]+i), |
|
|
|
|
|
sample_size) |
|
|
|
|
|
< sample_size) |
|
|
|
|
|
overruns++; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
put_write_buffer (buf); |
|
|
|
|
|
|
|
|
|
|
|
/* tell the disk thread that there is work to do */ |
|
|
|
|
|
|
|
|
|
|
|
pthread_cond_signal (&data_ready); |
|
|
|
|
|
pthread_mutex_unlock (&buffer_lock); |
|
|
|
|
|
|
|
|
/* Tell the disk thread there is work to do. If it is already |
|
|
|
|
|
* running, the lock will not be available. We can't wait |
|
|
|
|
|
* here in the process() thread, but we don't need to signal |
|
|
|
|
|
* in that case, because the disk thread will read all the |
|
|
|
|
|
* data queued before waiting again. */ |
|
|
|
|
|
if (pthread_mutex_trylock (&disk_thread_lock) == 0) { |
|
|
|
|
|
pthread_cond_signal (&data_ready); |
|
|
|
|
|
pthread_mutex_unlock (&disk_thread_lock); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
return 0; |
|
|
|
|
|
|
|
|
return 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void |
|
|
void |
|
|
jack_shutdown (void *arg) |
|
|
jack_shutdown (void *arg) |
|
|
{ |
|
|
{ |
|
|
fprintf (stderr, "JACK shutdown\n"); |
|
|
fprintf (stderr, "JACK shutdown\n"); |
|
|
exit (0); |
|
|
|
|
|
|
|
|
// exit (0); |
|
|
|
|
|
abort(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void |
|
|
void |
|
|
@@ -273,6 +200,13 @@ run_disk_thread (thread_info_t *info) |
|
|
info->can_capture = 1; |
|
|
info->can_capture = 1; |
|
|
pthread_join (info->thread_id, NULL); |
|
|
pthread_join (info->thread_id, NULL); |
|
|
sf_close (info->sf); |
|
|
sf_close (info->sf); |
|
|
|
|
|
if (overruns > 0) { |
|
|
|
|
|
fprintf (stderr, |
|
|
|
|
|
"jackrec failed with %ld overruns.\n", overruns); |
|
|
|
|
|
fprintf (stderr, " try a bigger buffer than -B %ld.\n", |
|
|
|
|
|
info->rb_size); |
|
|
|
|
|
info->status = EPIPE; |
|
|
|
|
|
} |
|
|
if (info->status) { |
|
|
if (info->status) { |
|
|
unlink (info->path); |
|
|
unlink (info->path); |
|
|
} |
|
|
} |
|
|
@@ -282,10 +216,22 @@ void |
|
|
setup_ports (int sources, char *source_names[], thread_info_t *info) |
|
|
setup_ports (int sources, char *source_names[], thread_info_t *info) |
|
|
{ |
|
|
{ |
|
|
unsigned int i; |
|
|
unsigned int i; |
|
|
|
|
|
size_t in_size; |
|
|
|
|
|
|
|
|
|
|
|
/* Allocate data structures that depend on the number of ports. */ |
|
|
nports = sources; |
|
|
nports = sources; |
|
|
|
|
|
|
|
|
ports = (jack_port_t **) malloc (sizeof (jack_port_t *) * nports); |
|
|
ports = (jack_port_t **) malloc (sizeof (jack_port_t *) * nports); |
|
|
|
|
|
in_size = nports * sizeof (jack_default_audio_sample_t *); |
|
|
|
|
|
in = (jack_default_audio_sample_t **) malloc (in_size); |
|
|
|
|
|
rb = ringbuffer_create (nports * sample_size * info->rb_size); |
|
|
|
|
|
|
|
|
|
|
|
/* When JACK is running realtime, jack_activate() will have |
|
|
|
|
|
* called mlockall() to lock our pages into memory. But, we |
|
|
|
|
|
* still need to touch any newly allocated pages before |
|
|
|
|
|
* process() starts using them. Otherwise, a page fault could |
|
|
|
|
|
* create a delay that would force JACK to shut us down. */ |
|
|
|
|
|
memset(in, 0, in_size); |
|
|
|
|
|
memset(rb->buf, 0, rb->size); |
|
|
|
|
|
|
|
|
for (i = 0; i < nports; i++) { |
|
|
for (i = 0; i < nports; i++) { |
|
|
char name[64]; |
|
|
char name[64]; |
|
|
@@ -307,7 +253,7 @@ setup_ports (int sources, char *source_names[], thread_info_t *info) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
info->can_process = 1; |
|
|
|
|
|
|
|
|
info->can_process = 1; /* process() can start, now */ |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
int |
|
|
int |
|
|
@@ -320,16 +266,18 @@ main (int argc, char *argv[]) |
|
|
int longopt_index = 0; |
|
|
int longopt_index = 0; |
|
|
extern int optind, opterr; |
|
|
extern int optind, opterr; |
|
|
int show_usage = 0; |
|
|
int show_usage = 0; |
|
|
char *optstring = "d:f:b:h"; |
|
|
|
|
|
|
|
|
char *optstring = "d:f:b:B:h"; |
|
|
struct option long_options[] = { |
|
|
struct option long_options[] = { |
|
|
{ "help", 1, 0, 'h' }, |
|
|
|
|
|
|
|
|
{ "help", 0, 0, 'h' }, |
|
|
{ "duration", 1, 0, 'd' }, |
|
|
{ "duration", 1, 0, 'd' }, |
|
|
{ "file", 1, 0, 'f' }, |
|
|
{ "file", 1, 0, 'f' }, |
|
|
{ "bitdepth", 1, 0, 'b' }, |
|
|
{ "bitdepth", 1, 0, 'b' }, |
|
|
|
|
|
{ "bufsize", 1, 0, 'B' }, |
|
|
{ 0, 0, 0, 0 } |
|
|
{ 0, 0, 0, 0 } |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
memset (&thread_info, 0, sizeof (thread_info)); |
|
|
memset (&thread_info, 0, sizeof (thread_info)); |
|
|
|
|
|
thread_info.rb_size = DEFAULT_RB_SIZE; |
|
|
opterr = 0; |
|
|
opterr = 0; |
|
|
|
|
|
|
|
|
while ((c = getopt_long (argc, argv, optstring, long_options, &longopt_index)) != -1) { |
|
|
while ((c = getopt_long (argc, argv, optstring, long_options, &longopt_index)) != -1) { |
|
|
@@ -350,6 +298,9 @@ main (int argc, char *argv[]) |
|
|
case 'b': |
|
|
case 'b': |
|
|
thread_info.bitdepth = atoi (optarg); |
|
|
thread_info.bitdepth = atoi (optarg); |
|
|
break; |
|
|
break; |
|
|
|
|
|
case 'B': |
|
|
|
|
|
thread_info.rb_size = atoi (optarg); |
|
|
|
|
|
break; |
|
|
default: |
|
|
default: |
|
|
fprintf (stderr, "error\n"); |
|
|
fprintf (stderr, "error\n"); |
|
|
show_usage++; |
|
|
show_usage++; |
|
|
@@ -358,7 +309,7 @@ main (int argc, char *argv[]) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (show_usage || thread_info.path == NULL || optind == argc) { |
|
|
if (show_usage || thread_info.path == NULL || optind == argc) { |
|
|
fprintf (stderr, "usage: jackrec -f filename [ -d second ] [ -b bitdepth ] port1 [ port2 ... ]\n"); |
|
|
|
|
|
|
|
|
fprintf (stderr, "usage: jackrec -f filename [ -d second ] [ -b bitdepth ] [ -B bufsize ] port1 [ port2 ... ]\n"); |
|
|
exit (1); |
|
|
exit (1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -374,7 +325,7 @@ main (int argc, char *argv[]) |
|
|
setup_disk_thread (&thread_info); |
|
|
setup_disk_thread (&thread_info); |
|
|
|
|
|
|
|
|
jack_set_process_callback (client, process, &thread_info); |
|
|
jack_set_process_callback (client, process, &thread_info); |
|
|
jack_on_shutdown (client, jack_shutdown, NULL); |
|
|
|
|
|
|
|
|
jack_on_shutdown (client, jack_shutdown, &thread_info); |
|
|
|
|
|
|
|
|
if (jack_activate (client)) { |
|
|
if (jack_activate (client)) { |
|
|
fprintf (stderr, "cannot activate client"); |
|
|
fprintf (stderr, "cannot activate client"); |
|
|
@@ -385,5 +336,8 @@ main (int argc, char *argv[]) |
|
|
run_disk_thread (&thread_info); |
|
|
run_disk_thread (&thread_info); |
|
|
|
|
|
|
|
|
jack_client_close (client); |
|
|
jack_client_close (client); |
|
|
|
|
|
|
|
|
|
|
|
ringbuffer_free (rb); |
|
|
|
|
|
|
|
|
exit (0); |
|
|
exit (0); |
|
|
} |
|
|
} |