آموزش ضبط صدا با کاتلین در برنامه نویسی اندروید در 5 گام

آموزش ضبط صدا با کاتلین در برنامه نویسی اندروید در 5 گام
در این پست می‌خوانید:

به نام خدا، در این مقاله قصد دارم به آموزش ضبط صدا در برنامه نویسی اندروید با استفاده از زبان کاتلین بپردازم. در انتهای مقاله ما یک اپلیکیشنی خواهیم داشت که صدا های داخلی (مثلا آهنگی که درون گوشی صبط می شه) و خارجی (صحبت های حودمون از طریق میکروفن) رو ضبط و به صورت فایل mp3 می کنه. همچنین می تونیم از طریق اپلیکیشن صدای ضبط شده رو گوش بدیم.

ایجاد یک پروژه کوچک با موضوع ضبط صدا در اندروید

ما قصد داریم در قالب یک پروژه کوچک، نحوۀ ضبط صدا رو آموزش بدیم؛ لطفا پس از مطالعۀ کامل مقاله برای ما دیدگاهتون رو در بخش دیدگاه ها در پایین مقاله بنویسید.

پیاده سازی رابط کاربری

برای شروع و پایان ضبط صدا ما از یک ToggleButton استفاده می کنیم؛ برای پخش صدا هم از یک دکمۀ معمولی استفاده می شه.

از یک SeekBar هم استفاده می کنیم تا قابل فهم باشه چقدر از صدای ضبط شده تا الان پخش شده و چقدر دیگه مونده تا به انتهای پخشش برسه. علت استفاده از این SeekBar صرفا سعیِ بر خارج کردن پروژۀ کوچکمون از حالت ایستا و ملموس تر کردن پخش صداست.

کد های xml ما در MainActivity به صورت زیر هستند:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="پخش"
        android:id="@+id/play"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <ToggleButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="record"
        android:enabled="false"
        android:textOn="در حال ضبط کردن"
        android:textOff="ضبط کن"
        android:id="@+id/record"
        app:layout_constraintTop_toBottomOf="@id/play"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <SeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:id="@+id/seekBar"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

تنها نکته ای که شایان توضیح هست آنکه، در ToggleButton، اتریوبوت های android:textOn="در حال ضبط کردن" و android:textOff="ضبط کن" متن های جداگانه ای برای ToggleButton در دو حالت Checked و unChecked در نظر می گیرن.

سپس از طریق ViewBinding به رابط کاربری از طریق Activity متصل می شیم.

private var _binding: ActivityMainBinding? = null
private val binding get() = _binding

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding?.root)
        binding?.apply {
          //ourCodes

       }
}

override fun onDestroy() {
        super.onDestroy()
        _binding = null
}

در دورۀ نخبگان اندروید استاد نوری همون اول کار ViewBinding آموزش داده شده.

سر انجام ما تصویر زیر را خواهیم داشت:

رابط کاربری پروژۀ کوچک ضبط صدا در اندروید

 

دریافت مجوز ضبط صدا از کاربر حین اجرای برنامه

برای ضبط صدا باید مجوز RECORD_AUDIO رو در فایل Manifest درج کنیم:

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

سپس با توجه به الزامات اندروید 6 و اندروید های بالاترش مبنی بر اعطای مجوز های حساس از نظر امنیتی توسط کاربران اپلیکیشن؛ باید به صورت runtime و حین اجرای اپلیکیشن از کاربر بخواهیم که مجوز ضبط صدا توسط اپلیکیشنمون رو بهمون بده.

برای این کار کد زیر رو در ابتدای متد  onCreate می نویسیم:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
          requestPermissions(
                 arrayOf(android.Manifest.permission.RECORD_AUDIO), 0
           )
}

در دمی که onCreate حین اجرای برنامممون صدا زده بشه، یک دیالوگ مبنی بر درخواست مجوز ضبط صدا برای کاربر نمایش داده می شه. واکنش کاربر به این درخواست از طریق متدی با نام onRequestPermissionsResult قابل رصد هست. یعنی می تونیم بفهمیم مجوزی که می خوایم رو بهمون داده یا نه. این تابع درون Activityمون، از قبل  توسط اندروید تعبیه شده.

