요즘은 원만한 디바이스에도 리눅스가 포팅되기에 오디오나 비디오 코덱 적용이 어렵지 않다.
하지만 아직도 펌웨어로 오디오 코덱을 개발해야 하는 경우도 있고, MP3를 재생하기 위해 Helix MP3 Decoder를 포팅하여 사용했다.
포팅방법에 대해서는 그다지 복잡하지 않고, 인터넷에 샘플 소스가 많아 찾기 어렵지 않으며
http://ww1.microchip.com/downloads/en/Appnotes/01367A.pdf
https://www.silabs.com/documents/public/application-notes/an1112-efm32-helix-mp3-decoder.pdf문서들을
참조하면 좋다. 또 MP3 대한 괜찮은 자료를 찾았는데, 아래 문서도 정리가 잘 되어 있는듯 하다.
https://www.diva-portal.org/smash/get/diva2:830195/FULLTEXT01.pdf
이제 Helix MP3 Decoder로 개발하는 도중 해결한, 아직도 궁금한 내용을 정리한다.
Input Buffer가 1940 인 이유?
MP3 인코딩된 데이터는 1 frame씩 저장되어 있다. 한 프레임씩 읽어서 디코딩 후, 디코딩 데이터를 I2S로 오디오 코덱에 전송하면 음악이 재생된다. 이때 1 프레임의 크기를 API에서는 1940 바이트로 처리한다.
찾아보면 mainDataBegin과 max nSlots 크기라는 데, 정확한 설명을 확인하지 못했다. 이는 추후 갱신하도록 할 예정이다. Output 버퍼가 2304 인 이유는, MP3 한 프레임당 1152 샘플데이터를 가지고 있고 16비트로 처리하기 때문.
ID3 파싱에 대한 이야기
ID3는 MP3 파일에서 사용하는 메타데이터 포맷이며, 음악의 제목, 음악가 이름 등의 음악 파일에 관련된 정보를 저장한다. 재미있는게 MP3는 AAU+Tag 데이터의 묶음으로 되어 있는데, ID3는 파일 맨 앞단에 위치한다. "ID3" 3바이트가 있는 00h 오프셋이 ID3 10바이트 헤더의 시작지점이고, 100h 의 FF F3으로 시작하는 구간이 MP3 AAU 첫번째 프레임 헤더의 시작이다.
Helix API를 쓴 소스들을 보면 아래와 같이 처리가 되어 있다. READBUF_SIZE (1940) 바이트를 파일에서 첫 읽은 다음, 첫번째 프레임을 SyncWord (all 11 bit '1')를 찾는다. 그리고 MP3 frame header를 파싱한다.
위 그림과 같이 1940 바이트 내에 AAU가 있는 경우 문제가 되지 않는다.
ID3 헤더의 SIZE 값 00 00 01 76 을 자리수당 128씩 곱해서 풀면
(((((1st byte * 0x80) + 2rd byte) * 0x80) + 3rd byte) *0x80)+4th byte = 246 이다.
기본적으로 여기서 헤더길이 10을 더하며, footer 플래그에 따라 20을 더하긴 하지만 지금은 크게 중요하지 않다.
아무튼 246 + 10 = 0x100 이며, 위에서 100h 오프셋을 보면 AAU헤더가 보인다.
아래 소스는 MP3를 처음 연 다음, ID3는 무시하고 첫번째 프레임을 찾은 다음, 프레임 정보를 파싱한다. 다른 소스도 비슷하게 되어 있고, ID3가 짧은 (1940 버퍼 내에 위치하는...) 경우 정상 처리되어 재생된다.
n_read = fillReadBuffer(read_buf, read_ptr, READBUF_SIZE, bytes_left, fp);
bytes_left += n_read;
read_ptr = read_buf;
n_read = MP3FindSyncWord(read_ptr, READBUF_SIZE);
cliPrintf("Offset: %d\n", n_read);
bytes_left -= n_read;
read_ptr += n_read;
n_read = fillReadBuffer(read_buf, read_ptr, READBUF_SIZE, bytes_left, fp);
bytes_left += n_read;
read_ptr = read_buf;
err = MP3GetNextFrameInfo(h_dec, &frameInfo, read_ptr);
if (err != ERR_MP3_INVALID_FRAMEHEADER)
{
cliPrintf("samplerate %d\n", frameInfo.samprate);
cliPrintf("bitrate %d\n", frameInfo.bitrate);
cliPrintf("nChans %d\n", frameInfo.nChans);
cliPrintf("outputSamps %d\n", frameInfo.outputSamps);
cliPrintf("bitsPerSample %d\n", frameInfo.bitsPerSample);
i2sSetSampleRate(_DEF_I2S1, frameInfo.samprate);
q_buf_len = frameInfo.outputSamps / frameInfo.nChans;
q_in = 0;
q_out = 0;
q_len = I2S_BUF_LEN / q_buf_len;
i2sStart();
}
while(cliKeepLoop())
{
if (bytes_left < READBUF_SIZE)
{
n_read = fillReadBuffer(read_buf, read_ptr, READBUF_SIZE, bytes_left, fp);
if (n_read == 0 )
{
break;
}
bytes_left += n_read;
read_ptr = read_buf;
}
n_read = MP3FindSyncWord(read_ptr, bytes_left);
if (n_read >= 0)
{
read_ptr += n_read;
bytes_left -= n_read;
//fill the inactive outbuffer
err = MP3Decode(h_dec, &read_ptr, (int*) &bytes_left, out_buf, 0);
하지만 앨범 이미지가 들어가서 ID3가 엄청 커지는 MP3인 경우 이야기가 달라진다.
아래 MP3파일을 보면 ID3 사이즈가... (((0x30 * 0x80) + 0x42) * 0x80) + 0x3F = C213F , 794,943 가 나온다.
776Kbyte가 넘는다.
15h 오프셋을 보면 FF Fx로 되어 있다. SyncWord로 오해할수 있을 정도이다. 하지만 프레임정보가 없음으로 에러가 발생한다. 첫 프레임 데이터에서 샘플레이트, 채널값을 가져와서 코덱을 재설정 해야 하기 때문이다. 파일을 쭉 찾는다 하더라도 700K 넘는 데이터를 일일히 찾아야 하는 문제가 있다.
위에서 계산한 위치를 기반으로 오프셋을 이동하니.. C2149h에 첫 프레임이 나타난다.
그래서 다음과 같이 프로그램을 수정했다. 길이가 짧던 길던 정상적으로 재생되는 것을 확인했다.
APP_SDCARD_AUDIO_Card_ReadCurrentFile(MP3readBuf, READBUF_SIZE); //첫 파일 리딩
AppDataAudioPlayerPtr->readBytes = READBUF_SIZE;
AppDataAudioPlayerPtr->readbyte_flag = true;
MP3bytesLeft = AppDataAudioPlayerPtr->readBytes;
uint8_t *scratch = (uint8_t*)MP3outBuf; //typically need 512 bytes; will steal from outbuf
read_ID3Tags(appData.fileHandle, scratch, (ID3_EVENT_HANDLER)APP_ID3_EventHandler);
tmpOffset=Get_ID3Size(); // ID3 길이 가져오기
SYS_CONSOLE_PRINT("Get_ID3Size %d / %d \r\n",tmpOffset);
MP3offset += tmpOffset;
//ID3 길이로 오프셋 이동!
APP_SDCARD_AUDIO_Card_SetFilePosition(appData.fileHandle,MP3offset);
APP_SDCARD_AUDIO_Card_ReadCurrentFile(MP3readBuf, READBUF_SIZE); //이동후 파일읽기
tmpOffset = MP3_FindSyncWord(hMP3Decoder,MP3readPtr, MP3bytesLeft);
if (tmpOffset < 0){
MP3readPtr = MP3readBuf;
SYS_CONSOLE_PRINT("MP3_FindSyncWord err %d / %d \r\n",tmpOffset,MP3offset);
break;
}
MP3readPtr += tmpOffset;
MP3bytesLeft -= tmpOffset;
MP3offset += tmpOffset;
if (MP3_GetChannels() != 2)
{
// This means that the MP3 file is either a mono audio stream or
// not sampled at 44100Hz. We currently don't want to handle
// these type of files even if the decoder can decode them
//appData.state = APP_STATE_CLOSE_FILE;
break;
}
appData.bitRate = MP3_GetBitRate(appData.fileHandle, MP3offset, scratch);
if (0==appData.bitRate)
{
//appData.state = APP_STATE_CLOSE_FILE;
SYS_CONSOLE_PRINT("\r\n err.bitRat: %d\r\n",(int)appData.bitRate);
break;
}
//정상으로 처리 후 mp3 정보 확인.
appData.numOfChnls = MP3_GetChannels();
appData.sampleRate = MP3_GetSampleRate();
appData.bitDepth = MP3_GetBitsPerSample();
appData.playbackDuration = (appData.fileSize - MP3offset) / 125 / appData.bitRate;
appData.mp3FirstFrame = MP3offset;
SYS_CONSOLE_PRINT("\r\nbitrate: %d\r\n",(int)appData.bitRate);
SYS_CONSOLE_PRINT("num channels: %d\r\n",(int)appData.numOfChnls);
SYS_CONSOLE_PRINT("sample rate: %d\r\n",(int)appData.sampleRate);
SYS_CONSOLE_PRINT("bits/sample: %d\r\n",(int)appData.bitDepth);
SYS_CONSOLE_PRINT("duration: %d\r\n",(int)appData.playbackDuration);
SYS_CONSOLE_PRINT("first frame: %d\r\n",(int)appData.mp3FirstFrame);
DECODER_EventHandler ( DECODER_EVENT_SAMPLERATE,appData.sampleRate);
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
'개발이야기' 카테고리의 다른 글
MP3 총 재생 시간 계산하기 (0) | 2022.01.24 |
---|---|
펌웨어에서 FAT파일시스템의 확장자 구분 (0) | 2022.01.18 |
동적 라이브러리 SO의 체크섬이 달라지는 이유 (0) | 2021.12.18 |
크로스컴파일러 개발환경 구축에 대한 팁 (0) | 2021.12.18 |
무거운 프로그램에 대한 최적화 #sleep과 #select (0) | 2021.11.28 |