音楽ファイルの分割 2020冬

MP3形式の音楽ファイルを、指定の位置で分割するプログラムを作ってみます。

2022/MAY/03

更新履歴
日付 変更内容
2020/JAN/19 新規作成
2020/JAN/25 再生位置をGUIに表示 追加
2020/JAN/26 GUIで分割 追加
2020/JAN/27 GUIで分割した領域の保存 追加
2020/FEB/01 kon_ut対応 追加
2020/MAY/30 wx_ut対応 追加
2020/SEP/23 snd_ut対応 追加
2022/MAY/02 大きなサイズのファイルの対応 追加
2022/MAY/03 大きなサイズのファイルの対応 typo修正など
再生速度追加など 追加

目次


はじめに

音楽ファイルから特定の部分を切り出したく。

自分でプログラムを作ってみます。

実はずいぶん以前に、手動でデータを切り出す作業をしてた記録がありました。

SMFを読み込み音の波形データを作るプログラム (C言語)ボコーダー にて


以前の記述

さて試すためには音声入力をどうしよう?

通勤のおとものMP3プレーヤーのSDカードに、ファイルが転がってます。

日曜日の昼下がりのFMラジオ。 敬愛する山下達郎さんの番組「サンデーソングブック」を録音したファイルがありました。

このオープニグ・トークの部分を拝借してみます。

t-140216.MP3 ファイルサイズ 81754363 (81M)

2014年2月16日の放送分

MP3プレーヤーで再生してみると、先頭から3分11秒のあたりで、
山下達郎大先生の声が入ります。

ここから3分間程切り出してみます。

とりあえずMP3形式からwav形式に変換します。

$ lame --decode t-140126.MP3 t-140216.wav
input:  t-140126.MP3  (44.1 kHz, 2 channels, MPEG-1 Layer III)
output: t-140216.wav  (16 bit, Microsoft WAVE)
nskipping initial 529 samples (encoder+decoder delay)
hip: Can't step back 333 bytes!
Frame#  9176/130404 192 kbps

表示から
- サンプリング周波数 44100 Hz
- 2 channel
- 16 bit

.wav ファイルのサイズは 600890344 バイトでした
  :
  略
  :
soxコマンドでモノラルのraw形式に変換して、
ddコマンドで切り出してみます。

モノラル16 bitなので、1サンプルあたり2バイト
44100 Hz で 3分11秒の位置は、

2*44100*(3*60+11) バイト目
ddコマンドで1ブロック4Kバイトで扱うとなると

2*44100*(3*60+11)/4096 = 4112.84179687500000000000
4112ブロック

3分間のデータサイズは
2*44100*3*60 バイト

4Kのブロックに換算すると

2*44100*3*60/4096 = 3875.97656250000000000000
3876ブロック分になります。

$ sox t140216.wav -t raw -c 1 - | dd bs=4096 skip=4112 count=3876 > a.raw
3876+0 records in
3876+0 records out
15876096 bytes (16 MB) copied, 1.44943 s, 11.0 MB/s

a.rawのサイズは16Mバイト程度

再生して確認してみます

$ play -t raw -r 44100 -c 1 -b 16 -s a.raw
  :


原理はこの通り。

lameコマンドでMP3形式からWAV形式に。

soxコマンドでWAV形式のサンプリング・レートなどの情報を取得。

soxコマンドでWAV形式からRAW形式に。

あとは分割したい位置の時間から、RAW形式のデータ量を算出して分割。

問題は、いかに分割したい位置を見つけるか?

よくあるツールのように、実際に再生/停止を繰り返して、 分割位置を調整できるようにしたいです。

複数の分割位置が決まれば、必要な領域に名前をつけて、MP3形式のファイルに落とせればOK。

こんなストーリーで考えてみます。


条件

原理からしてlameコマンドとsoxコマンドを使います。

MacでBSDな環境でpython3で試してます。

が、まぁ各種のLinux系でも大丈夫かと。

それがしのpythonのユーティリティ pythonのユーティリティ・プログラム 2020冬 も使っていきます ;-p)


RAW形式のデータに変換

まずはMP3からWAVそしてRAWへと変換するところから。

snddiv.py

(ある程度コーディング進めてから、この部分だけに切り出してます)

  :
import empty
import cmd_ut
import thr
import dbg
  :

なので、カレントディレクトリに pythonのユーティリティ・プログラム 2020冬 からファイルを落として用意しておきます。

$ wget http://kondoh.html.xdomain.jp/kon_ut/empty.py

$ wget http://kondoh.html.xdomain.jp/kon_ut/cmd_ut.py

$ wget http://kondoh.html.xdomain.jp/kon_ut/thr.py

$ wget http://kondoh.html.xdomain.jp/kon_ut/dbg.py

実行してみると

$ ./snddiv.py
Usage: ./snddiv.py filename_mp3
$

引数に対象のMP3ファイルを指定します。

打ち込みのフォトムジーク

のファイルptmsk-14.mp3も落としておきます。

$ wget http://kondoh.html.xdomain.jp/midi_mp3/ptmsk-14.mp3

MP3ファイルを指定して実行すると

$ ./snddiv.py ptmsk-14.mp3
last_sec=179.34
$

データの秒数が表示されます。

$ ls -lt
-rw-r--r--  1 kondoh  staff  31635576  1 19 16:18 ptmsk-14.raw
-rw-r--r--  1 kondoh  staff  31635620  1 19 16:18 ptmsk-14.wav
  :

カレントディレクトリに、WAV形式とRAW形式のファイルが生成されます。

音楽データのサンプリング・レートなどのパラメータは、 soxコマンドから情報を取得しています。

sox --info の出力が次のような形式である事を、前提としています。

$ sox --info ptmsk-14.wav

Input File     : 'ptmsk-14.wav'
Channels       : 2
Sample Rate    : 44100
Precision      : 16-bit
Duration       : 00:02:59.34 = 7908894 samples = 13450.5 CDDA sectors
File Size      : 31.6M
Bit Rate       : 1.41M
Sample Encoding: 16-bit Signed Integer PCM


RAW形式のデータを再生

とりあえず音を出してみます。

soxのplayコマンドにデータをくべて、音を再生させます。

今書き込んだデータが音として鳴るまで、タイムラグがあります。

playコマンドの内部でバッファリングされるので当然です。

分割位置を調整するのが目的なので、 今の瞬間鳴ってる音が、データのどの部分か知りたいです。

そこで、playコマンドの表示を信じる事にします。

$ cat ptmsk-14.raw | play -t raw -r 44100 -b 16 -c 2 -e signed-integer -

-: (raw)

  Encoding: Signed PCM
  Channels: 2 @ 16-bit
Samplerate: 44100Hz
Replaygain: off
  Duration: unknown

In:0.00% 00:00:02.88 [00:00:00.00] Out:127k  [      |      ]        Clip:0

この最後の行の内容が、'\r'による復帰が使われていて、どんどん更新されます。

”In:x.xx% " の後の "00:00:02.88" の部分。

ここが今鳴ってる音の、先頭からの経過時間です。

これを逐次取り込んで利用します。

パッチ

v2.patch

$ cat v2.patch | patch -p1

(コーディングを進めてから、もどって切り出してるので、後々使う機能が先に入ってますが...)

再生のお試し

プログラムの末尾で、「何でもあり」な、コマンドラインの実行を追加してあります。

$ tail v2.patch
+	opt = data.opt
+	dat = data.get_dat_sec(10, False)
+	play = play_new(dat, opt)
+
+	while True:
+		dbg.out('snd> ', '')
+		s = sys.stdin.readline()
+		if not s:
+			break
+		exec(s)
 # EOF

実行してみると

./snddiv.py ptmsk-14.mp3
last_sec=179.34
snd>

と、プロンプト "snd> " が表示されます。

snd> play.start()

で、10秒の位置から再生開始。

snd> play.stop()

で、停止。

snd> print(play.sec)
5.48

などと入力すると、再生した秒数が表示されます。

snd> ^D$

コントロール+DキーのEOF入力で、プログラム終了です。


playコマンドでsudo必要な場合の対応

Ubuntuなpython2の環境で試してみたら、うまく動作しませんでした。

playコマンドの実行にsudoが必要な環境だったので、 sudo_passwdを指定して試してみたのですが、 それでもplay.stop()による終了でひっかかります。

サブプロセスの終了時もsudo killを使うように、 sudoの対応を kon_utcmd_ut.py 側に入れてしまいました。

-S オプションで sudo_passwd を指定するように変更してます。

パッチ

v3.patch

$ cat v3.patch | patch -p1

再生のお試し

データが長いと最初の変換であまりに待たされるので、 少し表示を追加しました。

$ ./snddiv.py
Usage: ./snddiv.py [-S sudo_passwd] filename_mp3

$ ./snddiv.py -S xxxxx ptmsk-14.mp3
make ptmsk-14.wav ... OK
make ptmsk-14.raw ... OK
make smp_lst ... OK
last_sec=179.34
snd>

snd> play.start()

# 再生

snd> play.stop()

# 停止

snd> print(play.sec)
13.56

snd>^D
$


再生速度指定と逆再生