اگر مجوز رو نداد کلا دکمۀ ضبط صدا رو کلا غیر فعال می کنیم چون وقتی مجوز ضبط صدا رو نداشته باشیم دیگه اون دکمه به چه دردی می خوره؟ فعال بودنش فقط باعث می شه برنامه کرش کنه پس همون بهتر غیر فعال بشه

override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        //Check about request permission for audio recorder
        if (requestCode == 0) {
            binding?.record?.isEnabled =
                grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
        }
    }

اگر مجوز داشتیم آنگاه binding?.record?.isEnabled برابر با true می شه و اگر مجوز نداشتیم برابر با false می شه. همون اول برنامه تکلیفمون روشن می شه که مجاز به ضبط صدا هستیم یا نه؟

به تصویر اسکرین شات اپلیکیشن در بالا توجه کن؛ اگر مجوز نداشته باشیم دکمۀ ضبط کن کمرنگ و غیر قابل کلیک می شه؛ عکس بالایی از قضا حالتیه که دکمۀ ضبط غیر فعاله.

کلاس ضبط صدا یا AudioRecorder

package academy.nouri.audiorecorder

import android.content.Context
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.Build
import java.io.File
import java.io.FileOutputStream

class AudioRecorder(private val context: Context) {

    private var recorder : MediaRecorder? = null

    //Create Recorder
    private fun createRecorder() : MediaRecorder{
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
            MediaRecorder(context)
        else
            MediaRecorder()
    }

    fun start(outputFile : File){
        //Start recording by the created recorder
        createRecorder().apply {
            setAudioSource(MediaRecorder.AudioSource.MIC)
            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            setOutputFile(FileOutputStream(outputFile).fd)

            prepare()
            start()

            recorder = this
        }
    }

    fun stop(){
        //Stop recording
        recorder?.stop()
        recorder?.reset()
        recorder = null
    }
}

اکنون به تشریح کد های کلاس ضبط صدا می پردازیم:

برای ضبط صدا ما نیاز داریم یک شئ از کلاس MediaRecorder بسازیم. از اندروید 11 و به بالا باید context رو به عنوان پارامتر ورودی به سازندۀ کلاس MediaRecorder بدیم و برای اندروید های قبل 11 باید از سازندۀ بدون پارامتر این کلاس (که منسوخ یا Deprecate شده) استفاده بکنیم.

ازیرا در یک تابع با سطح دسترسی private ، یک شئ از کلاس MediaRecorder با توجه به شرطی که دربارش صحبت کردیم و مورد نیاز ما برای ضبط صدا هست، می سازیم:

private fun createRecorder() : MediaRecorder{
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
            MediaRecorder(context)
        else
            MediaRecorder()
    }

اکنون با صدا زدن این تابع ما یک شئ ساخته شده از کلاس MediaRecorder که با تمام نسخه های اندرویدی انتشار یافتۀ  این زمان (اکنون آخرین نسخۀ منتشر شدۀ اندروید اندروید 13 هست) سازگار هست دم دست داریم.

حالا باید خصوصیات شئ خود را پیکر بندی کنیم، مثلا ضبط صدا با چه فرمتی باشد یا با چه Encoder ای کار کند و …

مد نظر داریم که به محض پیکر بندی شدن کلاس MediaRecorder شروع به ضبط صدا کنیم؛ ازیرا پیکربندی و شروع به ضبط صدا را از طریق تابعی با نام start عملیاتی می کنیم. تابع start هم کارهای مربوط به پیکر بندی صدا رو انجام بده و هم بلافاصله عملیات ضبط صدا رو شروع می کنه:

fun start(outputFile : File){
        createRecorder().apply {
            setAudioSource(MediaRecorder.AudioSource.MIC)
            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            setOutputFile(FileOutputStream(outputFile).fd)

            prepare()
            start()

            recorder = this
        }
    }

