Skip to content

FREE Voice Changer Broadcasting APP using the AgoraIO SDK

Voice Changer
Become a developer with our complete learning paths
Become a developer with our complete learning paths

You might be wondering how the Agora.io Voice Changer Android Application works, seems awesome and tricky at the same time right?

Before we dive right in, let’s explain some terms you’ll see often in this tutorial. The terms can be found on this link

The Agora.IO Voice Changer

In this tutorial, we will be building a simple Voice Changing Android Application.
You will learn how to use Agora.io SDK to handle voice changing.

Voice Changer

 

Creating A New Compose Project

Github Link: https://github.com/tutorialseu/Voice-Changer-App/tree/build_ui

To get started, we will be creating a new Empty Compose Activity Project.

Voice Changer

After the project is created successfully, open MainActivity.kt and remove the Greeting Function and the references to it in onCreate and DefaultPreview. So that we now have the below codes in the file.

MainActivity.Kt

We have also added a new composable MainView

Building The User Interface.

The User interface will be a very simple one, using the screenshot above,

  1. is a card view hosting a TextView that will show the selected voice effect,
  2. is a button that will enable users to join and leave a channel,
  3. is where the user will enter channel name to join,
  4. is a list of voice effects.

To begin, we will call MainView function within DefaultPreview function and setContent, MainActivity will now look like so

 

In MainView composable we will add a Column layout to align all elements vertically on the screen

 

Using the Modifier, we set the Column to fillMaxSize which includes the vertical and horizontal axis with a bottom padding of 60dp. We also align the elements vertically to the bottom using verticalArrangement.

Designing UI number 1 from the screenshot

 

Within on Column block in MainView, Add a Card to get the Card view, using Modifier to set the Card to fillMaxWidth to fill both horizontal and vertical sides of the screen. Add padding of 15dp to give space around the Card. Add elevation of 10dp to give the elevated shadow around to Card. Within the Card block, Add a Text with a default text “Show Text”, add padding of 24dp, and textAlign to align the text to the center.

MainView function will now look like this

Designing UI numbers 2 and 3 from the screenshot

Let’s start by designing number 3, which is a TextField.

Create a composable function TextFieldView

 

Within the TextFieldView block, add a TextField, within the TextField label block, add a Text with default text “Enter channel name”

 

Since we know that we will be calling the TextFieldView function in MainView, we need to create a TextFieldValue MutableState parameter in TextFieldView constructor, and set it to the TextField value and onValueChange lambda function

Complete TextFieldView composable function will look like below

 

Call the function in MainView below the Card, Also create the TextFieldValue MutableState parameter to MainView, once that is done, we should have an error in DefaultPreview and setContent where we called the MainView, to fix these errors, we also create an instance of TextFieldValue MutableState and call it inside MainView.

Our current full code:

 

Our current design

To complete numbers 2 and 3 from the screenshot, we need to add TextFieldView and an Image in a Row.

First, we create a Row

 

Then add wrapContentWidth, padding top 20dp, start 15dp and verticalAlignment Modifiers, move TextFieldView function inside Row block, Add an Image after TextFieldView, giving default height and width 50dp, contentScale to Crop, contentDescription, padding start 16dp and clickable modifier, since we are using a custom image icon, we use painter, then add ic_join_mic image to drawable resource, images can be download from this link https://drive.google.com/drive/folders/1G3uK7GI1TasHSdLgzC2q5EgCgkYbdM3H?usp=sharing

 

Our current design now looks like this

Designing UI number 4 from the screenshot

Below UI 2 and 3(TextField and Mic icon) Row, we will create a LazyColumn

 

Create LazyColumn, LazyColumn needs a scrollState, so we create

val scrollState = *rememberLazyListState*() inside MainView block, then use the variablescrollState for LazyColumn state, we also create a default list

val list = *listOf*("A","B","C") inside MainView block. *give space at the top of the view

Within LazyColumn block we create Items(){}, use items.size to get the list of items

 

Within items(list.size) block, create Row to hold Image and Text horizontally for Voice Effect icon and title, define Modifiers clickable, fillMaxWidth, padding 20dp and verticalAlignment

 