まず、再生を停止した位置から続きを再生できるようにしました。

さらに、再生速度を 2^N 倍で指定可能にしました。

N = 0 が初期値で 2^0 == 1 倍

N = 1 を指定すると 2^1 == 2 倍

N = 2 を指定すると 2^2 == 4 倍

N = -1 を指定すると 2^-1 == 1/2 == 0.5 倍

N = -2 を指定すると 2^-2 == 1/4 == 0.25 倍

N を何と呼ぶべきか?

しっくりとくる名前が思いつかず、パラメータ名は shift にしてます。

さらに、逆再生。

こちらは、パラメータ名はそのまま reverse。

初期値 False で、reverse を True に設定すると、逆再生します。

パッチ

v4.patch

$ cat v4.patch | patch -p1

snd_new()関数を追加して、その中でどこまで再生したかを、変数secに記録してます。

shiftが正の値で、等倍よりも速く再生する場合は、 間引いたデータを与えるようにしてます。

この部分は実は以前のバージョンから仕込んであります。

data_new()の内部関数get_dat_sec(sec, reverse, shift=0)

def get_dat_sec(sec, reverse, shift=0):
	lst = get_smp_sec(sec, reverse)
	if shift > 0:
		lst = lst[:: 2**shift ]
	return b''.join(lst)

ここでパラメータshiftで間引いてます。

shiftの値が負で、等倍よりも遅く再生する場合は、データは等倍のまま。

playコマンドで指定するデータのサンプリング・レートの指定値を変えて、 遅く再生するようにしています。

これも以前の v2.patch に仕込み済。

play_new()の引数でr_rate=1.0で指定するようにして、 内部関数proc_new()から opt.get_raw_opt(r_rate) の呼び出し。

+def play_new(dat, opt, r_rate=1.0):
+	e = empty.new()
+	e.dat = dat
  :
+	def proc_new():
+		sudo_passwd = ''
+		cmd = 'play {} -'.format( opt.get_raw_opt(r_rate) )

data_new()のraw_opt_new()のget_raw_opt()

get_raw_opt = lambda r_rate=1.0: '-t raw -r {} -b {} -c {} -e {}'.format(r*r_rate, b, c, e)

ここで、playコマンドに与えるサンプリング・レートをr_rate倍にして対応してます。

再生のお試し

$ tail v4.patch
 	data = data_new(filename_mp3)
 	dbg.out( 'last_sec={}'.format( data.last_sec ) )

-	opt = data.opt
-	dat = data.get_dat_sec(10, False)
-	play = play_new(dat, opt, sudo_passwd)
+	snd = snd_new(data, sudo_passwd)

 	while True:
 		dbg.out('snd> ', '')

末尾の箇所は変数playからsndに変わってます。

操作用のコマンドの入力はsndに対して行ないます。

$ ./snddiv.py
Usage: ./snddiv.py [-S sudo_passwd] filename_mp3

$ ./snddiv.py -S xxxxx ptmsk-14.mp3
make ptmsk-14.wav ... OK
make ptmsk-14.raw ... OK
make smp_lst ... OK
last_sec=179.34
snd>

snd> snd.start()

# 再生

snd> snd.stop()

# 停止

snd> snd.start()

# 停止したところから続きの再生

snd> snd.stop()

# 停止

snd> snd.shift=1

# 倍速指定(x2)

snd> start()

# 停止したところから続きを2倍速で再生

snd> snd.stop()

# 停止

snd> snd.shift=-1

# 倍速指定(x1/2)

snd> start()

# 停止したところから続きを0.5倍速で再生

snd> snd.stop()

# 停止

snd> snd.shift=0

# 倍速指定(x1)

snd> snd.reverse=True

# 逆再生指定

snd> snd.start()

# 停止したところから逆再生

snd> snd.stop()

# 停止

snd> ^D
$


分割してファイルに保存

指定の位置で分割して、ファイルに保存する仕組みを実装してみます。

パッチ

v5.patch

$ cat v5.patch | patch -p1

snd_new()にdivsというリストを追加してます。

ここに分割位置の時刻(秒)を登録します。

時刻の登録/削除は、内部関数のdiv_join()を呼び出します。

+	def div_join():
+		if e.sec in divs:
+			i = divs.index(e.sec)
+			if i not in (0, len(divs)-1):
+				divs.pop(i)
+		else:
+			divs.append(e.sec)
+			divs.sort()

今の再生位置 e.sec をリストdivsに登録。

e.sec の時刻が既に登録されていたら、削除する動作になります。

登録した時刻までの「頭出し」や「巻戻し」の機能も、 内部関数ff()とrw()として追加しました。

+	def ff():
+		if e.sec >= divs[-1]:
+			return
+		for div in divs:
+			if div > e.sec:
+				e.sec = div
+				break
+
+	def rw():
+		if e.sec <= 0:
+			return
+		for div in divs[::-1]:
+			if div < e.sec:
+				e.sec = div
+				break

これらを呼び出すと、現在位置 e.sec から、 リストdivsに登録された、前方や後方にある時刻に移動します。

リストdivsで分割された領域のデータをファイルに落す機能は、 内部関数 save(i, name) を呼び出します。

+	def save(i, name):
+		if 0 <= i and i < len(divs)-1:
+			data.save( divs[i], divs[i+1], name )

処理の本体はdata_new()関数側に追加してます。

+	def save(sec_f, sec_t, name):
+		dbg.out('save ... ', '')
+		dat = get_dat_from_to(sec_f, sec_t)
+		with open( '{}.raw'.format(name), 'wb' ) as f:
+			f.write(dat)
+		cmd = 'sox {} {}.raw {}.wav'.format( opt.get_raw_opt(), name, name )
+		cmd_ut.call(cmd)
+		cmd = 'lame {}.wav {}.mp3 > /dev/null 2>&1'.format( name, name )
+		cmd_ut.call(cmd)
+		cmd = 'rm -f {}.raw {}.wav'.format( name, name )
+		cmd_ut.call(cmd)
+		dbg.out('OK')

実行のお試し

$ ./snddiv.py
Usage: ./snddiv.py [-S sudo_passwd] filename_mp3

$ ./snddiv.py -S xxxxx ptmsk-14.mp3
make ptmsk-14.wav ... OK
make ptmsk-14.raw ... OK
make smp_lst ... OK
last_sec=179.34
snd>

snd> snd.start()

# 再生

snd> snd.stop()

# 停止

snd> snd.div_join()

# 分割

snd> print(snd.divs)
[0, 12.26, 179.34]

# divsの内容を確認

snd> snd.start()

# 停止したところから続きの再生

snd> snd.stop()

# 停止

snd> snd.div_join()

# 分割

snd> print(snd.divs)
[0, 12.26, 18.39, 179.34]

# divsの内容を確認

snd> snd.save(1, 'hoge')
save ... OK

# 12.26秒から18.39秒までのデータをファイル hoge.mp3 として保存

snd> snd.start()

# 停止したところから続きの再生

snd> snd.stop()

# 停止

snd> print(snd.sec)
24.52

# 現在位置の時刻(秒)を表示

snd> snd.rw()

# 後方の分割位置まで「巻戻し」

snd> print(snd.sec)
18.39

# 現在位置の時刻(秒)を表示

snd> snd.rw()
snd> print(snd.sec)
12.16

# 後方の分割位置まで「巻戻し」
# 現在位置の時刻(秒)を表示

snd> snd.ff()
snd> print(snd.sec)
18.39

# 前方の分割位置まで「頭出し」
# 現在位置の時刻(秒)を表示

snd> snd.start()

# 18.39秒の位置から再生

snd> snd.stop()

# 停止

snd> snd.rw()
snd> print(snd.sec)
18.39

# 後方の分割位置まで「巻戻し」
# 現在位置の時刻(秒)を表示

snd> snd.div_join()

# 結合 (既に分割された位置に居るので)

snd> print(snd.divs)
[0, 12.16, 179.34]

# divsの内容を確認

snd >^D
$

$ ls -lt *.mp3
-rw-rw-r-- 1 kondoh root  100727 Jan 20 21:19 hoge.mp3
-rw-r--r-- 1 kondoh root 2870542 Dec 19 16:21 ptmsk-14.mp3

# 切り出した hoge.mp3 が生成されている


$ ./snddiv.py -S xxxxx hoge.mp3
make hoge.wav ... OK
make hoge.raw ... OK
make smp_lst ... OK
last_sec=6.23
snd>

snd> snd.start()

# 切り出したファイルが再生できるか確認

snd> snd.stop()

# 停止

snd> ^D
$

コマンドライン入力のユーザ・インターフェースからの脱却を計らねば...


GUIのお試し

wxPythonでGUIを試してみます。

wxPythonは以前に 簡易な3Dプロット でも少しだけ使ってたはずで、その wxPythonインストール の箇所で、とりあえずMacに入れる記述がありました。

gui.py

とりあえず、音楽再生の部分だけを作ってみました。

何だか、親のカタキかと言うくらい、できるだけclassというキーワードを使わずに、 何とかしてるようなソースコードになってしまいました。

奇をてらい過ぎかも。

単体で実行すると、こんな感じでテキストを出力します。

