[1편] https://shs2783.tistory.com/6
WAV 파일을 분석해보자 (with 파이썬) - 1
1. 전체적인 구조 wav 파일은 크게 헤더(header)와 데이터(data)로 구성되어 있습니다. 헤더는 파일에 대한 정보입니다. 파일의 크기, 포맷 형식, 채널 수 등을 나타내는 설명서라고 보시면 됩니다. 데
shs2783.tistory.com
[3편]https://shs2783.tistory.com/12
WAV 파일을 분석해보자 (with 파이썬) - 3
[1편] https://shs2783.tistory.com/6 WAV 파일을 분석해보자 (with 파이썬) - 1 1. 전체적인 구조 wav 파일은 크게 헤더(header)와 데이터(data)로 구성되어 있습니다. 헤더는 파일에 대한 정보입니다. 파일의 크기
shs2783.tistory.com
이번 포스팅에서는 파이썬 코드로 직접 wav 파일 구조를 살펴보겠습니다.
1. struct 라이브러리
wav 파일은 bytes 형식으로 이루어져 있습니다.
파이썬에서는 bytes 데이터를 struct 라는 라이브러리로 쉽게 파싱할 수 있습니다.
파이썬 기본 모듈이기 때문에 별도의 설치과정이 필요 없습니다.
라이브러리에 대한 설명은 밑의 포스팅을 참고바랍니다.
https://shs2783.tistory.com/11
python struct 라이브러리
파이썬에서는 bytes 데이터를 struct 라는 라이브러리로 쉽게 파싱할 수 있습니다. 파이썬 기본 모듈이기 때문에 별도의 설치과정이 필요 없습니다. struct는 기본적으로 pack과 unpack이라는 메소드가
shs2783.tistory.com
2. wav 파일 불러오기
먼저 wav파일을 열어봅시다. 밑의 음원은 제가 직접 연주한 음원입니다. (발 연주 ㅈㅅ..)
with open('guitar.wav', 'rb') as wav_file:
wav = wav_file.read()
print(wav)
>>> b'RIFF\xc8"\x0c\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x02\x00D\xac\x00\x00\x10\xb1\x02\x00\x04... (생략)
결과값을 보시면 RIFF, WAVE, fmt 등 이전 포스팅에서 봤던 내용들이 보이죠?
RIFF\xc8"\x0c\x00WAVE... -> ChunkID (4byte)
RIFF\xc8"\x0c\x00WAVE... -> ChunkSize (4byte)
RIFF\xc8"\x0c\x00WAVE... -> Format (4byte)
* 참고로 중간에 따옴표(")는 아스키코드에 해당하는 값이 문자열로 변형 되어서 나타난 것입니다.
아스키코드로 " = 22입니다. 그래서 \xc8"\x0c\x00 = \xc8\x22\x0c\x00와 같습니다.
(RIFF, WAV, fmt 등 \x 없이 문자열 또는 기호만 나오는 것들 모두 같은 이유입니다! )
3. chunk 정보 저장 및 파싱 함수 만들기
이제 본격적으로 파싱을 해봅시다.
이전 포스팅과 비교해서 보시면 좋습니다.
일단 각 chunk에 대한 정보를 깔끔하게 리스트로 저장해 줍시다.
# [시작인덱스, 바이트길이, 엔디언]
RIFF_chunkID = [0, 4, 'big']
RIFF_ChunkSize = [4, 4, 'little']
RIFF_Format = [8, 4, 'big']
fmt_SubChunk1ID = [12, 4, 'big']
fmt_SubChunk1Size = [16, 4, 'little']
fmt_AudioFormat = [20, 2, 'little']
fmt_NumChannels = [22, 2, 'little']
fmt_SampleRate = [24, 4, 'little']
fmt_ByteRate = [28, 4, 'little']
fmt_BlockAlign = [32, 2, 'little']
fmt_BitsPerSample = [34, 2, 'little']
data_SubChunk2ID = [36, 4, 'big']
data_SubChunk2Size = [40, 2, 'little']
data_data = [44, None, 'little']
리스트에 저장된 값은 [시작인덱스, 바이트길이, 엔디언] 으로
이전 포스팅 도식화 그림의 [File Offset, Field Size, endian]에 해당합니다.
그리고 파싱할 함수를 만들어야겠죠?
import struct
def parse(data, info):
start, length, endian = info
parse 함수는 data와 info를 파라미터로 받습니다.
data는 파싱하고 싶은 wav파일의 byte 데이터이고
info에는 위에서 만든 정보 리스트입니다.
if endian == 'little':
endian_str = '<'
else:
endian_str = '>'
먼저 엔디안에 따라서 포맷 문자열을 지정해줍니다.
리틀엔디언은 '<', 빅 엔디언은 '>'입니다.
if length == 2:
length_str = 'H'
elif length == 4:
if endian_str == '>':
length_str = '4s'
else:
length_str = 'I'
else:
return data[start:]
그다음은 바이트 길이에 따라 포맷 문자열을 지정해줍니다.
길이가 2인 데이터도 있고, 4인 데이터도 있는데
길이가 2면 모두 숫자이기 때문에 포맷은 'H' (부호 없는 2bytes 정수)
길이가 4면 문자열일수도, 숫자일 수도 있기 때문에
숫자일 경우에는 'I' (부호 없는 4bytes 정수)로
문자열일 경우에는 '4s' (4자리 문자열)로 지정해줍니다.
정리하면
길이 2 (숫자) => 'H'
길이 4 (숫자) = 'I'
길이 4 (문자열) = '4s'
그리고 else 문은 마지막 음원 데이터일 경우를 위해서 입니다.
음원 데이터는 일단 파싱하지 않고 바이트 그대로 출력합니다.
음원 데이터에 대한 설명은 다음 포스팅에서 하겠습니다.
format_str = endian_str + length_str
data_crop = data[start: start+length]
result = struct.unpack(format_str, data_crop)
return result[0], data_crop
마지막으로 포맷형식을 합쳐준 다음
필요한 부분만 데이터를 뽑아내서
struct의 unpack 메소드를 이용해 데이터를 언패킹 해줍니다.
그리고 언패킹한 값과 바이트값을 리턴해줍니다.
unpack 메소드는 리턴값을 튜플로 반환하는데,
지금은 튜플의 요소가 1개 밖에 없기 때문에 결과값의 0번째값만 가져옵니다.
전체 코드입니다.
import struct
def parse(data, info):
start, length, endian = info
if endian == 'little':
endian_str = '<'
else:
endian_str = '>'
if length == 2:
length_str = 'H'
elif length == 4:
if endian_str == '>':
length_str = '4s'
else:
length_str = 'I'
else:
return data[start:]
format_str = endian_str + length_str
data_crop = data[start: start+length]
result = struct.unpack(format_str, data_crop)
return result[0], data_crop
4. 결과
결과를 한번 볼까요?
일단 RIFF chunk 부터 봅시다.
RIFF_chunkID = parse(wav, RIFF_chunkID)
RIFF_ChunkSize = parse(wav, RIFF_ChunkSize)
RIFF_Format = parse(wav, RIFF_Format)
print(f'RIFF_chunkID: {RIFF_chunkID}')
print(f'RIFF_chunkSize: {RIFF_ChunkSize}')
print(f'RIFF_Format: {RIFF_Format}')
>>> RIFF_chunkID: (b'RIFF', b'RIFF')
RIFF_chunkSize: (795336, b'\xc8"\x0c\x00')
RIFF_Format: (b'WAVE', b'WAVE')
파싱 함수의 반환값은 (언패킹 데이터, 바이트 데이터) 입니다.
chunkID는 RIFF
chunkSize는 795336 bytes
Format은 WAVE입니다.
chunckSize는 전체 파일크기에서 8바이트를 뺀 값이라고 했습니다.
한 번 확인해볼까요?

실제 wav 파일 크기에서 8을 빼면 chunkSize와 동일한 것을 볼 수 있습니다.
(795344 - 8 = 795336)
두번째 fmt sub-chunck를 보겠습니다.
fmt_SubChunk1ID = parse(wav, fmt_SubChunk1ID)
fmt_SubChunk1Size = parse(wav, fmt_SubChunk1Size)
fmt_AudioFormat = parse(wav, fmt_AudioFormat)
fmt_NumChannels = parse(wav, fmt_NumChannels)
fmt_SampleRate = parse(wav, fmt_SampleRate)
fmt_ByteRate = parse(wav, fmt_ByteRate)
fmt_BlockAlign = parse(wav, fmt_BlockAlign)
fmt_BitsPerSample = parse(wav, fmt_BitsPerSample)
print(f'fmt_SubChunk1ID: {fmt_SubChunk1ID}')
print(f'fmt_SubChunk1Size: {fmt_SubChunk1Size}')
print(f'fmt_AudioFormat: {fmt_AudioFormat}')
print(f'fmt_NumChannels: {fmt_NumChannels}')
print(f'fmt_SampleRate: {fmt_SampleRate}')
print(f'fmt_ByteRate: {fmt_ByteRate}')
print(f'fmt_BlockAlign: {fmt_BlockAlign}')
print(f'fmt_BitsPerSample: {fmt_BitsPerSample}')
>>> fmt_SubChunk1ID: (b'fmt ', b'fmt ')
fmt_SubChunk1Size: (16, b'\x10\x00\x00\x00')
fmt_AudioFormat: (1, b'\x01\x00')
fmt_NumChannels: (2, b'\x02\x00')
fmt_SampleRate: (44100, b'D\xac\x00\x00')
fmt_ByteRate: (176400, b'\x10\xb1\x02\x00')
fmt_BlockAlign: (4, b'\x04\x00')
fmt_BitsPerSample: (16, b'\x10\x00')
fmt_SubChunk1ID은 'fmt ' (마지막 공백 오타 아님!!),
fmt_SubChunk1Size는 16bytes,
fmt_AudioFormat은 1 (=PCM 형식) 입니다.
fmt_NumChannels은 2채널이고
fmt_SampleRate는 44100Hz
fmt_ByteRate는 SampleRate * NumChannels * BitsPerSample / 8 = 176400Hz
( 44100 ) ( 2 ) ( 16 / 8 )
fmt_BlockAlign은 NumChannels * BitsPerSample / 8 = 4bytes입니다.
( 2 ) ( 16 / 8 )
fmt_BitsPerSample은 16비트 입니다.
마지막으로 data sub-chunk를 보겠습니다.
data_SubChunk2ID = parse(wav, data_SubChunk2ID)
data_SubChunk2Size = parse(wav, data_SubChunk2Size)
data_data = parse(wav, data_data)
print(f'data_SubChunk2ID: {data_SubChunk2ID}')
print(f'data_SubChunk2Size: {data_SubChunk2Size}')
print(f'data_data: {data_data[:20]}')
print('음원데이터 길이:', len(data_data))
>>> data_SubChunk2ID: (b'data', b'data')
data_SubChunk2Size: (795300, b'\xa4"\x0c\x00')
data_data: b'\xac\x00\x0b\x00\xa1\x00%\x00\xa1\x00\x1f\x00\x94\x00\x18\x00\x86\x00\x1e\x00'
음원데이터 길이: 795300
data_SubChunk2ID는 'data'
data_SubChunk2Size는 795300 bytes
data_data는 음원데이터로, 길이가 너무 길기 때문에 20번째 까지만 출력했습니다.
실제 음원 데이터의 길이를 출력해보면 data_SubChunk2Size와 똑같은 795300임을 알 수 있습니다.
여기까지 파이썬으로 wav 파일 구조를 파싱해봤습니다
다음시간에는 마지막 음원 데이터의 정체를 한번 살펴보겠습니다.
'Audio' 카테고리의 다른 글
WAV 파일을 분석해보자 (with 파이썬) - 3 (0) | 2023.02.04 |
---|---|
WAV 파일을 분석해보자 (with 파이썬) - 1 (0) | 2023.01.17 |