Inside Row block, add Image, give it a default image ic_join_mic, contentDescription, contentScale Crop, Modifiers(padding 12dp, height and width 60dp), then below Image, add Text and use the default list created above to populate the title by calling list[index] and give the text black color, finally, add a divider(the line that separates each item).*

Full Code right now for the Voice Changer

Design

Now that we have our UI ready, we will go ahead to set up Agora SDK

 

Setup Agora SDK

Adding Agora.IO Voice Changer dependency

To use the Agora.io voice effect, we need to add a dependency that will give us access to the Voice Effect.

The dependency can be found on the Agora.io documentation page, we can follow the Project setup process, but we already have some things set up for us by Android Studio, so we’ll jump to the. necessary ones.

 

This particular dependency gives us all access to all Agora features, not just the Voice Effect. They are other ways we can get the Agora SDK to our project, but having to try out all other methods, I found out that this is the easier way to go. So now head to your Android Studio and navigate to app/build.gradle file.

 

Inside the build.gradle file, add the dependency and click on sync to download everything that comes with the dependency. Once the dependency has synced successfully, it means we are good to proceed.

So let us go AndroidManifest.xml file to add some needed permissions, Manifest file can be located on /app/Manifests/AndroidManifest.xml directory of your project.

 

We need permission for internet, for recording, and audio

Now lets create functions that will help us request and check for this permissions on our device.

So we go to were we have our MainActivity.kt and create CommonUtils object,

 

Within the CommonUtils block, we first create our request code in the CommonUtils object

const val PERMISSIONS_REQUEST_CODE = 10 then define an array of required permissions we want to check for

Then we create a requestPermission function that’ll help us request for permissions from our device.

Then create hasPermissions function to check if our needed permissions has ben ranted

Now let’s add the core Agora functionalities

 

Getting our first edited audio

Github link to the final app: https://github.com/tutorialseu/Voice-Changer-App/tree/get_our_first_edited_audio

You can download the project with a complete UI here: https://github.com/tutorialseu/Voice-Changer-App/tree/build_ui If you want to learn more about how the UI was built, read this article: https://www.notion.so/Creating-A-New-Compose-Project-ed0857a10a054241baa616da6d4a73d9

On our app, we already have our design with a dummy audio list, an input field, an image button to join the channel, and a text that’ll display our selected Voice Effect, so let’s see how we can join a call and change the voice.

First, we need to join a channel, so let’s implement a function to join a channel.

To make our code clean, let’s create a utils class called RtcEngineUtils to handle our joinChannel and setAudioEffect functions.

Within RtcEngineUtils constructor, initialize RtcEngine and Context, which will be needed later.

Then create joinChannel function, the joinChannel will carry a String parameter channelId in its constructor.

Using the engine that was initialized in the class constructor, call joinChannel method

engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, mediaOption)

The joined joinChannel method needs some parameters such as accessToken, channelId, optionalIfo, optionalUid, and mediaOption. Now let’s get those parameters.

AccessToken:

https://agora.io is a platform that allows users to use its SDK to build Live, interactive voice and video for real-time engagement. We will be engaging with the Voice part of the SDK.

To get started, you can log in to Agora if you have an account already, or click on Get Started, then enter your email and accept TNC, then follow the Sign-up process to create an account.

 

After creating an account, you’ll be asked to create a project, simply enter the desired name for your project and select Use Case, then create a project, next step you will be asked to try a demo call(we’ll skip that), click on Next button, next step, click on Documentation (you can also add card)

You can then click on the X icon to close the dialog and go to Dashboard.

 

On the dashboard you’ll find your App ID, which we will need later, you’ll also need to generate a token, to generate token click on Config,

then click on Generate temp RTC token,

enter the channel name, channel name could be any name you like, here we are using voice, then click on Generate Temp Token to get the token.

Now let’s go over to the android studio and start writing some codes

Once you’ve got the token, you can simply add it to your strings.xml for test purposes

var accessToken: String? = context.getString(R.string.agora_access_token)