そう。この出力をsnddiv.pyに与えれば、 コマンドラインのインターフェースでキー入力してるのと等価な訳です。

再生中に、reverseのチェックボックスをONにしたり、 再生速度のポップアップ・メニューを変更したりすると、 一旦、snd.stop()してから、変数の値を変更して、さらにsnd.start()しなおすという、 ややこしい事もしてます。

では、試してみます。

GIFの動画なので音は入ってません。;-p)

端末にプロンプト'snd> 'が出てますね。

$ ./gui.py | ./snddiv.py ptmsk-14.mp3 > /dev/null

などとリダイレクトした方が良いかもしれません。


再生位置をGUIに表示

現在の再生位置を示す先頭から秒数を、GUIに表示してみます。

パッチ

v7.patch

$ cat v7.patch | patch -p1

結構盛りだくさんになってます。

GUIとの通信経路

./gui.py | ./snddiv.py

のパイプ接続では、gui.py側のアクションからsnddiv.py側へ指令する方向だけです。

例えば、今の再生位置の情報をGUI側に表示させるには、 snddiv.py側からgui.py側へと伝える経路が必要です。

stdinとstdoutの双方の接続は、以前 パックマンもどき2017秋 の、第6弾の箇所で作ってました。 (このときは章のアンカを作って無かった...)


以前の記述

stdio.py と fuga.py を追加しました。

stdio.py は2つの引数を取り、2つのコマンドとして起動します。 その2つのコマンドの標準入出力を、互い違いに繋ぎます。

:

$ ./stdio.py "./pac.py -o -r" ./fuga.py

として、pac.py と fuga.py の標準入出力を相互に繋いだ状態で起動します。


パックマンもどき2017秋 の v6.patch をみると

--- v5/stdio.py	1970-01-01 09:00:00.000000000 +0900
+++ v6/stdio.py	2018-02-21 12:28:53.000000000 +0900
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+
+import sys
+import subprocess
+
+#
+# ./stdio.py "./hoge.py 123" ./hoge.py
+#
+
+if __name__ == "__main__":
+
+	if len(sys.argv) < 3:
+		print 'Usage: {} "cmd1 prm .." "cmd2 prm .."'.format(sys.argv[0])
+		sys.exit(0)
+
+	cmd1 = sys.argv[1]
+	cmd2 = sys.argv[2]
+	cmd = 'rm -f fifo ; mkfifo fifo ; cat fifo | {} | {} > fifo'.format(cmd1, cmd2)
+	#print cmd
+
+	subprocess.call(cmd, shell=True)
+
+# EOF

さらに遡ると

CUI14ネットワーク対応 の中で


以前の記述

-e オプションや -c オプションが使えない ncコマンドの場合は、man nc を見てみると例が載ってました。
それに習って次のように試してみるとOKでした

host_a $ rm -f /tmp/f ; mkfifo /tmp/f
host_a $ cat /tmp/f | ./cui_test | nc -l -p 9002 > /tmp/f

host_b $ stty raw -echo ; nc host_a 9002 ; stty -raw echo


待ち受けと繋ぎにいく関係が逆の場合は

host_b $ stty raw -echo ; nc -l -p 9003 ; stty -raw echo

host_a $ cat /tmp/f | ./cui_test | nc host_b 9003 > /tmp/f


プロセス置換を使うべきかなど、もっとスマートな方法を模索してみましたが、、、

解り易さからして結局これ。fifoです。

v7.patch に含まれる。stdio.sh を追加です。

--- v6/stdio.sh	1970-01-01 09:00:00.000000000 +0900
+++ v7/stdio.sh	2020-01-25 00:55:00.000000000 +0900
@@ -0,0 +1,10 @@
+#!/bin/bash
+rm -f fifo
+mkfifo fifo
+
+$1 < fifo | $2 | tee fifo
+#$1 < fifo | $2 > fifo
+
+rm -f fifo
+# EOF

snddiv.py から fifo にリダイレクトしてしまうと、 本当にダンマリになってしまうので、デバッグがキツイです。

tee で端末にも表示を出すようにしてみました。

kon_utにio_ut.py追加

snddiv.pyのreadable()でselect()を使ってました。

これ単体でよく使うので、 pythonのユーティリティ・プログラム 2020冬 に入れてしまおうかと。

io_ut.py として追加してみました。

となると、リード部分も色々欲しくなり、じゃんじゃん入れてしまいました。

なので、今回のバージョンからio_ut.pyも必要です。

$ wget http://kondoh.html.xdomain.jp/kon_ut/io_ut.py

で落としときます。

pythonのstdinのバッファリング

ここでバッファリングされてると、プロセス間のやりとりが滞ります。

ネットに色々 fdopen() を使う情報がありました。

が、python3 で text モードなら行単位までしか無理っぽい様子?

まぁ、元々 pythonのユーティリティ・プログラム 2020冬io_ut.py のリーダー( reader_new() ) では、 to_str=True 指定で、bytes のバイナリなら .decode() で str にしてたりします。

なので、fdopen() でのモード指定を 'rb' にして、 buffering=0 で指定してみると、それなりに動いてしまいました。

はたして、この方法で良いのか、よくわかりませんが...

snd> のプロンプト表示

GUIと対話させるに当たり、やはり「邪魔」です。

せっかくstdinのバッファリングをなしにできたので、 この改行なしの 'snd> ' 表示もスキップさせて頑張ってみたのですが...

再生中に、reverse を切り替えたり、速度を切り替えると、 原理的に 'snd> ' が2回でます。

この面倒さが決め手でした。

プロンプトを読み飛ばす方針から「撤退」を決意しました。

すなおにsnddiv.py側にオプション指定を追加して、 「プロンプト自体を出さないようにする方針」に切り替えました。

snddiv.py に -silentオプションを付けると、'snd> 'の表示を出しません。

でも、初期化が結構長いので、起動してからコマンドを受け付ける直前まで処理が進むと、 1度だけ'ready'を表示するようにしてみました。

GUI側の再生位置表示の処理

試してみると、けっこう複雑なコードになってしまいました。

これは後になって見てみると「何でこうしてるの?」ってなるパターンです。

今のうちに説明を書いておきます。

$ cat v7.patch
  :
+
+	pos = wx.StaticText( frame, wx.ID_ANY,'-' )
+	def pos_update():
+		cmd = 'dbg.out("{}".format( snd.get_sec() ) )'
+		s = comm(cmd, rdr.readline).strip()
+		s = str( Decimal(s).quantize( Decimal('.01') ) )
+		wx.CallAfter( pos.SetLabel, s )
+
+	pos_ev = threading.Event()
+	def th_func():
+		tmout = 0.2 if is_play() else None
+		pos_ev.wait(tmout)
+		pos_ev.clear()
+		pos_update()
+
+	th = thr.loop_new(th_func)
+	def th_stop():
+		th.quit_ev.set()
+		pos_ev.set()
+		th.stop()
+	th.start()
  :

pos

posはGUIのラベルの部品のオブジェクトです。

pos_update()

pos_update()を呼び出すと、posに位置を示す秒数の文字列を表示します。

表示する秒数をsnddiv.pyから取得するために、 cmdの文字列を標準出力に表示する事でsnddiv.pyへと送信します。

snddiv.pyの中では、受け取ったcmdの文字列はexec(cmd)で実行されます。

snd.get_sec()で取得した秒数を "{}".format(v)で数値の文字列にして、 「snddiv.py側のdbg.out()関数」で「snddiv.pyの標準出力」に表示させます。

その秒数の文字列はfifoを経由して、gui.pyの標準入力に返ってくきます。 io_utのreader_new()で生成したリーダの、readline()メソッドで読み込み、変数sに保持。

snddiv.pyから送るときにpythonの「素のfloat」を"{}".format(v)で文字列にしてるので、 精度の都合で少数点以下の桁数が大きくなる場合があります。

GUIのラベルに表示するだけなので、Decimalで適当に丸めてます。

wx.CallAfter()

そして、ラベル部品のposに文字列をセットすれば良いのですが...

pos_update()はwxPythonのイベントハンドラなどのメインスレッド以外からも、 呼び出したいなと。

wxPythonには制約があります。 部品オブジェクトに値を設定するメソッドは、 wxPythonのメインスレッドからしか呼び出さねばなりません。

適当に生成した他のスレッドから呼び出すと、 しれっと異常終了したりします。

なので、定番の方法。 wx.CallAfter()を使って、メインスレッドの実行キューに登録して実行させます。

th_func()

で、その適当に作る他のスレッドの実行関数が、th_func()です。 kou_utのthr.loop_new()で、生成したスレッドからth_func()を繰り返し実行します。

th_func()では、事前に生成してある位置更新用のイベントpos_evを待ちます。 再生中はタイムアウト0.2秒なので、何もしなくても0.2秒間隔で表示を更新します。

再生停止中は、タイムアウトなしでイベントを待つので、 どこかで誰かがpos_ev.set()を実行すれば、表示を更新します。

th.start()

最後にth.start()でスレッドの実行を開始。

th_stop()

プログラム終了時に、更新用のスレッドを終了させるための処理関数がth_stop()です。