ما از طریق تابع createRecorder به شئ کلاس MediaRecorder که سازگار با تمام نسخه های اندرویدی هست دسترسی داریم.

اکنون پیکر بندی هایی رو برای آن شئ در نظر می گیریم:

setAudioSource(MediaRecorder.AudioSource.MIC)

اول از هر چیزی اول باید تابع setAudioSource رو صدا بزنیم و بهش MIC رو پاس بدیم. MIC یعنی میکروفن. فراموش نکن که کلاس MediaRecorder فقط برای ضبط صدا از طریق میکروفن نیست و کاربرد های دیگه ای هم داره پس حتما باید بگیم که هدف با ضبط صدا با میکروفن هست.

setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)

بعد از setAudioSource باید تابع setOutputFormat رو صدا بزنیم و فرمت صوتی مدنظرمون رو براش بفرستیم. اگر دربارۀ بحث فرمت های صوتی هیچی نمی دونی این مقاله بدک نیست.

setAudioEncoder(MediaRecorder.AudioEncoder.AAC)

سپس اقدام به تعیین encoder مناسب می کنیم

setOutputFile(FileOutputStream(outputFile).fd)

اگر توجه کرده باشیم ما یک نمونه از کلاس Flie رو به تابع start به عنوان پارامتر پاس دادیم. با قرار دادن File یادشده به عنوان پارامتر ورودی سازندۀ کلاس FileOutputStream و دریافت FileDescriptor، فایلی که می خوایم صدای ضبط شده در آن ذخیره بشه رو مورد هدف قرار می دیم تا صدا رو روش ذخیره کنه.

سپس تابع prepare رو صدا می زنیم تا آماده سازی پیکر بندی هایی که انجام دادیم صورت بپذیره و در آخر تابع start رو صدا می زنیم تا قرآیند ضبط شروع بشه.

لازمه که ما به شئ یاد شده در کل کلاس AudioRecorder دسترسی داشته باشیم تا بعدا در تابع stop بتونیم فرآیند ضبط صدا رو متوقف کنیم. ازیرا یک پراپرتی به صورت زیر در کلاس تعریف می کنیم که پس از شروع فرآیند ضبط شئ MediaRecorder در حال ضبط رو درون خودش داره:

private var recorder : MediaRecorder? = null

سپس یک تابع با نام stop تعریف می کنیم تا با صدا زدن اون در درون Activity بتونیم فرآیند ضبط صدا رو متوقف کنیم:

fun stop(){
        //Stop recording
        recorder?.stop()
        recorder?.reset()
        recorder = null
    }

اول MediaRecorder رو stop می کنیم و سپس اونو reset می کنیم. کسایی که با MediaPlayer برای پخش صدای ضبط شده استفاده کردن می دونن تابعی با نام release وجود داره که در پایان فرآیند پخش صدا صدا زده می شه برای آزاد سازی منابع؛ اما ما نمی خواهیم از این تابع برای MediaRecorder استفاده بکنیم و فقط از تابع reset استفاده می کنیم.

علتش آنه که ما می خوایم بعدا که دکمۀ ضبط صدا کلیک شد دوباره از MediaRecorder استفاده کنیم، پس نباید منابع رو آزاد کنیم و فقط کافیه تنظیماتی که انجام دادیم reset بشه و بعدا با خیال راحت تابع start رو صدا بزنیم.

در نمودار زیر نحوۀ کارکرد MediaRecorder روببین:

ضبط صدا در اندروید با استفاده از MediaRecorder

اگر دوست داری با انواع نمودار های UML آشنا بشی این مقالمو بخون خیلی مفصله

کلاس AudioRecorder ما تکمیل شد.

حالا می مونه با استفاده از کلاس MediaPlayer اقدام به پخش صدای ضبط شده در اندروید کنیم و حین پخش صدا موقعیت زمانیِ صدایِ در حال پخش رو روی SeekBar نمایش بدیم. برای این کار یک کلاس جدی با نام AudioPlayer می سازیم.

ایجاد کلاس AudioPlayer

کلاس ما به این صورته:

