Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions karriz-mp3/mp3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# mp3 web player
테스트를 위해 임시 서버 https://xym0.kalee.land:10443/docs 에서 확인 가능하도록 하였습니다.

## 모듈 구성
### mp3.reader
### mp3.trimmer
### mp3.streamer

## 실행 방법
해당 프로젝트는 `poetry`를 기반으로 만들어졌습니다. 반드시 실행 전 `poetry` 설치가 필요합니다.

### poetry 명령어
```sh
poetry install # 환경 기본 설치
poetry shell # 가상 shell 진입
poetry build # 패키지 빌드
```

### 서버 실행 명령어
```sh
uvicorn main:app --reload --host 0.0.0.0 --port 10001 # 외부 오픈을 위해서는 host가 0.0.0.0으로 되어야함
```

Binary file added karriz-mp3/mp3/__pycache__/main.cpython-310.pyc
Binary file not shown.
Empty file added karriz-mp3/mp3/assets/empty.mp3
Empty file.
Binary file added karriz-mp3/mp3/assets/input.mp3
Binary file not shown.
Binary file added karriz-mp3/mp3/assets/input.mp3.skip
Binary file not shown.
Binary file added karriz-mp3/mp3/assets/trimmed_input.mp3
Binary file not shown.
Binary file added karriz-mp3/mp3/assets/wrong.mp3
Binary file not shown.
33 changes: 33 additions & 0 deletions karriz-mp3/mp3/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

from mp3.reader import MP3FileReader
from mp3.trimmer import MP3FileTrimmer
from mp3.streamer import MP3Streamer

app = FastAPI()

@app.get("/")
async def root():
return {"code": 200, "message": "success", "data": None}

@app.get("/mp3/play")
async def play(filename: str, offset: int = -1, size: int = -1, skip: int = 1):
reader = MP3FileReader(f"assets/{filename}")

mp3_file = reader.read_skip_frame(skip)

if offset < 0 and size < 0:
mp3_streamer = MP3Streamer(mp3_file)
return StreamingResponse(mp3_streamer.streaming(), media_type="audio/mpeg")

mp3_file_trimmer = MP3FileTrimmer(mp3_file)
trimmed_mp3 = mp3_file_trimmer.trim(offset, size, f"assets/trimmed_{filename}")

mp3_streamer = MP3Streamer(trimmed_mp3)

mp3_streamer.set_speed("assets/input.x2.mp3", 2.0)

return StreamingResponse(mp3_streamer.streaming(), media_type="audio/mpeg")


Empty file added karriz-mp3/mp3/mp3/__init__.py
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
52 changes: 52 additions & 0 deletions karriz-mp3/mp3/mp3/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
KB = 1024
MB = KB * 1024
GB = MB * 1024

SYNC_WORD_MASK = 0b11111111111
VERSION_MASK = 0b11
LAYER_MASK = 0b11
PROTECTION_MASK = 0b1
BITRATE_MASK = 0b1111
SAMPLING_RATE_MASK = 0b11
PADDING_MASK = 0b1
PRIVATE_MASK = 0b1
CHANNEL_MODE_MASK = 0b11
MODE_EXTENSION_MASK = 0b11
COPYRIGHT_MASK = 0b1
ORIGINAL_MASK = 0b1
EMPHASIS_MASK = 0b11

BITRATE_TABLE = [
[
[0], # Reserved
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2.5 Layer 3
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2.5 Layer 2
[0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256], # MPEG-2.5 Layer 1
],
[
# Reserved
[0],
[0],
[0],
[0],
],
[
[0], # Reserved
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2 Layer 3
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], # MPEG-2 Layer 2
[0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256] # MPEG-2 Layer 1
],
[
[0], # Reserved
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320], # MPEG-1 Layer 3
[0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384], # MPEG-1 Layer 2
[0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448] # MPEG-1 Layer 1
]
]

SAMPLE_RATE_TABLE = [
[11025, 12000, 8000], # MPEG2.5
[0, 0, 0], # Reserved
[22050, 24000, 16000], # MPEG2
[44100, 48000, 32000], # MPEG1
]
File renamed without changes.
10 changes: 7 additions & 3 deletions karriz/mp3_file_reader/error.py → karriz-mp3/mp3/mp3/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ class FileStreamNotLoadedError(Exception):
def __init__(self):
super().__init__('MP3 파일이 로드 되지않았습니다.')

class FileStreamNotLoadedError(Exception):
class EmptySizeFileError(Exception):
def __init__(self):
super().__init__('MP3 파일이 로드 되지않았습니다.')