th.stop()の呼び出しだけではダメ。

再生停止の状態だと、スレッドはth_func()の中でイベントを待ってます。 pos_ev.set()で起こす必要があります。

ですが、その前にスレッド自体の終了をかけておかねば、 また繰り返しth_func()を呼び出してしまい、またイベントを待って寝てしまいます。

ですが、ですが、先にth.stop()を呼び出ししまうと、これまた問題あり。 th.stop()では、スレッド終了用のイベントquit_evをセットして、 th.join()で、スレッドの終了を待ち合わせます。

なので、今度はこのjoinで待ってしまい、肝心のpos_evのセットができません。

そこで、最初にスレッドの終了用のイベントをセットしてから、 pos_evをセットして寝てるスレッドを起こします。 これで、スレッドはloop_new()の中のwhileループから脱出します。

最後にth.stop()で冗長ですが終了イベントのセットをしてから、 th.join()でめでたく終了。

ああ、大変。

実行のお試し

$ ./stdio.sh ./gui.py "./snddiv.py -silent ptmsk-14.mp3"
make ptmsk-14.wav ... OK
make ptmsk-14.raw ... OK
make smp_lst ... OK
ready

コマンド入力が長い...
そしてreadyが表示されるまでも長い...

「Play |>」ボタンで再生を開始すると

  :
ready
0
0
0.09
0.28
0.46
0.65
0.84
1.02
1.21
  :

な感じで再生位置が更新されます。

クローズボックスをクリックして終了すると

quit
2.88
$

最後に再生位置更新の処理をしてしまうのは、ご愛嬌という事で。

GIF

動画

音つきの動画ファイルはサイトの制限の3Mバイトを超えてるので、切れてます。


GUIで分割

肝心の分割をGUIから出来るようにしてみます。

ただし、ファイルへの保存は未だ出来てません。

パッチ

v8.patch

$ cat v8.patch | patch -p1

起動方法

あまりに呪文が多いので、スクリプトを追加しました。

$ cat snddiv.sh
#!/bin/bash

./stdio.sh ./gui.py "./snddiv.py -silent $1"

# EOF

MP3ファイル名の引数指定を忘れた時に、ヘルプ表示が出た場合、 gui.py側がひたすら'ready'を待ってしまい、終了しませんでした。

パッチで修正してます。

-	while comm('', rdr.readline) != 'ready\n':
-		pass
+	while True:
+		s = comm('', rdr.readline)
+		if s == 'ready\n':
+			break
+		if not s:
+			sys.exit(1)

現在位置の領域

分割で切り刻むと、領域の数が増えていきます。

例えば領域に名前をつけて保存する場合。

現在の再生位置が領域の中ほどなら、 その現在位置の属する領域を対象にして問題なし。

現在位置が2つの領域のつなぎ目の分割位置にあるときが問題です。

分割した直後にその切り出した領域を、 別ファイルに保存する事を想定したとすると、、、

保存する対象の領域は、分割位置の巻き戻し方向にある領域にした方が便利そうです。

ただし、現在位置がデータの先頭のときだけは、巻き戻し方向に領域が無いので、 例外として頭出し方向の領域を対象とします。

GUIのdivボタンの右の表示は、その方針での現在位置の領域の情報になります。

領域番号 / 領域数

領域番号は1から領域数までの値をとります。

実行のお試し

今回から、追加したsnddiv.shを使います。

$ ./snddiv.sh
Usage: ./snddiv.py [-S sudo_passwd] [-silent] filename_mp3

ああ、そうか。snddiv.pyのヘルプ!

snddiv.shで手を抜き過ぎですね。また次回修正ということで。

$ ./snddiv.sh ptmsk-14.mp3
make ptmsk-14.wav ... OK
make ptmsk-14.raw ... OK
make smp_lst ... OK
ready


GUIで分割した領域の保存

「save」ボタンを追加して、現在位置の領域をファイルに保存できるように仕上げます。

パッチ

v9.patch

$ cat v9.patch | patch -p1

gui.pyの変更内容

「save」ボタンの追加が本質ですが、 色々と整理したので、変更量が多いです。

「save」ボタンのクリックで、 保存ファイルを指定するダイアログが表示されます。

ファイル名の拡張子には .raw .wav .mp3 のいづれかを指定します。

拡張子が無ければ、デフォルトとして .mp3 が追加されます。

snddiv.py側に保存の指令を送った後は、 完了するとsnddiv.pyから文字列'OK'が返る仕様にしました。

保存処理中は、GUIのフレーム全体を操作不可の状態にして、 'OK'が返るのを待ちます。

+	def save():
  :
+		cmd = 'snd.save({}, "{}")'.format(i, name)
+
+		def do_save():
+			s = comm(cmd, rdr.readline) # skip 'OK'
+			frame.Enable(True)
+
+		frame.Enable(False)
+		wx.CallAfter(do_save)

ここで、CallAfter()にしてるのは、部品の表示更新のための都合です。

「save」ボタンのハンドラからsave()を実行してるので、 この中で部品をEnable(False)で非活性に設定しても、 すぐには反映されません。

一度ハンドラ関数を終了して、 メインスレッドをwxPythonのシステム側に戻してやる必要があります。

なので、do_save()のクロージャをCallAfterでキューに登録して、 システム側で非活性表示に更新されてから、 do_save()を呼び出しなおしてもらいます。

do_save()の中で、保存処理が終って'OK'が返るまでは、 比較的長い時間がかかります。

が、フレーム全体が操作不可状態なので、 メインスレッドを引っ張っていてもまぁ大丈夫でしょう ;-p)

snddiv.pyの変更内容

主に表示関係の修正です。

他、data_new()のsave()で、引数nameの拡張子を見て、 できるだけname要求されてる種類のファイルだけを残すようにしてます。

stdio.shのスクリプト

そろそろデバッグ用の表示を止めておきます。

stdio.sh の処理を

$1 < fifo | $2 | tee fifo

から

$1 < fifo | $2 > fifo

に変えてます。

実行のお試し

$ ./snddiv.sh
Usage: ./snddiv.sh [-S sudo_pass] filename_mp3

playコマンドの実行にsudoが必要な環境ならば

$ ./snddiv.sh -S xxxx ptmsk-14.mp3

不要であれば

$ ./snddiv.sh ptmsk-14.mp3

こんな感じで操作して、冒頭の「キカコ」と「さび」の箇所を切り出してみました。

kikako.mp3

sabi.mp3

Ubunut16.04な環境でのお試し


kon_ut対応

2つのコマンドの標準入出力を相互に接続する処理を、 pythonのユーティリティ・プログラム 2020冬 のcmd_ut.pyに追加しました。

なので、snddivからもそれを利用するように変更してみました。

あわせて、 kon_pageのpythonモジュールのインストール に snddiv を追加しました。

パッチ

v10.patch

$ cat v10.patch | patch -p1

は廃止。

gui.pyはsnddiv_gui.pyに名前を変更しました。

snddiv.py の -g オプションで、 snddiv.py と snddiv_gui.py の標準入出力をつないだ状態で、 起動しなおします。

ダウンロードとインストール

kon_pageのpythonモジュールのインストール にsnddivを含めてしまいました。

$ cd ${SOME_WHERE}
$ wget kondoh.html.xdomain.jp/inst.sh
$ chmod +x inst.sh
$ ./inst.sh

実行のお試し

-g で GUI版です。

$ cd /tmp
$ wget http://kondoh.html.xdomain.jp/midi_mp3/ptmsk-14.mp3

$ pthon -m snddiv
Usage: /Users/kondoh/kon_page/snddiv/snddiv.py [-g] [-S sudo_passwd] [-silent] filename_mp3

$ python -m snddiv -g ptmsk-14.mp3

やっぱりGIFの動画なので音は入ってません。;-p)


wx_ut対応

kon_utに wxPythonのユーティリティ・プログラム 2020春 を追加したので、GUI部分でそれを使用するように書き直してみました。

v11.patch

$ cat v11.patch | patch -p1

まだ細かく動作確認してませんが...

kon_pageのpythonモジュールのインストール からダウンロード、インストールすれば最新の状態になるはずです。


snd_ut対応

前回の wx_ut対応 案の定、バグがありました。

snddiv_gui.py の変数frameの修正が抜けてました。

今回のsnd_ut対応に含めて、しれっと直しておきます。

今回のsnd_ut対応ですが、適当に kon_utsnd_ut.py に「のれん分け」したものを、 逆輸入する事になります。

ですが、kon_utへの輸出が適当過ぎたようで、簡単に逆輸入できませんでした。

kon_utへの輸出を増やしつつ、逆輸入して書き換えてみました。

v12.patch


大きなサイズのファイルの対応

最後の更新から1年半ぶりです。

番組を録音した大きなファイルから、曲を切り出そうとしてみて愕然。

全部メモリに読み込む作りになっているので、、、これでは使えない orz

対策してみます。

方針

従来の動作も残しつつ、なるべく互換線を保ちながら、 大きなファイルでも動作するように対応する事を目指します。

起動時のコマンドライン引数で、 「最初に一気にメモリにロードしない」動作も可能にします。

データを使う箇所は

