در قسمت قبل از آموزشمون، ساخت یک ردیاب کامل رو با ماژول MC60 و هدربرد جدیدی که معرفی کرده بودیم پیش بردیم. حالا توی این قسمت میخوایم که بیشتر با بخش کارت حافظه و صدای ماژول کار بکنیم و یک MC60 MP3 player بسازیم.
ما یک MP3 Player میسازیم که آهنگها را از کارت حافظه خوانده و به ترتیب پخش میکند. همچنین چند کلید برای کنترل پخش قرار میدیم که بشه با اونها آهنگ قبلی و بعدی رو پخش کرد و صدا رو کم و زیاد کرد و پخش رو متوقف کرد و شروع کرد. برای اتصال کلیدها به ماژول هم از ADC کمک گرفتیم تا با یک پین بتونیم چندها کلید رو تشخیص بدیم.
در زیر نحوه اتصال کلیدها به پین ADC رو میبینید.
اول از همه با یک تقسیم مقاومتی ولتاژ 5 ولت رو به 2.8 تبدیل کردیم (چون ولتاژ IO های ماژول 2.8V هست) و بعد هم هر کدوم از کلیدها یک ولتاژ متفاوت رو ایجاد میکنه که توی کد مقدار اونها رو وارد کردم.
این هم تصویر مداری که خودم وصل کردم:
حالا بریم سراغ کد MC60 MP3 player
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | #ifdef __CUSTOMER_CODE__ #include "custom_feature_def.h" #include "ril.h" #include "ril_util.h" #include "ql_stdlib.h" #include "ql_error.h" #include "ql_uart.h" #include "ql_system.h" #include "ql_time.h" #include "ql_fs.h" #include "ril_audio.h" #include "ql_adc.h" ///other /////////////////////////////////// #define LENGTH 100 u8 listSongs[150][LENGTH] = {0}; u8 listSongs_last = 0; u8 songPlaying = 0; u8 songVolume = 5; static u32 ADC_CustomParam = 1; u8 RIL_STATUS = 0; bool PLAYING = TRUE; ////////////////////////////////////////////// ///for DEBUG/////////////////////////////////////////////////////////////////////////////////////////////////////////// #define SERIAL_RX_BUFFER_LEN 2048 #define DEBUG_PORT UART_PORT1 #define DBG_BUF_LEN 512 static char DBG_BUFFER[DBG_BUF_LEN]; #define APP_DEBUG(FORMAT, ...) \ { \ Ql_memset(DBG_BUFFER, 0, DBG_BUF_LEN); \ Ql_sprintf(DBG_BUFFER, FORMAT, ##__VA_ARGS__); \ Ql_UART_Write((Enum_SerialPort)(DEBUG_PORT), (u8 *)(DBG_BUFFER), Ql_strlen((const char *)(DBG_BUFFER))); \ } char m_RxBuf_Uart[SERIAL_RX_BUFFER_LEN]; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static void ADC_Program(); void listSong(); void playSong(); static void CallBack_UART_Hdlr(Enum_SerialPort port, Enum_UARTEventType msg, bool level, void *customizedPara); static void HTTP_Program(); static void Hdlr_RecvNewSMS(u32 nIndex, bool bAutoReply); void SMS_TextMode_Send(char strPhNum[], char strTextMsg[]); static void Callback_AudPlay(s32 errCode); void proc_main_task(s32 taskId) { s32 ret; ST_MSG msg; while (TRUE) { Ql_OS_GetMessage(&msg); switch (msg.message) { case MSG_ID_RIL_READY: APP_DEBUG("LOAD LEVEL 1 (RIL READY)\r\n"); Ql_RIL_Initialize(); RIL_STATUS = 1; ADC_Program(); RIL_AUD_SetChannel(1); RIL_AUD_SetVolume(2, songVolume); RIL_AUD_RegisterPlayCB(Callback_AudPlay); listSong(); playSong(songPlaying); break; case MSG_ID_URC_INDICATION: APP_DEBUG("Received URC: type: %d\r\n", msg.param1); break; } } } static void Callback_OnADCSampling(Enum_ADCPin adcPin, u32 adcValue, void *customParam) { if (adcValue > 400) { if (adcValue > 910 && 930 > adcValue) { PLAYING = TRUE; playSong(++songPlaying); APP_DEBUG("KEY 1\r\n"); } else if (adcValue > 680 && 700 > adcValue) { songVolume += 2; RIL_AUD_SetVolume(2, songVolume); APP_DEBUG("KEY 2\r\n"); } else if (adcValue > 550 && 570 > adcValue) { if (PLAYING) { PLAYING = FALSE; RIL_AUD_StopPlay(); } else { PLAYING = TRUE; playSong(songPlaying); } APP_DEBUG("KEY 3\r\n"); } else if (adcValue > 460 && 480 > adcValue) { songVolume -= 2; RIL_AUD_SetVolume(2, songVolume); APP_DEBUG("KEY 4\r\n"); } else if (adcValue > 400 && 420 > adcValue) { PLAYING = TRUE; playSong(--songPlaying); APP_DEBUG("KEY 5\r\n"); } Ql_Sleep(100); } } static void ADC_Program(void) { Enum_PinName adcPin = PIN_ADC0; // Register callback foR ADC Ql_ADC_Register(adcPin, Callback_OnADCSampling, (void *)&ADC_CustomParam); // Initialize ADC (sampling count, sampling interval) Ql_ADC_Init(adcPin, 1, 10); // Start ADC sampling Ql_ADC_Sampling(adcPin, TRUE); } void playSong(int n) { s32 ret; ret = RIL_AUD_PlayFile((char *)listSongs[n], FALSE); if (ret != RIL_AT_SUCCESS) { playSong(++songPlaying); } } static void Callback_AudPlay(s32 errCode) { if (AUD_PLAY_IND_OK == errCode) { APP_DEBUG("PLAYING FINISHED\r\n"); playSong(++songPlaying); } else if (AUD_PLAY_IND_INTERRUPT == errCode) { APP_DEBUG("<-- Playing is interrupted -->\r\n"); } } void listSong() { static s32 handle = -1; static s32 handle2 = -1; bool isdir = FALSE; u8 filePath[LENGTH] = {0}; u8 filePath2[LENGTH] = {0}; u8 filePath3[LENGTH] = {0}; u8 filename[LENGTH] = {0}; s32 filesize = 0; Ql_memset(filePath, 0, LENGTH); Ql_memset(filename, 0, LENGTH); Ql_sprintf(filePath, "%s%s\0", "SD:", "*"); handle = Ql_FS_FindFirst(filePath, filename, LENGTH, &filesize, &isdir); if (handle > 0) { do { if (isdir) { Ql_strcpy(filePath2, filePath); filePath2[Ql_strlen(filePath2) - 1] = '\0'; Ql_sprintf(filePath2, "%s%s\\*\0", filePath2, filename); handle2 = Ql_FS_FindFirst(filePath2, filename, LENGTH, &filesize, &isdir); if (handle2 > 0) { do { if (isdir) { } else { Ql_strcpy(filePath3, filePath2); filePath3[Ql_strlen(filePath3) - 1] = '\0'; Ql_sprintf(filePath3, "%s\\%s\0", filePath3, filename); Ql_strcpy(listSongs[listSongs_last], filePath3); listSongs_last++; } } while ((Ql_FS_FindNext(handle2, filename, LENGTH, &filesize, &isdir)) == QL_RET_OK); Ql_FS_FindClose(handle2); handle2 = -1; } } else { Ql_strcpy(filePath3, filePath); filePath3[Ql_strlen(filePath3) - 1] = '\0'; Ql_sprintf(filePath3, "%s%s\0", filePath3, filename); Ql_strcpy(listSongs[listSongs_last], filePath3); listSongs_last++; } Ql_memset(filename, 0, LENGTH); } while ((Ql_FS_FindNext(handle, filename, LENGTH, &filesize, &isdir)) == QL_RET_OK); Ql_FS_FindClose(handle); handle = -1; // for (u32 i = 0; i < 100; i++) // { // Ql_Sleep(100); // APP_DEBUG("\r\nsong path = %s\r\n", listSongs[i]); // } } else { APP_DEBUG("\r\n<-- No file in the dir -->\r\n"); } } //Read serial port void proc_subtask1(s32 TaskId) { s32 ret; ST_MSG msg; ST_UARTDCB dcb; Enum_SerialPort mySerialPort = UART_PORT1; dcb.baudrate = 115200; dcb.dataBits = DB_8BIT; dcb.stopBits = SB_ONE; dcb.parity = PB_NONE; dcb.flowCtrl = FC_NONE; Ql_UART_Register(mySerialPort, CallBack_UART_Hdlr, NULL); Ql_UART_OpenEx(mySerialPort, &dcb); Ql_UART_ClrRxBuffer(mySerialPort); APP_DEBUG("START PROGRAM (SISOOG.COM)\r\n"); while (TRUE) { Ql_OS_GetMessage(&msg); switch (msg.message) { case MSG_ID_USER_START: break; default: break; } } } static s32 ReadSerialPort(Enum_SerialPort port, /*[out]*/ u8 *pBuffer, /*[in]*/ u32 bufLen) { s32 rdLen = 0; s32 rdTotalLen = 0; if (NULL == pBuffer || 0 == bufLen) { return -1; } Ql_memset(pBuffer, 0x0, bufLen); while (1) { rdLen = Ql_UART_Read(port, pBuffer + rdTotalLen, bufLen - rdTotalLen); if (rdLen <= 0) { break; } rdTotalLen += rdLen; } return rdTotalLen; } static void CallBack_UART_Hdlr(Enum_SerialPort port, Enum_UARTEventType msg, bool level, void *customizedPara) { switch (msg) { case EVENT_UART_READY_TO_READ: { char *p = NULL; s32 totalBytes = ReadSerialPort(port, m_RxBuf_Uart, sizeof(m_RxBuf_Uart)); if (totalBytes <= 0) { break; } if (Ql_strstr(m_RxBuf_Uart, "ok")) { APP_DEBUG("ok\r\n"); break; } break; } } } #endif // __CUSTOMER_CODE__ |
و این هم فایل custom_task_cfg.h
:
1 2 | TASK_ITEM(proc_subtask1, subtask1_id, 5*1024, DEFAULT_VALUE1, DEFAULT_VALUE2) TASK_ITEM(proc_subtask2, subtask2_id, 10*1024, DEFAULT_VALUE1, DEFAULT_VALUE2) |
قسمت شروع کد اینجا در خط 59 تا 69 هست
1 2 3 4 5 6 7 8 9 10 11 | case MSG_ID_RIL_READY: APP_DEBUG("LOAD LEVEL 1 (RIL READY)\r\n"); Ql_RIL_Initialize(); RIL_STATUS = 1; ADC_Program(); RIL_AUD_SetChannel(1); RIL_AUD_SetVolume(2, 2); RIL_AUD_RegisterPlayCB(Callback_AudPlay); listSong(); playSong(songPlaying); |
بعد از آماده بودن توابع RIL، اول از همه بخش ADC رو با فراخوانی تابع زیر راه اندازی میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 | static void ADC_Program(void) { Enum_PinName adcPin = PIN_ADC0; // Register callback foR ADC Ql_ADC_Register(adcPin, Callback_OnADCSampling, (void *)&ADC_CustomParam); // Initialize ADC (sampling count, sampling interval) Ql_ADC_Init(adcPin, 1, 10); // Start ADC sampling Ql_ADC_Sampling(adcPin, TRUE); } |
با تابع Ql_ADC_Register پین ADC رو مشخص کرده و در پارامتر دوم یک تابع رو برای اون مشخص میکنیم که هر بار که مقدار ADC رو خوند این تابع فراخوانی بشه و کدهای توش اجرا بشن، (لازم نیست که مثل میکروکنترلر ها توی loop هر بار مقدار ADC رو بخونیم، چون اینجا ما یک سیستم عامل داریم!) بعد هم با تابع Ql_ADC_Init توی پارامتر دوم تعداد نمونه برداریها (خودش چند نمونه برداری میکنه و میانگین رو به ما میده) و توی پارامتر سوم اینکه هر چند میلی ثانیه باید اجرا بشه رو براش مشخص میکنیم، در آخر هم که میگیم شروع کن.
شاید برای شما مفید باشد: همه چیز درباره دیکد نرمافزاری MP3 به کمک میکروکنترلر
توی این سه خط هم اول از همه مشخص میکنیم که می خوایم صدا از طریق هدفون وصل باشه (RIL_AUD_SetChannel)، بعد هم میزان صدا رو تعیین کرده (که پارامتر اول شماره کانال هست و پارامتر دوم میزان صدا) و بعد از اون هم توی پارامتر اول RIL_AUD_RegisterPlayCB آدرس یه تابع دیگه رو میدیم که هر موقع پخش تموم شد یا متوقف شد این تابع اجرا بشه.
1 2 3 | RIL_AUD_SetChannel(1); RIL_AUD_SetVolume(2, songVolume); RIL_AUD_RegisterPlayCB(Callback_AudPlay); |
حالا میریم سراغ تابع listSong که یکم پیچیدس:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | void listSong() { static s32 handle = -1; static s32 handle2 = -1; bool isdir = FALSE; u8 filePath[LENGTH] = {0}; u8 filePath2[LENGTH] = {0}; u8 filePath3[LENGTH] = {0}; u8 filename[LENGTH] = {0}; s32 filesize = 0; Ql_memset(filePath, 0, LENGTH); Ql_memset(filename, 0, LENGTH); Ql_sprintf(filePath, "%s%s\0", "SD:", "*"); handle = Ql_FS_FindFirst(filePath, filename, LENGTH, &filesize, &isdir); if (handle > 0) { do { if (isdir) { Ql_strcpy(filePath2, filePath); filePath2[Ql_strlen(filePath2) - 1] = '\0'; Ql_sprintf(filePath2, "%s%s\\*\0", filePath2, filename); handle2 = Ql_FS_FindFirst(filePath2, filename, LENGTH, &filesize, &isdir); if (handle2 > 0) { do { if (isdir) { } else { Ql_strcpy(filePath3, filePath2); filePath3[Ql_strlen(filePath3) - 1] = '\0'; Ql_sprintf(filePath3, "%s\\%s\0", filePath3, filename); Ql_strcpy(listSongs[listSongs_last], filePath3); listSongs_last++; } } while ((Ql_FS_FindNext(handle2, filename, LENGTH, &filesize, &isdir)) == QL_RET_OK); Ql_FS_FindClose(handle2); handle2 = -1; } } else { Ql_strcpy(filePath3, filePath); filePath3[Ql_strlen(filePath3) - 1] = '\0'; Ql_sprintf(filePath3, "%s%s\0", filePath3, filename); Ql_strcpy(listSongs[listSongs_last], filePath3); listSongs_last++; } Ql_memset(filename, 0, LENGTH); } while ((Ql_FS_FindNext(handle, filename, LENGTH, &filesize, &isdir)) == QL_RET_OK); Ql_FS_FindClose(handle); handle = -1; // for (u32 i = 0; i < 100; i++) // { // Ql_Sleep(100); // APP_DEBUG("\r\nsong path = %s\r\n", listSongs[i]); // } } else { APP_DEBUG("\r\n<-- No file in the dir -->\r\n"); } } |
برای پخش آهنگ ها توسط MC60 MP3 player لازمه که لیست اونها رو به همراه آدرس داشته باشیم. این تابع هم کارش همین هست. تو خط 178 با تابع Ql_FS_FindFirst اول یک دایرکتوری رو مشخص می کنیم و بعد از پیدا کردن اولین فایل باید با تابع Ql_FS_FindNext فایل بعدی رو پیدا کنیم ، این کار تا زمانیکه فایل وجود داشته باشه انجام میشه و به همین خاطر توی یک while اجرا میشه. من توی خط 183 گفتم که اگر یک دایرکتوری دیگه پیدا کردی دوباره داخل اون رو هم سرچ کن و به این ترتیب این کد تا دوتا دایرکتوری داخل هم پیش میره که اگه میخواید بیشتر بشه میتونید تغییرش بدید?. بعد از اون هم توی خط 201 و 215 گفتم که هر فایلی که پیدا کردی رو آدرسش رو بریز داخل متغیر listSongs .
در آخر هم توسط تابع playSong اولین آهنگ رو پخش میکنیم. در خط 146 چک کردیم که به درستی آهنگ پخش شده یا نه ، چون ممکن است فایلی با فرمت نا مربوط انتخاب شود و نشه اون رو پخش کرد ، برای همین میره سراغ فایل بعدی.
1 2 3 4 5 6 7 8 9 10 | void playSong(int n) { s32 ret; ret = RIL_AUD_PlayFile((char *)listSongs[n], FALSE); if (ret != RIL_AT_SUCCESS) { playSong(++songPlaying); } } |
همچنین توی تابع Callback_AudPlay خط 157 گفتیم که هر موقع پخش یک فایل به اتمام رسید ، سراغ فایل بعدی بره:
1 2 3 4 5 6 7 8 9 10 11 12 | static void Callback_AudPlay(s32 errCode) { if (AUD_PLAY_IND_OK == errCode) { APP_DEBUG("PLAYING FINISHED\r\n"); playSong(++songPlaying); } else if (AUD_PLAY_IND_INTERRUPT == errCode) { APP_DEBUG("<-- Playing is interrupted -->\r\n"); } } |
داخل تابع Callback_OnADCSampling هم که هر 10ms فراخوانی میشه مقادیر ADC رو بررسی کردیم و با یک تلرانس 10mV از هر طرف برای هر کلید، فشرده شدن کلیدها رو بررسی کردیم و برای هر کدوم از اونها عملکردی رو مشخص کردیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | static void Callback_OnADCSampling(Enum_ADCPin adcPin, u32 adcValue, void *customParam) { if (adcValue > 400) { if (adcValue > 910 && 930 > adcValue) { PLAYING = TRUE; playSong(++songPlaying); APP_DEBUG("KEY 1\r\n"); } else if (adcValue > 680 && 700 > adcValue) { songVolume += 2; RIL_AUD_SetVolume(2, songVolume); APP_DEBUG("KEY 2\r\n"); } else if (adcValue > 550 && 570 > adcValue) { if (PLAYING) { PLAYING = FALSE; RIL_AUD_StopPlay(); } else { PLAYING = TRUE; playSong(songPlaying); } APP_DEBUG("KEY 3\r\n"); } else if (adcValue > 460 && 480 > adcValue) { songVolume -= 2; RIL_AUD_SetVolume(2, songVolume); APP_DEBUG("KEY 4\r\n"); } else if (adcValue > 400 && 420 > adcValue) { PLAYING = TRUE; playSong(--songPlaying); APP_DEBUG("KEY 5\r\n"); } Ql_Sleep(100); } } |
یک نکته:
یکی دیگر از امکانات ماژول این هست که 120KB حافظه داخلی داره که میتونید از اون برای نگهداری فایل استفاده کنید، بزارید تا یکی از کاربردهای این قابلیت رو بگم، مثلاً شما یک دستگاه با این ماژول ساختید که دسترسی مستقیم به اون ندارید، برای بررسی وضعیت دستگاه میتونید از پیامک استفاده کنید که وضعیت خودش رو به صورت متنی ارسال کنه، اما در مواقعی برقراری تماس گزینه خیلی بهتری هست، مثلاً زمانی که یک وضعیت اضطراری پیش اومده، توی این شرایط میتونید یک پیام ضبط شده رو داخل ماژول ذخیره کنید و بگید که به شمارهای تماس بگیره و این پیغام رو براش پخش کنه.
در این بخش میتونید به همه قسمتهای سری آموزش ماژول mc60 دسترسی پیدا کنید:
- کار با ماژول تمام عیار mc60 – قسمت اول – برد راه انداز
- کار با ماژول تمام عیار mc60 – قسمت دوم – راه اندازی OpenCPU
- کار با ماژول تمام عیار mc60 – قسمت سوم – ساخت ردیاب
- کار با ماژول تمام عیار mc60 – قسمت چهارم – OpenCPU و تکمیل ردیاب
- کار با ماژول تمام عیار mc60 – قسمت ششم – نمایشگر oled
- کار با ماژول تمام عیار mc60 – قسمت هفتم – کار با MQTT
- کار با ماژول تمام عیار mc60 – قسمت هشتم – کدنویسی با Eclipse
سلام وقت بخیر
من هدفون به ماژول وصل کردم. مشکلی که دارم این هست که صدا فقط از یک گوشی پخش میشه. نحوه اتصالم به صورت زیر بوده:
سیم زرد و سبز هدفون به spk + ماژول
سیم آبی هدفون به زمین ماژول
ضمن اینکه بین spk+ و spk- یه خازن 10n گذاشتم و spk+ و spk- را با دو خازن 10n دیگه هم به زمین وصل کردم.
ممنون میشم راهنماییم کنید
سلام شماتیک برد DB MC60 که توی فروشگاه سیسوگ هست رو میتونید بررسی کنید و ازش کمک بگیرید
سلام ، وقت بخیر
وقتی که میخوام مقدار ADC رو با تابع Ql_OS_GetMessage و Switch case بگیرم ، توی callback ADC میمونه و باقی کد ها اجرا نمیشه.
دلیلش چی میتونه باشه ؟
راهی هست که مقدار ADC رو فقط زمانی که نیاز دارم بگیرم و دیگه توی یه دوره زمانی ثابت مدام چک نشه؟
سلام
خودتون نمیتونید مستقیم تابع رو فراخوانی کنید. باید از توابع استاندارد مطابق چیزی که در مطلب توضیح داده شده استفاده کنید
قطعا کدتون مشکل داره و گرنه که با این کد ما همچین مشکلی نداشتیم
ممکنه توی حلقه ای میرید و بیرون نمیاد یا کدی دارید که باعث میشه ماژول هنگ کنه و کلا غیر فعال بشه
سلام
ببخشید من توی قسمت ریکورد مشکلی که دارم کیفیت صدا ضعیفه
همه تنظیمات رو روی بیشترین کیفیت صدا قرار دادم
اما صدایی که ضبط شده خیلی کمه و واضح نیس
سلام
احتمال زیاد مشکل از نرم افزار نیست.
از چه بردی استفاده میکنید؟(طراحی بخش صدا به چه صورت هست؟)
بهتره که بخش audio بر اساس این داکیومنت طراحی بشه
https://sisoog.com/wp-content/uploads/2020/01/Quectel_MC60_Reference_Design_Rev.B_20160801.pdf
کیفیت خازن های مورد استفاده هم خیلی مهمه.
فک نمیکنم از برد باشه چون از نزدیک کیفیت خوبه
ولی از دور خوب نیس
انگار ولوم صدا کمه
یا GAIN صدا
خب مشخصا این ماژول برای کاربرد ضبط صدا نیست و در کنار کار های دیگه این کار رو هم میکنه (که بیشتر استفادش با هندزفری هست) ، شما برای صدای با کیفیت تر میتونید از چیپ هایی دارای پروتکل PCM استفاده کنید و به mc60 وصلشون کنید که خود این چیپ ها کارهای کم کردن نویز و غیره رو انجام بدند (مثل ALC5616 و NAU88C10GY )
سلام … یه سوال … این ماژول بصورت سخت افزاری فایل mp3 رو میتونه دیکد کنه با نرم افزاری ….
احتمالا به شکل نرم افزاری یا شاید هم سخت افزاری
ولی فرقی نمیکنه در نهایت شما صدای صوت رو خواهید داشت بدون مشکل خاصی