super().__init__('빈 파일은 사용할 수 없습니다.')

class InValidFrameHeaderSizeError(ValueError):
def __init__(self):
super().__init__('프레임 헤더의 크기가 올바르지 않습니다.')

class InvalidFrameSyncError(Exception):
def __init__(self):
super().__init__('유효하지 않은 FrameSync 값 입니다.')
Expand Down
24 changes: 24 additions & 0 deletions karriz-mp3/mp3/mp3/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from dataclasses import dataclass

from .enum import MP3Version, MP3Layer, MP3Protection, MP3PaddingBit, MP3ChannelMode ,MP3Copyright ,MP3Original, MP3Emphasis

@dataclass
class MP3FrameHeader:
version: MP3Version = MP3Version.MPEG_2_5
layer: MP3Layer = MP3Layer.RESERVED
protection: MP3Protection = MP3Protection.PROTECTED_BY_CRC
bitrate: int = 0
sampling_rate_freq: int = 0
padding: MP3PaddingBit = MP3PaddingBit.NOT_PADDED
private: int = 0
channel_mode: MP3ChannelMode = MP3ChannelMode.STEREO
mode_extension: int = 0
copyright: MP3Copyright = MP3Copyright.NOT_COPYRIGHTED
original: MP3Original = MP3Original.COPY
emphasis: MP3Emphasis = MP3Emphasis.NONE

@dataclass
class MP3:
file_path: str = ""
total_frame: int = 0
total_duration: float = 0 # seconds 단위
73 changes: 73 additions & 0 deletions karriz-mp3/mp3/mp3/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from . import model, const, enum, error

class MP3FrameHeaderParser:
def __init__(self, data:bytes):
if len(data) != 4:
raise error.InValidFrameHeaderSizeError

header_bits = int.from_bytes(data, byteorder='big')

syncword = (header_bits >> 21) & const.SYNC_WORD_MASK
if syncword != const.SYNC_WORD_MASK:
raise error.InvalidFrameSyncError

# parse & masking all attributes
version = (header_bits >> 19) & const.VERSION_MASK
layer = (header_bits >> 17) & const.LAYER_MASK
protection = (header_bits >> 16) & const.PROTECTION_MASK
bitrate = (header_bits >> 12) & const.BITRATE_MASK
sampling_rate_freq = (header_bits >> 10) & const.SAMPLING_RATE_MASK
padding = (header_bits >> 9) & const.PADDING_MASK
private = (header_bits >> 8) & const.PRIVATE_MASK
channel_mode = (header_bits >> 6) & const.CHANNEL_MODE_MASK
mode_extension = (header_bits >> 4) & const.MODE_EXTENSION_MASK
copyright = (header_bits >> 3) & const.COPYRIGHT_MASK
original = (header_bits >> 2) & const.ORIGINAL_MASK
emphasis = (header_bits >> 0) & const.EMPHASIS_MASK

# instantiate mp3_frame_header dataclass
mp3_frame_header = model.MP3FrameHeader()

# version
mp3_frame_header.version = enum.MP3Version(version)

# layer
mp3_frame_header.layer = enum.MP3Layer(layer)

# protection bit
mp3_frame_header.protection = enum.MP3Protection(protection)

# bitrate
mp3_frame_header.bitrate = const.BITRATE_TABLE[version][layer][bitrate] * 1000 # kbps -> bps로 변환

# sampling rate frequency
mp3_frame_header.sampling_rate_freq = const.SAMPLE_RATE_TABLE[version][sampling_rate_freq]

# padding
mp3_frame_header.padding = enum.MP3PaddingBit(padding)

# private
mp3_frame_header.private = private

# channel mode
mp3_frame_header.channel_mode = enum.MP3ChannelMode(channel_mode)

# mode extension
mp3_frame_header.mode_extension = mode_extension

# copyright
mp3_frame_header.copyright = enum.MP3Copyright(copyright)

# original
mp3_frame_header.original = enum.MP3Original(original)

#emphasis
mp3_frame_header.emphasis = enum.MP3Emphasis(emphasis)

self.__mp3_frame_header = mp3_frame_header

def get_frame_size(self) -> int:
return (144 * self.__mp3_frame_header.bitrate) // self.__mp3_frame_header.sampling_rate_freq + self.__mp3_frame_header.padding.value

def get_frame_duration(self) -> float:
return 1152 / self.__mp3_frame_header.sampling_rate_freq
70 changes: 70 additions & 0 deletions karriz-mp3/mp3/mp3/reader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os

from . import model, error
from . import parser