package academy.nouri.audiorecorder

import android.content.Context
import android.media.MediaPlayer
import android.widget.SeekBar
import androidx.core.net.toUri
import kotlinx.coroutines.*
import java.io.File

class AudioPlayer(private val context: Context) {

    private var player: MediaPlayer? = null
    private var job: Job? = null

    fun playFile(file : File,seekBar: SeekBar) {
        //if it is playing at first stop it
        player?.let {
            if (it.isPlaying) {
                stop()
            }
        }
        //play the mp3 recorded file in directory
        MediaPlayer.create(context,file.toUri()).apply {
            player = this
            start()
        }

        attachSeekToPlayer(seekBar)
    }

    private fun attachSeekToPlayer(seekBar: SeekBar) {
        //Show the position of playing by sekk bar
        player?.let {
            seekBar.max = it.duration
            job = CoroutineScope(Dispatchers.Main).launch {
                while (it.isPlaying) {
                    delay(100)
                    seekBar.progress = it.currentPosition
                }
            }
        }
    }


    fun stop() {
        player?.stop()
        player?.release()
        player = null
        job?.cancel()
    }


}

هدف این کلاس آنه که صدای ذخیرۀ شده در حافظۀ گوشی رو پخش کنه و SeekBar رو متناسب با آن موقعیت پخش صدا جلو ببره.

برای پخش صدا یک property از نوع MediaPlayer می سازیم که وظیفش نگهداری از یک شئ MediaPlayer هست. در تابع های PlayFile و AttachSeekbarToPlayer و stop ما با این property جهت شروع پخش صدا، پیوند زدن موقعیت صدای در حال پخش با SeekBar و متوقف کردن پخش صدا در تابع stop کار خواهیم داشت:

private var player: MediaPlayer? = null

در تابع PlayFile ما با استفاده از فایلی که صدای ضبط شده در آن ذخیره شده و درون حافظۀ گوشی کاربر وجود داره MediaPlayer خودمون رو می سازیم و سپس شروع به پخش صدا می کنیم و شئ MediaPlayer رو درون property مربوطش ذخیره می کنیم تا در تابع stop بتونیم متوقفش کنیم:

MediaPlayer.create(context,file.toUri()).apply {
            player = this
            start()
        }

حالا یک نکته ای؛ از کجا مطمئن بشیم حینی که این تابع رو درون Activity صدا می زنیم، صدا از قبل در حال پخش نباشه و با صدا زدن مجدد این تابع تداخل رخ نده؟ باید قبل ایجاد و پخش یک MediaPalyer جدید، MediaPlayer احتمالی ای که داره صدا رو پخش می کنه رو با استفاده از دستور زیر متوقف کنیم:

player?.let {
            if (it.isPlaying) {
                stop()
            }
        }

مطمئن می شیم که اگر MediaPlayer برابر با null نیست و در حال پخش صدا هست، حتما stop بشه. خب تا اینجا بحث پخش صدا حله و فقط می مونه با استفاده از تابع attachSeekToPlayer بیایم زمان طی شده و زمان مونده از صدای در حال پخش رو روی SeekBar نشون بدیم.

برای اینکار باید زمان MediaPlayerمون که مدت زمان صدای درحال پخش هست رو به عنوان حداکثر SeekBarمون صبت کنیم و در گام بعد یک CoroutineScope بسازیم که هر 100 میلی ثانیه یک بار، موقعیت کنونی صدای در حال پخش رو روی progress اِ SeekBarمون ثبت کنه. و این اتفاق تا زمانی که صدا در حال پخش هست تداوم داشته باشه.

لازمه که کوروتین‌مون رو به صورت یک property در کلاس تعریف کنیم؛ تا در تابع stop هم زمان با متوقف شدن صدا، کوروتین‌مون هم از بین بره و زمانی که صدایی در حال پخش نیست،  پردازش بی هوده صورت نگیره و ui رو نیز درگیر نکنه.

private var job: Job? = null