ChannelId:

The channel Id will be gotten from the input of the user on the input field provided on the design and it has to match the channel name used when creating Token.

MediaOption

We will get our mediaOption from ChannelMediaOptions() method provided to us by the SDK,

The ChannelMediaOptions() gives us access to about four options but we’re interested in the autoSubscribeAudio option. so we can just create an Immutable variable mediaOption and assign ChannelMediaOptions() to it, then get the options from there.

OptionalInfo and OptionalUid

You can assign any desired text to OptionalIfo, we will use "Extra Optional Data" and assign a default value of 0 for our OptionalUid

Now we have gotten all we need to populate joinChannel required parameters.

Before we start with the next feature, let’s look at what RtcEngine does: https://docs.google.com/presentation/d/1JzWLqyGlRP5yl7nfjUmRADOK6kCeTrJeRFP72tLb3OQ/edit#slide=id.g11d57d77427_0_42

Slide 10

RtcEngineUtils full code

Let’s go to MainView function on MainActivity and implement it on our UI.

On MainView constructor let’s create a lambda function for joinChannel

joinChannel: () -> Unit ,

 

then in the Image clickable function in the Row block where we have the Textfield, call the joinChannel() function.

Now we’ll be having errors in two places, on MainView in DefaultPreview function and MainView in setContent block, in DefaultPreview we can just add a lambda argument {}

Then in setContent block, let’s implement the joinChannel callback function in the MainView.

Beofore we implement the joinChannel callback, let’s first declare and initialize RtcEngine and RtcEngineUtils

Initialize RtcEngineUtils

First we declare RtcEngineUtils and RtcEngine in MainActivity block before on onCreate

private var engine: RtcEngine? = null private lateinit var rtcEngineUtils: RtcEngineUtils

 

Then create a function to handle the initialization with a try-catch block in case we have an error we can be able to detect it.

Initialize RtcEngineUtils

In the try block let’s initialise RtcEngineUtils, which requires two parameters which are engine and context

rtcEngineUtils = RtcEngineUtils(engine!!, this.applicationContext)

Initialize RtcEngine

engine = RtcEngine.create(this.*applicationContext*, appId, cEngine)

To initialize engine, we call RtcEngine.create which requires context, appId and IRtcEngineEventHandler

Context:

We can get it using this.*applicationContext*

AppId:

AppId can be gotten from the Agora Dashboard

Click on the red highlighted icon to copy the ID and add it to your strings.xml for testing,

<string name="agora_app_id" translatable="false">baaff38e97c747f39b767787424be07b</string>

 

then declare an immutable variable appId and assign the appId from the string resource to it in the initialiseEngine() try block

val appId = getString(R.string.agora_app_id)

IRtcEngineEventHandler:

RtcEngineEventHandler class will basically handle reports from the Agora SDK so we know what happens at each point of action we trigger.

Let’s create a RtcEngineEventHandler class and extend IRtcEngineEventHandler abstract class which is from the SDK

Then press the shortcut keys Contol + O (on Mac and Windows) to override needed methods, the methods we need to override are :

Right now we don’t need to implement them.

After creating RtcEngineEventHandler and overriding all needed methods, let’s declare the method on MainActivity class and complete Engine initialization.

Once you have gotten all the needed parameters, we can now initialize the engine

engine = RtcEngine.create(this.*applicationContext*, appId, rtcHandler )

then you can pass the engine into RtcEngineUtils

rtcEngineUtils = RtcEngineUtils(engine!!, this.*applicationContext*)

Now the initialization is complete

Let’s call the function inside onCreate before setContent

initialiseEngine()

 

Inside the joinChannel callback block, using rtcEngineUtils call the joinChannel function we created in RtcEngineUtils class and assign the value from the Textfield to it textState.value.text.trim()

We can now run our code, enter channel name, click on the mic image beside text input field to join channel, but It won’t work yet because we need to give permissions to read our Audio and others.

Check Permissions

We had created a function that’ll help us perform permissions check

Now let us go to where we have the button to join a channel and check for permission