だけなので、最初に一気にロードせずとも、何とかなりそうです。

前者は、ちょびちょびロードしては、デバイスに書き込みます。

後者は、必要な範囲だけを一気にロードして、ファイルに書き込みめば 良さそうです。

まず再生のためにデータをくべてる箇所から

もう久しぶりすぎて、全部忘れてます。

復習して見ていきます。

v12 の snddiv.py

デバイスへの書き込みは

def play_new(dat, inf, sudo_passwd='', r_rate=1.0):
	:
	def wfunc(f):
		try:
			f.write(dat)
		except:
			pass
		f.close()
	:
	wth = thr.th_new( wfunc, ( proc.get_stdin(), ) )

	def start():
		e.run = True
		rth.start()
		wth.start()

datのバイナリデータを、スレッドで一気に書き込み。

引数で渡されるdatが、今から再生したい位置以降のデータ全部。なるほど。

このスレッドでループを回して、ちょびちょび書き込むように変更 してみます。

def play_new( load_div, inf, sudo_passwd='', r_rate=1.0 ):
	:
	def wfunc(f):
		while True:
			b = load_div.get_dat()
			if not b:
				break
			try:
				f.write( b )
			except:
				break
		f.close()
	:

引数datのバイナリデータを、仮にload_divというオブジェクトに変更します。

load_divのget_dat()メソッドで、データをちょびちょびロードして、 小さいバイナリデータを返す事にします。

get_dat()がNoneを返せば、末尾まできて終了です。

従来の動作をさせるときは、初回のget_dat()呼び出しで、 従来通りに以降分の全データを返して、 2回目のget_dat()呼び出しでNoneを返すようにします。

では、load_div オブジェクトをどう作るべしか?

元々バイナリデータ dat は snd_new() の中で取得していて

def start():
	if e.play:
		stop()

	dat = get_dat_sec( e.sec, e.reverse, max(e.shift, 0) )

	e.play = play_new( dat, inf, sudo_passwd, min(2**e.shift, 1.0) )
	e.play.start()

でした。

ならば、ここで load_div を生成します。

def start():
	if e.play:
		stop()

	load_div = load_div_new( e.sec, e.reverse, max( e.shift, 0 ) )

	e.play = play_new( load_div, inf, sudo_passwd, min(2**e.shift, 1.0) )
	e.play.start()

互換性を保つだけの load_div_new() を仮に作っておくと

def load_div_new( sec, reverse, shift ):
	e = empty.new()
	e.fin = False

	def get_dat():
		if e.fin:
			return None

		e.fin = True
		return get_dat_sec( e.sec, reverse, shift )

	return empty.add( e, locals() )

こんな感じでしょうか。

get_dat_sec()

従来の get_dat_sec( sec, reverse, shift=0 ) の動作を見てみると

def get_dat_sec(sec, reverse, shift=0):
	lst = get_smp_sec(sec, reverse)
	if shift > 0:
		lst = lst[:: 2**shift ]
	return b''.join(lst)

reverseでなければ、引数 sec の位置から最後までのバイナリデータを 返しています。

reverseならば、引数 sec の位置から先頭までのバイナリデータを、 1サンプリングデータの単位で逆順に並び変えて返しています。

shiftが正の値ならば、 1サンプリングデータの単位で、 「1 / 2のshift乗」に間引いてから、 バイナリデータを返しています。

何とややこしい。

従来通りの動作をさせるなら、これを1回呼び出して返せばそれで良いのですが、 ちょびちょび分割してロードするならば

「1サンプリング単位のデータに直す」

「元のフラットなバイナリ形式に戻す」

この処理を用意しておきます。

前者は

def snd_new(data, sudo_passwd):
	inf = data.inf
	last_sec = inf.smp_to_sec( inf.smp_n )
	dbg.out( 'last_sec={}'.format( last_sec ) )

	b = data.get_bytes()
	n = inf.byte_per_smp
	smp_lst = list( map( lambda i: b[i*n:i*n+n], range( inf.smp_n ) ) )
	smp_rlst = smp_lst[::-1]
		:

data.get_bytes() で全部ロードして smp_lst のところで、全部1サンプル単位のデータに直してました。

後者は

def get_dat_sec(sec, reverse, shift=0):
	lst = get_smp_sec(sec, reverse)
	if shift > 0:
		lst = lst[:: 2**shift ]
	return b''.join(lst)

return行のところで1サンプル単位のデータであるlstを元のフラットなバイナリ形式にして返してます。

これをsnd_new()のメソッドとして

def snd_new( data, sudo_passwd, cache=True ):
		:
	def bytes_to_smp( b ):
		smp_n = len( b ) // n
		return list( map( lambda i: b[ i * n : i * n + n ], range( smp_n ) ) )

	def smp_to_bytes( lst ):
		return b''.join( lst )

として追加しておきます。

load_div_new() のちょびちょびロード処理を考える前に、 従来の動作指定について用意しておきます。

コマンドライン引数指定

「互換性を保つ」と言っておきながら、なんですが、、、、

ちょびちょびロード動作の方をデフォルトにしたいな〜、と。

従来の動作は「最初に全部ロード」なので、 ファイルの内容がキャッシュされていると考えて

-cache

というオプションを指定すると、従来の一気読み動作する事にします。

snddiv.py メイン処理から

if __name__ == "__main__":
	:
	silent = a.is_pop('-silent')
	cache = a.is_pop( '-cache' )
	filename_mp3 = a.pop()
	:

	snd = snd_new( data, sudo_passwd, cache )
	:

として、snd_new() に boolの 引数 cache 追加です。

では snd_new() 側。

まずは、最初の一気読み処理を抑制せねば、始まりません。

def snd_new( data, sudo_passwd, cache ):
	:
	n = inf.byte_per_smp
	b = None
	smp_lst = None
	smp_rlst = None
	:
	if cache:
		b = data.get_bytes()
		smp_lst = bytes_to_smp( b )
		smp_rlst = smp_lst[::-1]
	:

b, smp_lst, rmp_rlst は、cache指定のときだけ最初に生成。

そして、データを取得するメソッドは大きく4つあります。

def get_smp_sec(sec, reverse):
	m = inf.smp_sec(sec)
	return smp_rlst[-m:] if reverse else smp_lst[m:]

def get_dat_sec(sec, reverse, shift=0):
	lst = get_smp_sec(sec, reverse)
	if shift > 0:
		lst = lst[:: 2**shift ]
	return b''.join(lst)

def get_smp_from_to(sec_f, sec_t):
	f = inf.smp_sec(sec_f)
	t = inf.smp_sec(sec_t)
	return smp_lst[f:t]

def get_dat_from_to(sec_f, sec_t):
	lst = get_smp_from_to(sec_f, sec_t)
	return b''.join(lst)

2つ目の get_dat_sec() は先述の通り。

先頭の get_smp_sec() は、その下請け。

3つ目と4つ目は、秒範囲指定で、 1サンプリング単位の形式か、フラットなバイナリ形式か。

この4つ目のメソッドが、ちょびちょびロード処理の基本かなと。

cache False ならば、指定範囲をロードして返すようにして、 上流側でうまく指定範囲を回していけば、使えそうです。

ファイルからのロード箇所

それでは、実際にファイルからロードしている箇所。

def snd_new(data, sudo_passwd):
	inf = data.inf
	last_sec = inf.smp_to_sec( inf.smp_n )
	dbg.out( 'last_sec={}'.format( last_sec ) )

	b = data.get_bytes()
	:

ここで一気にロード。

引数のdata生成箇所では

data = snd_ut.data_new(filename_mp3)

snd_ut.py !

snd_ut対応 で kon_ut に「のれん分け」してます。

snd_ut.py

def data_new(name_mp3):
	:
	def get_bytes(start_sec=0, end_sec=-1):
		if e.raw_bytes == None:
			make( name, 'raw', 'load' )
			e.raw_bytes = load_bytes( name + '.raw' )

		if start_sec == 0 and end_sec < 0:
			return e.raw_bytes

		start = inf.byte_sec( start_sec )
		end = inf.byte_sec( end_sec ) if end_sec >= 0 else len( e.raw_bytes )
		return e.raw_bytes[ start : end ]

からの load_bytes()

def load_bytes(name):
	wait_msg = wait_msg_new( 'load ' + name )
	b = bytes( [] )
	with open( name, 'rb' ) as f:
		b = f.read()
	wait_msg.stop()
	return b

もうここで、一気ロードが前提。

snd_ut.py に部分ロードの関数を追加します。

def load_bytes_part( name, from_byte=None, to_byte=None ):
	if from_byte is None and to_byte is None:
		return load_bytes( name )

	if from_byte is None:
		from_byte = 0

	if to_byte is None:
		st = os.lstat( name )
		to_byte = st.st_size

	b = bytes( [] )
	with open( name, 'rb' ) as f:
		if from_byte > 0:
			f.seek( from_byte )
		b = f.read( to_byte - from_byte )
	return b

そして data_new() の get_bytes() メソッドは、 従来の処理として get_bytes_cache() に名前を変えて、 get_bytes() に引数 cache を追加して

