음악 파일 파형 뽑아내기
설명
WAV·MP3 음악에서 원하는 구간의 파형을 표로 뽑아 CSV로 저장하고 그래프로 확인해요
필요 데이터
음악 파일 1개 (WAV/MP3/FLAC/OGG)
출력
파형 CSV + 미리보기 그래프
필요 패키지
- pandas
- matplotlib
- miniaudio
태그
science, visualization
작성자
MDS 메이커 | MDS 1.0
WAV·MP3 음악에서 원하는 구간의 파형을 표로 뽑아 CSV로 저장하고 그래프로 확인해요
Extracts a section of an audio file as a waveform table (CSV) with a preview plot
🔍 클릭하면 원본 크기로 보기README
WAV·MP3 음악에서 원하는 구간의 파형을 표로 뽑아 CSV로 저장하고 그래프로 확인해요
음악 파일 1개 (WAV/MP3/FLAC/OGG)
파형 CSV + 미리보기 그래프
science, visualization
MDS 메이커 | MDS 1.0
# MDS_TITLE: 음악 파일 파형 뽑아내기
# MDS_REQUIRES: pandas, matplotlib, miniaudio
# MDS_DESC: WAV·MP3 음악에서 원하는 구간의 파형을 표로 뽑아 CSV로 저장하고 그래프로 확인해요
# MDS_DESC[en]: Extracts a section of an audio file as a waveform table (CSV) with a preview plot
# MDS_TAGS: science, visualization
# MDS_INPUT: 음악 파일 1개 (WAV/MP3/FLAC/OGG)
# MDS_OUTPUT: 파형 CSV + 미리보기 그래프
# MDS_AUTHOR: MDS 메이커
# MDS_VERSION: 1.0
# MDS_DATE: 2026-09-15
#
import os
from FileDialogManager import MY_DATA_PATH
def choose_audio_file() -> str:
path = mIO.open_file_single(
dialog_title="음악 파일 선택",
file_filter="오디오 파일 (*.mp3 *.wav *.flac *.ogg)",
initial_dir=None,
)
return path or ""
def ask_wave_options(default_name: str) -> dict:
values = mIO.form([
{"type": "spin", "label": "시작 시각(초)", "min": 0, "max": 600, "default": 0, "step": 1},
{"type": "spin", "label": "추출 길이(초)", "min": 1, "max": 60, "default": 10, "step": 1},
{"type": "spin", "label": "샘플 간격(1=원본)", "min": 1, "max": 100, "default": 1, "step": 1},
{"type": "radio", "label": "채널 처리",
"options": ["왼쪽 채널만", "좌우 평균(모노)", "좌·우 모두"],
"default": "왼쪽 채널만"},
{"type": "text", "label": "저장 파일명", "default": default_name},
], title="원본 파형 추출 설정")
return values or {}
def extract_channels(samples: object, nch: int, start_frame: int, end_frame: int,
step: int, mode: str) -> dict:
# 블록 단위로 끊어 읽어 원본 샘플값을 그대로 모은다 — 요약하지 않는다.
# 무거운 수치 루프이므로 진행바를 쓰지 않는다(7-12절).
block_frames = 200000
left_parts, right_parts = [], []
need_right = (mode != "왼쪽 채널만") and nch > 1
cursor = start_frame
n_blocks = max(1, (end_frame - start_frame + block_frames - 1) // block_frames)
for _ in range(n_blocks):
if cursor >= end_frame:
break
stop = min(cursor + block_frames, end_frame)
raw = np.asarray(samples[cursor * nch: stop * nch], dtype="float32")
if raw.size == 0:
break
left_parts.append(np.ascontiguousarray(raw[0::nch][::step]) if nch > 1
else np.ascontiguousarray(raw[::step]))
if need_right:
right_parts.append(np.ascontiguousarray(raw[1::nch][::step]))
del raw
cursor = stop
out = {"left": np.concatenate(left_parts) if left_parts else np.empty(0, dtype="float32")}
if need_right and right_parts:
out["right"] = np.concatenate(right_parts)
del left_parts, right_parts
return out
def build_wave_table(channels: dict, sample_rate: int, start_frame: int,
step: int, mode: str) -> pd.DataFrame:
left = channels["left"]
right = channels.get("right")
n = left.size
times = ((np.arange(n, dtype="float64") * step + start_frame) / sample_rate).astype("float32")
if mode == "좌우 평균(모노)" and right is not None:
m = min(n, right.size)
return pd.DataFrame({
"time_sec": times[:m],
"amplitude": ((left[:m] + right[:m]) * 0.5).astype("float32"),
})
if mode == "좌·우 모두" and right is not None:
m = min(n, right.size)
return pd.DataFrame({
"time_sec": times[:m],
"amp_left": left[:m],
"amp_right": right[:m],
})
return pd.DataFrame({"time_sec": times, "amplitude": left})
def save_wave_csv(out: pd.DataFrame, filename: str) -> str:
name = filename.strip() or "audio_wave"
if not name.lower().endswith(".csv"):
name = name + ".csv"
path = os.path.join(MY_DATA_PATH, name)
out.to_csv(path, index=False, encoding="utf-8-sig")
return path
def plot_wave_preview(out: pd.DataFrame, value_col: str) -> None:
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
n = len(out)
step = max(1, n // 5000) # 그리기용으로만 간격을 벌린다(6절)
view = out.iloc[::step]
fig, ax = plt.subplots(figsize=(7, 5))
ax.plot(view["time_sec"], view[value_col], linewidth=0.6, color="#4C72B0")
ax.set_title("추출 파형 미리보기", fontsize=12, fontweight="semibold")
ax.set_xlabel("시간(초)"); ax.set_ylabel("진폭")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
plt.close(fig)
def run_extract_waveform(df: pd.DataFrame) -> str:
path = choose_audio_file()
if not path:
mIO.print("음악 파일을 선택하지 않아 종료합니다.")
return "취소됨"
default_name = os.path.splitext(os.path.basename(path))[0] + "_wave"
opts = ask_wave_options(default_name)
if not opts:
mIO.print("설정이 취소되어 종료합니다.")
return "취소됨"
start_sec = int(opts["시작 시각(초)"])
length_sec = int(opts["추출 길이(초)"])
step = max(1, int(opts["샘플 간격(1=원본)"]))
mode = str(opts["채널 처리"])
est_rows = int(44100 * length_sec / step)
if est_rows > 600000 and not mIO.confirm(
f"예상 {est_rows:,}행 — CSV가 커지고 시간이 걸립니다. 계속할까요?"):
mIO.print("사용자가 취소했습니다.")
return "취소됨"
mIO.print(
f"원본 파형 추출을 시작합니다 (약 {est_rows:,}행 예상).\n"
"진행바 없이 처리되므로 완료 안내가 뜰 때까지 기다려 주세요."
)
audio = mAudio.load(path, duration_sec=float(start_sec + length_sec))
sample_rate = int(audio.sample_rate)
nch = int(audio.nchannels)
total_frames = len(audio.samples) // nch
start_frame = min(int(start_sec * sample_rate), total_frames)
end_frame = min(start_frame + int(length_sec * sample_rate), total_frames)
if end_frame <= start_frame:
mIO.print("지정한 구간에 데이터가 없습니다. 시작 시각을 줄여 다시 시도하세요.")
return "구간 없음"
channels = extract_channels(audio.samples, nch, start_frame, end_frame, step, mode)
out = build_wave_table(channels, sample_rate, start_frame, step, mode)
del channels
if out.empty:
mIO.print("추출된 샘플이 없습니다.")
return "추출 결과 없음"
saved = save_wave_csv(out, str(opts["저장 파일명"]))
n_rows, n_cols = len(out), len(out.columns)
value_col = "amp_left" if "amp_left" in out.columns else "amplitude"
peak = float(np.max(np.abs(out[value_col].to_numpy())))
eff_rate = sample_rate / step
size_mb = os.path.getsize(saved) / (1024 ** 2)
plot_wave_preview(out, value_col)
mIO.show_df(out, title=f"원본 파형 ({n_rows:,}행 × {n_cols}열)")
del out
mIO.print(
f"파형 추출 완료: {n_rows:,}행 / {n_cols}열\n"
f"구간 {start_sec}~{start_sec + length_sec}초, 실효 샘플레이트 {eff_rate:,.0f}Hz\n"
f"원본 {sample_rate:,}Hz / {nch}채널, 최대 진폭 {peak:,.1f}\n"
f"채널 처리: {mode}, 샘플 간격 {step}\n\n"
f"CSV 저장 완료 ({size_mb:,.2f}MB)\n{saved}\n"
f"폴더: {os.path.dirname(saved)}\n\n"
"각 행이 실제 샘플 하나입니다 — 구간 요약이 아니라 파형 원본값입니다."
)
return f"{n_rows:,}행 저장 완료 — {os.path.basename(saved)}"
result = run_extract_waveform(df)