diff --git a/linphone/console/commands.c b/linphone/console/commands.c index e9c7a23c6dafa2f44ed631093f565373bd5f27a1..75421bad8da6772297d11465465e7cf908fa097c 100644 --- a/linphone/console/commands.c +++ b/linphone/console/commands.c @@ -1635,7 +1635,7 @@ static int lpc_cmd_echocancellation(LinphoneCore *lc, char *args){ int delay, tail_len, frame_size; int n; - linphone_core_enable_echo_cancelation(lc,1); + linphone_core_enable_echo_cancellation(lc,1); if (arg2 != 0) { n = sscanf(arg2, "%d %d %d", &delay, &tail_len, &frame_size); @@ -1655,11 +1655,11 @@ static int lpc_cmd_echocancellation(LinphoneCore *lc, char *args){ } } else if (strcmp(arg1,"off")==0){ - linphone_core_enable_echo_cancelation(lc,0); + linphone_core_enable_echo_cancellation(lc,0); } else if (strcmp(arg1,"show")==0){ linphonec_out("echo cancellation is %s; delay %d, tail length %d, frame size %d\n", - linphone_core_echo_cancelation_enabled(lc) ? "on" : "off", + linphone_core_echo_cancellation_enabled(lc) ? "on" : "off", lp_config_get_int(lc->config,"sound","ec_delay",0), lp_config_get_int(lc->config,"sound","ec_tail_len",0), lp_config_get_int(lc->config,"sound","ec_framesize",0)); diff --git a/linphone/console/shell.c b/linphone/console/shell.c index 68e8d6d480bbf2af8804e7f32eb6c950f3695180..c1c152d5e1990f2e13a7f612aa78c3f406334ce3 100644 --- a/linphone/console/shell.c +++ b/linphone/console/shell.c @@ -142,7 +142,11 @@ static void spawn_linphonec(int argc, char *argv[]){ } args[j++]=NULL; +#ifdef __BFIN__ /* is a uClinux target */ + pid = vfork(); +#else pid = fork(); +#endif if (pid < 0){ fprintf(stderr,"Could not fork\n"); exit(-1); diff --git a/linphone/coreapi/exevents.c b/linphone/coreapi/exevents.c index 9178b970827b839c5715430dc0abd95fefa2ccea..1c29bb820ff90b06a08305135b30f880e11b1d4e 100644 --- a/linphone/coreapi/exevents.c +++ b/linphone/coreapi/exevents.c @@ -835,6 +835,7 @@ void linphone_call_ringing(LinphoneCore *lc, eXosip_event_t *ev){ sdp_message_t *sdp=eXosip_get_sdp_info(ev->response); LinphoneCall *call=lc->call; + lc->vtable.display_status(lc,_("Remote ringing.")); linphone_call_proceeding(lc,ev); if (call==NULL) return; if (sdp==NULL){ diff --git a/linphone/coreapi/linphonecore.c b/linphone/coreapi/linphonecore.c index d7d2b2f3d7f22a5c89f56ed5d1d3000f7312b59d..f63a3dc9ac4e470bcb1df48ee25eae5c0faf9e47 100644 --- a/linphone/coreapi/linphonecore.c +++ b/linphone/coreapi/linphonecore.c @@ -419,8 +419,10 @@ void sound_config_read(LinphoneCore *lc) check_sound_device(lc); lc->sound_conf.latency=0; - linphone_core_enable_echo_cancelation(lc, - lp_config_get_int(lc->config,"sound","echocancelation",0)); + linphone_core_enable_echo_cancellation(lc, + lp_config_get_int(lc->config,"sound","echocancelation",0) | + lp_config_get_int(lc->config,"sound","echocancellation",0) + ); linphone_core_enable_echo_limiter(lc, lp_config_get_int(lc->config,"sound","echolimiter",0)); @@ -1622,14 +1624,18 @@ void linphone_core_init_media_streams(LinphoneCore *lc){ audio_stream_enable_echo_limiter(lc->audiostream,ELControlSpeaker); } audio_stream_enable_gain_control(lc->audiostream,TRUE); - if (linphone_core_echo_cancelation_enabled(lc)){ + if (linphone_core_echo_cancellation_enabled(lc)){ int len,delay,framesize; len=lp_config_get_int(lc->config,"sound","ec_tail_len",0); delay=lp_config_get_int(lc->config,"sound","ec_delay",0); framesize=lp_config_get_int(lc->config,"sound","ec_framesize",0); - audio_stream_set_echo_canceler_params(lc->audiostream,len,delay,framesize); + audio_stream_set_echo_canceller_params(lc->audiostream,len,delay,framesize); } audio_stream_enable_automatic_gain_control(lc->audiostream,linphone_core_agc_enabled(lc)); + { + int enabled=lp_config_get_int(lc->config,"sound","noisegate",0); + audio_stream_enable_noise_gate(lc->audiostream,enabled); + } if (lc->a_rtp) rtp_session_set_transports(lc->audiostream->session,lc->a_rtp,lc->a_rtcp); @@ -1700,6 +1706,12 @@ static void post_configure_audio_streams(LinphoneCore *lc){ ms_filter_call_method(f,MS_VOLUME_SET_EA_SUSTAIN,&sustain); } + if (st->volsend){ + float ng_thres=lp_config_get_float(lc->config,"sound","ng_thres",0.05); + float ng_floorgain=lp_config_get_float(lc->config,"sound","ng_floorgain",0); + ms_filter_call_method(st->volsend,MS_VOLUME_SET_NOISE_GATE_THRESHOLD,&ng_thres); + ms_filter_call_method(st->volsend,MS_VOLUME_SET_NOISE_GATE_FLOORGAIN,&ng_floorgain); + } parametrize_equalizer(lc,st); if (lc->vtable.dtmf_received!=NULL){ /* replace by our default action*/ @@ -1742,7 +1754,7 @@ void linphone_core_start_media_streams(LinphoneCore *lc, LinphoneCall *call){ jitt_comp, playcard, captcard, - linphone_core_echo_cancelation_enabled(lc)); + linphone_core_echo_cancellation_enabled(lc)); }else{ audio_stream_start_with_files( lc->audiostream, @@ -2166,13 +2178,13 @@ const char * linphone_core_get_ringback(const LinphoneCore *lc){ return lc->sound_conf.remote_ring; } -void linphone_core_enable_echo_cancelation(LinphoneCore *lc, bool_t val){ +void linphone_core_enable_echo_cancellation(LinphoneCore *lc, bool_t val){ lc->sound_conf.ec=val; if (lc->ready) - lp_config_set_int(lc->config,"sound","echocancelation",val); + lp_config_set_int(lc->config,"sound","echocancellation",val); } -bool_t linphone_core_echo_cancelation_enabled(LinphoneCore *lc){ +bool_t linphone_core_echo_cancellation_enabled(LinphoneCore *lc){ return lc->sound_conf.ec; } diff --git a/linphone/coreapi/linphonecore.h b/linphone/coreapi/linphonecore.h index 08567ba8c30522733475e50a2671e41348a151af..aac57eb93cc33f822b149c8fd80378abdde1c88d 100644 --- a/linphone/coreapi/linphonecore.h +++ b/linphone/coreapi/linphonecore.h @@ -684,8 +684,8 @@ const char *linphone_core_get_ring(const LinphoneCore *lc); void linphone_core_set_ringback(LinphoneCore *lc, const char *path); const char * linphone_core_get_ringback(const LinphoneCore *lc); int linphone_core_preview_ring(LinphoneCore *lc, const char *ring,LinphoneCoreCbFunc func,void * userdata); -void linphone_core_enable_echo_cancelation(LinphoneCore *lc, bool_t val); -bool_t linphone_core_echo_cancelation_enabled(LinphoneCore *lc); +void linphone_core_enable_echo_cancellation(LinphoneCore *lc, bool_t val); +bool_t linphone_core_echo_cancellation_enabled(LinphoneCore *lc); void linphone_core_enable_echo_limiter(LinphoneCore *lc, bool_t val); bool_t linphone_core_echo_limiter_enabled(const LinphoneCore *lc); diff --git a/linphone/gtk-glade/propertybox.c b/linphone/gtk-glade/propertybox.c index 91c403fc008a22f4befd064eba2d1126c4c51591..35d60009eeb5d75efa8b8f4fab505b4831014608 100644 --- a/linphone/gtk-glade/propertybox.c +++ b/linphone/gtk-glade/propertybox.c @@ -209,7 +209,7 @@ void linphone_gtk_play_ring_file(GtkWidget *w){ } void linphone_gtk_echo_cancelation_toggled(GtkWidget *w){ - linphone_core_enable_echo_cancelation(linphone_gtk_get_core(), + linphone_core_enable_echo_cancellation(linphone_gtk_get_core(), gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(w))); } @@ -755,7 +755,7 @@ void linphone_gtk_show_parameters(void){ linphone_core_get_video_device(lc),CAP_IGNORE); linphone_gtk_fill_video_sizes(linphone_gtk_get_widget(pb,"video_size")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(linphone_gtk_get_widget(pb,"echo_cancelation")), - linphone_core_echo_cancelation_enabled(lc)); + linphone_core_echo_cancellation_enabled(lc)); gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(linphone_gtk_get_widget(pb,"ring_chooser")), linphone_core_get_ring(lc)); diff --git a/linphone/mediastreamer2/include/mediastreamer2/mediastream.h b/linphone/mediastreamer2/include/mediastreamer2/mediastream.h index 88544f655fc4b49b6d6129504f3d18985a37e604..47f3070a55b1d3385d7deac23aa10e1ebb1248d6 100644 --- a/linphone/mediastreamer2/include/mediastreamer2/mediastream.h +++ b/linphone/mediastreamer2/include/mediastreamer2/mediastream.h @@ -117,7 +117,7 @@ void audio_stream_enable_gain_control(AudioStream *stream, bool_t val); void audio_stream_enable_automatic_gain_control(AudioStream *stream, bool_t val); /*to be done before start */ -void audio_stream_set_echo_canceler_params(AudioStream *st, int tail_len_ms, int delay_ms, int framesize); +void audio_stream_set_echo_canceller_params(AudioStream *st, int tail_len_ms, int delay_ms, int framesize); void audio_stream_set_mic_gain(AudioStream *stream, float gain); diff --git a/linphone/mediastreamer2/include/mediastreamer2/msvolume.h b/linphone/mediastreamer2/include/mediastreamer2/msvolume.h index 052e3d1b99eeb325244c0a2cb9ab50ebee9a2e10..cf2f02b211306ed2d930ece9e74fae363cb87846 100644 --- a/linphone/mediastreamer2/include/mediastreamer2/msvolume.h +++ b/linphone/mediastreamer2/include/mediastreamer2/msvolume.h @@ -56,6 +56,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #define MS_VOLUME_SET_EA_SUSTAIN MS_FILTER_METHOD(MS_VOLUME_ID,11,int) +#define MS_VOLUME_SET_NOISE_GATE_FLOORGAIN MS_FILTER_METHOD(MS_VOLUME_ID,12,float) extern MSFilterDesc ms_volume_desc; diff --git a/linphone/mediastreamer2/src/audiostream.c b/linphone/mediastreamer2/src/audiostream.c index fd5c3d3bc2b243d0b5a41a597d9a794ff0e3eb24..8eb949272105c322e40aabf4f522799226ec7df5 100644 --- a/linphone/mediastreamer2/src/audiostream.c +++ b/linphone/mediastreamer2/src/audiostream.c @@ -437,7 +437,7 @@ void audio_stream_set_relay_session_id(AudioStream *stream, const char *id){ ms_filter_call_method(stream->rtpsend, MS_RTP_SEND_SET_RELAY_SESSION_ID,(void*)id); } -void audio_stream_set_echo_canceler_params(AudioStream *st, int tail_len_ms, int delay_ms, int framesize){ +void audio_stream_set_echo_canceller_params(AudioStream *st, int tail_len_ms, int delay_ms, int framesize){ st->ec_tail_len=tail_len_ms; st->ec_delay=delay_ms; st->ec_framesize=framesize; diff --git a/linphone/mediastreamer2/src/msvolume.c b/linphone/mediastreamer2/src/msvolume.c index ba8b068eec4c774178eae6ac044f2ff8d5691a1e..e9a747cebb0e986d67a817bafd7a3e8f7876b746 100644 --- a/linphone/mediastreamer2/src/msvolume.c +++ b/linphone/mediastreamer2/src/msvolume.c @@ -57,6 +57,7 @@ typedef struct Volume{ int ng_cut_time; /*noise gate cut time, after last speech detected*/ int ng_noise_dur; float ng_threshold; + float ng_floorgain; MSBufferizer *buffer; bool_t ea_active; bool_t agc_enabled; @@ -84,6 +85,7 @@ static void volume_init(MSFilter *f){ v->ng_cut_time=100;/*milliseconds*/ v->ng_noise_dur=0; v->ng_threshold=noise_thres; + v->ng_floorgain=0; #ifdef HAVE_SPEEXDSP v->speex_pp=NULL; #endif @@ -188,7 +190,7 @@ static void volume_noise_gate_process(Volume *v , float energy, mblk_t *om){ if ((energy/max_e)<v->ng_threshold){ v->ng_noise_dur+=(nsamples*1000)/v->sample_rate; if (v->ng_noise_dur>v->ng_cut_time){ - v->target_gain=0; + v->target_gain=v->ng_floorgain; } }else{ v->ng_noise_dur=0; @@ -271,6 +273,12 @@ static int volume_set_noise_gate_threshold(MSFilter *f, void *arg){ return 0; } +static int volume_set_noise_gate_floorgain(MSFilter *f, void *arg){ + Volume *v=(Volume*)f->data; + v->ng_floorgain=*(float*)arg; + return 0; +} + static inline int16_t saturate(float val){ return (val>32767) ? 32767 : ( (val<-32767) ? -32767 : val); } @@ -381,6 +389,7 @@ static MSFilterMethod methods[]={ { MS_VOLUME_ENABLE_AGC , volume_set_agc }, { MS_VOLUME_ENABLE_NOISE_GATE, volume_enable_noise_gate}, { MS_VOLUME_SET_NOISE_GATE_THRESHOLD, volume_set_noise_gate_threshold}, + { MS_VOLUME_SET_NOISE_GATE_FLOORGAIN, volume_set_noise_gate_floorgain}, { 0 , NULL } }; diff --git a/linphone/mediastreamer2/src/speexec.c b/linphone/mediastreamer2/src/speexec.c index 5bba68cb3ab2d4382cacef9252a99d00d8c68a36..6e55faf27236c98afc6fe38887e3a26bb9581ab0 100644 --- a/linphone/mediastreamer2/src/speexec.c +++ b/linphone/mediastreamer2/src/speexec.c @@ -105,10 +105,9 @@ static void speex_ec_preprocess(MSFilter *f){ } /* inputs[0]= reference signal (sent to soundcard) - inputs[1]= echo signal (read from soundcard) + * inputs[1]= near speech & echo signal (read from soundcard) + * outputs[1]= near end speech, echo removed - towards far end */ - - static void speex_ec_process(MSFilter *f){ SpeexECState *s=(SpeexECState*)f->data; int nbytes=s->framesize*2; @@ -120,6 +119,7 @@ static void speex_ec_process(MSFilter *f){ mblk_t *m; mblk_t *md; + /* first fill delayed buffer until playback delay is reached (only in first n calls) */ if (s->size_delay<s->playback_delay){ while((m=ms_queue_get(f->inputs[0]))!=NULL && s->size_delay<s->playback_delay){ // Duplicate queue : one to write to the output speaker, the other will be delayed for AEC @@ -187,7 +187,7 @@ static void speex_ec_process(MSFilter *f){ ms_bufferizer_read(&s->in[1],in1,nbytes); /* we have echo signal */ om1=allocb(nbytes,0); - speex_echo_cancel(s->ecstate,(short*)in1,(short*)om0->b_rptr,(short*)om1->b_wptr,NULL); + speex_echo_cancellation(s->ecstate,(short*)in1,(short*)om0->b_rptr,(short*)om1->b_wptr); speex_preprocess_run(s->den, (short*)om1->b_wptr); ms_filter_notify(f, MS_SPEEX_EC_ECHO_STATE, (void*)s->ecstate); ms_filter_notify(f, MS_SPEEX_EC_PREPROCESS_MIC, (void*)s->den); @@ -346,7 +346,7 @@ static MSFilterMethod speex_ec_methods[]={ MSFilterDesc ms_speex_ec_desc={ MS_SPEEX_EC_ID, "MSSpeexEC", - N_("Echo canceler using speex library"), + N_("Echo canceller using speex library"), MS_FILTER_OTHER, NULL, 2, @@ -364,7 +364,7 @@ MSFilterDesc ms_speex_ec_desc={ MSFilterDesc ms_speex_ec_desc={ .id=MS_SPEEX_EC_ID, .name="MSSpeexEC", - .text=N_("Echo canceler using speex library"), + .text=N_("Echo canceller using speex library"), .category=MS_FILTER_OTHER, .ninputs=2, .noutputs=2, diff --git a/linphone/mediastreamer2/tests/mediastream.c b/linphone/mediastreamer2/tests/mediastream.c index bbca9964c8e055a0d86d82c7769153328c9cd935..c15362f122d2b55f3cfa26cb8200a82c7d946280 100644 --- a/linphone/mediastreamer2/tests/mediastream.c +++ b/linphone/mediastreamer2/tests/mediastream.c @@ -45,6 +45,10 @@ static const char * capture_card=NULL; static float ng_threshold=-1; static bool_t use_ng=FALSE; +/* starting values echo canceller */ +static int ec_len_ms=0, ec_delay_ms=250, ec_framesize; + + static void stop_handler(int signum) { cond--; @@ -136,8 +140,9 @@ const char *usage="mediastream --local <port> --remote <ip:port> --payload <payl "[ --ng (enable noise gate)]\n" "[ --ng-threshold <(float) [0-1]> (noise gate threshold)]\n" "[ --capture-card <index>] \n"; -static void run_media_streams(int localport, const char *remote_ip, int remoteport, int payload, const char *fmtp, int jitter, bool_t ec, int bitrate, MSVideoSize vs, bool_t agc, bool_t eq); +static void run_media_streams(int localport, const char *remote_ip, int remoteport, int payload, const char *fmtp, + int jitter, int bitrate, MSVideoSize vs, bool_t ec, bool_t agc, bool_t eq); int main(int argc, char * argv[]) { @@ -219,11 +224,12 @@ int main(int argc, char * argv[]) } } - run_media_streams(localport,ip,remoteport,payload,fmtp,jitter,ec,bitrate,vs, agc,eq); + run_media_streams(localport,ip,remoteport,payload,fmtp,jitter,bitrate,vs,ec,agc,eq); return 0; } -void run_media_streams(int localport, const char *remote_ip, int remoteport, int payload, const char *fmtp, int jitter, bool_t ec, int bitrate, MSVideoSize vs, bool_t agc, bool_t eq) +static void run_media_streams(int localport, const char *remote_ip, int remoteport, int payload, const char *fmtp, + int jitter, int bitrate, MSVideoSize vs, bool_t ec, bool_t agc, bool_t eq) { AudioStream *audio=NULL; #ifdef VIDEO_ENABLED @@ -245,13 +251,14 @@ void run_media_streams(int localport, const char *remote_ip, int remoteport, in if (bitrate>0) pt->normal_bitrate=bitrate; if (pt->type!=PAYLOAD_VIDEO){ - printf("Starting audio stream.\n"); MSSndCardManager *manager=ms_snd_card_manager_get(); MSSndCard *capt= capture_card==NULL ? ms_snd_card_manager_get_default_capture_card(manager) : ms_snd_card_manager_get_card(manager,capture_card); audio=audio_stream_new(localport,ms_is_ipv6(remote_ip)); audio_stream_enable_automatic_gain_control(audio,agc); audio_stream_enable_noise_gate(audio,use_ng); + audio_stream_set_echo_canceller_params(audio,ec_len_ms,ec_delay_ms,ec_framesize); + printf("Starting audio stream.\n"); audio_stream_start_now(audio,profile,remote_ip,remoteport,remoteport+1,payload,jitter, ms_snd_card_manager_get_default_playback_card(manager), capt, @@ -281,7 +288,56 @@ void run_media_streams(int localport, const char *remote_ip, int remoteport, in printf("Error: video support not compiled.\n"); #endif } - if (!eq){ + if (eq || ec){ /*read from stdin interactive commands */ + char commands[128]; + commands[127]='\0'; + ms_sleep(1); /* ensure following text be printed after ortp messages */ + if (eq) + printf("\nPlease enter equalizer requests, such as 'eq active 1', 'eq active 0', 'eq 1200 0.1 200'\n"); + if (ec) + printf("\nPlease enter echo canceller requests: ec reset; ec <delay ms> <tail_length ms'\n"); + while(fgets(commands,sizeof(commands)-1,stdin)!=NULL){ + int active,freq,freq_width; + int delay_ms, tail_ms; + float gain; + if (sscanf(commands,"eq active %i",&active)==1){ + audio_stream_enable_equalizer(audio,active); + printf("OK\n"); + }else if (sscanf(commands,"eq %i %f %i",&freq,&gain,&freq_width)==3){ + audio_stream_equalizer_set_gain(audio,freq,gain,freq_width); + printf("OK\n"); + }else if (sscanf(commands,"eq %i %f",&freq,&gain)==2){ + audio_stream_equalizer_set_gain(audio,freq,gain,0); + printf("OK\n"); + }else if (strstr(commands,"dump")){ + int n=0,i; + float *t; + ms_filter_call_method(audio->equalizer,MS_EQUALIZER_GET_NUM_FREQUENCIES,&n); + t=(float*)alloca(sizeof(float)*n); + ms_filter_call_method(audio->equalizer,MS_EQUALIZER_DUMP_STATE,t); + for(i=0;i<n;++i){ + if (fabs(t[i]-1)>0.01){ + printf("%i:%f:0 ",(i*pt->clock_rate)/(2*n),t[i]); + } + } + printf("\nOK\n"); + }else if (sscanf(commands,"ec reset %i",&active)==1){ + //audio_stream_enable_equalizer(audio,active); + //printf("OK\n"); + }else if (sscanf(commands,"ec active %i",&active)==1){ + //audio_stream_enable_equalizer(audio,active); + //printf("OK\n"); + }else if (sscanf(commands,"ec %i %i",&delay_ms,&tail_ms)==2){ + audio_stream_set_echo_canceller_params(audio,tail_ms,delay_ms,128); + // revisit: workaround with old method call to force echo reset + delay_ms*=8; + ms_filter_call_method(audio->ec,MS_FILTER_SET_PLAYBACKDELAY,&delay_ms); + printf("OK\n"); + }else if (strstr(commands,"quit")){ + break; + }else printf("Cannot understand this.\n"); + } + }else{ /* no interactive stuff - continuous debug output */ rtp_session_register_event_queue(session,q); while(cond) { @@ -312,41 +368,9 @@ void run_media_streams(int localport, const char *remote_ip, int remoteport, in parse_events(q); } } - }else{/*read from stdin equalizer commands */ - char commands[128]; - commands[127]='\0'; - printf("Please enter equalizer requests, such as 'eq active 1', 'eq active 0', 'eq 1200 0.1'\n"); - while(fgets(commands,sizeof(commands)-1,stdin)!=NULL){ - int active,freq,freq_width; - float gain; - if (sscanf(commands,"eq active %i",&active)==1){ - audio_stream_enable_equalizer(audio,active); - printf("OK\n"); - }else if (sscanf(commands,"eq %i:%f:%i",&freq,&gain,&freq_width)==3){ - audio_stream_equalizer_set_gain(audio,freq,gain,freq_width); - printf("OK\n"); - }else if (sscanf(commands,"eq %i:%f",&freq,&gain)==2){ - audio_stream_equalizer_set_gain(audio,freq,gain,0); - printf("OK\n"); - }else if (strstr(commands,"dump")){ - int n=0,i; - float *t; - ms_filter_call_method(audio->equalizer,MS_EQUALIZER_GET_NUM_FREQUENCIES,&n); - t=(float*)alloca(sizeof(float)*n); - ms_filter_call_method(audio->equalizer,MS_EQUALIZER_DUMP_STATE,t); - for(i=0;i<n;++i){ - if (fabs(t[i]-1)>0.01){ - printf("%i:%f:0 ",(i*pt->clock_rate)/(2*n),t[i]); } - } - printf("\nOK\n"); - }else if (strstr(commands,"quit")){ - break; - }else printf("Cannot understand this.\n"); - } - } - printf("stoping all...\n"); + printf("stopping all...\n"); if (audio) audio_stream_stop(audio); #ifdef VIDEO_ENABLED