def get_bytes_cache(start_sec=0, end_sec=-1):
	if e.raw_bytes == None:
		make( name, 'raw', 'load' )
		e.raw_bytes = load_bytes( name + '.raw' )

	if start_sec == 0 and end_sec < 0:
		return e.raw_bytes

	start = inf.byte_sec( start_sec )
	end = inf.byte_sec( end_sec ) if end_sec >= 0 else len( e.raw_bytes )
	return e.raw_bytes[ start : end ]

def get_bytes( start_sec=0, end_sec=-1, cache=True ):
	if cache:
		return get_bytes_cache( start_sec, end_sec )

	from_byte = inf.byte_sec( start_sec )
	to_byte = inf.byte_sec( end_sec ) if end_sec >= 0 else None
	return load_bytes_part( name + '.raw', from_byte, to_byte )

これで、get_bytes() を使う側では、従来の引数指定で、従来の動作。

明示的に cache=False を指定すると、その場で部分的にロードしてバイナリデータを返します。

snddiv.py に戻って、4つ目のメソッド get_dat_from_to()

def get_dat_from_to(sec_f, sec_t):
	lst = get_smp_from_to(sec_f, sec_t)
	return b''.join(lst)

これを、cache かどうかで分岐させて

def get_dat_from_to(sec_f, sec_t):
	if cache:
		lst = get_smp_from_to(sec_f, sec_t)
		return smp_to_bytes( lst )

	return data.get_bytes( sec_f, sec_t, cache=False )

このように。

load_divオブジェクト

snddiv.py

snd_new() の get_dat_rom_to() メソッドを修正して、 ちょびちょびロードの土台が整ってきました。

従来通りの動作前提の仮のコード

def load_div_new( sec, reverse, shift ):
	e = empty.new()
	e.fin = False

	def get_dat():
		if e.fin:
			return None

		e.fin = True
		return get_dat_sec( e.sec, reverse, shift )

	return empty.add( e, locals() )

を、ちょびちょびロード対応させてみます。

まずは簡単な、reverse=False, shift=0 のときだけのコードから

def load_div_new( sec, reverse, shift ):
	e = empty.new()
	e.fin = False
	e.sec = sec

	def get_dat():
		if e.fin:
			return None

		if cache:
			e.fin = True
			return get_dat_sec( e.sec, reverse, shift )

		step_sec = 5.0

		sec_f = e.sec
		sec_t = min( e.sec + step_sec, last_sec )
		b = get_dat_from_to( sec_f, sec_t )
		e.sec = sec_t
		e.fin = ( e.sec >= last_sec )

		return b

	return empty.add( e, locals() )

step_sec = 5.0 刻みで、ちょびちょびロードにしてみました。

続いて reverse=True の場合もあるときは

def load_div_new( sec, reverse, shift ):
	e = empty.new()
	e.fin = False
	e.sec = sec

	def get_dat():
		if e.fin:
			return None

		if cache:
			e.fin = True
			return get_dat_sec( e.sec, reverse, shift )

		step_sec = 5.0

		if reverse:
			sec_t = e.sec
			sec_f = max( e.sec - step_sec, 0 )
			b = get_dat_from_to( sec_f, sec_t )
			e.sec = sec_f
			e.fin = ( e.sec <= 0 )

			lst = bytes_to_smp( b )
			lst = lst[ :: -1 ]
			b = smp_to_bytes( lst )
		else:
			sec_f = e.sec
			sec_t = min( e.sec + step_sec, last_sec )
			b = get_dat_from_to( sec_f, sec_t )
			e.sec = sec_t
			e.fin = ( e.sec >= last_sec )

		return b

	return empty.add( e, locals() )

shift > 0 も考慮すると

def load_div_new( sec, reverse, shift ):
	e = empty.new()
	e.fin = False
	e.sec = sec

	def get_dat():
		if e.fin:
			return None

		if cache:
			e.fin = True
			return get_dat_sec( e.sec, reverse, shift )

		step_sec = 5.0

		if reverse:
			sec_t = e.sec
			sec_f = max( e.sec - step_sec, 0 )
			b = get_dat_from_to( sec_f, sec_t )
			e.sec = sec_f
			e.fin = ( e.sec <= 0 )

			lst = bytes_to_smp( b )
			lst = lst[ :: -1 ]
			b = smp_to_bytes( lst )
		else:
			sec_f = e.sec
			sec_t = min( e.sec + step_sec, last_sec )
			b = get_dat_from_to( sec_f, sec_t )
			e.sec = sec_t
			e.fin = ( e.sec >= last_sec )

		if shift > 0:
			lst = bytes_to_smp( b )
			lst = lst[ :: 2 ** shift ]
			b = smp_to_bytes( lst )

		return b

	return empty.add( e, locals() )

何回も形式を変換するのはアレなので

def load_div_new( sec, reverse, shift ):
	e = empty.new()
	e.fin = False
	e.sec = sec

	def get_dat():
		if e.fin:
			return None

		if cache:
			e.fin = True
			return get_dat_sec( e.sec, reverse, shift )

		step_sec = 5.0

		if reverse:
			sec_t = e.sec
			sec_f = max( e.sec - step_sec, 0 )
			b = get_dat_from_to( sec_f, sec_t )
			e.sec = sec_f
			e.fin = ( e.sec <= 0 )
		else:
			sec_f = e.sec
			sec_t = min( e.sec + step_sec, last_sec )
			b = get_dat_from_to( sec_f, sec_t )
			e.sec = sec_t
			e.fin = ( e.sec >= last_sec )

		if not reverse and shift == 0:
			return b

		lst = bytes_to_smp( b )

		if reverse:
			lst = lst[ :: -1 ]

		if shift > 0:
			lst = lst[ :: 2 ** shift ]

		return smp_to_bytes( lst )

	return empty.add( e, locals() )

その他も

基本これでなんとかなってる感じですが、 snd_new() の 例の4つのメソッドのうち、 最後のメソッドだけ cache 対応になります。

1つ目、2つ目は、cache=Falseとしても、「範囲広過ぎ感」がいなめません。

2つ目のを、追加したsmp_to_byte()を使うように書き換えるに留めるとして

def get_dat_sec(sec, reverse, shift=0):
	lst = get_smp_sec(sec, reverse)
	if shift > 0:
		lst = lst[:: 2**shift ]
	return smp_to_bytes( lst )

せめて3つ目のを何とか対応してみます。

def get_smp_from_to(sec_f, sec_t):
	if cache:
		f = inf.smp_sec(sec_f)
		t = inf.smp_sec(sec_t)
		return smp_lst[f:t]

	b = get_dat_from_to( sec_f, sec_t )
	return bytes_to_smp( b )

4つ目のは

def get_dat_from_to(sec_f, sec_t):
	if cache:
		lst = get_smp_from_to(sec_f, sec_t)
		return smp_to_bytes( lst )

	return data.get_bytes( sec_f, sec_t, cache=False )

こうなので、cache の値によって、依存の関係が逆転します。;-p)

デバッグ

play_new() でひっかかります。

def play_new( load_div, inf, sudo_passwd='', r_rate=1.0 ):
	e = empty.new()
	e.dat = dat
	:

dat は load_div に置き換えました。

dat参照箇所は....

last_sec = inf.byte_to_sec( len(dat) )

うーむ。

datの長さを再生時の秒数にしてlast_secに保持してた。

これは load_div に聞くしかないですね。

def snd_new( data, sudo_passwd, cache ):
	:
	def load_div_new( sec, reverse, shift ):
		e = empty.new()
		e.fin = False
		e.sec = sec

		all_sec = ( sec if reverse else last_sec - sec )

		def get_dat():
		:

all_sec として保持させておいて

def play_new( load_div, inf, sudo_passwd='', r_rate=1.0 ):
	e = empty.new()
	e.run = False
	e.sec = 0
	e.rbuf = ''
	e.evt = thr.event_new()

	last_sec = load_div.all_sec
	:

として all_sec を見る事にします。

wx_ut.pyも

Ubuntu 18.04 で使ってみると、どうもフレームのサイズがしっくりこなくなってますね。

ちょっと修正しました。

snd_ut.py と合わせて、kon_ut 側に反映しておきます。

kon_ut.diff
diff -ur kon_ut.old/snd_ut.py kon_ut.now/snd_ut.py
--- kon_ut.old/snd_ut.py	2020-09-23 20:58:44.000000000 +0900
+++ kon_ut.now/snd_ut.py	2022-05-02 18:46:14.487021370 +0900
@@ -49,6 +49,23 @@
 	wait_msg.stop()
 	return b

+def load_bytes_part( name, from_byte=None, to_byte=None ):
+	if from_byte is None and to_byte is None:
+		return load_bytes( name )
+
+	if from_byte is None:
+		from_byte = 0
+
+	if to_byte is None:
+		st = os.lstat( name )
+		to_byte = st.st_size
+
+	b = bytes( [] )
+	with open( name, 'rb' ) as f:
+		if from_byte > 0:
+			f.seek( from_byte )
+		b = f.read( to_byte - from_byte )
+	return b

 def save_bytes(name, b):
 	wait_msg = wait_msg_new( 'save ' + name )
