Commit 95030951 authored by Simon Morlat's avatar Simon Morlat

add new function to play a file locally, in or out of calls.

add new function to define a tone or wav file to be played automatically upon call errors
parent 6734882d
......@@ -375,70 +375,98 @@ SalReason sal_reason_to_sip_code(SalReason r){
case SalReasonNoMatch:
ret=481;
break;
case SalReasonRequestTimeout:
ret=408;
break;
case SalReasonMovedPermanently:
ret=301;
break;
case SalReasonGone:
ret=410;
break;
case SalReasonAddressIncomplete:
ret=484;
break;
case SalReasonNotImplemented:
ret=501;
break;
case SalReasonServerTimeout:
ret=504;
break;
case SalReasonBadGateway:
ret=502;
break;
}
return ret;
}
void sal_compute_sal_errors_from_code(int code ,SalError* sal_err,SalReason* sal_reason) {
*sal_err=SalErrorFailure;
switch(code) {
case 301:
*sal_reason=SalReasonMovedPermanently;
break;
case 302:
*sal_reason=SalReasonRedirect;
*sal_err=SalErrorFailure;
break;
case 400:
*sal_err=SalErrorUnknown;
break;
case 401:
case 407:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonUnauthorized;
break;
case 403:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonForbidden;
break;
case 404:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonNotFound;
break;
case 408:
*sal_reason=SalReasonRequestTimeout;
break;
case 410:
*sal_reason=SalReasonGone;
break;
case 415:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonUnsupportedContent;
break;
case 422:
ms_error ("422 not implemented yet");;
break;
case 480:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonTemporarilyUnavailable;
break;
case 481:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonNoMatch;
break;
case 484:
*sal_reason=SalReasonAddressIncomplete;
break;
case 486:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonBusy;
break;
case 487:
break;
case 488:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonNotAcceptable;
break;
case 491:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonRequestPending;
break;
case 501:
*sal_reason=SalReasonNotImplemented;
break;
case 502:
*sal_reason=SalReasonBadGateway;
break;
case 504:
*sal_reason=SalReasonServerTimeout;
break;
case 600:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonDoNotDisturb;
break;
case 603:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonDeclined;
break;
case 503:
*sal_err=SalErrorFailure;
*sal_reason=SalReasonServiceUnavailable;
break;
default:
......
......@@ -721,9 +721,8 @@ static void call_failure(SalOp *op, SalError error, SalReason sr, const char *de
linphone_call_set_state(call,LinphoneCallEnd,"Call declined.");
}else{
linphone_call_set_state(call,LinphoneCallError,details);
if (sr==SalReasonBusy)
linphone_core_play_named_tone(lc,LinphoneToneBusy);
}
linphone_core_play_call_error_tone(lc,call->reason);
if (referer){
/*notify referer of the failure*/
......
......@@ -154,7 +154,7 @@ static void ecc_play_tones(EcCalibrator *ecc){
memset(&tone,0,sizeof(tone));
memset(&expected_tone,0,sizeof(expected_tone));
ms_filter_set_notify_callback(ecc->det,on_tone_received,ecc);
ms_filter_add_notify_callback(ecc->det,on_tone_received,ecc,TRUE);
/* configure the tones to be scanned */
......@@ -188,7 +188,7 @@ static void ecc_play_tones(EcCalibrator *ecc){
ms_filter_call_method(ecc->gen,MS_DTMF_GEN_PLAY_CUSTOM,&tone);
ms_sleep(2);
ms_filter_set_notify_callback(ecc->gen,on_tone_sent,ecc);
ms_filter_add_notify_callback(ecc->gen,on_tone_sent,ecc,TRUE);
/* play the three tones*/
......
......@@ -609,6 +609,8 @@ static void sound_config_read(LinphoneCore *lc)
/*just parse requested stream feature once at start to print out eventual errors*/
linphone_core_get_audio_features(lc);
_linphone_core_set_call_error_tone(lc,LinphoneReasonBusy,LinphoneToneBusy,NULL);
}
static void certificates_config_read(LinphoneCore *lc)
......@@ -5356,17 +5358,24 @@ void linphone_core_set_record_file(LinphoneCore *lc, const char *file){
}
}
typedef enum{
LinphoneToneGenerator,
LinphoneLocalPlayer
}LinphoneAudioResourceType;
static MSFilter *get_dtmf_gen(LinphoneCore *lc){
static MSFilter *get_audio_resource(LinphoneCore *lc, LinphoneAudioResourceType rtype){
LinphoneCall *call=linphone_core_get_current_call (lc);
AudioStream *stream=NULL;
RingStream *ringstream;
if (call){
stream=call->audiostream;
}else if (linphone_core_is_in_conference(lc)){
stream=lc->conf_ctx.local_participant;
}
if (stream){
return stream->dtmfgen;
if (rtype==LinphoneToneGenerator) return stream->dtmfgen;
if (rtype==LinphoneLocalPlayer) return stream->local_player;
return NULL;
}
if (lc->ringstream==NULL){
float amp=lp_config_get_float(lc->config,"sound","dtmf_player_amp",0.1);
......@@ -5374,14 +5383,21 @@ static MSFilter *get_dtmf_gen(LinphoneCore *lc){
if (ringcard == NULL)
return NULL;
lc->ringstream=ring_start(NULL,0,ringcard);
ringstream=lc->ringstream=ring_start(NULL,0,ringcard);
ms_filter_call_method(lc->ringstream->gendtmf,MS_DTMF_GEN_SET_DEFAULT_AMPLITUDE,&amp);
lc->dmfs_playing_start_time=time(NULL);
}else{
ringstream=lc->ringstream;
if (lc->dmfs_playing_start_time!=0)
lc->dmfs_playing_start_time=time(NULL);
}
return lc->ringstream->gendtmf;
if (rtype==LinphoneToneGenerator) return ringstream->gendtmf;
if (rtype==LinphoneLocalPlayer) return ringstream->source;
return NULL;
}
static MSFilter *get_dtmf_gen(LinphoneCore *lc){
return get_audio_resource(lc,LinphoneToneGenerator);
}
/**
......@@ -5403,6 +5419,26 @@ void linphone_core_play_dtmf(LinphoneCore *lc, char dtmf, int duration_ms){
else ms_filter_call_method(f, MS_DTMF_GEN_START, &dtmf);
}
/**
* Plays an audio file to the local user.
* This function works at any time, during calls, or when no calls are running.
* It doesn't request the underlying audio system to support multiple playback streams.
* @param lc the linphone core
* @param audiofile path to audio file in wav PCM 16 bit format.
* @ingroup misc
**/
int linphone_core_play_local(LinphoneCore *lc, const char *audiofile){
MSFilter *f=get_audio_resource(lc,LinphoneLocalPlayer);
int loopms=-1;
if (!f) return -1;
ms_filter_call_method(f,MS_PLAYER_SET_LOOP,&loopms);
if (ms_filter_call_method(f,MS_PLAYER_OPEN,(void*)audiofile)!=0){
return -1;
}
ms_filter_call_method_noarg(f,MS_PLAYER_START);
return 0;
}
void linphone_core_play_named_tone(LinphoneCore *lc, LinphoneToneID toneid){
if (linphone_core_tone_indications_enabled(lc)){
MSFilter *f=get_dtmf_gen(lc);
......@@ -5443,6 +5479,19 @@ void linphone_core_play_named_tone(LinphoneCore *lc, LinphoneToneID toneid){
}
}
void linphone_core_play_call_error_tone(LinphoneCore *lc, LinphoneReason reason){
if (linphone_core_tone_indications_enabled(lc)){
LinphoneToneDescription *tone=linphone_core_get_call_error_tone(lc,reason);
if (tone){
if (tone->audiofile){
linphone_core_play_local(lc,tone->audiofile);
}else if (tone->toneid!=LinphoneToneUndefined){
linphone_core_play_named_tone(lc,tone->toneid);
}
}
}
}
/**
* @ingroup media_parameters
*
......@@ -5672,7 +5721,7 @@ static void sound_config_uninit(LinphoneCore *lc)
if (config->local_ring) ms_free(config->local_ring);
if (config->remote_ring) ms_free(config->remote_ring);
lc->tones=ms_list_free_with_data(lc->tones, (void (*)(void*))linphone_tone_description_destroy);
}
static void video_config_uninit(LinphoneCore *lc)
......@@ -6057,7 +6106,7 @@ const char *linphone_reason_to_string(LinphoneReason err){
return "No error";
case LinphoneReasonNoResponse:
return "No response";
case LinphoneReasonBadCredentials:
case LinphoneReasonForbidden:
return "Bad credentials";
case LinphoneReasonDeclined:
return "Call declined";
......@@ -6079,6 +6128,20 @@ const char *linphone_reason_to_string(LinphoneReason err){
return "Not acceptable here";
case LinphoneReasonNoMatch:
return "No match";
case LinphoneReasonMovedPermanently:
return "Moved permanently";
case LinphoneReasonGone:
return "Gone";
case LinphoneReasonTemporarilyUnavailable:
return "Temporarily unavailable";
case LinphoneReasonAddressIncomplete:
return "Address incomplete";
case LinphoneReasonNotImplemented:
return "Not implemented";
case LinphoneReasonBadGateway:
return "Bad gateway";
case LinphoneReasonServerTimeout:
return "Server timeout";
}
return "unknown error";
}
......
......@@ -164,19 +164,28 @@ typedef struct _LinphoneCall LinphoneCall;
enum _LinphoneReason{
LinphoneReasonNone,
LinphoneReasonNoResponse, /**<No response received from remote*/
LinphoneReasonBadCredentials, /**<Authentication failed due to bad*/
LinphoneReasonForbidden, /**<Authentication failed due to bad credentials or resource forbidden*/
LinphoneReasonDeclined, /**<The call has been declined*/
LinphoneReasonNotFound, /**<Destination of the calls was not found.*/
LinphoneReasonNotAnswered, /**<The call was not answered in time*/
LinphoneReasonNotFound, /**<Destination of the call was not found.*/
LinphoneReasonNotAnswered, /**<The call was not answered in time (request timeout)*/
LinphoneReasonBusy, /**<Phone line was busy */
LinphoneReasonUnsupportedContent, /**<Unsupported content */
LinphoneReasonIOError, /**<Transport error: connection failures, disconnections etc...*/
LinphoneReasonDoNotDisturb, /**<Do not disturb reason*/
LinphoneReasonUnauthorized, /**<Operation is unauthorized because missing credential*/
LinphoneReasonNotAcceptable, /**<Operation like call update rejected by peer*/
LinphoneReasonNoMatch /**<Operation could not be executed by server or remote client because it didn't have any context for it*/
LinphoneReasonNoMatch, /**<Operation could not be executed by server or remote client because it didn't have any context for it*/
LinphoneReasonMovedPermanently, /**<Resource moved permanently*/
LinphoneReasonGone, /**<Resource no longer exists*/
LinphoneReasonTemporarilyUnavailable, /**<Temporarily unavailable*/
LinphoneReasonAddressIncomplete, /**<Address incomplete*/
LinphoneReasonNotImplemented, /**<Not implemented*/
LinphoneReasonBadGateway, /**<Bad gateway*/
LinphoneReasonServerTimeout /**<Server timeout*/
};
#define LinphoneReasonBadCredentials LinphoneReasonForbidden
/*for compatibility*/
#define LinphoneReasonMedia LinphoneReasonUnsupportedContent
......@@ -1821,6 +1830,7 @@ LINPHONE_PUBLIC void linphone_core_set_remote_ringback_tone(LinphoneCore *lc,con
LINPHONE_PUBLIC const char *linphone_core_get_remote_ringback_tone(const LinphoneCore *lc);
LINPHONE_PUBLIC int linphone_core_preview_ring(LinphoneCore *lc, const char *ring,LinphoneCoreCbFunc func,void * userdata);
LINPHONE_PUBLIC int linphone_core_play_local(LinphoneCore *lc, const char *audiofile);
LINPHONE_PUBLIC void linphone_core_enable_echo_cancellation(LinphoneCore *lc, bool_t val);
LINPHONE_PUBLIC bool_t linphone_core_echo_cancellation_enabled(LinphoneCore *lc);
......@@ -2317,6 +2327,8 @@ LINPHONE_PUBLIC const char* linphone_core_get_provisioning_uri(const LinphoneCor
LINPHONE_PUBLIC bool_t linphone_core_is_provisioning_transient(LinphoneCore *lc);
LINPHONE_PUBLIC int linphone_core_migrate_to_multi_transport(LinphoneCore *lc);
LINPHONE_PUBLIC void linphone_core_set_call_error_tone(LinphoneCore *lc, LinphoneReason reason, const char *audiofile);
#ifdef __cplusplus
}
#endif
......
......@@ -194,7 +194,7 @@ int lsd_player_play(LsdPlayer *b, const char *filename ){
ms_warning("Could not play %s",filename);
return -1;
}
ms_filter_set_notify_callback (b->player,lsd_player_on_eop,b);
ms_filter_add_notify_callback (b->player,lsd_player_on_eop,b,FALSE);
lsd_player_configure(b);
ms_filter_call_method_noarg (b->player,MS_PLAYER_START);
return 0;
......@@ -249,7 +249,7 @@ LinphoneSoundDaemon * linphone_sound_daemon_new(const char *cardname, int rate,
mp.pin=0;
lsd_player_init(&lsd->branches[0],mp,MS_ITC_SOURCE_ID,lsd);
ms_filter_set_notify_callback(lsd->branches[0].player,(MSFilterNotifyFunc)lsd_player_configure,&lsd->branches[0]);
ms_filter_add_notify_callback(lsd->branches[0].player,(MSFilterNotifyFunc)lsd_player_configure,&lsd->branches[0],FALSE);
for(i=1;i<MAX_BRANCHES;++i){
mp.pin=i;
lsd_player_init(&lsd->branches[i],mp,MS_FILE_PLAYER_ID,lsd);
......
......@@ -1099,13 +1099,13 @@ SalReason linphone_reason_to_sal(LinphoneReason reason){
return SalReasonUnknown;
case LinphoneReasonNoResponse:
return SalReasonUnknown;
case LinphoneReasonBadCredentials:
case LinphoneReasonForbidden:
return SalReasonForbidden;
case LinphoneReasonDeclined:
return SalReasonDeclined;
case LinphoneReasonNotFound:
return SalReasonNotFound;
case LinphoneReasonNotAnswered:
case LinphoneReasonTemporarilyUnavailable:
return SalReasonTemporarilyUnavailable;
case LinphoneReasonBusy:
return SalReasonBusy;
......@@ -1121,6 +1121,20 @@ SalReason linphone_reason_to_sal(LinphoneReason reason){
return SalReasonUnsupportedContent;
case LinphoneReasonNoMatch:
return SalReasonNoMatch;
case LinphoneReasonMovedPermanently:
return SalReasonMovedPermanently;
case LinphoneReasonGone:
return SalReasonGone;
case LinphoneReasonAddressIncomplete:
return SalReasonAddressIncomplete;
case LinphoneReasonNotImplemented:
return SalReasonNotImplemented;
case LinphoneReasonBadGateway:
return SalReasonBadGateway;
case LinphoneReasonServerTimeout:
return SalReasonServerTimeout;
case LinphoneReasonNotAnswered:
return SalReasonRequestTimeout;
}
return SalReasonUnknown;
}
......@@ -1153,7 +1167,7 @@ LinphoneReason linphone_reason_from_sal(SalReason r){
ret=LinphoneReasonNone;
break;
case SalReasonTemporarilyUnavailable:
ret=LinphoneReasonNone;
ret=LinphoneReasonTemporarilyUnavailable;
break;
case SalReasonServiceUnavailable:
ret=LinphoneReasonIOError;
......@@ -1170,6 +1184,27 @@ LinphoneReason linphone_reason_from_sal(SalReason r){
case SalReasonNoMatch:
ret=LinphoneReasonNoMatch;
break;
case SalReasonRequestTimeout:
ret=LinphoneReasonNotAnswered;
break;
case SalReasonMovedPermanently:
ret=LinphoneReasonMovedPermanently;
break;
case SalReasonGone:
ret=LinphoneReasonGone;
break;
case SalReasonAddressIncomplete:
ret=LinphoneReasonAddressIncomplete;
break;
case SalReasonNotImplemented:
ret=LinphoneReasonNotImplemented;
break;
case SalReasonBadGateway:
ret=LinphoneReasonBadGateway;
break;
case SalReasonServerTimeout:
ret=LinphoneReasonServerTimeout;
break;
}
return ret;
}
......@@ -1275,4 +1310,45 @@ int linphone_core_migrate_to_multi_transport(LinphoneCore *lc){
return 0;
}
LinphoneToneDescription * linphone_tone_description_new(LinphoneReason reason, LinphoneToneID id, const char *audiofile){
LinphoneToneDescription *obj=ms_new0(LinphoneToneDescription,1);
obj->reason=reason;
obj->toneid=id;
obj->audiofile=audiofile ? ms_strdup(audiofile) : NULL;
return obj;
}
void linphone_tone_description_destroy(LinphoneToneDescription *obj){
if (obj->audiofile) ms_free(obj->audiofile);
ms_free(obj);
}
LinphoneToneDescription *linphone_core_get_call_error_tone(const LinphoneCore *lc, LinphoneReason reason){
const MSList *elem;
for (elem=lc->tones;elem!=NULL;elem=elem->next){
LinphoneToneDescription *tone=(LinphoneToneDescription*)elem->data;
if (tone->reason==reason) return tone;
}
return NULL;
}
void _linphone_core_set_call_error_tone(LinphoneCore *lc, LinphoneReason reason, LinphoneToneID id, const char *audiofile){
LinphoneToneDescription *tone=linphone_core_get_call_error_tone(lc,reason);
if (tone){
lc->tones=ms_list_remove(lc->tones,tone);
linphone_tone_description_destroy(tone);
}
tone=linphone_tone_description_new(reason,id,audiofile);
lc->tones=ms_list_append(lc->tones,tone);
}
/**
* Assign an audio file to played locally upon call failure, for a given reason.
* @param lc the core
* @param reason the #LinphoneReason representing the failure error code.
* @param audiofile a wav file to be played when such call failure happens.
* @ingroup misc
**/
void linphone_core_set_call_error_tone(LinphoneCore *lc, LinphoneReason reason, const char *audiofile){
_linphone_core_set_call_error_tone(lc,reason,LinphoneToneUndefined, audiofile);
}
......@@ -343,8 +343,6 @@ int linphone_proxy_config_normalize_number(LinphoneProxyConfig *cfg, const char
void linphone_core_message_received(LinphoneCore *lc, SalOp *op, const SalMessage *msg);
void linphone_core_is_composing_received(LinphoneCore *lc, SalOp *op, const SalIsComposing *is_composing);
void linphone_core_play_tone(LinphoneCore *lc);
void linphone_call_init_stats(LinphoneCallStats *stats, int type);
void linphone_call_fix_call_parameters(LinphoneCall *call);
void linphone_call_init_audio_stream(LinphoneCall *call);
......@@ -588,6 +586,26 @@ struct _LinphoneConference{
bool_t local_muted;
};
typedef enum _LinphoneToneID{
LinphoneToneUndefined,
LinphoneToneBusy,
LinphoneToneCallWaiting,
LinphoneToneCallOnHold,
LinphoneToneCallFailed
}LinphoneToneID;
typedef struct _LinphoneToneDescription{
LinphoneReason reason;
LinphoneToneID toneid;
char *audiofile;
}LinphoneToneDescription;
LinphoneToneDescription * linphone_tone_description_new(LinphoneReason reason, LinphoneToneID id, const char *audiofile);
void linphone_tone_description_destroy(LinphoneToneDescription *obj);
LinphoneToneDescription *linphone_core_get_call_error_tone(const LinphoneCore *lc, LinphoneReason reason);
void linphone_core_play_call_error_tone(LinphoneCore *lc, LinphoneReason reason);
void _linphone_core_set_call_error_tone(LinphoneCore *lc, LinphoneReason reason, LinphoneToneID id, const char *audiofile);
typedef struct _LinphoneConference LinphoneConference;
struct _LinphoneCore
......@@ -653,10 +671,10 @@ struct _LinphoneCore
bool_t use_preview_window;
time_t network_last_check;
bool_t network_last_status;
bool_t ringstream_autorelease;
bool_t pad[3];
bool_t pad[2];
int device_rotation;
int max_calls;
LinphoneTunnel *tunnel;
......@@ -670,6 +688,7 @@ struct _LinphoneCore
UpnpContext *upnp;
#endif //BUILD_UPNP
belle_http_provider_t *http_provider;
MSList *tones;
};
......@@ -779,12 +798,6 @@ void linphone_chat_message_store_state(LinphoneChatMessage *msg);
void linphone_core_message_storage_init(LinphoneCore *lc);
void linphone_core_message_storage_close(LinphoneCore *lc);
typedef enum _LinphoneToneID{
LinphoneToneBusy,
LinphoneToneCallWaiting,
LinphoneToneCallOnHold,
LinphoneToneCallFailed
}LinphoneToneID;
void linphone_core_play_named_tone(LinphoneCore *lc, LinphoneToneID id);
bool_t linphone_core_tone_indications_enabled(LinphoneCore*lc);
const char *linphone_core_create_uuid(LinphoneCore *lc);
......
......@@ -528,6 +528,7 @@ void linphone_gtk_text_received ( LinphoneCore *lc, LinphoneChatRoom *room,
linphone_gtk_push_text ( w,linphone_chat_message_get_from ( msg ),
FALSE,room,msg,FALSE );
}
linphone_core_play_local(lc,linphone_gtk_get_sound_path("incoming_chat.wav"));
linphone_gtk_show_friends();
}
......
......@@ -170,4 +170,5 @@ void linphone_gtk_schedule_restart(void);
void linphone_gtk_set_configuration_uri(void);
GtkWidget * linphone_gtk_show_config_fetching(void);
void linphone_gtk_close_config_fetching(GtkWidget *w, LinphoneConfiguringState state);
const char *linphone_gtk_get_sound_path(const char *file);
......@@ -203,6 +203,27 @@ void linphone_gtk_set_ui_config(const char *key , const char * val){
lp_config_set_string(cfg,"GtkUi",key,val);
}
const char *linphone_gtk_get_sound_path(const char *name){
static char *ret=NULL;
const char *file;
file=linphone_gtk_get_ui_config(name,NULL);
if (file==NULL){
char *dirname=g_path_get_dirname(name);
if (strcmp(dirname,".")!=0){
g_free(dirname);
return name;
}
g_free(dirname);
file=name;
}
if (ret){
g_free(ret);
ret=NULL;
}
ret=g_build_filename(PACKAGE_SOUND_DIR,name,NULL);
return ret;
}
static void parse_item(const char *item, const char *window_name, GtkWidget *w, gboolean show){
char tmp[64];
char *dot;
......
......@@ -314,6 +314,7 @@ typedef enum SalReason{
SalReasonBusy,
SalReasonRedirect,
SalReasonTemporarilyUnavailable,
SalReasonRequestTimeout,
SalReasonNotFound,
SalReasonDoNotDisturb,
SalReasonUnsupportedContent,
......@@ -323,7 +324,13 @@ typedef enum SalReason{
SalReasonRequestPending,
SalReasonUnauthorized,
SalReasonNotAcceptable,
SalReasonNoMatch /*equivalent to 481 Transaction/Call leg does not exist*/
SalReasonNoMatch, /*equivalent to 481 Transaction/Call leg does not exist*/
SalReasonMovedPermanently,
SalReasonGone,
SalReasonAddressIncomplete,
SalReasonNotImplemented,
SalReasonBadGateway,
SalReasonServerTimeout
}SalReason;
const char* sal_reason_to_string(const SalReason reason);
......
mediastreamer2 @ 9cd81464
Subproject commit 0e6be4c7cad7ce3d3cb4feed9a6ad52439ba365e
Subproject commit 9cd81464a916a95a5e4157276ae661d68ed3a7ee
SUBDIRS=C fr it ja cs xml
LINPHONE_SOUNDS=ringback.wav hello8000.wav hello16000.wav
LINPHONE_SOUNDS=ringback.wav hello8000.wav hello16000.wav incoming_chat.wav
LINPHONE_RINGS=rings/orig.wav \
rings/oldphone.wav \
rings/oldphone-mono.wav \
......
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