Added support for sound effects
This commit is contained in:
parent
9bc58bc83b
commit
1f47e41c47
@ -1,12 +1,412 @@
|
||||
#ifdef _WIN32
|
||||
char* sndserver_filename = "./sndserver ";
|
||||
|
||||
#define SAMPLECOUNT 512
|
||||
#define NUM_CHANNELS 8
|
||||
// It is 2 for 16bit, and 2 for two channels.
|
||||
#define BUFMUL 4
|
||||
#define MIXBUFFERSIZE (SAMPLECOUNT*BUFMUL)
|
||||
|
||||
#define SAMPLERATE 11025 // Hz
|
||||
#define SAMPLESIZE 2 // 16bit
|
||||
|
||||
signed short mixbuffer[MIXBUFFERSIZE];
|
||||
|
||||
int lengths[NUMSFX];
|
||||
|
||||
// The channel data pointers, start and end.
|
||||
unsigned char* channels[NUM_CHANNELS];
|
||||
unsigned char* channelsend[NUM_CHANNELS];
|
||||
|
||||
|
||||
// Time/gametic that the channel started playing,
|
||||
// used to determine oldest, which automatically
|
||||
// has lowest priority.
|
||||
// In case number of active sounds exceeds
|
||||
// available channels.
|
||||
int channelstart[NUM_CHANNELS];
|
||||
|
||||
// The sound in channel handles,
|
||||
// determined on registration,
|
||||
// might be used to unregister/stop/modify,
|
||||
// currently unused.
|
||||
int channelhandles[NUM_CHANNELS];
|
||||
|
||||
// The channel step amount...
|
||||
unsigned int channelstep[NUM_CHANNELS];
|
||||
// ... and a 0.16 bit remainder of last step.
|
||||
unsigned int channelstepremainder[NUM_CHANNELS];
|
||||
|
||||
|
||||
// SFX id of the playing sound effect.
|
||||
// Used to catch duplicates (like chainsaw).
|
||||
int channelids[NUM_CHANNELS];
|
||||
|
||||
// Volume lookups.
|
||||
int vol_lookup[128*256];
|
||||
|
||||
// Pitch to stepping lookup, unused.
|
||||
int steptable[256];
|
||||
|
||||
// Hardware left and right channel volume lookup.
|
||||
int* channelleftvol_lookup[NUM_CHANNELS];
|
||||
int* channelrightvol_lookup[NUM_CHANNELS];
|
||||
|
||||
|
||||
//
|
||||
// This function loads the sound data from the WAD lump,
|
||||
// for single sound.
|
||||
//
|
||||
void*
|
||||
getsfx
|
||||
( char* sfxname,
|
||||
int* len )
|
||||
{
|
||||
unsigned char* sfx;
|
||||
unsigned char* paddedsfx;
|
||||
int i;
|
||||
int size;
|
||||
int paddedsize;
|
||||
char name[20];
|
||||
int sfxlump;
|
||||
|
||||
|
||||
// Get the sound data from the WAD, allocate lump
|
||||
// in zone memory.
|
||||
sprintf(name, "ds%s", sfxname);
|
||||
|
||||
// Now, there is a severe problem with the
|
||||
// sound handling, in it is not (yet/anymore)
|
||||
// gamemode aware. That means, sounds from
|
||||
// DOOM II will be requested even with DOOM
|
||||
// shareware.
|
||||
// The sound list is wired into sounds.c,
|
||||
// which sets the external variable.
|
||||
// I do not do runtime patches to that
|
||||
// variable. Instead, we will use a
|
||||
// default sound for replacement.
|
||||
if ( W_CheckNumForName(name) == -1 )
|
||||
sfxlump = W_GetNumForName("dspistol");
|
||||
else
|
||||
sfxlump = W_GetNumForName(name);
|
||||
|
||||
size = W_LumpLength( sfxlump );
|
||||
|
||||
// Debug.
|
||||
// fprintf( stderr, "." );
|
||||
//fprintf( stderr, " -loading %s (lump %d, %d bytes)\n",
|
||||
// sfxname, sfxlump, size );
|
||||
//fflush( stderr );
|
||||
|
||||
sfx = (unsigned char*)W_CacheLumpNum( sfxlump, PU_STATIC );
|
||||
|
||||
// Pads the sound effect out to the mixing buffer size.
|
||||
// The original realloc would interfere with zone memory.
|
||||
paddedsize = ((size-8 + (SAMPLECOUNT-1)) / SAMPLECOUNT) * SAMPLECOUNT;
|
||||
|
||||
// Allocate from zone memory.
|
||||
paddedsfx = (unsigned char*)Z_Malloc( paddedsize+8, PU_STATIC, 0 );
|
||||
// ddt: (unsigned char *) realloc(sfx, paddedsize+8);
|
||||
// This should interfere with zone memory handling,
|
||||
// which does not kick in in the soundserver.
|
||||
|
||||
// Now copy and pad.
|
||||
memcpy( paddedsfx, sfx, size );
|
||||
for (i=size ; i<paddedsize+8 ; i++)
|
||||
paddedsfx[i] = 128;
|
||||
|
||||
// Remove the cached lump.
|
||||
Z_Free( sfx );
|
||||
|
||||
// Preserve padded length.
|
||||
*len = paddedsize;
|
||||
|
||||
// Return allocated padded data.
|
||||
return (void *) (paddedsfx + 8);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
int
|
||||
addsfx
|
||||
( int sfxid,
|
||||
int volume,
|
||||
int step,
|
||||
int seperation )
|
||||
{
|
||||
static unsigned short handlenums = 0;
|
||||
|
||||
int i;
|
||||
int rc = -1;
|
||||
|
||||
int oldest = gametic;
|
||||
int oldestnum = 0;
|
||||
int slot;
|
||||
|
||||
int rightvol;
|
||||
int leftvol;
|
||||
|
||||
// Chainsaw troubles.
|
||||
// Play these sound effects only one at a time.
|
||||
if ( sfxid == sfx_sawup
|
||||
|| sfxid == sfx_sawidl
|
||||
|| sfxid == sfx_sawful
|
||||
|| sfxid == sfx_sawhit
|
||||
|| sfxid == sfx_stnmov
|
||||
|| sfxid == sfx_pistol )
|
||||
{
|
||||
// Loop all channels, check.
|
||||
for (i=0 ; i<NUM_CHANNELS ; i++)
|
||||
{
|
||||
// Active, and using the same SFX?
|
||||
if ( (channels[i])
|
||||
&& (channelids[i] == sfxid) )
|
||||
{
|
||||
// Reset.
|
||||
channels[i] = 0;
|
||||
// We are sure that iff,
|
||||
// there will only be one.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop all channels to find oldest SFX.
|
||||
for (i=0; (i<NUM_CHANNELS) && (channels[i]); i++)
|
||||
{
|
||||
if (channelstart[i] < oldest)
|
||||
{
|
||||
oldestnum = i;
|
||||
oldest = channelstart[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Tales from the cryptic.
|
||||
// If we found a channel, fine.
|
||||
// If not, we simply overwrite the first one, 0.
|
||||
// Probably only happens at startup.
|
||||
if (i == NUM_CHANNELS)
|
||||
slot = oldestnum;
|
||||
else
|
||||
slot = i;
|
||||
|
||||
// Okay, in the less recent channel,
|
||||
// we will handle the new SFX.
|
||||
// Set pointer to raw data.
|
||||
channels[slot] = (unsigned char *) S_sfx[sfxid].data;
|
||||
// Set pointer to end of raw data.
|
||||
channelsend[slot] = channels[slot] + lengths[sfxid];
|
||||
|
||||
// Reset current handle number, limited to 0..100.
|
||||
if (!handlenums)
|
||||
handlenums = 100;
|
||||
|
||||
// Assign current handle number.
|
||||
// Preserved so sounds could be stopped (unused).
|
||||
channelhandles[slot] = rc = handlenums++;
|
||||
|
||||
// Set stepping???
|
||||
// Kinda getting the impression this is never used.
|
||||
channelstep[slot] = step;
|
||||
// ???
|
||||
channelstepremainder[slot] = 0;
|
||||
|
||||
// Should be gametic, I presume.
|
||||
channelstart[slot] = gametic;
|
||||
|
||||
// Separation, that is, orientation/stereo.
|
||||
// range is: 1 - 256
|
||||
seperation += 1;
|
||||
|
||||
// Per left/right channel.
|
||||
// x^2 seperation,
|
||||
// adjust volume properly.
|
||||
leftvol =
|
||||
volume - ((volume*seperation*seperation) >> 16); ///(256*256);
|
||||
seperation = seperation - 257;
|
||||
rightvol =
|
||||
volume - ((volume*seperation*seperation) >> 16);
|
||||
|
||||
// Sanity check, clamp volume.
|
||||
if (rightvol < 0 || rightvol > 127)
|
||||
I_Error("rightvol out of bounds");
|
||||
|
||||
if (leftvol < 0 || leftvol > 127)
|
||||
I_Error("leftvol out of bounds");
|
||||
|
||||
// Get the proper lookup table piece
|
||||
// for this volume level???
|
||||
channelleftvol_lookup[slot] = &vol_lookup[leftvol*256];
|
||||
channelrightvol_lookup[slot] = &vol_lookup[rightvol*256];
|
||||
|
||||
// Preserve sound SFX id,
|
||||
// e.g. for avoiding duplicates of chainsaw.
|
||||
channelids[slot] = sfxid;
|
||||
|
||||
// You tell me.
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
// Init at program start...
|
||||
void I_InitSound(){}
|
||||
void I_InitSound(){
|
||||
int i;
|
||||
|
||||
// Initialize external data (all sounds) at start, keep static.
|
||||
fprintf( stderr, "I_InitSound: ");
|
||||
|
||||
for (i=1 ; i<NUMSFX ; i++)
|
||||
{
|
||||
// Alias? Example is the chaingun sound linked to pistol.
|
||||
if (!S_sfx[i].link)
|
||||
{
|
||||
// Load data from WAD file.
|
||||
S_sfx[i].data = getsfx( S_sfx[i].name, &lengths[i] );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Previously loaded already?
|
||||
S_sfx[i].data = S_sfx[i].link->data;
|
||||
lengths[i] = lengths[(S_sfx[i].link - S_sfx)/sizeof(sfxinfo_t)];
|
||||
}
|
||||
}
|
||||
|
||||
fprintf( stderr, " pre-cached all sound data\n");
|
||||
}
|
||||
|
||||
//
|
||||
// This function loops all active (internal) sound
|
||||
// channels, retrieves a given number of samples
|
||||
// from the raw sound data, modifies it according
|
||||
// to the current (internal) channel parameters,
|
||||
// mixes the per channel samples into the global
|
||||
// mixbuffer, clamping it to the allowed range,
|
||||
// and sets up everything for transferring the
|
||||
// contents of the mixbuffer to the (two)
|
||||
// hardware channels (left and right, that is).
|
||||
//
|
||||
// This function currently supports only 16bit.
|
||||
//
|
||||
void I_UpdateSound( void )
|
||||
{
|
||||
#ifdef SNDINTR
|
||||
// Debug. Count buffer misses with interrupt.
|
||||
static int misses = 0;
|
||||
#endif
|
||||
|
||||
|
||||
// Mix current sound data.
|
||||
// Data, from raw sound, for right and left.
|
||||
register unsigned int sample;
|
||||
register int dl;
|
||||
register int dr;
|
||||
|
||||
// Pointers in global mixbuffer, left, right, end.
|
||||
signed short* leftout;
|
||||
signed short* rightout;
|
||||
signed short* leftend;
|
||||
// Step in mixbuffer, left and right, thus two.
|
||||
int step;
|
||||
|
||||
// Mixing channel index.
|
||||
int chan;
|
||||
|
||||
// Left and right channel
|
||||
// are in global mixbuffer, alternating.
|
||||
leftout = mixbuffer;
|
||||
rightout = mixbuffer+1;
|
||||
step = 2;
|
||||
|
||||
// Determine end, for left channel only
|
||||
// (right channel is implicit).
|
||||
leftend = mixbuffer + SAMPLECOUNT*step;
|
||||
|
||||
// Mix sounds into the mixing buffer.
|
||||
// Loop over step*SAMPLECOUNT,
|
||||
// that is 512 values for two channels.
|
||||
while (leftout != leftend)
|
||||
{
|
||||
// Reset left/right value.
|
||||
dl = 0;
|
||||
dr = 0;
|
||||
|
||||
// Love thy L2 chache - made this a loop.
|
||||
// Now more channels could be set at compile time
|
||||
// as well. Thus loop those channels.
|
||||
for ( chan = 0; chan < NUM_CHANNELS; chan++ )
|
||||
{
|
||||
// Check channel, if active.
|
||||
if (channels[ chan ])
|
||||
{
|
||||
// Get the raw data from the channel.
|
||||
sample = *channels[ chan ];
|
||||
// Add left and right part
|
||||
// for this channel (sound)
|
||||
// to the current data.
|
||||
// Adjust volume accordingly.
|
||||
dl += channelleftvol_lookup[ chan ][sample];
|
||||
dr += channelrightvol_lookup[ chan ][sample];
|
||||
// Increment index ???
|
||||
channelstepremainder[ chan ] += channelstep[ chan ];
|
||||
// MSB is next sample???
|
||||
channels[ chan ] += channelstepremainder[ chan ] >> 16;
|
||||
// Limit to LSB???
|
||||
channelstepremainder[ chan ] &= 65536-1;
|
||||
|
||||
// Check whether we are done.
|
||||
if (channels[ chan ] >= channelsend[ chan ])
|
||||
channels[ chan ] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp to range. Left hardware channel.
|
||||
// Has been char instead of short.
|
||||
// if (dl > 127) *leftout = 127;
|
||||
// else if (dl < -128) *leftout = -128;
|
||||
// else *leftout = dl;
|
||||
|
||||
dl *= 8;
|
||||
dr *= 8;
|
||||
|
||||
if (dl > 0x7fff)
|
||||
*leftout = 0x7fff;
|
||||
else if (dl < -0x8000)
|
||||
*leftout = -0x8000;
|
||||
else
|
||||
*leftout = dl;
|
||||
|
||||
// Same for right hardware channel.
|
||||
if (dr > 0x7fff)
|
||||
*rightout = 0x7fff;
|
||||
else if (dr < -0x8000)
|
||||
*rightout = -0x8000;
|
||||
else
|
||||
*rightout = dr;
|
||||
|
||||
// Increment current pointers in mixbuffer.
|
||||
leftout += step;
|
||||
rightout += step;
|
||||
}
|
||||
|
||||
#ifdef SNDINTR
|
||||
// Debug check.
|
||||
if ( flag )
|
||||
{
|
||||
misses += flag;
|
||||
flag = 0;
|
||||
}
|
||||
|
||||
if ( misses > 10 )
|
||||
{
|
||||
fprintf( stderr, "I_SoundUpdate: missed 10 buffer writes\n");
|
||||
misses = 0;
|
||||
}
|
||||
|
||||
// Increment flag for update.
|
||||
flag++;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ... update sound buffer and audio device at runtime...
|
||||
void I_UpdateSound(void){}
|
||||
void I_SubmitSound(void){}
|
||||
|
||||
// ... shut down and relase at program termination.
|
||||
@ -17,22 +417,82 @@ void I_ShutdownSound(void){}
|
||||
// SFX I/O
|
||||
//
|
||||
|
||||
// Initialize channels?
|
||||
void I_SetChannels(){}
|
||||
// SFX API
|
||||
// Note: this was called by S_Init.
|
||||
// However, whatever they did in the
|
||||
// old DPMS based DOS version, this
|
||||
// were simply dummies in the Linux
|
||||
// version.
|
||||
// See soundserver initdata().
|
||||
//
|
||||
void I_SetChannels()
|
||||
{
|
||||
// Init internal lookups (raw data, mixing buffer, channels).
|
||||
// This function sets up internal lookups used during
|
||||
// the mixing process.
|
||||
int i;
|
||||
int j;
|
||||
|
||||
int* steptablemid = steptable + 128;
|
||||
|
||||
// Okay, reset internal mixing channels to zero.
|
||||
/*for (i=0; i<NUM_CHANNELS; i++)
|
||||
{
|
||||
channels[i] = 0;
|
||||
}*/
|
||||
|
||||
// This table provides step widths for pitch parameters.
|
||||
// I fail to see that this is currently used.
|
||||
for (i=-128 ; i<128 ; i++)
|
||||
steptablemid[i] = (int)(pow(2.0, (i/64.0))*65536.0);
|
||||
|
||||
|
||||
// Generates volume lookup tables
|
||||
// which also turn the unsigned samples
|
||||
// into signed samples.
|
||||
for (i=0 ; i<128 ; i++)
|
||||
for (j=0 ; j<256 ; j++)
|
||||
vol_lookup[i*256+j] = (i*(j-128)*256)/127;
|
||||
}
|
||||
|
||||
// Get raw data lump index for sound descriptor.
|
||||
int I_GetSfxLumpNum (struct sfxinfo_t* sfxinfo ){
|
||||
return 0; }
|
||||
int I_GetSfxLumpNum (struct sfxinfo_t* sfxinfo ){ return 0; }
|
||||
|
||||
|
||||
// Starts a sound in a particular sound channel.
|
||||
//
|
||||
// Starting a sound means adding it
|
||||
// to the current list of active sounds
|
||||
// in the internal channels.
|
||||
// As the SFX info struct contains
|
||||
// e.g. a pointer to the raw data,
|
||||
// it is ignored.
|
||||
// As our sound handling does not handle
|
||||
// priority, it is ignored.
|
||||
// Pitching (that is, increased speed of playback)
|
||||
// is set, but currently not used by mixing.
|
||||
//
|
||||
int
|
||||
I_StartSound
|
||||
( int id,
|
||||
int vol,
|
||||
int sep,
|
||||
int pitch,
|
||||
int priority ){ return 0; }
|
||||
int priority )
|
||||
{
|
||||
|
||||
// UNUSED
|
||||
priority = 0;
|
||||
|
||||
pitch = NORM_PITCH;
|
||||
|
||||
// Returns a handle (not used).
|
||||
id = addsfx( id, vol, steptable[pitch], sep );
|
||||
|
||||
// fprintf( stderr, "/handle is %d\n", id );
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Stops a sound channel.
|
||||
@ -62,10 +522,20 @@ struct music_t {
|
||||
int left_over;
|
||||
} music = { NULL, 1, 0 };
|
||||
|
||||
void I_InitMusic(void){}
|
||||
void I_ShutdownMusic(void){}
|
||||
thread_mutex_t mus_mutex;
|
||||
|
||||
void I_InitMusic(void){
|
||||
}
|
||||
|
||||
void I_ShutdownMusic(void){
|
||||
}
|
||||
int g_volume = 7;
|
||||
// Volume.
|
||||
void I_SetMusicVolume(int volume){}
|
||||
void I_SetMusicVolume(int volume){
|
||||
if( volume < 16 ) {
|
||||
g_volume = volume;
|
||||
}
|
||||
}
|
||||
// PAUSE game handling.
|
||||
void I_PauseSong(int handle){}
|
||||
void I_ResumeSong(int handle){}
|
||||
@ -80,7 +550,7 @@ void
|
||||
I_PlaySong
|
||||
( int handle,
|
||||
int looping ){
|
||||
|
||||
thread_mutex_lock( &mus_mutex );
|
||||
void* data = (void*)(uintptr_t)handle;
|
||||
if( music.mus ) {
|
||||
mus_destroy( music.mus );
|
||||
@ -90,14 +560,17 @@ I_PlaySong
|
||||
music.mus = mus_create( data, size, NULL );
|
||||
music.left_over = 0;
|
||||
music.reset = 1;
|
||||
thread_mutex_unlock( &mus_mutex );
|
||||
}
|
||||
// Stops a song over 3 seconds.
|
||||
void I_StopSong(int handle){
|
||||
thread_mutex_lock( &mus_mutex );
|
||||
// TODO: fadeout
|
||||
mus_destroy( music.mus );
|
||||
music.mus = NULL;
|
||||
music.left_over = 0;
|
||||
music.reset = 1;
|
||||
thread_mutex_unlock( &mus_mutex );
|
||||
}
|
||||
// See above (register), then think backwards
|
||||
void I_UnRegisterSong(int handle){}
|
||||
@ -105,14 +578,18 @@ void I_UnRegisterSong(int handle){}
|
||||
|
||||
|
||||
void render_music( short* sample_pairs, int sample_pairs_count, tsf* sound_font ) {
|
||||
thread_mutex_lock( &mus_mutex );
|
||||
mus_t* mus = music.mus;
|
||||
if( !mus ) {
|
||||
memset( sample_pairs, 0, sizeof( short ) * sample_pairs_count * 2 );
|
||||
thread_mutex_unlock( &mus_mutex );
|
||||
return;
|
||||
}
|
||||
if( music.reset ) {
|
||||
tsf_reset( sound_font );
|
||||
music.reset = 0;
|
||||
}
|
||||
tsf_set_volume( sound_font, g_volume / 5.0f );
|
||||
int left_over_from_previous = music.left_over;
|
||||
int remaining = sample_pairs_count;
|
||||
short* output = sample_pairs;
|
||||
@ -123,12 +600,13 @@ void render_music( short* sample_pairs, int sample_pairs_count, tsf* sound_font
|
||||
left_over = count - remaining;
|
||||
count = remaining;
|
||||
}
|
||||
tsf_render_short( sound_font, output, count, 0 );
|
||||
tsf_render_short( sound_font, output, count, 1 );
|
||||
remaining -= count;
|
||||
output += count * 2;
|
||||
}
|
||||
if( left_over ) {
|
||||
music.left_over = left_over;
|
||||
thread_mutex_unlock( &mus_mutex );
|
||||
return;
|
||||
}
|
||||
|
||||
@ -216,13 +694,14 @@ void render_music( short* sample_pairs, int sample_pairs_count, tsf* sound_font
|
||||
left_over = count - remaining;
|
||||
count = remaining;
|
||||
}
|
||||
tsf_render_short( sound_font, output, count, 0 );
|
||||
tsf_render_short( sound_font, output, count, 1 );
|
||||
remaining -= count;
|
||||
output += count * 2;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
music.left_over = left_over;
|
||||
thread_mutex_unlock( &mus_mutex );
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
@ -46,6 +46,14 @@ thread_signal_t vblank_signal;
|
||||
|
||||
void sound_callback( APP_S16* sample_pairs, int sample_pairs_count, void* user_data ) {
|
||||
tsf* sound_font = (tsf*) user_data;
|
||||
if( sample_pairs_count == SAMPLECOUNT * 4 ) {
|
||||
I_UpdateSound();
|
||||
for( int i = 0; i < SAMPLECOUNT; ++i ) {
|
||||
for( int j = 0; j < 8; ++j ) {
|
||||
sample_pairs[ i * 8 + j ] = mixbuffer[ i * 2 + ( j % 2 ) ];
|
||||
}
|
||||
}
|
||||
}
|
||||
render_music( sample_pairs, sample_pairs_count, sound_font );
|
||||
}
|
||||
|
||||
@ -66,7 +74,7 @@ int app_proc( app_t* app, void* user_data )
|
||||
app_pointer( app, 1, 1, &empty, 0, 0 );
|
||||
|
||||
tsf* sound_font = tsf_load_memory( soundfont, sizeof( soundfont ) );
|
||||
app_sound( app, 5880, sound_callback, sound_font );
|
||||
app_sound( app, SAMPLECOUNT * 4 * 2, sound_callback, sound_font );
|
||||
|
||||
while( thread_atomic_int_load(&app_running) && app_yield( app ) != APP_STATE_EXIT_REQUESTED )
|
||||
{
|
||||
@ -98,7 +106,9 @@ int app_proc( app_t* app, void* user_data )
|
||||
|
||||
int app_proc_thread()
|
||||
{
|
||||
return app_run( app_proc, 0, 0, 0, 0 );
|
||||
thread_mutex_init( &mus_mutex );
|
||||
return app_run( app_proc, 0, 0, 0, 0 );
|
||||
thread_mutex_term( &mus_mutex );
|
||||
}
|
||||
|
||||
void I_InitGraphics (void)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user