@@ -168,7 +185,7 @@
 	e = empty.new()
 	e.raw_bytes = None

-	def get_bytes(start_sec=0, end_sec=-1):
+	def get_bytes_cache(start_sec=0, end_sec=-1):
 		if e.raw_bytes == None:
 			make( name, 'raw', 'load' )
 			e.raw_bytes = load_bytes( name + '.raw' )
@@ -180,6 +197,14 @@
 		end = inf.byte_sec( end_sec ) if end_sec >= 0 else len( e.raw_bytes )
 		return e.raw_bytes[ start : end ]

+	def get_bytes( start_sec=0, end_sec=-1, cache=True ):
+		if cache:
+			return get_bytes_cache( start_sec, end_sec )
+
+		from_byte = inf.byte_sec( start_sec )
+		to_byte = inf.byte_sec( end_sec ) if end_sec >= 0 else None
+		return load_bytes_part( name + '.raw', from_byte, to_byte )
+
 	def get_arr(start_sec=0, end_sec=-1):
 		b = get_bytes( start_sec, end_sec )
 		return bytes_to_arr( b )
diff -ur kon_ut.old/wx_ut.py kon_ut.now/wx_ut.py
--- kon_ut.old/wx_ut.py	2020-10-01 20:03:50.000000000 +0900
+++ kon_ut.now/wx_ut.py	2022-05-02 18:46:17.824626208 +0900
@@ -1,5 +1,6 @@
 #!/usr/bin/env python

+import os
 import wx

 import empty
@@ -333,19 +334,36 @@
 			e.app = self
 			e.frame = wx_new( wx.Frame, title, parent=None )
 			bind( e.frame, quit_hdl, wx.EVT_CLOSE )
+			set_icon()

 			init( e )

 			e.frame.Layout()
 			e.frame.Fit()
+			e.frame.FitInside()
 			e.app.SetTopWindow( e.frame )

+			( w, h ) = e.frame.GetSize()
+			e.frame.SetMinSize( ( w, h + 10 ) )  # title bar height ?
+
 			if e.on_init:
 				e.on_init( e )
 			else:
 				e.frame.Show()
 			return 1

+	def set_icon( path='hoge.png' ):
+		if not e.frame:
+			return
+
+		if not os.path.exists( path ):
+			return
+
+		bm = wx.Bitmap( path )
+		icon = wx.EmptyIcon()
+		icon.CopyFromBitmap( bm )
+		e.frame.SetIcon( icon )
+
 	def poll_start(poll_func, sec, hz, gc):
 		th = None
 		if poll_func:

snddivのパッチ

v13.patch
Only in v13: snd_ut.py
diff -ur v12/snddiv.py v13/snddiv.py
--- v12/snddiv.py	2020-09-23 20:59:26.000000000 +0900
+++ v13/snddiv.py	2022-05-02 18:32:46.842678228 +0900
@@ -10,15 +10,16 @@
 import dbg
 import arg

-def play_new(dat, inf, sudo_passwd='', r_rate=1.0):
+#nc_out = lambda s: cmd_ut.call( 'echo {} | nc -N localhost 7075'.format( s ) )
+
+def play_new( load_div, inf, sudo_passwd='', r_rate=1.0 ):
 	e = empty.new()
-	e.dat = dat
 	e.run = False
 	e.sec = 0
 	e.rbuf = ''
 	e.evt = thr.event_new()

-	last_sec = inf.byte_to_sec( len(dat) )
+	last_sec = load_div.all_sec

 	def proc_new():
 		cmd = 'play {} -'.format( inf.get_raw_opt(r_rate) )
@@ -50,10 +51,14 @@
 			update(s)

 	def wfunc(f):
-		try:
-			f.write(dat)
-		except:
-			pass
+		while True:
+			b = load_div.get_dat()
+			if not b:
+				break
+			try:
+				f.write( b )
+			except:
+				break
 		f.close()

 	proc = proc_new()
@@ -76,15 +81,27 @@

 	return empty.to_attr( e, locals() )

-def snd_new(data, sudo_passwd):
+def snd_new( data, sudo_passwd, cache ):
 	inf = data.inf
 	last_sec = inf.smp_to_sec( inf.smp_n )
 	dbg.out( 'last_sec={}'.format( last_sec ) )

-	b = data.get_bytes()
 	n = inf.byte_per_smp
-	smp_lst = list( map( lambda i: b[i*n:i*n+n], range( inf.smp_n ) ) )
-	smp_rlst = smp_lst[::-1]
+	b = None
+	smp_lst = None
+	smp_rlst = None
+
+	def bytes_to_smp( b ):
+		smp_n = len( b ) // n
+		return list( map( lambda i: b[ i * n : i * n + n ], range( smp_n ) ) )
+
+	def smp_to_bytes( lst ):
+		return b''.join( lst )
+
+	if cache:
+		b = data.get_bytes()
+		smp_lst = bytes_to_smp( b )
+		smp_rlst = smp_lst[::-1]

 	e = empty.new()
 	e.sec = 0
@@ -102,16 +119,23 @@
 		lst = get_smp_sec(sec, reverse)
 		if shift > 0:
 			lst = lst[:: 2**shift ]
-		return b''.join(lst)
+		return smp_to_bytes( lst )

 	def get_smp_from_to(sec_f, sec_t):
-		f = inf.smp_sec(sec_f)
-		t = inf.smp_sec(sec_t)
-		return smp_lst[f:t]
+		if cache:
+			f = inf.smp_sec(sec_f)
+			t = inf.smp_sec(sec_t)
+			return smp_lst[f:t]
+
+		b = get_dat_from_to( sec_f, sec_t )
+		return bytes_to_smp( b )

 	def get_dat_from_to(sec_f, sec_t):
-		lst = get_smp_from_to(sec_f, sec_t)
-		return b''.join(lst)
+		if cache:
+			lst = get_smp_from_to(sec_f, sec_t)
+			return smp_to_bytes( lst )
+
+		return data.get_bytes( sec_f, sec_t, cache=False )

 	def get_sec():
 		add = 0
@@ -121,13 +145,59 @@
 				add = -add
 		return e.sec + add

+	def load_div_new( sec, reverse, shift ):
+		e = empty.new()
+		e.fin = False
+		e.sec = sec
+
+		all_sec = ( sec if reverse else last_sec - sec )
+
+		def get_dat():
+			if e.fin:
+			#if e.sec <= fin_sec if reverse else e.sec >= fin_sec:
+				return None
+
+			if cache:
+				e.fin = True
+				return get_dat_sec( e.sec, reverse, shift )
+
+			step_sec = 5.0
+
+			if reverse:
+				sec_t = e.sec
+				sec_f = max( e.sec - step_sec, 0 )
+				b = get_dat_from_to( sec_f, sec_t )
+				e.sec = sec_f
+				e.fin = ( e.sec <= 0 )
+			else:
+				sec_f = e.sec
+				sec_t = min( e.sec + step_sec, fin_sec )
+				b = get_dat_from_to( sec_f, sec_t )
+				e.sec = sec_t
+				e.fin = ( e.sec >= last_sec )
+
+			if not reverse and shift == 0:
+				return b
+
+			lst = bytes_to_smp( b )
+
+			if reverse:
+				lst = lst[ :: -1 ]
+
+			if shift > 0:
+				lst = lst[ :: 2 ** shift ]
+
+			return smp_to_bytes( lst )
+
+		return empty.add( e, locals() )
+
 	def start():
 		if e.play:
 			stop()

-		dat = get_dat_sec( e.sec, e.reverse, max(e.shift, 0) )
+		load_div = load_div_new( e.sec, e.reverse, max( e.shift, 0 ) )

-		e.play = play_new( dat, inf, sudo_passwd, min(2**e.shift, 1.0) )
+		e.play = play_new( load_div, inf, sudo_passwd, min(2**e.shift, 1.0) )
 		e.play.start()

 	def stop():
@@ -203,6 +273,7 @@

 	sudo_passwd = a.pop_str('-S')
 	silent = a.is_pop('-silent')
+	cache = a.is_pop( '-cache' )
 	filename_mp3 = a.pop()
 	if not filename_mp3:
 		dbg.help_exit( '[-g] [-S sudo_passwd] [-silent] filename_mp3' )
@@ -212,7 +283,7 @@

 	data = snd_ut.data_new(filename_mp3)

-	snd = snd_new(data, sudo_passwd)
+	snd = snd_new( data, sudo_passwd, cache )

 	rdr = io_ut.reader_new( sys.stdin, 1, no_buf=True )

さらにデバッグ

v13.patch では不完全でした。

さらにデバッグ続けます。

さらに、kon_ut/snd_ut.py に大きなミス!

def get_bytes_cache(start_sec=0, end_sec=-1):
	if e.raw_bytes == None:
		make( name, 'raw', 'load' )
		e.raw_bytes = load_bytes( name + '.raw' )

	if start_sec == 0 and end_sec < 0:
		return e.raw_bytes

	start = inf.byte_sec( start_sec )
	end = inf.byte_sec( end_sec ) if end_sec >= 0 else len( e.raw_bytes )
	return e.raw_bytes[ start : end ]

