Commit c0e8ae07 authored by Gildas Bazin's avatar Gildas Bazin

* src/audio_output/filters.c, src/audio_output/input.c, include/aout_internal.h,
   modules/audio_filter/resampler/*: Changes that allow the resamplers to set
   the start and end date of the frame it outputs.
   This allows us for instance to output a smaller frame than what we should, and
   keep the rest of the data to compute the resampling of the next frame.
   In short, we can implement much more complex resampling algorithms than before.

* modules/audio_filter/resampler/linear.c: rewrote the linear resampler. The audio
   quality doesn't seem to be better than the ugly resampler, maybe I shouldn't
   have wasted my time on this...
parent 255b41ac
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* aout_internal.h : internal defines for audio output * aout_internal.h : internal defines for audio output
***************************************************************************** *****************************************************************************
* Copyright (C) 2002 VideoLAN * Copyright (C) 2002 VideoLAN
* $Id: aout_internal.h,v 1.31 2002/11/11 14:39:11 sam Exp $ * $Id: aout_internal.h,v 1.32 2002/11/11 22:27:00 gbazin Exp $
* *
* Authors: Christophe Massiot <massiot@via.ecp.fr> * Authors: Christophe Massiot <massiot@via.ecp.fr>
* *
...@@ -112,6 +112,7 @@ typedef struct aout_filter_t ...@@ -112,6 +112,7 @@ typedef struct aout_filter_t
struct aout_buffer_t *, struct aout_buffer_t *,
struct aout_buffer_t * ); struct aout_buffer_t * );
vlc_bool_t b_in_place; vlc_bool_t b_in_place;
vlc_bool_t b_reinit;
} aout_filter_t; } aout_filter_t;
/***************************************************************************** /*****************************************************************************
......
/***************************************************************************** /*****************************************************************************
* ugly.c : linear interpolation resampler * linear.c : linear interpolation resampler
***************************************************************************** *****************************************************************************
* Copyright (C) 2002 VideoLAN * Copyright (C) 2002 VideoLAN
* $Id: linear.c,v 1.3 2002/11/11 19:16:21 gbazin Exp $ * $Id: linear.c,v 1.4 2002/11/11 22:27:01 gbazin Exp $
* *
* Authors: Sigmund Augdal <sigmunau@idi.ntnu.no> * Authors: Gildas Bazin <gbazin@netcourrier.com>
* Sigmund Augdal <sigmunau@idi.ntnu.no>
* *
* 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
...@@ -36,21 +37,33 @@ ...@@ -36,21 +37,33 @@
* Local prototypes * Local prototypes
*****************************************************************************/ *****************************************************************************/
static int Create ( vlc_object_t * ); static int Create ( vlc_object_t * );
static void Close ( vlc_object_t * );
static void DoWork ( aout_instance_t *, aout_filter_t *, aout_buffer_t *, static void DoWork ( aout_instance_t *, aout_filter_t *, aout_buffer_t *,
aout_buffer_t * ); aout_buffer_t * );
/*****************************************************************************
* Local structures
*****************************************************************************/
struct aout_filter_sys_t
{
int32_t *p_prev_sample; /* this filter introduces a 1 sample delay */
int i_remainder; /* remainder of previous sample */
audio_date_t end_date;
};
/***************************************************************************** /*****************************************************************************
* Module descriptor * Module descriptor
*****************************************************************************/ *****************************************************************************/
vlc_module_begin(); vlc_module_begin();
set_description( _("audio filter for linear interpolation resampling") ); set_description( _("audio filter for linear interpolation resampling") );
set_capability( "audio filter", 0 ); set_capability( "audio filter", 10 );
set_callbacks( Create, NULL ); set_callbacks( Create, Close );
vlc_module_end(); vlc_module_end();
/***************************************************************************** /*****************************************************************************
* Create: allocate ugly resampler * Create: allocate linear resampler
*****************************************************************************/ *****************************************************************************/
static int Create( vlc_object_t *p_this ) static int Create( vlc_object_t *p_this )
{ {
...@@ -63,12 +76,37 @@ static int Create( vlc_object_t *p_this ) ...@@ -63,12 +76,37 @@ static int Create( vlc_object_t *p_this )
return VLC_EGENERIC; return VLC_EGENERIC;
} }
/* Allocate the memory needed to store the module's structure */
p_filter->p_sys = malloc( sizeof(struct aout_filter_sys_t) );
if( p_filter->p_sys == NULL )
{
msg_Err( p_filter, "out of memory" );
return VLC_ENOMEM;
}
p_filter->p_sys->p_prev_sample = malloc( p_filter->input.i_channels
* sizeof(int32_t) );
if( p_filter->p_sys->p_prev_sample == NULL )
{
msg_Err( p_filter, "out of memory" );
return VLC_ENOMEM;
}
p_filter->pf_do_work = DoWork; p_filter->pf_do_work = DoWork;
p_filter->b_in_place = VLC_FALSE; p_filter->b_in_place = VLC_FALSE;
return VLC_SUCCESS; return VLC_SUCCESS;
} }
/*****************************************************************************
* Close: free our resources
*****************************************************************************/
static void Close( vlc_object_t * p_this )
{
aout_filter_t * p_filter = (aout_filter_t *)p_this;
free( p_filter->p_sys->p_prev_sample );
free( p_filter->p_sys );
}
/***************************************************************************** /*****************************************************************************
* DoWork: convert a buffer * DoWork: convert a buffer
*****************************************************************************/ *****************************************************************************/
...@@ -77,41 +115,82 @@ static void DoWork( aout_instance_t * p_aout, aout_filter_t * p_filter, ...@@ -77,41 +115,82 @@ static void DoWork( aout_instance_t * p_aout, aout_filter_t * p_filter,
{ {
float* p_in = (float*)p_in_buf->p_buffer; float* p_in = (float*)p_in_buf->p_buffer;
float* p_out = (float*)p_out_buf->p_buffer; float* p_out = (float*)p_out_buf->p_buffer;
float* p_prev_sample = (float*)p_filter->p_sys->p_prev_sample;
int i_nb_channels = aout_FormatNbChannels( &p_filter->input ); int i_nb_channels = aout_FormatNbChannels( &p_filter->input );
int i_in_nb = p_in_buf->i_nb_samples; int i_in_nb = p_in_buf->i_nb_samples;
int i_out_nb = i_in_nb * p_filter->output.i_rate int i_chan, i_in, i_out = 0;
/ p_filter->input.i_rate;
int i_frame_bytes = i_nb_channels * sizeof(s32); /* Take care of the previous input sample (if any) */
int i_in, i_chan, i_out = 0; if( p_filter->b_reinit )
double f_step = (float)p_filter->input.i_rate / p_filter->output.i_rate;
float f_pos = 1;
for( i_in = 0 ; i_in < i_in_nb - 1; i_in++ )
{ {
f_pos--; p_filter->b_reinit = VLC_FALSE;
while( f_pos <= 1 ) p_filter->p_sys->i_remainder = 0;
aout_DateInit( &p_filter->p_sys->end_date, p_filter->output.i_rate );
}
else
{
while( p_filter->p_sys->i_remainder < p_filter->output.i_rate )
{ {
for( i_chan = i_nb_channels ; i_chan ; ) for( i_chan = i_nb_channels ; i_chan ; )
{ {
i_chan--; i_chan--;
p_out[i_chan] = p_in[i_chan] + p_out[i_chan] = p_prev_sample[i_chan];
( p_in[i_chan + i_nb_channels] - p_in[i_chan] ) * f_pos; p_out[i_chan] += ( (p_prev_sample[i_chan] - p_in[i_chan])
i_out++; * p_filter->p_sys->i_remainder
/ p_filter->output.i_rate );
} }
f_pos += f_step;
p_out += i_nb_channels; p_out += i_nb_channels;
i_out++;
p_filter->p_sys->i_remainder += p_filter->input.i_rate;
} }
p_filter->p_sys->i_remainder -= p_filter->output.i_rate;
}
/* Take care of the current input samples (minus last one) */
for( i_in = 0; i_in < i_in_nb - 1; i_in++ )
{
while( p_filter->p_sys->i_remainder < p_filter->output.i_rate )
{
for( i_chan = i_nb_channels ; i_chan ; )
{
i_chan--;
p_out[i_chan] = p_in[i_chan];
p_out[i_chan] += ( (p_in[i_chan] -
p_in[i_chan + i_nb_channels])
* p_filter->p_sys->i_remainder / p_filter->output.i_rate );
}
p_out += i_nb_channels;
i_out++;
p_filter->p_sys->i_remainder += p_filter->input.i_rate;
}
p_in += i_nb_channels; p_in += i_nb_channels;
p_filter->p_sys->i_remainder -= p_filter->output.i_rate;
} }
if ( i_out != i_out_nb * i_nb_channels ) /* Backup the last input sample for next time */
for( i_chan = i_nb_channels ; i_chan ; )
{ {
msg_Warn( p_aout, "mismatch in sample numbers: %d requested, " i_chan--;
"%d generated", i_out_nb* i_nb_channels, i_out); p_prev_sample[i_chan] = p_in[i_chan];
} }
p_out_buf->i_nb_samples = i_out_nb;
p_out_buf->i_nb_bytes = i_out_nb * i_frame_bytes;
}
p_out_buf->i_nb_samples = i_out;
p_out_buf->start_date = p_in_buf->start_date;
if( p_in_buf->start_date !=
aout_DateGet( &p_filter->p_sys->end_date ) )
{
aout_DateSet( &p_filter->p_sys->end_date, p_in_buf->start_date );
}
p_out_buf->end_date = aout_DateIncrement( &p_filter->p_sys->end_date,
p_out_buf->i_nb_samples );
p_out_buf->i_nb_bytes = p_out_buf->i_nb_samples *
i_nb_channels * sizeof(int32_t);
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* trivial.c : trivial resampler (skips samples or pads with zeroes) * trivial.c : trivial resampler (skips samples or pads with zeroes)
***************************************************************************** *****************************************************************************
* Copyright (C) 2002 VideoLAN * Copyright (C) 2002 VideoLAN
* $Id: trivial.c,v 1.7 2002/10/15 23:10:54 massiot Exp $ * $Id: trivial.c,v 1.8 2002/11/11 22:27:01 gbazin Exp $
* *
* Authors: Christophe Massiot <massiot@via.ecp.fr> * Authors: Christophe Massiot <massiot@via.ecp.fr>
* *
...@@ -80,24 +80,27 @@ static void DoWork( aout_instance_t * p_aout, aout_filter_t * p_filter, ...@@ -80,24 +80,27 @@ static void DoWork( aout_instance_t * p_aout, aout_filter_t * p_filter,
int i_in_nb = p_in_buf->i_nb_samples; int i_in_nb = p_in_buf->i_nb_samples;
int i_out_nb = i_in_nb * p_filter->output.i_rate int i_out_nb = i_in_nb * p_filter->output.i_rate
/ p_filter->input.i_rate; / p_filter->input.i_rate;
int i_frame_bytes = aout_FormatNbChannels( &p_filter->input ) * sizeof(s32); int i_sample_bytes = aout_FormatNbChannels( &p_filter->input )
* sizeof(int32_t);
if ( p_out_buf != p_in_buf ) if ( p_out_buf != p_in_buf )
{ {
/* For whatever reason the buffer allocator decided to allocate /* For whatever reason the buffer allocator decided to allocate
* a new buffer. Currently, this never happens. */ * a new buffer. Currently, this never happens. */
p_aout->p_vlc->pf_memcpy( p_out_buf->p_buffer, p_in_buf->p_buffer, p_aout->p_vlc->pf_memcpy( p_out_buf->p_buffer, p_in_buf->p_buffer,
__MIN(i_out_nb, i_in_nb) * i_frame_bytes ); __MIN(i_out_nb, i_in_nb) * i_sample_bytes );
} }
if ( i_out_nb > i_in_nb ) if ( i_out_nb > i_in_nb )
{ {
/* Pad with zeroes. */ /* Pad with zeroes. */
memset( p_out_buf->p_buffer + i_in_nb * i_frame_bytes, memset( p_out_buf->p_buffer + i_in_nb * i_sample_bytes,
0, (i_out_nb - i_in_nb) * i_frame_bytes ); 0, (i_out_nb - i_in_nb) * i_sample_bytes );
} }
p_out_buf->i_nb_samples = i_out_nb; p_out_buf->i_nb_samples = i_out_nb;
p_out_buf->i_nb_bytes = i_out_nb * i_frame_bytes; p_out_buf->i_nb_bytes = i_out_nb * i_sample_bytes;
p_out_buf->start_date = p_in_buf->start_date;
p_out_buf->end_date = p_out_buf->start_date + p_out_buf->i_nb_samples *
1000000 / p_filter->output.i_rate;
} }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* ugly.c : ugly resampler (changes pitch) * ugly.c : ugly resampler (changes pitch)
***************************************************************************** *****************************************************************************
* Copyright (C) 2002 VideoLAN * Copyright (C) 2002 VideoLAN
* $Id: ugly.c,v 1.4 2002/10/15 23:10:54 massiot Exp $ * $Id: ugly.c,v 1.5 2002/11/11 22:27:01 gbazin Exp $
* *
* Authors: Samuel Hocevar <sam@zoy.org> * Authors: Samuel Hocevar <sam@zoy.org>
* *
...@@ -77,14 +77,14 @@ static int Create( vlc_object_t *p_this ) ...@@ -77,14 +77,14 @@ static int Create( vlc_object_t *p_this )
static void DoWork( aout_instance_t * p_aout, aout_filter_t * p_filter, static void DoWork( aout_instance_t * p_aout, aout_filter_t * p_filter,
aout_buffer_t * p_in_buf, aout_buffer_t * p_out_buf ) aout_buffer_t * p_in_buf, aout_buffer_t * p_out_buf )
{ {
s32* p_in = (s32*)p_in_buf->p_buffer; int32_t* p_in = (int32_t*)p_in_buf->p_buffer;
s32* p_out = (s32*)p_out_buf->p_buffer; int32_t* p_out = (int32_t*)p_out_buf->p_buffer;
int i_nb_channels = aout_FormatNbChannels( &p_filter->input ); int i_nb_channels = aout_FormatNbChannels( &p_filter->input );
int i_in_nb = p_in_buf->i_nb_samples; int i_in_nb = p_in_buf->i_nb_samples;
int i_out_nb = i_in_nb * p_filter->output.i_rate int i_out_nb = i_in_nb * p_filter->output.i_rate
/ p_filter->input.i_rate; / p_filter->input.i_rate;
int i_frame_bytes = i_nb_channels * sizeof(s32); int i_sample_bytes = i_nb_channels * sizeof(int32_t);
int i_out, i_chan, i_remainder = 0; int i_out, i_chan, i_remainder = 0;
for( i_out = i_out_nb ; i_out-- ; ) for( i_out = i_out_nb ; i_out-- ; )
...@@ -105,6 +105,8 @@ static void DoWork( aout_instance_t * p_aout, aout_filter_t * p_filter, ...@@ -105,6 +105,8 @@ static void DoWork( aout_instance_t * p_aout, aout_filter_t * p_filter,
} }
p_out_buf->i_nb_samples = i_out_nb; p_out_buf->i_nb_samples = i_out_nb;
p_out_buf->i_nb_bytes = i_out_nb * i_frame_bytes; p_out_buf->i_nb_bytes = i_out_nb * i_sample_bytes;
p_out_buf->start_date = p_in_buf->start_date;
p_out_buf->end_date = p_out_buf->start_date + p_out_buf->i_nb_samples *
1000000 / p_filter->output.i_rate;
} }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* filters.c : audio output filters management * filters.c : audio output filters management
***************************************************************************** *****************************************************************************
* Copyright (C) 2002 VideoLAN * Copyright (C) 2002 VideoLAN
* $Id: filters.c,v 1.12 2002/10/20 12:23:48 massiot Exp $ * $Id: filters.c,v 1.13 2002/11/11 22:27:00 gbazin Exp $
* *
* Authors: Christophe Massiot <massiot@via.ecp.fr> * Authors: Christophe Massiot <massiot@via.ecp.fr>
* *
...@@ -60,6 +60,8 @@ static aout_filter_t * FindFilter( aout_instance_t * p_aout, ...@@ -60,6 +60,8 @@ static aout_filter_t * FindFilter( aout_instance_t * p_aout,
return NULL; return NULL;
} }
p_filter->b_reinit = VLC_TRUE;
return p_filter; return p_filter;
} }
...@@ -292,10 +294,15 @@ void aout_FiltersPlay( aout_instance_t * p_aout, ...@@ -292,10 +294,15 @@ void aout_FiltersPlay( aout_instance_t * p_aout,
aout_filter_t * p_filter = pp_filters[i]; aout_filter_t * p_filter = pp_filters[i];
aout_buffer_t * p_output_buffer; aout_buffer_t * p_output_buffer;
/* We need this because resamplers can produce more samples than
(i_in_nb * p_filter->output.i_rate / p_filter->input.i_rate) */
int i_compensate_rounding = 2 * p_filter->input.i_rate
/ p_filter->output.i_rate;
aout_BufferAlloc( &p_filter->output_alloc, aout_BufferAlloc( &p_filter->output_alloc,
(mtime_t)(*pp_input_buffer)->i_nb_samples * 1000000 ((mtime_t)(*pp_input_buffer)->i_nb_samples + i_compensate_rounding)
/ p_filter->input.i_rate, *pp_input_buffer, * 1000000 / p_filter->input.i_rate,
p_output_buffer ); *pp_input_buffer, p_output_buffer );
if ( p_output_buffer == NULL ) if ( p_output_buffer == NULL )
{ {
msg_Err( p_aout, "out of memory" ); msg_Err( p_aout, "out of memory" );
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* input.c : internal management of input streams for the audio output * input.c : internal management of input streams for the audio output
***************************************************************************** *****************************************************************************
* Copyright (C) 2002 VideoLAN * Copyright (C) 2002 VideoLAN
* $Id: input.c,v 1.19 2002/11/10 14:31:46 gbazin Exp $ * $Id: input.c,v 1.20 2002/11/11 22:27:00 gbazin Exp $
* *
* Authors: Christophe Massiot <massiot@via.ecp.fr> * Authors: Christophe Massiot <massiot@via.ecp.fr>
* *
...@@ -98,13 +98,14 @@ int aout_InputNew( aout_instance_t * p_aout, aout_input_t * p_input ) ...@@ -98,13 +98,14 @@ int aout_InputNew( aout_instance_t * p_aout, aout_input_t * p_input )
return -1; return -1;
} }
aout_FiltersHintBuffers( p_aout, p_input->pp_resamplers,
p_input->i_nb_resamplers,
&p_input->input_alloc );
/* Setup the initial rate of the resampler */ /* Setup the initial rate of the resampler */
p_input->pp_resamplers[0]->input.i_rate = p_input->input.i_rate; p_input->pp_resamplers[0]->input.i_rate = p_input->input.i_rate;
p_input->i_resampling_type = AOUT_RESAMPLING_NONE; p_input->i_resampling_type = AOUT_RESAMPLING_NONE;
aout_FiltersHintBuffers( p_aout, p_input->pp_resamplers,
p_input->i_nb_resamplers,
&p_input->input_alloc );
} }
p_input->input_alloc.i_alloc_type = AOUT_ALLOC_HEAP; p_input->input_alloc.i_alloc_type = AOUT_ALLOC_HEAP;
...@@ -153,7 +154,7 @@ int aout_InputDelete( aout_instance_t * p_aout, aout_input_t * p_input ) ...@@ -153,7 +154,7 @@ int aout_InputDelete( aout_instance_t * p_aout, aout_input_t * p_input )
int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input, int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input,
aout_buffer_t * p_buffer ) aout_buffer_t * p_buffer )
{ {
mtime_t start_date, duration; mtime_t start_date;
/* We don't care if someone changes the start date behind our back after /* We don't care if someone changes the start date behind our back after
* this. We'll deal with that when pushing the buffer, and compensate * this. We'll deal with that when pushing the buffer, and compensate
...@@ -172,9 +173,11 @@ int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input, ...@@ -172,9 +173,11 @@ int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input,
vlc_mutex_lock( &p_aout->input_fifos_lock ); vlc_mutex_lock( &p_aout->input_fifos_lock );
aout_FifoSet( p_aout, &p_input->fifo, 0 ); aout_FifoSet( p_aout, &p_input->fifo, 0 );
vlc_mutex_unlock( &p_aout->input_fifos_lock ); vlc_mutex_unlock( &p_aout->input_fifos_lock );
if ( p_input->i_resampling_type != AOUT_RESAMPLING_NONE )
msg_Warn( p_aout, "timing screwed, stopping resampling" );
p_input->i_resampling_type = AOUT_RESAMPLING_NONE; p_input->i_resampling_type = AOUT_RESAMPLING_NONE;
p_input->pp_resamplers[0]->input.i_rate = p_input->input.i_rate; p_input->pp_resamplers[0]->input.i_rate = p_input->input.i_rate;
msg_Warn( p_aout, "timing screwed, stopping resampling" ); p_input->pp_resamplers[0]->b_reinit = VLC_TRUE;
start_date = 0; start_date = 0;
} }
...@@ -261,7 +264,7 @@ int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input, ...@@ -261,7 +264,7 @@ int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input,
} }
else if( p_input->i_resamp_start_drift && else if( p_input->i_resamp_start_drift &&
( abs( p_buffer->start_date - start_date ) > ( abs( p_buffer->start_date - start_date ) >
abs( p_input->i_resamp_start_drift ) ) ) abs( p_input->i_resamp_start_drift ) * 3 / 2 ) )
{ {
/* If the drift is increasing and not decreasing, than something /* If the drift is increasing and not decreasing, than something
* is bad. We'd better stop the resampling right now. */ * is bad. We'd better stop the resampling right now. */
...@@ -269,8 +272,14 @@ int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input, ...@@ -269,8 +272,14 @@ int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input,
p_input->i_resampling_type = AOUT_RESAMPLING_NONE; p_input->i_resampling_type = AOUT_RESAMPLING_NONE;
p_input->pp_resamplers[0]->input.i_rate = p_input->input.i_rate; p_input->pp_resamplers[0]->input.i_rate = p_input->input.i_rate;
} }
} }
/* Adding the start date will be managed by aout_FifoPush(). */
p_buffer->start_date = start_date;
p_buffer->end_date = start_date +
(p_buffer->end_date - p_buffer->start_date);
/* Actually run the resampler now. */ /* Actually run the resampler now. */
if ( p_aout->mixer.mixer.i_rate != if ( p_aout->mixer.mixer.i_rate !=
p_input->pp_resamplers[0]->input.i_rate ) p_input->pp_resamplers[0]->input.i_rate )
...@@ -280,13 +289,6 @@ int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input, ...@@ -280,13 +289,6 @@ int aout_InputPlay( aout_instance_t * p_aout, aout_input_t * p_input,
&p_buffer ); &p_buffer );
} }
/* Adding the start date will be managed by aout_FifoPush(). */
duration = ( p_buffer->end_date - p_buffer->start_date ) *
p_input->pp_resamplers[0]->input.i_rate / p_input->input.i_rate;
p_buffer->start_date = start_date;
p_buffer->end_date = start_date + duration;
vlc_mutex_lock( &p_aout->input_fifos_lock ); vlc_mutex_lock( &p_aout->input_fifos_lock );
aout_FifoPush( p_aout, &p_input->fifo, p_buffer ); aout_FifoPush( p_aout, &p_input->fifo, p_buffer );
vlc_mutex_unlock( &p_aout->input_fifos_lock ); vlc_mutex_unlock( &p_aout->input_fifos_lock );
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment