Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
420 lines
12 KiB
Python
420 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
#
|
|
# wav2pcm.py
|
|
#
|
|
# 11Khz or 8KHzの8bitモノラルのwavファイルからPCMデータを抽出して保存する。
|
|
# なお、wavコンテナの各情報は以下。
|
|
#
|
|
# RIFFチャンク(アドレス固定)
|
|
# - +0x0000〜+0x0003 4byte 0x52, 0x49, 0x46, 0x46 ('RIFF')
|
|
# - +0x0004〜+0x0007 4byte +0x0008以降のデータサイズ
|
|
# - +0x0008〜+0x000B 4vyte 0x57, 0x41, 0x56, 0x45 ('WAVE')
|
|
# fmtチャンク(開始アドレスはRIFFチャンク直後、0x000cで固定)
|
|
# - +0x0000〜+0x0003 4byte 0x66, 0x6d, 0x74, 0x20 ('fmt ')
|
|
# - +0x0004〜+0x0007 4byte fmtチャンクバイト数
|
|
# - +0x0008〜+0x0009 2byte フォーマットコード (0x0001=PCM)
|
|
# - +0x000A〜+0x000B 2byte チャンネル数
|
|
# - +0x000C〜+0x000F 4byte サンプリングレート 0x2b11=11025(11KHz),0x1f40=8000(8KHz)
|
|
# - +0x0010〜+0x0013 4byte データ速度 ステレオだとサンプリングレートx2となる
|
|
# - +0x0014〜+0x0015 2byte ブロックサイズ
|
|
# - +0x0016〜+0x0017 2byte サンプルあたりのビット数 0x0008=8bit,0x0010=16bit
|
|
# - +0x0018〜+0x0019 2byte 拡張部分のサイズ、リニアPCMは存在しない
|
|
# - +0x001A〜 拡張部分
|
|
# dataチャンク(開始アドレスは任意)
|
|
# - +0x0000〜+0x0003 4byte 0x64, 0x61, 0x74, 0x61 ('data')
|
|
# - +0x0004〜+0x0007 4byte 波形データのバイト数
|
|
# - +0x0008〜 波形データ
|
|
# LISTチャンク(ない場合もある)
|
|
# - +0x0000〜+0x0003 4byte 0x4c, 0x49, 0x53, 0x54 ('LIST')
|
|
# - +0x0004〜+0x0007 4byte +0x0008以降のデータサイズ
|
|
# - +0x0008〜 データ
|
|
|
|
import os
|
|
import sys
|
|
import traceback
|
|
import argparse
|
|
import binascii
|
|
import ffmpeg
|
|
|
|
# wavデータ
|
|
data = ""
|
|
|
|
# 現在の処理対象位置
|
|
pos = 0x0000
|
|
|
|
def main():
|
|
'''
|
|
メイン処理\n
|
|
'''
|
|
global data
|
|
global pos
|
|
|
|
# 変換要否
|
|
_isNeedConv = False
|
|
|
|
# 引数パース
|
|
_argparser = argparse.ArgumentParser(description="Extract PCM data from a WAV file.")
|
|
_argparser.add_argument("infile", help="WAV file name to input.")
|
|
_argparser.add_argument("--outfile", "-o", help="PCM file name for output.", default="out.pcm")
|
|
_argparser.add_argument("--rate", "-r", default="8", help="Specify the sampling rate of the PCM data to be created as 11 or 8. The default is 8 KHz.")
|
|
_argparser.add_argument("--force", "-f", action="store_const", const="", help="Ignore output even if the file exists.")
|
|
_argparser.add_argument("--ver", "-v", action="version", version="%(prog)s 0.4.0")
|
|
_args = _argparser.parse_args()
|
|
|
|
# 入力ファイルのフルパスを設定
|
|
_inFilePath = filePathUtil(_args.infile)
|
|
|
|
# 出力ファイルのフルパスを設定
|
|
_outFilePath = filePathUtil(_args.outfile)
|
|
|
|
# 引数チェック
|
|
try:
|
|
# 入力ファイルの拡張子は.wav以外はエラー
|
|
if os.path.splitext(_args.infile)[1] != ".wav":
|
|
raise Exception("File extension is not '.wav'.")
|
|
|
|
# 入力ファイルの存在チェック
|
|
if os.path.exists(_inFilePath) == False:
|
|
raise Exception("File not found.")
|
|
|
|
# 存在チェック
|
|
# オプション --force(-f)が設定されている場合はエラーとしない
|
|
if os.path.exists(_outFilePath) and _args.force == None:
|
|
raise FileExistsError("Specified file already exists.")
|
|
|
|
# rateは8,11以外はエラー
|
|
if _args.rate not in["8", "11"]:
|
|
raise Exception("Rate Error.")
|
|
else:
|
|
_rate = int(_args.rate)
|
|
|
|
except Exception as e:
|
|
print(traceback.format_exception_only(type(e), e)[0])
|
|
sys.exit()
|
|
|
|
# 入力ファイル取得
|
|
getData(_inFilePath)
|
|
|
|
# 取得したデータの長さ分処理する
|
|
while pos < len(data)/2:
|
|
|
|
# Chank名取得
|
|
_chankName = getStringData()
|
|
if _chankName == "RIFF":
|
|
checkRIFFChank()
|
|
elif _chankName == "fmt ":
|
|
_isNeedConv = checkFmtChank(_rate)
|
|
if _isNeedConv:
|
|
# サンプリングレートを変更する必要がある場合は、
|
|
# サンプリングレートを変更してデータを読み直し、現在の処理対象位置をリセット
|
|
_tmpFilePath = convertRate(_inFilePath, _rate)
|
|
getData(_tmpFilePath)
|
|
pos = 0
|
|
os.remove(_tmpFilePath)
|
|
elif _chankName == "data":
|
|
checkDataChank(_outFilePath)
|
|
elif _chankName == "LIST":
|
|
checkListChank()
|
|
|
|
|
|
def getData(filePath:str):
|
|
'''
|
|
入力ファイル取得処理\n
|
|
\n
|
|
Parameters\n
|
|
----------\n
|
|
filePath : str
|
|
入力ファイルパス
|
|
'''
|
|
global data
|
|
|
|
try:
|
|
# ファイル読み込み
|
|
with open(filePath, mode="rb") as f:
|
|
data = f.read().hex()
|
|
f.close()
|
|
|
|
except Exception as e:
|
|
print(traceback.format_exception_only(type(e), e)[0])
|
|
sys.exit()
|
|
|
|
return
|
|
|
|
|
|
def checkRIFFChank():
|
|
'''
|
|
RIFFチャンクの処理\n
|
|
'''
|
|
global data
|
|
global pos
|
|
|
|
# # 全体のデータサイズ取得
|
|
# _size = getInt4byteData()
|
|
pos += 4
|
|
|
|
return
|
|
|
|
|
|
def checkFmtChank(rate:int) -> bool:
|
|
'''
|
|
fmtチャンクの処理\n
|
|
\n
|
|
Parameters\n
|
|
----------\n
|
|
rate : int\n
|
|
出力PCMデータのサンプリングレート\n
|
|
\n
|
|
Returns\n
|
|
-------\n
|
|
bool\n
|
|
サンプリングレートを変更する必要がある場合はTrue\n
|
|
'''
|
|
global data
|
|
global pos
|
|
|
|
# 変換要否
|
|
_isNeedConv = False
|
|
|
|
# fmtチャンクバイト数取得
|
|
_size = getInt4byteData()
|
|
_pos_sv = pos
|
|
|
|
try:
|
|
# フォーマットコードチェック
|
|
if getInt2byteData() != 0x0001:
|
|
# PCMでなければエラー
|
|
raise Exception("This is not PCM data.")
|
|
|
|
# チャンネル数はスキップ
|
|
pos += 2
|
|
|
|
# サンプリングレートチェック
|
|
_v = getInt4byteData()
|
|
if _v != 0xac44 and _v != 0x2b11 and _v != 0x1f40: # ac44は44KHz,2b11は11KHz,1f40は8KHz
|
|
# 8000以外であればエラー
|
|
raise Exception("Sampling rate is not 44KHz/11Khz/8KHz.")
|
|
|
|
# 変換要否チェック
|
|
print("samplingrate=" + str(_v))
|
|
if (_v == 0xac44) or (_v == 0x2b11 and rate != 11) or (_v == 0x1f40 and rate != 8):
|
|
_isNeedConv = True
|
|
else:
|
|
_isNeedConv = False
|
|
|
|
# データ速度はスキップ
|
|
pos += 4
|
|
|
|
# ブロックサイズはスキップ
|
|
pos += 2
|
|
|
|
# サンプルあたりのビット数はスキップ
|
|
pos += 2
|
|
|
|
# 次のチャンクの先頭にセット
|
|
pos = _pos_sv + _size
|
|
|
|
except Exception as e:
|
|
print(traceback.format_exception_only(type(e), e)[0])
|
|
sys.exit()
|
|
|
|
return _isNeedConv
|
|
|
|
|
|
def checkDataChank(filePath:str):
|
|
'''
|
|
dataチャンクの処理\n
|
|
\n
|
|
Parameters\n
|
|
----------\n
|
|
filePath : str\n
|
|
出力ファイルパス
|
|
'''
|
|
global data
|
|
global pos
|
|
|
|
_size = getInt4byteData()
|
|
_pos_sv = pos
|
|
|
|
# 波形データのサイズを取得
|
|
_datasize = getInt4byteData()
|
|
|
|
try:
|
|
# 波形データをバイトデータとして一時ファイルに出力
|
|
with open(filePath, mode="wb") as _f:
|
|
_f.write( bytes.fromhex( data[pos*2:(pos+_datasize)*2] ) )
|
|
_f.close()
|
|
print("output data size " + str(_size - 4) + "bytes.")
|
|
|
|
# 次のチャンクの先頭にセット
|
|
pos = _pos_sv + _size
|
|
|
|
except Exception as e:
|
|
print(traceback.format_exception_only(type(e), e)[0])
|
|
sys.exit()
|
|
|
|
return
|
|
|
|
|
|
def checkListChank():
|
|
'''
|
|
LISTチャンクの処理\n
|
|
'''
|
|
global data
|
|
global pos
|
|
|
|
_size = getInt4byteData()
|
|
|
|
# 次のチャンクの先頭にセット
|
|
pos += _size
|
|
|
|
return
|
|
|
|
|
|
def getStringData() -> str:
|
|
'''
|
|
現在の処理対象位置から4byteのバイナリデータを取得して、キャラクタ文字列として返却します。\n
|
|
この関数を実行すると、現在の処理対象位置が+4されます。\n
|
|
\n
|
|
Returns\n
|
|
-------\n
|
|
str\n
|
|
4バイトのバイナリデータをキャラクタ文字列に変換した値\n
|
|
'''
|
|
global data
|
|
global pos
|
|
|
|
_result = ""
|
|
for i in range(0, 4):
|
|
_result += chr(int(data[(pos)*2:(pos+1)*2], 16))
|
|
pos += 1
|
|
|
|
return _result
|
|
|
|
|
|
def getInt2byteData() -> int:
|
|
'''
|
|
現在の処理対象位置から2byteのバイナリデータを取得して、数値として返却します。\n
|
|
この関数を実行すると、現在の処理対象位置が+2されます。\n
|
|
\n
|
|
Returns\n
|
|
-------\n
|
|
int\n
|
|
2バイトのバイナリデータを10進に変換した値\n
|
|
'''
|
|
global data
|
|
global pos
|
|
|
|
_result = getIntValue(pos, pos+1)
|
|
pos += 2
|
|
|
|
return _result
|
|
|
|
|
|
def getInt4byteData() -> int:
|
|
'''
|
|
現在の処理対象位置から4byteのバイナリデータを取得して、数値として返却します。\n
|
|
この関数を実行すると、現在の処理対象位置が+4されます。\n
|
|
\n
|
|
Returns\n
|
|
-------\n
|
|
int\n
|
|
4バイトのバイナリデータを10進に変換した値\n
|
|
'''
|
|
global data
|
|
global pos
|
|
|
|
_result = getIntValue(pos, pos+3)
|
|
pos += 4
|
|
|
|
return _result
|
|
|
|
|
|
def getIntValue(startPos, endPos):
|
|
'''
|
|
dataに格納された16進数文字列の指定位置の値をint型の数値に変換して返却します。\n
|
|
\n
|
|
Parameters\n
|
|
----------\n
|
|
startPos : int\n
|
|
開始位置\n
|
|
endPos : int\n
|
|
終了位置\n
|
|
'''
|
|
global data
|
|
|
|
return int.from_bytes(binascii.a2b_hex(data[startPos*2:(endPos+1)*2]), "little")
|
|
|
|
|
|
def filePathUtil(path:str) -> str:
|
|
'''
|
|
ファイルパスユーティリティ\n
|
|
引数のパスにディレクトリを含んでいない場合、カレントディレクトリを付与したフルパスを生成して返却します。\n
|
|
\n
|
|
Parameters\n
|
|
----------\n
|
|
path : str\n
|
|
ファイルパス
|
|
\n
|
|
Returns\n
|
|
-------\n
|
|
str\n
|
|
フルパスに編集後の文字列\n
|
|
'''
|
|
# 入力ファイルのフルパスからファイル名を取得
|
|
_filename = os.path.basename(path)
|
|
|
|
# 入力ファイルのフルパスからファイルパスを取得
|
|
_filepath = os.path.dirname(path)
|
|
if _filepath == "" or _filepath == None:
|
|
# ファイルパスが取得できなかった場合(ファイル名のみ指定された場合)は現在のパスを設定
|
|
_filepath = os.path.dirname(__file__)
|
|
|
|
return _filepath + os.sep + _filename
|
|
|
|
|
|
def convertRate(filePath:str, rate:int) -> str:
|
|
'''
|
|
wavファイルのサンプリングレートを変更する\n
|
|
fmtチャンクのサンプリングレートと引数のサンプリングレートが違う場合は、
|
|
引数のサンプリングレートに変更します。\n
|
|
\n
|
|
Parameters\n
|
|
----------\n
|
|
filePath : str\n
|
|
入力ファイルパス
|
|
rate : int\n
|
|
変換対象のサンプリングレート
|
|
\n
|
|
Returns\n
|
|
-------\n
|
|
str\n
|
|
一時ファイルのフルパス\n
|
|
'''
|
|
|
|
# 一時ファイルパス生成
|
|
_tmpFilePath = filePathUtil("tmp.wav")
|
|
|
|
# ここでffmpegを実行
|
|
try:
|
|
_stream = ffmpeg.input(filePath)
|
|
_stream = ffmpeg.output(
|
|
_stream, \
|
|
_tmpFilePath, \
|
|
ac=1, \
|
|
ar=(8000 if rate == 8 else 11025), \
|
|
acodec="pcm_u8")
|
|
_stream = ffmpeg.overwrite_output(_stream)
|
|
ffmpeg.run(_stream)
|
|
|
|
except Exception as e:
|
|
print(traceback.format_exception_only(type(e), e)[0])
|
|
sys.exit()
|
|
|
|
# 生成した一時ファイルのフルパスを返却
|
|
return _tmpFilePath
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|