class MP3FileReader:
def __init__(self, file_path:str):
file_size = os.path.getsize(file_path)

if file_size == 0:
raise error.EmptySizeFileError

# CBR, VBR에 따라 비트레이트가 바뀔 수 있는데... reader가 필요할까
# read -> trim -> 제공 하면 memory에 데이터가 너무 많이 쌓이는데 ..?
self.__mp3 = model.MP3()
self.__mp3.file_path = file_path

def read(self) -> model.MP3:
mp3_file = open(self.__mp3.file_path, 'rb')

while True:
try:
frame_header_parser = parser.MP3FrameHeaderParser(mp3_file.read(4))

self.__mp3.total_duration += frame_header_parser.get_frame_duration()
mp3_file.seek(frame_header_parser.get_frame_size()-4, 1)

self.__mp3.total_frame += 1

except error.InValidFrameHeaderSizeError:
break

mp3_file.close()

return self.__mp3

def read_skip_frame(self, skipped_frame_size: int = 1) -> model.MP3:
mp3_file = open(self.__mp3.file_path, 'rb')
skipped_mp3_file = open(f"{self.__mp3.file_path}.skip", 'wb')

frame_count = 0

skipped_mp3 = model.MP3()
skipped_mp3.file_path = f"{self.__mp3.file_path}.skip"

while True:
try:
frame_header_bytes = mp3_file.read(4)
frame_header_parser = parser.MP3FrameHeaderParser(frame_header_bytes)

frame_bytes = mp3_file.read(frame_header_parser.get_frame_size()-4)

if frame_count % skipped_frame_size != 0:
skipped_mp3_file.write(frame_header_bytes)
skipped_mp3_file.write(frame_bytes)

skipped_mp3.total_duration += frame_header_parser.get_frame_duration()
skipped_mp3.total_frame += 1

frame_count += 1

except error.InvalidFrameSyncError:
mp3_file.seek(-3, 1)

except error.InValidFrameHeaderSizeError:
break

mp3_file.close()

return skipped_mp3
36 changes: 36 additions & 0 deletions karriz-mp3/mp3/mp3/streamer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pydub import AudioSegment
import numpy as np
from scipy.signal import resample
import lameenc

from .const import KB

from . import model

class MP3Streamer:
def __init__(self, mp3:model.MP3):
self.__mp3 = mp3
self.__audio = AudioSegment.from_mp3(mp3.file_path)
self.__pcm_data = np.array(self.__audio.get_array_of_samples())
self.__sample_rate = self.__audio.frame_rate
self.__channels = self.__audio.channels

def set_speed(self, output_file:str, speed_factor: float = 1.0):
num_samples = int(len(self.__pcm_data) / speed_factor)
adjusted_pcm_data = resample(self.__pcm_data, num_samples)

encoder = lameenc.Encoder()
encoder.set_bit_rate(128)
encoder.set_in_sample_rate(self.__sample_rate)
encoder.set_channels(self.__channels)
encoder.set_quality(2)

mp3_data = encoder.encode(adjusted_pcm_data.tobytes())
mp3_data += encoder.flush()

return mp3_data

def streaming(self):
with open(self.__mp3.file_path, "rb") as file:
while chunk := file.read(50 * KB):
yield chunk
43 changes: 43 additions & 0 deletions karriz-mp3/mp3/mp3/trimmer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from . import model, parser, error

class MP3FileTrimmer:
def __init__(self, mp3:model.MP3):
self.__mp3 = mp3

def trim(self, offset:int, size:int, output_file_path:str) -> model.MP3:
mp3_file = open(self.__mp3.file_path, 'rb')
trimmed_output_file = open(output_file_path, 'wb')

trimmed_mp3 = model.MP3()
trimmed_mp3.file_path = output_file_path

current_frame = 0
frame_count = 0

while True:
try:
frame_header_bytes = mp3_file.read(4)
frame_header_parser = parser.MP3FrameHeaderParser(frame_header_bytes)

trimmed_mp3.total_duration += frame_header_parser.get_frame_duration()
frame_bytes = mp3_file.read(frame_header_parser.get_frame_size()-4)

current_frame += 1

if current_frame >= offset and current_frame < (offset+size):
trimmed_output_file.write(frame_header_bytes)
trimmed_output_file.write(frame_bytes)
frame_count += 1

if current_frame > (offset+size):
break

except error.InValidFrameHeaderSizeError:
break

trimmed_mp3.total_frame = frame_count

mp3_file.close()
trimmed_output_file.close()

return trimmed_mp3
Loading