scribeWiz-team/ScribeWiz

View on GitHub
app/src/main/java/com/github/scribeWizTeam/scribewiz/fragments/RecFragment.kt

Summary

Maintainability
C
1 day
Test Coverage
B
88%
package com.github.scribeWizTeam.scribewiz.fragments

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaPlayer
import android.media.MediaRecorder
import android.os.Bundle
import android.os.CountDownTimer
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.github.scribeWizTeam.scribewiz.NotesStorageManager
import com.github.scribeWizTeam.scribewiz.PermissionsManager
import com.github.scribeWizTeam.scribewiz.R
import com.github.scribeWizTeam.scribewiz.transcription.MusicxmlBuilder
import com.github.scribeWizTeam.scribewiz.transcription.NoteGuesser
import com.github.scribeWizTeam.scribewiz.transcription.PitchDetector
import com.github.scribeWizTeam.scribewiz.transcription.Signal
import com.github.scribeWizTeam.scribewiz.transcription.Signature
import com.github.scribeWizTeam.scribewiz.transcription.Transcriber
import com.github.scribeWizTeam.scribewiz.ui.theme.ScribeWizTheme
import com.github.scribeWizTeam.scribewiz.util.RecordingParameters

class RecFragment(
    contentLayoutId: Int = 0,
    private val recordingParameters: RecordingParameters
) : Fragment(contentLayoutId) {

    companion object {
        private const val MILLIS_IN_FUTURE = 9999999L //number of milliseconds maximum record time
        private const val SAMPLE_RATE_IN_HZ = 44100
        private const val NOTE_SAMPLE_INTERVAL =
            80L //number of milliseconds between two note guesses

        //number of a samples used for each note guess
        private const val NOTE_SAMPLE_WINDOW_SIZE =
            (SAMPLE_RATE_IN_HZ * NOTE_SAMPLE_INTERVAL / 1000).toInt()
        private const val THRESHOLD = 0.9 // threshold to detect notes

    }

    private val audioSource = MediaRecorder.AudioSource.MIC
    private val channelConfig = AudioFormat.CHANNEL_IN_MONO
    private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
    private val bufferSize =
        2 * AudioRecord.getMinBufferSize(SAMPLE_RATE_IN_HZ, channelConfig, audioFormat)

    private lateinit var audioRecorder: AudioRecord //media recorder to record audio
    private lateinit var mediaPlayer: MediaPlayer//media player to play sound

    private lateinit var notesStorageManager: NotesStorageManager

    private lateinit var transcriber: Transcriber

    private var isRecording = false //boolean to check if recording is in progress

    private var beatCount = 0
    private var measureCount = 0

    private var metronomeIsPlaying = false

    private var processSamplesTimer = onTickTimer(1) {}
    private var metronomeTimer = onTickTimer(1) {}

    constructor() : this(0, RecordingParameters()) {
        // Default constructor
    }

    /**
     * Creates the view for the recording fragment.
     *
     * @param inflater           The layout inflater.
     * @param container          The container view.
     * @param savedInstanceState The saved instance state.
     * @return The created view.
     */
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        //check if the app has permission to record audio
        PermissionsManager().checkPermissionThenExecute(
            this,
            requireContext(),
            Manifest.permission.RECORD_AUDIO
        ) {}

        mediaPlayer = MediaPlayer.create(context, R.raw.tick)
        notesStorageManager = NotesStorageManager(this.requireContext())

        return ComposeView(requireContext()).apply {
            setContent {
                ScribeWizTheme {
                    val playMetronome = remember { mutableStateOf(metronomeIsPlaying) }
                    val counterText = remember { mutableStateOf("1.1") }
                    val recordButtonText = remember { mutableStateOf("Start recording") }
                    Box(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(all = 8.dp),
                        contentAlignment = Alignment.Center
                    ) {
                        Column(horizontalAlignment = CenterHorizontally) {
                            Text(
                                text = counterText.value,
                                fontSize = 24.sp,
                                modifier = Modifier.padding(10.dp),
                                textAlign = TextAlign.Center
                            )
                            PlayButton(recordButtonText) {
                                switchRecordState(context, counterText, recordButtonText)
                            }
                        }
                        Switch(
                            checked = playMetronome.value,
                            onCheckedChange = {
                                playMetronome.value = it
                                metronomeIsPlaying = it
                            }
                        )
                    }
                }
            }
        }
    }

    @Composable
    private fun PlayButton(text: MutableState<String>, onClick: () -> Unit) {
        Button(
            modifier = Modifier
                .height(50.dp)
                .width(190.dp)
                .padding(5.dp),
            onClick = onClick
        ) {
            Icon(
                Icons.Filled.PlayArrow,
                contentDescription = "play",
                modifier = Modifier.size(ButtonDefaults.IconSize)
            )
            Spacer(Modifier.size(ButtonDefaults.IconSpacing))
            Text(text.value)
        }
    }

    private fun onTickTimer(interval: Long, onTick: (Long) -> Unit): CountDownTimer {
        return object : CountDownTimer(MILLIS_IN_FUTURE, interval) {
            override fun onTick(millisUntilFinished: Long) {
                onTick(millisUntilFinished)
            }

            override fun onFinish() {
                // Do nothing
            }
        }
    }

    private fun switchRecordState(
        context: Context,
        counterText: MutableState<String>,
        recordButtonText: MutableState<String>
    ) {
        // Check for audio recording permissions
        if (ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.RECORD_AUDIO
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // No permissions, send a toast
            Toast.makeText(context, "Microphone permission required", Toast.LENGTH_LONG).show()
            return
        }

        if (!isRecording) {
            // Set the timer
            val tickTime = (1000 * 60 / recordingParameters.tempo).toLong()
            metronomeTimer = onTickTimer(tickTime) {
                val (beat, measure) = incrementMetronomeCounter()
                counterText.value = "$measure.$beat"
                if (this::mediaPlayer.isInitialized && metronomeIsPlaying) {
                    if (mediaPlayer.isPlaying) {
                        mediaPlayer.pause()
                        mediaPlayer.seekTo(0)
                    }
                    mediaPlayer.start()
                }
            }

            processSamplesTimer = onTickTimer(NOTE_SAMPLE_INTERVAL) {
                val rawSamples = ShortArray(NOTE_SAMPLE_WINDOW_SIZE)
                audioRecorder.read(rawSamples, 0, NOTE_SAMPLE_WINDOW_SIZE)
                val samples = Signal(NOTE_SAMPLE_WINDOW_SIZE)
                for (i in 0 until NOTE_SAMPLE_WINDOW_SIZE) {
                    samples[i] = rawSamples[i].toFloat() * (1.0f / 32768.0f)
                }
                transcriber.processSamples(samples)
            }
            //start recording
            recordButtonText.value = "Stop recording"
            // Start the timer
            metronomeTimer.start()
            processSamplesTimer.start()
            startRecording()
        } else {
            //stop recording
            // Stop the timer
            metronomeTimer.cancel()
            processSamplesTimer.cancel()
            // Set the recording time to 0
            counterText.value = "Recording saved!"
            recordButtonText.value = "Start recording"
            stopRecording()
        }
    }

    private fun incrementMetronomeCounter(): Pair<Int, Int> {
        if (beatCount == recordingParameters.beats) {
            measureCount += 1
            beatCount = 1
        } else {
            beatCount += 1
        }
        return Pair(beatCount, measureCount)
    }

    @SuppressLint("Permissions are checked before calling this method", "MissingPermission")
    private fun startRecording() {
        // Initialize the AudioRecord
        audioRecorder = AudioRecord(
            audioSource,
            SAMPLE_RATE_IN_HZ, channelConfig, audioFormat, bufferSize
        )

        // Initialize the Transcriber
        val pitchDetector = PitchDetector(
            SAMPLE_RATE_IN_HZ.toDouble(),
            THRESHOLD
        )
        val noteGuesser = NoteGuesser(
            NOTE_SAMPLE_INTERVAL / 1000.0,
            silenceMinDuration = 0.3,
            movingWindowNeighbors = 2
        )
        val signature = Signature(
            recordingParameters.fifths,
            recordingParameters.beats,
            recordingParameters.beatType,
            divisions = 2,
            tempo = recordingParameters.tempo,
            useGKeySignature = recordingParameters.useGKeySignature
        )
        val renderer = MusicxmlBuilder(recordingParameters.scoreName, signature)
        transcriber = Transcriber(pitchDetector, noteGuesser, renderer)

        // reset measure counter
        beatCount = 1
        measureCount = 1

        // Start recording
        audioRecorder.startRecording()
        isRecording = true

    }

    // Method to stop recording
    private fun stopRecording() {
        // Stop recording
        audioRecorder.stop()
        // Release the AudioRecord
        audioRecorder.release()
        // Set recording not in progress
        isRecording = false
        // end the transcription
        transcriber.endTranscription()
        val data = transcriber.getTranscription()
        notesStorageManager.writeNoteFile(recordingParameters.scoreName, data)
    }

    override fun onStop() {
        try {
            // recordTimer.cancel()
            metronomeTimer.cancel()
            processSamplesTimer.cancel()
            if (this::mediaPlayer.isInitialized && mediaPlayer.isPlaying) {
                mediaPlayer.stop()
                mediaPlayer.release()
            }
        } catch (e: IllegalStateException) {
            e.printStackTrace()
        }
        super.onStop()
    }
}