def get_bytes( start_sec=0, end_sec=-1, cache=True ):
	if cache:
		return get_bytes_cache( start_sec, end_sec )

	from_byte = inf.byte_sec( start_sec )
	to_byte = inf.byte_sec( end_sec ) if end_sec >= 0 else None
	return load_bytes_part( name + '.raw', from_byte, to_byte )

cache=False のときに make( name, 'raw, 'load' ) が抜けてました。

cache=True のときは、e.raw_byes のデータ有無で .raw 生成の 目印にしてましたが、、、

cache=False のときに毎回 make( name, 'raw', 'load' ) で、 .raw ファイルの存在チェックするのもなんなので、 専用の目印を e.exists_raw として設けておきます。

さらに、実行時の環境変数の問題もありました。

内部で実行する playコマンドの標準エラー出力の表示を利用して、 現在鳴ってる音の位置を取得する方式なのですが、 環境変数 LANG=C でないと、期待した動作になりませんでした。

LANG=ja_JP.UTF-8 としている場合だと、kon_ut/io_ut.py の read1_() で、 b.decode() でエラーが出てしまいました。

$ cd /tmp
$ wget http://kondoh.html.xdomain.jp/midi_mp3/ptmsk-14.mp3

$ pthon -m snddiv
Usage: /Users/kondoh/kon_page/snddiv/snddiv.py [-g] [-S sudo_passwd] [-silent] [-cache] filename_mp3

$ LANG=C python -m snddiv -g -S xxx ptmsk-14.mp3

などとLANGをCにして実行するようにします。

v14パッチ

kon_ut/snd_ut.py の前回からの差分

kon_ut.14.diff
diff -ur kon_ut.now/snd_ut.py kon_ut.v14/snd_ut.py
--- kon_ut.now/snd_ut.py	2022-05-02 19:32:50.000000000 +0900
+++ kon_ut.v14/snd_ut.py	2022-05-03 00:56:07.078974172 +0900
@@ -184,10 +184,16 @@

 	e = empty.new()
 	e.raw_bytes = None
+	e.exists_raw = False
+
+	def make_raw():
+		if not e.exists_raw:
+			make( name, 'raw', 'load' )
+			e.exists_raw = True

 	def get_bytes_cache(start_sec=0, end_sec=-1):
 		if e.raw_bytes == None:
-			make( name, 'raw', 'load' )
+			make_raw()
 			e.raw_bytes = load_bytes( name + '.raw' )

 		if start_sec == 0 and end_sec < 0:
@@ -201,6 +207,7 @@
 		if cache:
 			return get_bytes_cache( start_sec, end_sec )

+		make_raw()
 		from_byte = inf.byte_sec( start_sec )
 		to_byte = inf.byte_sec( end_sec ) if end_sec >= 0 else None
 		return load_bytes_part( name + '.raw', from_byte, to_byte )

v14.patch
diff -ur v13/snddiv.py v14/snddiv.py
--- v13/snddiv.py	2022-05-02 18:32:46.842678228 +0900
+++ v14/snddiv.py	2022-05-03 00:56:16.855661773 +0900
@@ -154,7 +154,6 @@

 		def get_dat():
 			if e.fin:
-			#if e.sec <= fin_sec if reverse else e.sec >= fin_sec:
 				return None

 			if cache:
@@ -171,7 +170,7 @@
 				e.fin = ( e.sec <= 0 )
 			else:
 				sec_f = e.sec
-				sec_t = min( e.sec + step_sec, fin_sec )
+				sec_t = min( e.sec + step_sec, last_sec )
 				b = get_dat_from_to( sec_f, sec_t )
 				e.sec = sec_t
 				e.fin = ( e.sec >= last_sec )
@@ -276,7 +275,7 @@
 	cache = a.is_pop( '-cache' )
 	filename_mp3 = a.pop()
 	if not filename_mp3:
-		dbg.help_exit( '[-g] [-S sudo_passwd] [-silent] filename_mp3' )
+		dbg.help_exit( '[-g] [-S sudo_passwd] [-silent] [-cache] filename_mp3' )

 	snd_ut.silent = silent
 	dbg_out = lambda s, tail='\n': None if silent else dbg.out


再生速度追加など

実際にラジオを録音した.mp3ファイルから曲の部分を切り出そうと試してみると、 頭出しの早送りがx4倍でもツラいです。

x32倍まで増やしてみました。

ついでにスローもx0.25も追加しておきます。

全体の秒数なども表示しておきたいので、起動時に元のファイルのパスと全体の秒数の表示を追加してみました。

あと、x32倍速だと、データを用意してデバイスに書き込む処理が、 間に合ってないのか、一瞬音が途切れるような気がしました。

step_sec = 5.0

ここで、5秒刻みに固定しているのを可変にします。

起動時のコマンドライン引数で、-step_sec xxx で指定できるようにして、 デフォルトは元の5秒です。。

v15.patch
diff -ur v14/snddiv.py v15/snddiv.py
--- v14/snddiv.py	2022-05-03 00:56:16.000000000 +0900
+++ v15/snddiv.py	2022-05-03 15:22:15.000000000 +0900
@@ -81,7 +81,7 @@

 	return empty.to_attr( e, locals() )

-def snd_new( data, sudo_passwd, cache ):
+def snd_new( data, sudo_passwd, cache, step_sec ):
 	inf = data.inf
 	last_sec = inf.smp_to_sec( inf.smp_n )
 	dbg.out( 'last_sec={}'.format( last_sec ) )
@@ -160,8 +160,6 @@
 				e.fin = True
 				return get_dat_sec( e.sec, reverse, shift )

-			step_sec = 5.0
-
 			if reverse:
 				sec_t = e.sec
 				sec_f = max( e.sec - step_sec, 0 )
@@ -273,16 +271,17 @@
 	sudo_passwd = a.pop_str('-S')
 	silent = a.is_pop('-silent')
 	cache = a.is_pop( '-cache' )
+	step_sec = a.pop_v( '-step_sec', 5.0 )
 	filename_mp3 = a.pop()
 	if not filename_mp3:
-		dbg.help_exit( '[-g] [-S sudo_passwd] [-silent] [-cache] filename_mp3' )
+		dbg.help_exit( '[-g] [-S sudo_passwd] [-silent] [-cache] [-step_sec 5.0] filename_mp3' )

 	snd_ut.silent = silent
 	dbg_out = lambda s, tail='\n': None if silent else dbg.out

 	data = snd_ut.data_new(filename_mp3)

-	snd = snd_new( data, sudo_passwd, cache )
+	snd = snd_new( data, sudo_passwd, cache, step_sec )

 	rdr = io_ut.reader_new( sys.stdin, 1, no_buf=True )

diff -ur v14/snddiv_gui.py v15/snddiv_gui.py
--- v14/snddiv_gui.py	2020-09-23 20:59:31.000000000 +0900
+++ v15/snddiv_gui.py	2022-05-03 15:22:15.000000000 +0900
@@ -36,6 +36,9 @@
 		if not s:
 			sys.exit(1)

+	cmd = "dbg.out( '{} ({})'.format( filename_mp3, dbg.quantize( snd.last_sec, '.01' ) ) )"
+	s = comm( cmd, rdr.readline ).strip()
+	name = wxo.label_new( s )

 	play_lbs = ( ' Play |>', ' Play <|', 'Pause ||' )
 	btn_play = wxo.toggle_new( play_lbs[ 0 ], min_w=104 )
@@ -45,8 +48,8 @@
 	cbox_reverse = wxo.wx_new( wx.CheckBox, 'reverse' )
 	is_reverse = lambda : cbox_reverse.GetValue()

-	speed_lbs = ('x4', 'x2', 'x1', 'x0.5')
-	shifts = (2, 1, 0, -1)
+	speed_lbs = ( 'x32', 'x16', 'x8', 'x4', 'x2', 'x1', 'x0.5', 'x0.25' )
+	shifts = ( 5, 4, 3, 2, 1, 0, -1, -2 )
 	menu_speed = wxo.menu_new( speed_lbs, None, 'x1', shifts )
 	get_shift = lambda : wxo.menu_get( menu_speed ).v

@@ -73,7 +76,8 @@
 	def pos_update():
 		cmd = 'dbg.out("{}".format( snd.get_sec() ) )'
 		s = comm(cmd, rdr.readline).strip()
-		s = str( Decimal(s).quantize( Decimal('.01') ) )
+		#s = str( Decimal(s).quantize( Decimal('.01') ) )
+		s = str( dbg.quantize( s, '.01' ) )
 		wx.CallAfter( pos.SetLabel, s )

 	btn_save = wxo.button_new( 'save', min_w=50 )
@@ -158,6 +162,7 @@

 	wp = wxo.wp
 	lsts = [
+		wp( [ wp( name ) ] ),
 		wp( [ wp( btn_rw ), wp( btn_play, prop=1 ), wp( btn_ff ) ], prop=1, flag=wx.EXPAND ),
 		wp( [ wp( cbox_reverse ), wp( menu_speed ) ] ),
 		wp( [ wp( pos ), wp( btn_div ), wp( area ), wp( btn_save, prop=1 ) ], flag=wx.EXPAND ),