private fun attachSeekToPlayer(seekBar: SeekBar) {
        //Show the position of playing by sekk bar
        player?.let {
            seekBar.max = it.duration
            job = CoroutineScope(Dispatchers.Main).launch {
                while (it.isPlaying) {
                    delay(100)
                    seekBar.progress = it.currentPosition
                }
            }
        }
    }

همونطور که دیدی ما تابع attachSeekToPlayer رو بلافاصله پس از پخش صدا در تابع playFile صدا می زنیم، این یعنی پخش صدا و حرکت SeekBar همگام خواهند بود. یکبار دیگه تابع playFile رو ملاظه کن:

fun playFile(file : File,seekBar: SeekBar) {
        //if it is playing at first stop it
        player?.let {
            if (it.isPlaying) {
                stop()
            }
        }
        //play the mp3 recorded file in directory
        MediaPlayer.create(context,file.toUri()).apply {
            player = this
            start()
        }

        attachSeekToPlayer(seekBar)
    }

در آخر میمونه که تابع stop رو برای متوقف کردن پخش صدا و کوروتین‌مون تعریف کنیم که به صورت زیر هست.

fun stop() {
        player?.stop() // توقف پخش صدا
        player?.release() // آزاد سازی منابع
        player = null
        job?.cancel() // لغو کردن کوروتین‌مون
    }

خب حالا که پروندۀ کلاس AudioPlayer که وظیفش پخش صدا و نمایش گذر زمانی صدای در حال پخش در SeekBarمون بود، بسته شد؛ می ریم سراغ کلاس MainActivity

کلاس MainActivity

خب اکنون باید یک شئ از کلاس های AudioPlayer و AudioRecorder به صورت propert در Activity بسازیم؛ برای این کار از by lazy استفاده می کنیم(اگر دوست دارید بیشتر دربارۀ lazy بدونید این مقاله رو بررسی کن)

private val audioRecorder: AudioRecorder by lazy { AudioRecorder(applicationContext) }
private val audioPlayer: AudioPlayer by lazy { AudioPlayer(applicationContext) }

سپس یک property دیگر از جنس File برای محل ذخیرۀ فایل تعریم می کنیم:

private var audioFile : File? = null

خب حالا به تابع onCreate می پردازیم. در عناوین بالاتر در خصوص بحث دریافت مجوز ضبط صدا و نحوۀ پیاده سازیش در Activity صحبت به اندازۀ کافی شد. پس انتظار می ره پس از اجرای برنامه از کاربر درخواست اعطای مجوز ضبط صدا بشه:

درخواست اعطای مجوز ضبط صدا پس از اجرای برنامه

در ادامه خاصیت setOnCheckedChangeListener دکمۀ «ضبط صدا» رو در تابع onCreate پیاده سازی می کنیم:

//Toggle button for record audio
            record.setOnCheckedChangeListener{ buttonView, isChecked ->
                if (isChecked){
                    //Start recording and save to this file
                    File(cacheDir,"audio.mp3").also { file ->
                        audioRecorder.start(file)
                        audioFile = file
                        play.isEnabled = false
                    }
                }else{
                    //Stop recording
                    audioRecorder.stop()
                    play.isEnabled = true
                }
            }

با استفاده از آرگمان isChecked می تونیم بفهمیم که آیا الان دکمه توسط کاربر در حالت فعال قرار گرفته یا یا نه، پس از شرط if برای پیاده سازی حالت شروع ضبط و پایان ضبط در دل ToggleButton می پردازیم.

می گیم اگر شرط برقرار بود یک شئ از کلاس File بساز که محل ذخیره شدنش در cacheDir باشه. cacheDir در واقع همون آدرس data/data اپلیکیشن ما هست، جایی که سیستم عامل اندروید به هر اپلیکیشن به دور از دسترس کاربر قرار می ده تا اطلاعاتمون رو به صورت cache در اونجا ذخیره کنیم. برای ذخیرۀ فایل در محل cachDir نیازی به مجوز های خواندن و نوشتن فایل نیست.