Now let us go to where we have the button to join a channel and check for permission

 

The hasPermissions and requestPermission requires a context, so we create an immutable context variable and assign LocalContext.current to it at the top of MainView composable function block.

val context = *LocalContext.current

Now run the app, and click on the join channel image, you should see a scree like this

accept permissions, enter channel name and join channel, our app works, but the voice is not changing.

You can test the app by joining a channel with someone, or using the web testing platform

The web testing platform is really helpful if you don’t have someone to test with, you can enter your App ID, Channel name, and Token, then click on the JOIN button to join and the LEAVE button to leave.

Implement Voice Changing

To be able to change the voice, we will just add a line of code in our joinChannel function

engine.setAudioEffectPreset(Constants.VOICE_CHANGER_EFFECT_HULK)

here we use the engine parameter to access setAudioEffectPreset which accepts an int,

using the Constants which is from the SDK we can access many voice effects, here we choose VOICE_CHANGER_EFFECT_HULK

press and hold the Command key(control key on Windows) and click on the Constant to see its value and other values

Our JoinChannel full code

Run App

Now we can run our app and enter the channel name and click on the mic icon to join the channel, go to the web test platform, click on join, say something and hear the voice effect

Make Voice changing dynamic

Now to be able to change the voice, we need to make the list of voice effects clickable and assign the selected effect to the setAudioEffectPreset function.

First, we create an Int lambda function setAudioEffect: (Int) -> Unit, on MainView composable construction,

then call the function on the Row clickable function in Items of LazyColumn.

setAudioEffect()

it requires an Int parameter, so let’s created a function getAudioEffectPreset in CommonUtils

getAudioEffectPreset takes in a string input and extends an Int to get the Preset values.

Now we can call the getAudioEffectPreset function in the setAudioEffect

Now we need to pass the list of audio effects into the getAudioEffectPreset()

So let’s create AudioEffectItem data class and the list.

The data class has two parameters, icon, and title

Using the AudioEffectItem we can create a simple audioEffectList

The images can be downloaded and added to drawable resource file from this link https://drive.google.com/drive/folders/1G3uK7GI1TasHSdLgzC2q5EgCgkYbdM3H?usp=sharing

We can now go back to MainView composable function in MainActivity, and replace list.size in items of LazyColumn with audioEffectList.size then*audioEffectList[index].title* in setAudioEffect(CommonUtils.getAudioEffectPreset(*audioEffectList[index].title*)) ,

replace the Image resource with *audioEffectList*[index].icon text with audioEffectList[].title

The audioEffectList[index].title and audioEffectList[index].icon are from AudioEffectItem data class we created, to get list of voice effects.

Finally, let’s implement the setAudioEffect callback on MainView in setContent block.

 

We simply use engine to get setAudioEffectPreset and assign the selected preset voice to it.

Our app now looks like this

 

Now we have two thing to do and we are done,

  1. Make the list items clickable to change the voice
  2. Show the selected item on the textview were we have “Show text”

Making the list items clickable to change the voice

To observe the items that is being clicked, let’s add an instance of selectedIndex mutableStateOf Int var selectedIndex by remember { mutableStateOf(0) } on top of MainView composable block, then call selectedIndex in the Row clickable function in LazyColumn times and assign the index to it

We can use the selectedIndex to change selected background color .background(color = if (selectedIndex == index) Color(0xFF7393B3) else Color.White)

 

Run app and see what changed

Show the selected item on the textview were we have “Show text”

To show the selected voice effect on the text, we simply replace the default text with

audioEffectList*[selectedIndex].title

Last thing to do, go the joinChannel function and comment out or remove

engine.setAudioEffectPreset(Constants.VOICE_CHANGER_EFFECT_HULK) as it is no more useful because setAudioEffect now does what it was doing

We can now run our code and test.

MainView in setContent now looks like so

MainActivity full code we have now:

The app

Lost in coding? Discover our Learning Paths!
Lost in coding? Discover our Learning Paths!
Enter your email and we will send you the PDF guide:
Enter your email and we will send you the PDF guide