From 3291666b570b1d20f59db42936eaa37dbeb9ca65 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Mon, 9 Mar 2009 19:25:26 +0100 Subject: output: play from a music_pipe object Instead of passing individual buffers to audio_output_all_play(), pass music_chunk objects. Append all those chunks asynchronously to a music_pipe instance. All output threads may then read chunks from this pipe. This reduces MPD's internal latency by an order of magnitude. --- src/output_all.c | 224 ++++++++++++++++++++++++++++++++++++++++---------- src/output_all.h | 23 ++++-- src/output_control.c | 36 ++++---- src/output_control.h | 9 +- src/output_init.c | 1 + src/output_internal.h | 29 ++++--- src/output_thread.c | 97 ++++++++++++++++++---- src/player_thread.c | 72 +++++++++++++--- 8 files changed, 378 insertions(+), 113 deletions(-) diff --git a/src/output_all.c b/src/output_all.c index 7b308a42..c1353fa7 100644 --- a/src/output_all.c +++ b/src/output_all.c @@ -20,6 +20,12 @@ #include "output_internal.h" #include "output_control.h" #include "conf.h" +#include "pipe.h" +#include "buffer.h" + +#ifndef NDEBUG +#include "chunk.h" +#endif #include #include @@ -32,6 +38,17 @@ static struct audio_format input_audio_format; static struct audio_output *audio_outputs; static unsigned int num_audio_outputs; +/** + * The #music_buffer object where consumed chunks are returned. + */ +static struct music_buffer *g_music_buffer; + +/** + * The #music_pipe object which feeds all audio outputs. It is filled + * by audio_output_all_play(). + */ +static struct music_pipe *g_mp; + unsigned int audio_output_count(void) { return num_audio_outputs; @@ -179,60 +196,63 @@ audio_output_all_update(void) for (i = 0; i < num_audio_outputs; ++i) ret = audio_output_update(&audio_outputs[i], - &input_audio_format) || ret; + &input_audio_format, g_mp) || ret; return ret; } bool -audio_output_all_play(const char *buffer, size_t length) +audio_output_all_play(struct music_chunk *chunk) { - bool ret = false; + bool ret; unsigned int i; - assert(length > 0); - /* no partial frames allowed */ - assert((length % audio_format_frame_size(&input_audio_format)) == 0); + assert(g_music_buffer != NULL); + assert(g_mp != NULL); + assert(chunk != NULL); + assert(music_chunk_check_format(chunk, &input_audio_format)); - audio_output_all_update(); + ret = audio_output_all_update(); + if (!ret) + return false; + + music_pipe_push(g_mp, chunk); for (i = 0; i < num_audio_outputs; ++i) if (audio_output_is_open(&audio_outputs[i])) - audio_output_play(&audio_outputs[i], - buffer, length); - - while (true) { - bool finished = true; - - for (i = 0; i < num_audio_outputs; ++i) { - struct audio_output *ao = &audio_outputs[i]; - - if (!audio_output_is_open(ao)) - continue; - - if (audio_output_command_is_finished(ao)) - ret = true; - else { - finished = false; - audio_output_signal(ao); - } - } - - if (finished) - break; - - notify_wait(&audio_output_client_notify); - }; + audio_output_play(&audio_outputs[i]); return ret; } bool -audio_output_all_open(const struct audio_format *audio_format) +audio_output_all_open(const struct audio_format *audio_format, + struct music_buffer *buffer) { bool ret = false, enabled = false; unsigned int i; + assert(buffer != NULL); + assert(g_music_buffer == NULL || g_music_buffer == buffer); + assert((g_mp == NULL) == (g_music_buffer == NULL)); + + g_music_buffer = buffer; + + /* the audio format must be the same as existing chunks in the + pipe */ + assert(audio_format == NULL || g_mp == NULL || + music_pipe_check_format(g_mp, audio_format)); + + if (g_mp == NULL) + g_mp = music_pipe_new(); + else + /* if the pipe hasn't been cleared, the the audio + format must not have changed */ + assert(music_pipe_size(g_mp) == 0 || + audio_format == NULL || + audio_format_equals(audio_format, + &input_audio_format)); + if (audio_format != NULL) input_audio_format = *audio_format; @@ -257,6 +277,121 @@ audio_output_all_open(const struct audio_format *audio_format) return ret; } +/** + * Has the specified audio output already consumed this chunk? + */ +static bool +chunk_is_consumed_in(const struct audio_output *ao, + const struct music_chunk *chunk) +{ + if (ao->chunk == NULL) + return false; + + assert(chunk == ao->chunk || music_pipe_contains(g_mp, ao->chunk)); + + if (chunk != ao->chunk) { + assert(chunk->next != NULL); + return true; + } + + return ao->chunk_finished && chunk->next == NULL; +} + +/** + * Has this chunk been consumed by all audio outputs? + */ +static bool +chunk_is_consumed(const struct music_chunk *chunk) +{ + for (unsigned i = 0; i < num_audio_outputs; ++i) { + const struct audio_output *ao = &audio_outputs[i]; + bool consumed; + + if (!ao->open) + continue; + + g_mutex_lock(ao->mutex); + consumed = chunk_is_consumed_in(ao, chunk); + g_mutex_unlock(ao->mutex); + + if (!consumed) + return false; + } + + return true; +} + +/** + * There's only one chunk left in the pipe (#g_mp), and all audio + * outputs have consumed it already. Clear the reference. + */ +static void +clear_tail_chunk(const struct music_chunk *chunk, bool *locked) +{ + assert(chunk->next == NULL); + assert(music_pipe_contains(g_mp, chunk)); + + for (unsigned i = 0; i < num_audio_outputs; ++i) { + struct audio_output *ao = &audio_outputs[i]; + + locked[i] = ao->open; + + if (!locked[i]) + continue; + + /* this mutex will be unlocked by the caller when it's + ready */ + g_mutex_lock(ao->mutex); + + assert(ao->chunk == chunk); + assert(ao->chunk_finished); + ao->chunk = NULL; + } +} + +unsigned +audio_output_all_check(void) +{ + const struct music_chunk *chunk; + bool is_tail; + struct music_chunk *shifted; + bool locked[num_audio_outputs]; + + assert(g_music_buffer != NULL); + assert(g_mp != NULL); + + while ((chunk = music_pipe_peek(g_mp)) != NULL) { + assert(music_pipe_size(g_mp) > 0); + + if (!chunk_is_consumed(chunk)) + /* at least one output is not finished playing + this chunk */ + return music_pipe_size(g_mp) - 1; + + is_tail = chunk->next == NULL; + if (is_tail) + /* this is the tail of the pipe - clear the + chunk reference in all outputs */ + clear_tail_chunk(chunk, locked); + + /* remove the chunk from the pipe */ + shifted = music_pipe_shift(g_mp); + assert(shifted == chunk); + + if (is_tail) + /* unlock all audio outputs which were locked + by clear_tail_chunk() */ + for (unsigned i = 0; i < num_audio_outputs; ++i) + if (locked[i]) + g_mutex_unlock(audio_outputs[i].mutex); + + /* return the chunk to the buffer */ + music_buffer_return(g_music_buffer, shifted); + } + + return 0; +} + void audio_output_all_pause(void) { @@ -278,12 +413,19 @@ audio_output_all_cancel(void) audio_output_all_update(); + /* send the cancel() command to all audio outputs */ + for (i = 0; i < num_audio_outputs; ++i) { if (audio_output_is_open(&audio_outputs[i])) audio_output_cancel(&audio_outputs[i]); } audio_output_wait_all(); + + /* clear the music pipe and return all chunks to the buffer */ + + if (g_mp != NULL) + music_pipe_clear(g_mp, g_music_buffer); } void @@ -293,16 +435,14 @@ audio_output_all_close(void) for (i = 0; i < num_audio_outputs; ++i) audio_output_close(&audio_outputs[i]); -} -void -audio_output_all_tag(const struct tag *tag) -{ - unsigned int i; + if (g_mp != NULL) { + assert(g_music_buffer != NULL); - for (i = 0; i < num_audio_outputs; ++i) - if (audio_output_is_open(&audio_outputs[i])) - audio_output_send_tag(&audio_outputs[i], tag); + music_pipe_clear(g_mp, g_music_buffer); + music_pipe_free(g_mp); + g_mp = NULL; + } - audio_output_wait_all(); + g_music_buffer = NULL; } diff --git a/src/output_all.h b/src/output_all.h index 6f55809f..deb33e0e 100644 --- a/src/output_all.h +++ b/src/output_all.h @@ -29,7 +29,8 @@ #include struct audio_format; -struct tag; +struct music_buffer; +struct music_chunk; /** * Global initialization: load audio outputs from the configuration @@ -68,10 +69,13 @@ audio_output_find(const char *name); * * @param audio_format the preferred audio format, or NULL to reuse * the previous format + * @param buffer the #music_buffer where consumed #music_chunk objects + * should be returned * @return true on success, false on failure */ bool -audio_output_all_open(const struct audio_format *audio_format); +audio_output_all_open(const struct audio_format *audio_format, + struct music_buffer *buffer); /** * Closes all audio outputs. @@ -80,19 +84,24 @@ void audio_output_all_close(void); /** - * Play a chunk of audio data. + * Enqueue a #music_chunk object for playing, i.e. pushes it to a + * #music_pipe. * + * @param chunk the #music_chunk object to be played * @return true on success, false if no audio output was able to play * (all closed then) */ bool -audio_output_all_play(const char *data, size_t size); +audio_output_all_play(struct music_chunk *chunk); /** - * Send metadata for the next chunk. + * Checks if the output devices have drained their music pipe, and + * returns the consumed music chunks to the #music_buffer. + * + * @return the number of chunks to play left in the #music_pipe */ -void -audio_output_all_tag(const struct tag *tag); +unsigned +audio_output_all_check(void); /** * Puts all audio outputs into pause mode. Most implementations will diff --git a/src/output_control.c b/src/output_control.c index 47bb844d..85bbea32 100644 --- a/src/output_control.c +++ b/src/output_control.c @@ -57,8 +57,11 @@ static void ao_command_async(struct audio_output *ao, static bool audio_output_open(struct audio_output *ao, - const struct audio_format *audio_format) + const struct audio_format *audio_format, + const struct music_pipe *mp) { + assert(mp != NULL); + if (ao->fail_timer != NULL) { g_timer_destroy(ao->fail_timer); ao->fail_timer = NULL; @@ -66,10 +69,13 @@ audio_output_open(struct audio_output *ao, if (ao->open && audio_format_equals(audio_format, &ao->in_audio_format)) { + assert(ao->pipe == mp); + return true; } ao->in_audio_format = *audio_format; + ao->chunk = NULL; if (audio_format_defined(&ao->config_audio_format)) { /* copy config_audio_format to out_audio_format only if the @@ -85,6 +91,8 @@ audio_output_open(struct audio_output *ao, audio_output_close(ao); } + ao->pipe = mp; + if (ao->thread == NULL) audio_output_thread_start(ao); @@ -96,12 +104,15 @@ audio_output_open(struct audio_output *ao, bool audio_output_update(struct audio_output *ao, - const struct audio_format *audio_format) + const struct audio_format *audio_format, + const struct music_pipe *mp) { + assert(mp != NULL); + if (ao->enabled) { if (ao->fail_timer == NULL || g_timer_elapsed(ao->fail_timer, NULL) > REOPEN_AFTER) - return audio_output_open(ao, audio_format); + return audio_output_open(ao, audio_format, mp); } else if (audio_output_is_open(ao)) audio_output_close(ao); @@ -115,16 +126,12 @@ audio_output_signal(struct audio_output *ao) } void -audio_output_play(struct audio_output *ao, const void *chunk, size_t size) +audio_output_play(struct audio_output *ao) { - assert(size > 0); - if (!ao->open) return; - ao->args.play.data = chunk; - ao->args.play.size = size; - ao_command_async(ao, AO_COMMAND_PLAY); + notify_signal(&ao->notify); } void audio_output_pause(struct audio_output *ao) @@ -163,14 +170,5 @@ void audio_output_finish(struct audio_output *ao) ao_plugin_finish(ao->plugin, ao->data); notify_deinit(&ao->notify); -} - -void -audio_output_send_tag(struct audio_output *ao, const struct tag *tag) -{ - if (ao->plugin->send_tag == NULL) - return; - - ao->args.tag = tag; - ao_command_async(ao, AO_COMMAND_SEND_TAG); + g_mutex_free(ao->mutex); } diff --git a/src/output_control.h b/src/output_control.h index fe3233e6..511aa120 100644 --- a/src/output_control.h +++ b/src/output_control.h @@ -26,8 +26,8 @@ struct audio_output; struct audio_format; -struct tag; struct config_param; +struct music_pipe; static inline GQuark audio_output_quark(void) @@ -46,7 +46,8 @@ audio_output_init(struct audio_output *ao, const struct config_param *param, */ bool audio_output_update(struct audio_output *ao, - const struct audio_format *audio_format); + const struct audio_format *audio_format, + const struct music_pipe *mp); /** * Wakes up the audio output thread. This is part of a workaround for @@ -57,14 +58,12 @@ void audio_output_signal(struct audio_output *ao); void -audio_output_play(struct audio_output *ao, const void *chunk, size_t size); +audio_output_play(struct audio_output *ao); void audio_output_pause(struct audio_output *ao); void audio_output_cancel(struct audio_output *ao); void audio_output_close(struct audio_output *ao); void audio_output_finish(struct audio_output *ao); -void -audio_output_send_tag(struct audio_output *ao, const struct tag *tag); #endif diff --git a/src/output_init.c b/src/output_init.c index 664ca9eb..5681256d 100644 --- a/src/output_init.c +++ b/src/output_init.c @@ -124,6 +124,7 @@ audio_output_init(struct audio_output *ao, const struct config_param *param, ao->thread = NULL; notify_init(&ao->notify); ao->command = AO_COMMAND_NONE; + ao->mutex = g_mutex_new(); ao->data = ao_plugin_init(plugin, format ? &ao->config_audio_format : NULL, diff --git a/src/output_internal.h b/src/output_internal.h index 498f45d0..7e1ac9b7 100644 --- a/src/output_internal.h +++ b/src/output_internal.h @@ -30,10 +30,8 @@ enum audio_output_command { AO_COMMAND_NONE = 0, AO_COMMAND_OPEN, AO_COMMAND_CLOSE, - AO_COMMAND_PLAY, AO_COMMAND_PAUSE, AO_COMMAND_CANCEL, - AO_COMMAND_SEND_TAG, AO_COMMAND_KILL }; @@ -110,16 +108,27 @@ struct audio_output { enum audio_output_command command; /** - * Command arguments, depending on the command. + * The music pipe which provides music chunks to be played. */ - union { - struct { - const void *data; - size_t size; - } play; + const struct music_pipe *pipe; - const struct tag *tag; - } args; + /** + * This mutex protects #chunk and #chunk_finished. + */ + GMutex *mutex; + + /** + * The #music_chunk which is currently being played. All + * chunks before this one may be returned to the + * #music_buffer, because they are not going to be used by + * this output anymore. + */ + const struct music_chunk *chunk; + + /** + * Has the output finished playing #chunk? + */ + bool chunk_finished; }; /** diff --git a/src/output_thread.c b/src/output_thread.c index a5526078..786b0420 100644 --- a/src/output_thread.c +++ b/src/output_thread.c @@ -19,6 +19,8 @@ #include "output_thread.h" #include "output_api.h" #include "output_internal.h" +#include "chunk.h" +#include "pipe.h" #include @@ -41,6 +43,12 @@ ao_close(struct audio_output *ao) { assert(ao->open); + ao->pipe = NULL; + + g_mutex_lock(ao->mutex); + ao->chunk = NULL; + g_mutex_unlock(ao->mutex); + ao_plugin_close(ao->plugin, ao->data); pcm_convert_deinit(&ao->convert_state); ao->open = false; @@ -48,16 +56,25 @@ ao_close(struct audio_output *ao) g_debug("closed plugin=%s name=\"%s\"", ao->plugin->name, ao->name); } -static void ao_play(struct audio_output *ao) +static bool +ao_play_chunk(struct audio_output *ao, const struct music_chunk *chunk) { - const char *data = ao->args.play.data; - size_t size = ao->args.play.size; + const char *data = chunk->data; + size_t size = chunk->length; GError *error = NULL; - assert(size > 0); + assert(!music_chunk_is_empty(chunk)); + assert(music_chunk_check_format(chunk, &ao->in_audio_format)); assert(size % audio_format_frame_size(&ao->in_audio_format) == 0); - if (!audio_format_equals(&ao->in_audio_format, &ao->out_audio_format)) { + if (chunk->tag != NULL) + ao_plugin_send_tag(ao->plugin, ao->data, chunk->tag); + + if (size == 0) + return true; + + if (!audio_format_equals(&ao->in_audio_format, + &ao->out_audio_format)) { data = pcm_convert(&ao->convert_state, &ao->in_audio_format, data, size, &ao->out_audio_format, &size); @@ -67,7 +84,7 @@ static void ao_play(struct audio_output *ao) investigated further, but for now, do this check as a workaround: */ if (data == NULL) - return; + return true; } while (size > 0) { @@ -87,7 +104,7 @@ static void ao_play(struct audio_output *ao) /* don't automatically reopen this device for 10 seconds */ ao->fail_timer = g_timer_new(); - break; + return false; } assert(nbytes <= size); @@ -97,7 +114,46 @@ static void ao_play(struct audio_output *ao) size -= nbytes; } - ao_command_finished(ao); + return true; +} + +static void ao_play(struct audio_output *ao) +{ + bool success; + const struct music_chunk *chunk; + + assert(ao->pipe != NULL); + + g_mutex_lock(ao->mutex); + chunk = ao->chunk; + if (chunk != NULL) + /* continue the previous play() call */ + chunk = chunk->next; + else + chunk = music_pipe_peek(ao->pipe); + ao->chunk_finished = false; + + while (chunk != NULL && ao->command == AO_COMMAND_NONE) { + assert(!ao->chunk_finished); + + ao->chunk = chunk; + g_mutex_unlock(ao->mutex); + + success = ao_play_chunk(ao, chunk); + + g_mutex_lock(ao->mutex); + + if (!success) { + assert(ao->chunk == NULL); + break; + } + + assert(ao->chunk == chunk); + chunk = chunk->next; + } + + ao->chunk_finished = true; + g_mutex_unlock(ao->mutex); } static void ao_pause(struct audio_output *ao) @@ -130,6 +186,8 @@ static gpointer audio_output_task(gpointer arg) case AO_COMMAND_OPEN: assert(!ao->open); assert(ao->fail_timer == NULL); + assert(ao->pipe != NULL); + assert(ao->chunk == NULL); error = NULL; ret = ao_plugin_open(ao->plugin, ao->data, @@ -170,35 +228,40 @@ static gpointer audio_output_task(gpointer arg) case AO_COMMAND_CLOSE: assert(ao->open); + assert(ao->pipe != NULL); + + ao->pipe = NULL; + ao->chunk = NULL; ao_plugin_cancel(ao->plugin, ao->data); ao_close(ao); ao_command_finished(ao); break; - case AO_COMMAND_PLAY: - ao_play(ao); - break; - case AO_COMMAND_PAUSE: ao_pause(ao); break; case AO_COMMAND_CANCEL: + ao->chunk = NULL; ao_plugin_cancel(ao->plugin, ao->data); ao_command_finished(ao); - break; - case AO_COMMAND_SEND_TAG: - ao_plugin_send_tag(ao->plugin, ao->data, ao->args.tag); - ao_command_finished(ao); - break; + /* the player thread will now clear our music + pipe - wait for a notify, to give it some + time */ + notify_wait(&ao->notify); + continue; case AO_COMMAND_KILL: + ao->chunk = NULL; ao_command_finished(ao); return NULL; } + if (ao->open) + ao_play(ao); + notify_wait(&ao->notify); } } diff --git a/src/player_thread.c b/src/player_thread.c index dee564ad..7205822e 100644 --- a/src/player_thread.c +++ b/src/player_thread.c @@ -177,10 +177,22 @@ player_check_decoder_startup(struct player *player) return false; } else if (!decoder_is_starting()) { /* the decoder is ready and ok */ + + if (audio_format_defined(&player->play_audio_format) && + audio_output_all_check() > 0) { + /* the output devices havn't finished playing + all chunks yet - wait for that */ + + /* XXX synchronize in a better way */ + g_usleep(1000); + return true; + } + player->decoder_starting = false; if (!player->paused && - !audio_output_all_open(&dc.out_audio_format)) { + !audio_output_all_open(&dc.out_audio_format, + player_buffer)) { char *uri = song_get_uri(dc.next_song); g_warning("problems opening audio device " "while playing \"%s\"", uri); @@ -268,7 +280,7 @@ static void player_process_command(struct player *player) audio_output_all_pause(); pc.state = PLAYER_STATE_PAUSE; } else { - if (audio_output_all_open(NULL)) { + if (audio_output_all_open(NULL, player_buffer)) { pc.state = PLAYER_STATE_PLAY; } else { assert(dc.next_song == NULL || dc.next_song->url != NULL); @@ -328,8 +340,6 @@ play_chunk(struct song *song, struct music_chunk *chunk, pc.bit_rate = chunk->bit_rate; if (chunk->tag != NULL) { - audio_output_all_tag(chunk->tag); - if (!song_is_file(song)) { /* always update the tag of remote streams */ struct tag *old_tag = song->tag; @@ -349,9 +359,6 @@ play_chunk(struct song *song, struct music_chunk *chunk, } } - if (chunk->length == 0) - return true; - success = pcm_volume(chunk->data, chunk->length, format, pc.software_volume); if (!success) { @@ -362,7 +369,7 @@ play_chunk(struct song *song, struct music_chunk *chunk, return false; } - if (!audio_output_all_play(chunk->data, chunk->length)) { + if (!audio_output_all_play(chunk)) { pc.errored_song = dc.current_song; pc.error = PLAYER_ERROR_AUDIO; return false; @@ -385,6 +392,15 @@ play_next_chunk(struct player *player) unsigned cross_fade_position; bool success; + if (audio_output_all_check() >= 64) { + /* the output pipe is still large + enough, don't send another chunk */ + + /* XXX synchronize in a better way */ + g_usleep(1000); + return true; + } + if (player->xfade == XFADE_ENABLED && dc.pipe != NULL && dc.pipe != player->pipe && (cross_fade_position = music_pipe_size(player->pipe)) @@ -440,10 +456,11 @@ play_next_chunk(struct player *player) success = play_chunk(player->song, chunk, &player->play_audio_format, player->size_to_time); - music_buffer_return(player_buffer, chunk); - if (!success) + if (!success) { + music_buffer_return(player_buffer, chunk); return false; + } /* this formula should prevent that the decoder gets woken up with each chunk; it @@ -472,7 +489,10 @@ player_song_border(struct player *player) music_pipe_free(player->pipe); player->pipe = dc.pipe; - return player_wait_for_decoder(player); + if (!player_wait_for_decoder(player)) + return false; + + return true; } static void do_play(void) @@ -488,7 +508,6 @@ static void do_play(void) .cross_fade_chunks = 0, .size_to_time = 0.0, }; - static const char silence[CHUNK_SIZE]; player.pipe = music_pipe_new(); @@ -585,6 +604,13 @@ static void do_play(void) if (!play_next_chunk(&player)) break; + } else if (audio_output_all_check() > 0) { + /* not enough data from decoder, but the + output thread is still busy, so it's + okay */ + + /* XXX synchronize in a better way */ + g_usleep(10000); } else if (dc.pipe != NULL && dc.pipe != player.pipe) { /* at the beginning of a new song */ @@ -593,15 +619,32 @@ static void do_play(void) } else if (decoder_is_idle()) { break; } else { + /* the decoder is too busy and hasn't provided + new PCM data in time: send silence (if the + output pipe is empty) */ + struct music_chunk *chunk; size_t frame_size = audio_format_frame_size(&player.play_audio_format); /* this formula ensures that we don't send partial frames */ unsigned num_frames = CHUNK_SIZE / frame_size; + chunk = music_buffer_allocate(player_buffer); + if (chunk == NULL) + continue; + +#ifndef NDEBUG + chunk->audio_format = player.play_audio_format; +#endif + + chunk->length = num_frames * frame_size; + memset(chunk->data, 0, chunk->length); + /*DEBUG("waiting for decoded audio, play silence\n");*/ - if (!audio_output_all_play(silence, num_frames * frame_size)) + if (!audio_output_all_play(chunk)) { + music_buffer_return(player_buffer, chunk); break; + } } } @@ -637,6 +680,9 @@ static gpointer player_task(G_GNUC_UNUSED gpointer arg) break; case PLAYER_COMMAND_STOP: + audio_output_all_cancel(); + /* fall through */ + case PLAYER_COMMAND_SEEK: case PLAYER_COMMAND_PAUSE: pc.next_song = NULL; -- cgit v1.2.3