Karaoke App
About Karaoke App
Karaoke apps rank among the most popular audio-related applications within the mobile app marketplace. Who doesn't like a good karaoke party?
The idea behind these apps is straightforward, yet their development can become highly complex without a fundamental understanding of audio programming. For example making sure that the backing track is in sync with the recording is not straightforward task.
Fortunately, the process of creating a karaoke app is significantly simplified when using the Switchboard SDK. This example will illustrate how you can easily construct one by essentially connecting various audio nodes.
Karaoke App
You can find the source code on the following link:
Karaoke App - iOS
You can find the source code on the following link:
Karaoke App - Android
Perfect Sync: Offset Below 10 ms
In karaoke-like apps, where there is a backing track and a vocal or voice recording over it, it's crucial that the vocals are synchronized with the music in the final recording to ensure a pleasant user experience. Fortunately, with the Switchboard SDK, the offset is reduced almost to zero, keeping the backing track perfectly in sync with the vocals.
Features:
The app has the following features:
- Vocal recording over a backing track
- Ability to use a volume mixer after recording
- Ability to apply different effects after recording
- Sharing of the completed recording
It consists of the following screens:
- Song List: Listen to the available songs, and choose one
- Sing: Play the selected song and record your voice over it
- Mixing: Apply effects on your voice and use the volume mixer to achieve perfect balance between your voice and the song. Export and share the rendered file.
You can find more info about the screens below.
This example uses the Superpowered Extension.
Why use the Superpowered Extension instead of Superpowered directly?
Song List Screen
The Song List screen contains a list of available songs.
You can play and pause and different songs, and press the "Sing" button when you have selected your favorite.
Audio Graph
The audio graph for the Song List screen contains a player node that is routed to the speaker output:
Code Example
- Swift
- Kotlin
import SwitchboardSDK
class SongListAudioSystem: AudioSystem {
let audioPlayerNode = SBAudioPlayerNode()
override init() {
super.init()
audioGraph.addNode(audioPlayerNode)
audioGraph.connect(audioPlayerNode, to: audioGraph.outputNode)
}
func play() {
audioPlayerNode.play()
}
func pause() {
audioPlayerNode.pause()
}
func loadSong(songURL: String) {
audioPlayerNode.load(songURL)
}
}
import android.content.Context
import com.synervoz.switchboard.sdk.Codec
import com.synervoz.switchboard.sdk.audioengine.AudioEngine
import com.synervoz.switchboard.sdk.audiograph.AudioGraph
import com.synervoz.switchboard.sdk.audiographnodes.AudioPlayerNode
import com.synervoz.switchboard.sdk.utils.AssetLoader
class SongListAudioEngine(context: Context) {
val audioEngine = AudioEngine(context)
val audioGraph = AudioGraph()
val audioPlayerNode = AudioPlayerNode()
init {
audioGraph.addNode(audioPlayerNode)
audioGraph.connect(audioPlayerNode, audioGraph.outputNode)
}
fun start() {
audioEngine.start(audioGraph)
}
fun stop() {
audioEngine.stop()
}
fun loadSong(context: Context, songName: String) {
audioPlayerNode.load(AssetLoader.load(context, songName), Codec.createFromFileName(songName))
}
fun close() {
audioEngine.close()
audioGraph.close()
audioPlayerNode.close()
}
}
Sing Screen
The Sing screen consists of a progress bar for the backing track, a start / finish recording button and a lyrics view.
When you are ready to sing, press the Start button. This will start the playback of the backing track, and your vocal input is recorded. Press finish when you want to stop singing. This will bring you to the Mixer screen.
On this screen please use wired headphones for the best experience!
Audio Graph
To make sure that the recording is in sync with the audio playback we use a SubgraphProcessorNode
. This ensures that the RecorderNode and the AudioPlayerNode for the backing track is started at the same time.
Code Example
- Swift
- Kotlin
import SwitchboardSDK
import SwitchboardSuperpowered
class SingAudioSystem: AudioSystem {
let internalAudioGraph = SBAudioGraph()
let subgraphNode = SBSubgraphProcessorNode()
let audioPlayerNode = SBAudioPlayerNode()
let recorderNode = SBRecorderNode()
let splitterNode = SBBusSplitterNode()
let multiChannelToMonoNode = SBMultiChannelToMonoNode()
let vuMeterNode = SBVUMeterNode()
override init() {
super.init()
vuMeterNode.smoothingDurationMs = 100.0
internalAudioGraph.addNode(audioPlayerNode)
internalAudioGraph.addNode(recorderNode)
internalAudioGraph.addNode(splitterNode)
internalAudioGraph.addNode(multiChannelToMonoNode)
internalAudioGraph.addNode(vuMeterNode)
internalAudioGraph.connect(internalAudioGraph.inputNode, to: splitterNode)
internalAudioGraph.connect(splitterNode, to: recorderNode)
internalAudioGraph.connect(splitterNode, to: multiChannelToMonoNode)
internalAudioGraph.connect(multiChannelToMonoNode, to: vuMeterNode)
internalAudioGraph.connect(audioPlayerNode, to: internalAudioGraph.outputNode)
subgraphNode.audioGraph = internalAudioGraph
audioGraph.addNode(subgraphNode)
audioGraph.connect(audioGraph.inputNode, to: subgraphNode)
audioGraph.connect(subgraphNode, to: audioGraph.outputNode)
audioEngine.microphoneEnabled = true
}
override func start() {
internalAudioGraph.start()
super.start()
}
func playAndRecord() {
audioPlayerNode.play()
recorderNode.start()
}
func loadSong(songURL: String) {
audioPlayerNode.load(songURL)
}
func getSongDurationInSeconds() -> Double {
return audioPlayerNode.duration()
}
func getPositionInSeconds() -> Double {
return audioPlayerNode.position
}
func getProgress() -> Float {
return Float(audioPlayerNode.position / audioPlayerNode.duration())
}
func isPlaying() -> Bool {
return audioPlayerNode.isPlaying
}
func finish() {
super.stop()
internalAudioGraph.stop()
audioPlayerNode.stop()
recorderNode.stop(Config.recordingFilePath, withFormat: Config.fileFormat)
}
}
import android.content.Context
import com.synervoz.switchboard.sdk.Codec
import com.synervoz.switchboard.sdk.SwitchboardSDK
import com.synervoz.switchboard.sdk.audioengine.AudioEngine
import com.synervoz.switchboard.sdk.audiograph.AudioGraph
import com.synervoz.switchboard.sdk.audiographnodes.AudioPlayerNode
import com.synervoz.switchboard.sdk.audiographnodes.RecorderNode
import com.synervoz.switchboard.sdk.audiographnodes.SubgraphProcessorNode
import com.synervoz.switchboard.sdk.utils.AssetLoader
class SingAudioEngine {
val audioEngine = AudioEngine(enableInput = true)
val audioGraph = AudioGraph()
val internalAudioGraph = AudioGraph()
val subgraphNode = SubgraphProcessorNode()
val audioPlayerNode = AudioPlayerNode()
val recorderNode = RecorderNode()
init {
internalAudioGraph.addNode(audioPlayerNode)
internalAudioGraph.addNode(recorderNode)
internalAudioGraph.connect(internalAudioGraph.inputNode, recorderNode)
internalAudioGraph.connect(audioPlayerNode, internalAudioGraph.outputNode)
subgraphNode.setAudioGraph(internalAudioGraph)
audioGraph.addNode(subgraphNode)
audioGraph.connect(audioGraph.inputNode, subgraphNode)
audioGraph.connect(subgraphNode, audioGraph.outputNode)
}
var recordingFilePath = SwitchboardSDK.getTemporaryDirectoryPath() + "recording.wav"
fun startAudioEngine() {
audioEngine.start(audioGraph)
}
fun stopAudioEngine() {
audioEngine.stop()
}
fun playAndRecord() {
audioPlayerNode.play()
recorderNode.start()
internalAudioGraph.start()
}
fun loadSong(context: Context, songName: String) {
audioPlayerNode.load(AssetLoader.load(context, songName), Codec.createFromFileName(songName))
}
fun getSongDurationInSeconds() : Double {
return audioPlayerNode.getDuration()
}
fun getPositionInSeconds() : Double {
return audioPlayerNode.position
}
fun getProgress(): Float {
return (audioPlayerNode.position / audioPlayerNode.getDuration()).toFloat()
}
fun isPlaying() = audioPlayerNode.isPlaying
fun finish() {
audioEngine.stop()
audioGraph.stop()
internalAudioGraph.stop()
audioPlayerNode.stop()
recorderNode.stop(recordingFilePath, Codec.WAV)
}
fun close() {
audioEngine.close()
audioGraph.close()
audioPlayerNode.close()
recorderNode.close()
}
}
Mixer Screen
The Mixer screen consists of a seek bar for the player which allows you to seek forward and backward in the mix of your vocal input and the backing track.
It also has volume sliders for the vocals and backing track to make you able to mix volumes to your liking.
You can also enable different effects on the vocals by using the effect switches.
When you are done with the mixing and editing you can save and share your recording using your favorite app by tapping the Export button.
Audio Graph
The same audio graph will be used with the Offline Graph Renderer to render the final mix to an output file which can be shared.
- Swift
- Kotlin
import SwitchboardSDK
import SwitchboardSuperpowered
class MixerAudioSystem: AudioSystem {
let musicPlayer = SBAudioPlayerNode()
let voicePlayer = SBAudioPlayerNode()
let mixerNode = SBMixerNode()
let offlineGraphRenderer = SBOfflineGraphRenderer()
let musicGainNode = SBGainNode()
let voiceGainNode = SBGainNode()
let reverbNode = SBReverbNode()
let compressorNode = SBCompressorNode()
let avpcNode = SBAutomaticVocalPitchCorrectionNode()
override init() {
super.init()
reverbNode.isEnabled = false
compressorNode.isEnabled = false
avpcNode.isEnabled = false
audioGraph.addNode(musicPlayer)
audioGraph.addNode(voicePlayer)
audioGraph.addNode(mixerNode)
audioGraph.addNode(musicGainNode)
audioGraph.addNode(voiceGainNode)
audioGraph.addNode(reverbNode)
audioGraph.addNode(compressorNode)
audioGraph.addNode(avpcNode)
audioGraph.connect(musicPlayer, to: musicGainNode)
audioGraph.connect(musicGainNode, to: mixerNode)
audioGraph.connect(voicePlayer, to: voiceGainNode)
audioGraph.connect(voiceGainNode, to: avpcNode)
audioGraph.connect(avpcNode, to: compressorNode)
audioGraph.connect(compressorNode, to: reverbNode)
audioGraph.connect(reverbNode, to: mixerNode)
audioGraph.connect(mixerNode, to: audioGraph.outputNode)
audioEngine.microphoneEnabled = false
}
func isPlaying() -> Bool {
return musicPlayer.isPlaying
}
func renderMix() -> String {
let sampleRate = max(musicPlayer.sourceSampleRate, voicePlayer.sourceSampleRate)
musicPlayer.position = 0.0
voicePlayer.position = 0.0
musicPlayer.play()
voicePlayer.play()
offlineGraphRenderer.sampleRate = sampleRate
offlineGraphRenderer.maxNumberOfSecondsToRender = musicPlayer.duration()
offlineGraphRenderer.processGraph(audioGraph, withOutputFile: Config.mixedFilePath, withOutputFileCodec: Config.fileFormat)
return Config.mixedFilePath
}
func play() {
musicPlayer.play()
voicePlayer.play()
}
func pause() {
musicPlayer.pause()
voicePlayer.pause()
}
func loadSong(songURL: String) {
musicPlayer.load(songURL)
}
func loadRecording(recordingPath: String) {
voicePlayer.load(recordingPath, withFormat: Config.fileFormat)
}
func getSongDurationInSeconds() -> Double {
return musicPlayer.duration()
}
func getPositionInSeconds() -> Double {
return musicPlayer.position
}
func setPositionInSeconds(position: Double) {
musicPlayer.position = position
if (voicePlayer.duration() > position) {
voicePlayer.position = position
}
}
func getProgress() -> Float {
return Float(musicPlayer.position / musicPlayer.duration())
}
func setMusicVolume(volume: Float) {
musicGainNode.gain = volume
}
func setVoiceVolume(volume: Float) {
voiceGainNode.gain = volume
}
func enableReverb(enable: Bool) {
reverbNode.isEnabled = enable
}
func enableCompressor(enable: Bool) {
compressorNode.isEnabled = enable
}
func enableAutomaticVocalPitchCorrection(enable: Bool) {
avpcNode.isEnabled = enable
}
}
import android.content.Context
import com.synervoz.switchboard.sdk.Codec
import com.synervoz.switchboard.sdk.SwitchboardSDK
import com.synervoz.switchboard.sdk.audioengine.AudioEngine
import com.synervoz.switchboard.sdk.audiograph.AudioGraph
import com.synervoz.switchboard.sdk.audiograph.OfflineGraphRenderer
import com.synervoz.switchboard.sdk.audiographnodes.AudioPlayerNode
import com.synervoz.switchboard.sdk.audiographnodes.GainNode
import com.synervoz.switchboard.sdk.audiographnodes.MixerNode
import com.synervoz.switchboard.sdk.utils.AssetLoader
import com.synervoz.switchboardsuperpowered.audiographnodes.AutomaticVocalPitchCorrectionNode
import com.synervoz.switchboardsuperpowered.audiographnodes.CompressorNode
import com.synervoz.switchboardsuperpowered.audiographnodes.ReverbNode
import kotlin.math.max
class MixerAudioEngine(context: Context) {
val audioEngine = AudioEngine(context)
val audioGraphToRender = AudioGraph()
val musicPlayer = AudioPlayerNode()
val voicePlayer = AudioPlayerNode()
val mixerNode = MixerNode()
val offlineGraphRenderer = OfflineGraphRenderer()
val musicGainNode = GainNode()
val voiceGainNode = GainNode()
val reverbNode = ReverbNode()
val compressorNode = CompressorNode()
val automaticVocalPitchCorrectionNode = AutomaticVocalPitchCorrectionNode()
private var mixedFilePath = SwitchboardSDK.getTemporaryDirectoryPath() + "mix.wav"
init {
audioGraphToRender.addNode(musicPlayer)
audioGraphToRender.addNode(voicePlayer)
audioGraphToRender.addNode(mixerNode)
audioGraphToRender.addNode(musicGainNode)
audioGraphToRender.addNode(voiceGainNode)
audioGraphToRender.addNode(reverbNode)
audioGraphToRender.addNode(compressorNode)
audioGraphToRender.addNode(automaticVocalPitchCorrectionNode)
audioGraphToRender.connect(musicPlayer, musicGainNode)
audioGraphToRender.connect(musicGainNode, mixerNode)
audioGraphToRender.connect(voicePlayer, voiceGainNode)
audioGraphToRender.connect(voiceGainNode, automaticVocalPitchCorrectionNode)
audioGraphToRender.connect(automaticVocalPitchCorrectionNode, compressorNode)
audioGraphToRender.connect(compressorNode, reverbNode)
audioGraphToRender.connect(reverbNode, mixerNode)
audioGraphToRender.connect(mixerNode, audioGraphToRender.outputNode)
audioEngine.start(audioGraphToRender)
}
fun isPlaying() = musicPlayer.isPlaying
fun getRenderedMixPath(): String {
val sampleRate =
max(musicPlayer.getSourceSampleRate(), voicePlayer.getSourceSampleRate())
musicPlayer.position = 0.0
voicePlayer.position = 0.0
musicPlayer.play()
voicePlayer.play()
offlineGraphRenderer.setSampleRate(sampleRate)
offlineGraphRenderer.setMaxNumberOfSecondsToRender(musicPlayer.getDuration())
offlineGraphRenderer.processGraph(audioGraphToRender, mixedFilePath, Codec.WAV)
return mixedFilePath
}
fun play() {
musicPlayer.play()
voicePlayer.play()
audioGraphToRender.start()
}
fun pause() {
audioGraphToRender.stop()
musicPlayer.pause()
voicePlayer.pause()
}
fun stopAudioEngine() {
audioEngine.stop()
}
fun startAudioEngine() {
audioEngine.start(audioGraphToRender)
}
fun close() {
audioGraphToRender.close()
musicPlayer.close()
voicePlayer.close()
mixerNode.close()
audioEngine.close()
}
fun loadSong(context: Context, songName: String) {
musicPlayer.load(AssetLoader.load(context, songName), Codec.createFromFileName(songName))
}
fun loadRecording(recordingPath: String) {
voicePlayer.load(recordingPath, Codec.createFromFileName(recordingPath))
}
fun getSongDurationInSeconds() : Double {
return musicPlayer.getDuration()
}
fun getPositionInSeconds() : Double {
return musicPlayer.position
}
fun setPositionInSeconds(position: Double) {
musicPlayer.position = position
if (voicePlayer.getDuration() > position) {
voicePlayer.position = position
}
}
fun getProgress(): Float {
return (musicPlayer.position / musicPlayer.getDuration()).toFloat()
}
fun setMusicVolume(volume: Int) {
musicGainNode.gain = volume / 100.0f;
}
fun setVoiceVolume(volume: Int) {
voiceGainNode.gain = volume / 100.0f;
}
fun enableReverb(enable: Boolean) {
reverbNode.isEnabled = enable
}
fun enableCompressor(enable: Boolean) {
compressorNode.isEnabled = enable
}
fun enableAutomaticVocalPitchCorrection(enable: Boolean) {
automaticVocalPitchCorrectionNode.isEnabled = enable
}
}
You can find the source code on the following link:
Karaoke App - iOS
You can find the source code on the following link:
Karaoke App - Android