پارامتر دوم کلاس File، اسم فایلمون هست، ما “audio.mp3” رو به عنوان نام فایلمون انتخاب می کنیم. سپس تابع start شئ کلاس audioRecorder رو صدا می زنیم تا فرآیند ضبط صدا شروع بشه و شئ کلاس فایلی که درست کردیم رو در پراپرتی audioFile که بالاتر در کلاسمون ذخیره کردیم، قرار می دیم تا بعدا یا استفاده از دکمۀ پخش صدا بتونیم صدا رو پخش کنیم:

File(cacheDir,"audio.mp3").also { file ->
                        audioRecorder.start(file)
                        audioFile = file
.
.
.

همچنین دکمۀ play رو غیر فعال می کنیم تا حین ضبط صدا کاربر روی دکمۀ پخش نزنه و برنامه کرش نکنه.

play.isEnabled = false

سپس می گیم اگر ToggleButton غیر فعال بود فرآیند ضبط متوقف بشه و دکمۀ پخش هم دوباره فعال بشه:

else{
     //Stop recording
     audioRecorder.stop()
     play.isEnabled = true
 }

اکنون به پیاده سازی دکمۀ پخش صدا می پردازیم:

play.setOnClickListener {
                //Play the audio recorded
                audioFile?.let { file ->
                    audioPlayer.playFile(file,seekBar)
                }
            }

با کمک کلاس AudioPlayer و تابع playFile، فایلی که در خاصیت setOnCheckedChangeListener اِ ToggleButton ایجاد کردیم رو پخش می کنیم.

در آخر در تابع onDestroy کلاس کلاس Activity، هنگامی که چرخۀ حیات Activity تمام شد، ضبط صدا و پخش صدا رو stop می کنیم تا ارگ احتمالا در حال انجام عملیات های تعیین شده، بودند؛ متوقف شوند:

override fun onDestroy() {
        super.onDestroy()
        _binding = null
        //Stop recording after end life of app
        audioRecorder.stop()
        audioPlayer.stop()
    }
اپلیکیشن ضبط صدا پس از اعطای مجوز و فعال شدن ToggleButton ضبط صدا

اپلیکیشن ضبط صدا پس از اعطای مجوز و فعال شدن ToggleButton ضبط صدا

 

اپلیکیشن ضبط صدا در حال ضبط صدا

اپلیکیشن ضبط صدا در حال ضبط صدا

ذخیرۀ صدای ضبط شده در حافظۀ شخصی داخلی گوشی کاربر

برای این کار باید هر بار که ضبط صدا انجام می شده یک فایل منحصر به فرد در حافظۀ گوشی کاربر ساخته بشه و باید از File.createTempFile کمک بگیرید.

مقالۀ آموزش ایجاد فایل با کاتلین از اندروید ۵ تا ۱۳ من رو کامل مطالعه کن تا بتونی توی همین اپلیکیشنی که درست کردیم، صداهایی که ضبط می کنی رو به کمک File.createTempFile در قالب فایل هایی منحصر به فرد در حافظۀ داخلی ایجاد کنی. باید این دوتا مقاله ای که نوشتم رو با هم تلفیق کنی.

مثلا من توی پوشۀ Document گوشی اندروید یک پوشه ای واسه ذخیرۀ صداهای ضبط شده ساختم. هر بار کاربر ضبط رو شروع و پایان بکنه یک فایل با نام تصادفی توی گوشی کاربر ساخته می شه:

ذخیرۀ صدا هیا ضبط شده در اپلیکیشن با نام تصادفی در FileManager کاربر

ذخیرۀ صدا هیا ضبط شده در اپلیکیشن با نام تصادفی در FileManager کاربر

این تمرین رو انجام بدید و نتیجش رو توی بخش دیدگاه های این مقالم واسم بفرستید.

ویدیوی زیر ویدیوی نحوۀ کارکرد اپلیکیشنی هست که ساختیم:

جعبه دانلود فایل۱ فایل
دانلود سورس کد آموزش داده شده
گزارش خرابی لینک دانلود
فرم گزارش خرابی لینک دانلود
دیدگاه‌ها ۲
ارسال دیدگاه جدید