/* audio_call.c * * * Copyright (C) 2014 Toxic All Rights Reserved. * * This file is part of Toxic. * * Toxic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Toxic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Toxic. If not, see . * */ #include "toxic.h" #include "windows.h" #include "audio_call.h" #include "audio_device.h" #include "chat_commands.h" #include "global_commands.h" #include "line_info.h" #include "notify.h" #include "friendlist.h" #include "chat.h" #ifdef VIDEO #include "video_call.h" #endif /* VIDEO */ #include #include #include #include #include #include #include #ifdef __APPLE__ #include #include #else #include #include /* compatibility with older versions of OpenAL */ #ifndef ALC_ALL_DEVICES_SPECIFIER #include #endif #endif extern FriendsList Friends; #define cbend pthread_exit(NULL) #define frame_size (CallControl.audio_sample_rate * CallControl.audio_frame_duration / 1000) static int set_call(Call* call, bool start) { call->in_idx = -1; call->out_idx = -1; #ifdef VIDEO call->vin_idx = -1; call->vout_idx = -1; #endif /* VIDEO */ if ( start ) { call->ttas = true; if ( pthread_mutex_init(&call->mutex, NULL) != 0 ) return -1; } else { call->ttid = 0; if ( pthread_mutex_destroy(&call->mutex) != 0 ) return -1; } return 0; } void call_cb ( ToxAV *av, uint32_t friend_number, bool audio_enabled, bool video_enabled, void *user_data ); void callstate_cb ( ToxAV *av, uint32_t friend_number, uint32_t state, void *user_data ); void receive_audio_frame_cb ( ToxAV *av, uint32_t friend_number, int16_t const *pcm, size_t sample_count, uint8_t channels, uint32_t sampling_rate, void *user_data ); void receive_video_frame_cb ( ToxAV *av, uint32_t friend_number, uint16_t width, uint16_t height, uint8_t const *y, uint8_t const *u, uint8_t const *v, uint8_t const *a, int32_t ystride, int32_t ustride, int32_t vstride, int32_t astride, void *user_data ); void callback_recv_invite ( Tox *m, uint32_t friend_number ); void callback_recv_ringing ( uint32_t friend_number ); void callback_recv_starting ( uint32_t friend_number ); void callback_recv_ending ( uint32_t friend_number ); void callback_call_started ( uint32_t friend_number ); void callback_call_canceled ( uint32_t friend_number ); void callback_call_rejected ( uint32_t friend_number ); void callback_call_ended ( uint32_t friend_number ); void write_device_callback( uint32_t friend_number, const int16_t* PCM, uint16_t sample_count, uint8_t channels, uint32_t sample_rate ); static void print_err (ToxWindow *self, const char *error_str) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "%s", error_str); } ToxAV *init_audio(ToxWindow *self, Tox *tox) { TOXAV_ERR_NEW error; CallControl.audio_errors = ae_None; CallControl.prompt = self; CallControl.pending_call = false; CallControl.av = toxav_new(tox, &error); CallControl.audio_enabled = true; CallControl.audio_bit_rate = 64; CallControl.audio_sample_rate = 48000; CallControl.audio_frame_duration = 20; CallControl.audio_channels = 1; #ifndef VIDEO CallControl.video_enabled = false; CallControl.video_bit_rate = 0; CallControl.video_frame_duration = 0; #endif /* VIDEO */ memset(CallControl.calls, 0, sizeof(CallControl.calls)); if ( !CallControl.av ) { CallControl.audio_errors |= ae_StartingCoreAudio; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to init ToxAV"); return NULL; } if ( init_devices(CallControl.av) == de_InternalError ) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to init devices"); toxav_kill(CallControl.av); return CallControl.av = NULL; } toxav_callback_call(CallControl.av, call_cb, tox); toxav_callback_call_state(CallControl.av, callstate_cb, NULL); toxav_callback_audio_receive_frame(CallControl.av, receive_audio_frame_cb, NULL); return CallControl.av; } void terminate_audio() { int i; for (i = 0; i < MAX_CALLS; ++i) stop_transmission(&CallControl.calls[i], i); if ( CallControl.av ) toxav_kill(CallControl.av); terminate_devices(); } void read_device_callback(const int16_t* captured, uint32_t size, void* data) { TOXAV_ERR_SEND_FRAME error; uint32_t friend_number = *((uint32_t *)data); /* TODO: Or pass an array of call_idx's */ int64_t sample_count = ((int64_t) CallControl.audio_sample_rate) * \ ((int64_t) CallControl.audio_frame_duration) / 1000; if ( sample_count <= 0 || toxav_audio_send_frame(CallControl.av, friend_number, captured, sample_count, CallControl.audio_channels, CallControl.audio_sample_rate, &error) == false ) {} } void write_device_callback(uint32_t friend_number, const int16_t* PCM, uint16_t sample_count, uint8_t channels, uint32_t sample_rate) { if ( CallControl.calls[friend_number].ttas ) write_out(CallControl.calls[friend_number].out_idx, PCM, sample_count, channels, sample_rate); } int start_transmission(ToxWindow *self, Call *call) { if ( !self || !CallControl.av ) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to prepare transmission"); return -1; } if (set_call(call, true) == -1) return -1; DeviceError error = open_primary_device(input, &call->in_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels); if ( error != de_None ) { if ( error == de_FailedStart) line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to start input device"); if ( error == de_InternalError ) line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Internal error with opening input device"); } if ( register_device_callback(self->num, call->in_idx, read_device_callback, &self->num, true) != de_None) /* Set VAD as true for all; TODO: Make it more dynamic */ line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to register input handler!"); if ( open_primary_device(output, &call->out_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels) != de_None ) { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Failed to open output device!"); call->has_output = 0; } return 0; } int stop_transmission(Call *call, uint32_t friend_number) { if ( call->ttas ) { TOXAV_ERR_CALL_CONTROL error = TOXAV_ERR_CALL_CONTROL_OK; if ( CallControl.call_state > TOXAV_FRIEND_CALL_STATE_FINISHED ) toxav_call_control(CallControl.av, friend_number, TOXAV_CALL_CONTROL_CANCEL, &error); if ( error == TOXAV_ERR_CALL_CONTROL_OK ) { call->ttas = false; if ( call->in_idx != -1 ) close_device(input, call->in_idx); if ( call->out_idx != -1 ) close_device(output, call->out_idx); if ( set_call(call, false) == -1 ) return -1; return 0; } else { return -1; } } return -1; } /* * End of transmission */ /* * Callbacks */ void call_cb(ToxAV *av, uint32_t friend_number, bool audio_enabled, bool video_enabled, void *user_data) { Tox *m = (Tox *) user_data; CallControl.pending_call = true; callback_recv_invite(m, friend_number); } void callstate_cb(ToxAV *av, uint32_t friend_number, uint32_t state, void *user_data) { CallControl.call_state = state; switch ( state ) { case ( TOXAV_FRIEND_CALL_STATE_ERROR ): line_info_add(CallControl.prompt, NULL, NULL, NULL, SYS_MSG, 0, 0, "ToxAV callstate error!"); #ifdef VIDEO callback_video_end(friend_number); #endif /* VIDEO */ stop_transmission(&CallControl.calls[friend_number], friend_number); callback_call_ended(friend_number); CallControl.pending_call = false; break; case ( TOXAV_FRIEND_CALL_STATE_FINISHED ): if ( CallControl.pending_call ) callback_call_rejected(friend_number); else callback_call_ended(friend_number); #ifdef VIDEO callback_recv_video_end(friend_number); callback_video_end(friend_number); #endif /* VIDEO */ stop_transmission(&CallControl.calls[friend_number], friend_number); /* Reset stored call state after finishing */ CallControl.call_state = 0; CallControl.pending_call = false; break; default: if ( CallControl.pending_call ) { /* Start answered call */ callback_call_started(friend_number); CallControl.pending_call = false; } else { #ifdef VIDEO /* Handle receiving client video call states */ if ( state & TOXAV_FRIEND_CALL_STATE_SENDING_V ) callback_recv_video_starting(friend_number); else if ( state & ~TOXAV_FRIEND_CALL_STATE_SENDING_V ) callback_recv_video_end(friend_number); #endif /* VIDEO */ } break; } } void receive_audio_frame_cb(ToxAV *av, uint32_t friend_number, int16_t const *pcm, size_t sample_count, uint8_t channels, uint32_t sampling_rate, void *user_data) { write_device_callback(friend_number, pcm, sample_count, channels, sampling_rate); } void audio_bit_rate_status_cb(ToxAV *av, uint32_t friend_number, uint32_t audio_bit_rate, uint32_t video_bit_rate, void *user_data) { CallControl.audio_bit_rate = audio_bit_rate; } void callback_recv_invite(Tox *m, uint32_t friend_number) { if (friend_number >= Friends.max_idx) return; if (Friends.list[friend_number].chatwin == -1) { if (get_num_active_windows() >= MAX_WINDOWS_NUM) return; Friends.list[friend_number].chatwin = add_window(m, new_chat(m, Friends.list[friend_number].num)); } ToxWindow *windows = CallControl.prompt; int i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i].onInvite != NULL && windows[i].num == friend_number) { windows[i].onInvite(&windows[i], CallControl.av, friend_number, CallControl.call_state); } } } void callback_recv_ringing(uint32_t friend_number) { ToxWindow *windows = CallControl.prompt; int i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i].onRinging != NULL && windows[i].num == friend_number) { windows[i].onRinging(&windows[i], CallControl.av, friend_number, CallControl.call_state); } } } void callback_recv_starting(uint32_t friend_number) { ToxWindow* windows = CallControl.prompt; int i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) { if ( windows[i].onStarting != NULL && windows[i].num == friend_number ) { windows[i].onStarting(&windows[i], CallControl.av, friend_number, CallControl.call_state); if ( 0 != start_transmission(&windows[i], &CallControl.calls[friend_number]) ) /* YEAH! */ line_info_add(&windows[i], NULL, NULL, NULL, SYS_MSG, 0, 0 , "Error starting transmission!"); return; } } } void callback_recv_ending(uint32_t friend_number) { ToxWindow *windows = CallControl.prompt; int i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i].onEnding != NULL && windows[i].num == friend_number) { windows[i].onEnding(&windows[i], CallControl.av, friend_number, CallControl.call_state); } } } void callback_call_started(uint32_t friend_number) { ToxWindow* windows = CallControl.prompt; int i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) if ( windows[i].onStart != NULL && windows[i].num == friend_number ) { windows[i].onStart(&windows[i], CallControl.av, friend_number, CallControl.call_state); if ( 0 != start_transmission(&windows[i], &CallControl.calls[friend_number]) ) {/* YEAH! */ line_info_add(&windows[i], NULL, NULL, NULL, SYS_MSG, 0, 0, "Error starting transmission!"); return; } } } void callback_call_canceled(uint32_t friend_number) { ToxWindow *windows = CallControl.prompt; int i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i].onCancel != NULL && windows[i].num == friend_number) { windows[i].onCancel(&windows[i], CallControl.av, friend_number, CallControl.call_state); } } } void callback_call_rejected(uint32_t friend_number) { ToxWindow *windows = CallControl.prompt; int i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i].onReject != NULL && windows[i].num == friend_number) { windows[i].onReject(&windows[i], CallControl.av, friend_number, CallControl.call_state); } } } void callback_call_ended(uint32_t friend_number) { ToxWindow *windows = CallControl.prompt; int i; for (i = 0; i < MAX_WINDOWS_NUM; ++i) { if (windows[i].onEnd != NULL && windows[i].num == friend_number) { windows[i].onEnd(&windows[i], CallControl.av, friend_number, CallControl.call_state); } } } /* * End of Callbacks */ /* * Commands from chat_commands.h */ void cmd_call(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { TOXAV_ERR_CALL error; const char *error_str; if ( argc != 0 ) { error_str = "Unknown arguments."; goto on_error; } if ( !CallControl.av ) { error_str = "ToxAV not supported!"; goto on_error; } if ( !self->stb->connection ) { error_str = "Friend is offline."; goto on_error; } if ( CallControl.pending_call ) { error_str = "Already a pending call!"; goto on_error; } toxav_call(CallControl.av, self->num, CallControl.audio_bit_rate, CallControl.video_bit_rate, &error); if ( error != TOXAV_ERR_CALL_OK ) { if ( error == TOXAV_ERR_CALL_FRIEND_ALREADY_IN_CALL ) error_str = "Already in a call!"; else if ( error == TOXAV_ERR_CALL_MALLOC ) error_str = "Memory allocation issue"; else if ( error == TOXAV_ERR_CALL_FRIEND_NOT_FOUND ) error_str = "Friend number invalid"; else if ( error == TOXAV_ERR_CALL_FRIEND_NOT_CONNECTED ) error_str = "Friend is valid but not currently connected"; else error_str = "Internal error!"; goto on_error; } CallControl.pending_call = true; callback_recv_ringing(self->num); return; on_error: print_err(self, error_str); } void cmd_answer(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { TOXAV_ERR_ANSWER error; const char *error_str; if ( argc != 0 ) { error_str = "Unknown arguments."; goto on_error; } if ( !CallControl.av ) { error_str = "Audio not supported!"; goto on_error; } if ( !CallControl.pending_call ) { error_str = "No incoming call!"; goto on_error; } toxav_answer(CallControl.av, self->num, CallControl.audio_bit_rate, CallControl.video_bit_rate, &error); if ( error != TOXAV_ERR_ANSWER_OK ) { if ( error == TOXAV_ERR_ANSWER_FRIEND_NOT_CALLING ) error_str = "No incoming call!"; else if ( error == TOXAV_ERR_ANSWER_CODEC_INITIALIZATION ) error_str = "Failed to initialize codecs!"; else if ( error == TOXAV_ERR_ANSWER_FRIEND_NOT_FOUND ) error_str = "Friend not found!"; else if ( error == TOXAV_ERR_ANSWER_INVALID_BIT_RATE ) error_str = "Invalid bit rate!"; else error_str = "Internal error!"; goto on_error; } /* Callback will print status... */ callback_recv_starting(self->num); CallControl.pending_call = false; return; on_error: print_err (self, error_str); } void cmd_reject(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if ( argc != 0 ) { error_str = "Unknown arguments."; goto on_error; } if ( !CallControl.av ) { error_str = "Audio not supported!"; goto on_error; } if ( !CallControl.pending_call ) { error_str = "No incoming call!"; goto on_error; } /* Manually send a cancel call control because call hasn't started */ toxav_call_control(CallControl.av, self->num, TOXAV_CALL_CONTROL_CANCEL, NULL); CallControl.pending_call = false; /* Callback will print status... */ callback_call_rejected(self->num); return; on_error: print_err (self, error_str); } void cmd_hangup(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str = NULL; if ( !CallControl.av ) { error_str = "Audio not supported!"; goto on_error; } if ( argc != 0 ) { error_str = "Unknown arguments."; goto on_error; } if ( !self->is_call && !CallControl.pending_call ) { error_str = "Not in a call."; goto on_error; } #ifdef VIDEO callback_video_end(self->num); #endif /* VIDEO */ stop_current_call(self); return; on_error: print_err (self, error_str); } void cmd_list_devices(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if ( argc != 1 ) { if ( argc < 1 ) error_str = "Type must be specified!"; else error_str = "Only one argument allowed!"; goto on_error; } DeviceType type; if ( strcasecmp(argv[1], "in") == 0 ) /* Input devices */ type = input; else if ( strcasecmp(argv[1], "out") == 0 ) /* Output devices */ type = output; else { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } print_devices(self, type); return; on_error: print_err (self, error_str); } /* This changes primary device only */ void cmd_change_device(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if ( argc != 2 ) { if ( argc < 1 ) error_str = "Type must be specified!"; else if ( argc < 2 ) error_str = "Must have id!"; else error_str = "Only two arguments allowed!"; goto on_error; } DeviceType type; if ( strcmp(argv[1], "in") == 0 ) /* Input devices */ type = input; else if ( strcmp(argv[1], "out") == 0 ) /* Output devices */ type = output; else { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } char *end; long int selection = strtol(argv[2], &end, 10); if ( *end ) { error_str = "Invalid input"; goto on_error; } if ( set_primary_device(type, selection) == de_InvalidSelection ) { error_str="Invalid selection!"; goto on_error; } return; on_error: print_err (self, error_str); } void cmd_ccur_device(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if ( argc != 2 ) { if ( argc < 1 ) error_str = "Type must be specified!"; else if ( argc < 2 ) error_str = "Must have id!"; else error_str = "Only two arguments allowed!"; goto on_error; } DeviceType type; if ( strcmp(argv[1], "in") == 0 ) /* Input devices */ type = input; else if ( strcmp(argv[1], "out") == 0 ) /* Output devices */ type = output; else { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } char *end; long int selection = strtol(argv[2], &end, 10); if ( *end ) { error_str = "Invalid input"; goto on_error; } if ( selection_valid(type, selection) == de_InvalidSelection ) { error_str="Invalid selection!"; goto on_error; } /* If call is active, change device */ if ( self->is_call ) { Call* this_call = &CallControl.calls[self->num]; if ( this_call->ttas ) { if ( type == output ) { pthread_mutex_lock(&this_call->mutex); close_device(output, this_call->out_idx); this_call->has_output = open_device(output, selection, &this_call->out_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels) == de_None ? 1 : 0; pthread_mutex_unlock(&this_call->mutex); } else { /* TODO: check for failure */ close_device(input, this_call->in_idx); open_device(input, selection, &this_call->in_idx, CallControl.audio_sample_rate, CallControl.audio_frame_duration, CallControl.audio_channels); /* Set VAD as true for all; TODO: Make it more dynamic */ register_device_callback(self->num, this_call->in_idx, read_device_callback, &self->num, true); } } } self->device_selection[type] = selection; return; on_error: print_err (self, error_str); } void cmd_mute(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if ( argc != 1 ) { if ( argc < 1 ) error_str = "Type must be specified!"; else error_str = "Only two arguments allowed!"; goto on_error; } DeviceType type; if ( strcasecmp(argv[1], "in") == 0 ) /* Input devices */ type = input; else if ( strcasecmp(argv[1], "out") == 0 ) /* Output devices */ type = output; else { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid type: %s", argv[1]); return; } /* If call is active, use this_call values */ if ( self->is_call ) { Call* this_call = &CallControl.calls[self->num]; pthread_mutex_lock(&this_call->mutex); if ( type == input ) { device_mute(type, this_call->in_idx); self->chatwin->infobox.in_is_muted ^= 1; } else { device_mute(type, this_call->out_idx); self->chatwin->infobox.out_is_muted ^= 1; } pthread_mutex_unlock(&this_call->mutex); } return; on_error: print_err (self, error_str); } void cmd_sense(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { const char *error_str; if ( argc != 1 ) { if ( argc < 1 ) error_str = "Must have value!"; else error_str = "Only two arguments allowed!"; goto on_error; } char *end; float value = strtof(argv[1], &end); if ( *end ) { error_str = "Invalid input"; goto on_error; } /* Call must be active */ if ( self->is_call ) { device_set_VAD_treshold(CallControl.calls[self->num].in_idx, value); self->chatwin->infobox.vad_lvl = value; } return; on_error: print_err (self, error_str); } void stop_current_call(ToxWindow* self) { if ( CallControl.pending_call ) { toxav_call_control(CallControl.av, self->num, TOXAV_CALL_CONTROL_CANCEL, NULL); callback_call_canceled(self->num); } else { stop_transmission(&CallControl.calls[self->num], self->num); callback_call_ended(self->num); } CallControl.pending_call = false; }