TonieToolbox 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """
2
+ TonieToolbox - Convert audio files to Tonie box compatible format
3
+ """
4
+
5
+ __version__ = '0.1.0'
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ Main entry point for the TonieToolbox package.
4
+ """
5
+
6
+ import argparse
7
+ import os
8
+ import sys
9
+ import logging
10
+
11
+ from . import __version__
12
+ from .audio_conversion import get_input_files, append_to_filename
13
+ from .tonie_file import create_tonie_file
14
+ from .tonie_analysis import check_tonie_file, split_to_opus_files
15
+ from .dependency_manager import get_ffmpeg_binary, get_opus_binary
16
+ from .logger import setup_logging, get_logger
17
+ from .filename_generator import guess_output_filename
18
+
19
+ def main():
20
+ """Entry point for the TonieToolbox application."""
21
+ parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
22
+ parser.add_argument('--version', action='version', version=f'TonieToolbox {__version__}',
23
+ help='show program version and exit')
24
+ parser.add_argument('input_filename', metavar='SOURCE', type=str,
25
+ help='input file or directory or a file list (.lst)')
26
+ parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
27
+ help='the output file name (default: ---ID---)')
28
+ parser.add_argument('--ts', dest='user_timestamp', metavar='TIMESTAMP', action='store',
29
+ help='set custom timestamp / bitstream serial')
30
+
31
+ parser.add_argument('--ffmpeg', help='specify location of ffmpeg', default=None)
32
+ parser.add_argument('--opusenc', help='specify location of opusenc', default=None)
33
+ parser.add_argument('--bitrate', type=int, help='set encoding bitrate in kbps (default: 96)', default=96)
34
+ parser.add_argument('--cbr', action='store_true', help='encode in cbr mode')
35
+ parser.add_argument('--append-tonie-tag', metavar='TAG', action='store',
36
+ help='append [TAG] to filename (must be an 8-character hex value)')
37
+ parser.add_argument('--no-tonie-header', action='store_true', help='do not write Tonie header')
38
+ parser.add_argument('--info', action='store_true', help='Check and display info about Tonie file')
39
+ parser.add_argument('--split', action='store_true', help='Split Tonie file into opus tracks')
40
+ parser.add_argument('--auto-download', action='store_true', help='Automatically download FFmpeg and opusenc if needed')
41
+ parser.add_argument('--keep-temp', action='store_true',
42
+ help='Keep temporary opus files in a temp folder for testing')
43
+ parser.add_argument('--compare', action='store', metavar='FILE2',
44
+ help='Compare input file with another .taf file for debugging')
45
+ parser.add_argument('--detailed-compare', action='store_true',
46
+ help='Show detailed OGG page differences when comparing files')
47
+
48
+ log_group = parser.add_argument_group('Logging Options')
49
+ log_level_group = log_group.add_mutually_exclusive_group()
50
+ log_level_group.add_argument('--debug', action='store_true', help='Enable debug logging')
51
+ log_level_group.add_argument('--trace', action='store_true', help='Enable trace logging (very verbose)')
52
+ log_level_group.add_argument('--quiet', action='store_true', help='Show only warnings and errors')
53
+ log_level_group.add_argument('--silent', action='store_true', help='Show only errors')
54
+
55
+ args = parser.parse_args()
56
+ if args.trace:
57
+ from .logger import TRACE
58
+ log_level = TRACE
59
+ elif args.debug:
60
+ log_level = logging.DEBUG
61
+ elif args.quiet:
62
+ log_level = logging.WARNING
63
+ elif args.silent:
64
+ log_level = logging.ERROR
65
+ else:
66
+ log_level = logging.INFO
67
+
68
+ setup_logging(log_level)
69
+ logger = get_logger('main')
70
+ logger.debug("Starting TonieToolbox with log level: %s", logging.getLevelName(log_level))
71
+
72
+ ffmpeg_binary = args.ffmpeg
73
+ if ffmpeg_binary is None:
74
+ ffmpeg_binary = get_ffmpeg_binary()
75
+ if ffmpeg_binary is None:
76
+ logger.error("Could not find FFmpeg. Please install FFmpeg or specify its location using --ffmpeg")
77
+ sys.exit(1)
78
+ logger.debug("Using FFmpeg binary: %s", ffmpeg_binary)
79
+
80
+ opus_binary = args.opusenc
81
+ if opus_binary is None:
82
+ opus_binary = get_opus_binary()
83
+ if opus_binary is None:
84
+ logger.error("Could not find opusenc. Please install opus-tools or specify its location using --opusenc")
85
+ sys.exit(1)
86
+ logger.debug("Using opusenc binary: %s", opus_binary)
87
+
88
+ if os.path.isdir(args.input_filename):
89
+ logger.debug("Input is a directory: %s", args.input_filename)
90
+ args.input_filename += "/*"
91
+ else:
92
+ logger.debug("Input is a file: %s", args.input_filename)
93
+ if args.info:
94
+ logger.info("Checking Tonie file: %s", args.input_filename)
95
+ ok = check_tonie_file(args.input_filename)
96
+ sys.exit(0 if ok else 1)
97
+ elif args.split:
98
+ logger.info("Splitting Tonie file: %s", args.input_filename)
99
+ split_to_opus_files(args.input_filename, args.output_filename)
100
+ sys.exit(0)
101
+ elif args.compare:
102
+ from .tonie_analysis import compare_taf_files
103
+ logger.info("Comparing Tonie files: %s and %s", args.input_filename, args.compare)
104
+ result = compare_taf_files(args.input_filename, args.compare, args.detailed_compare)
105
+ sys.exit(0 if result else 1)
106
+
107
+ files = get_input_files(args.input_filename)
108
+ logger.debug("Found %d files to process", len(files))
109
+
110
+ if len(files) == 0:
111
+ logger.error("No files found for pattern %s", args.input_filename)
112
+ sys.exit(1)
113
+
114
+ if args.output_filename:
115
+ out_filename = args.output_filename
116
+ else:
117
+ guessed_name = guess_output_filename(args.input_filename, files)
118
+ output_dir = './output'
119
+ if not os.path.exists(output_dir):
120
+ logger.debug("Creating default output directory: %s", output_dir)
121
+ os.makedirs(output_dir, exist_ok=True)
122
+ out_filename = os.path.join(output_dir, guessed_name)
123
+ logger.debug("Using default output location: %s", out_filename)
124
+
125
+ if args.append_tonie_tag:
126
+ # Validate that the value is an 8-character hex value
127
+ hex_tag = args.append_tonie_tag
128
+ logger.debug("Validating tag: %s", hex_tag)
129
+ if not all(c in '0123456789abcdefABCDEF' for c in hex_tag) or len(hex_tag) != 8:
130
+ logger.error("TAG must be an 8-character hexadecimal value")
131
+ sys.exit(1)
132
+ logger.debug("Appending [%s] to output filename", hex_tag)
133
+ out_filename = append_to_filename(out_filename, hex_tag)
134
+
135
+ if not out_filename.lower().endswith('.taf'):
136
+ out_filename += '.taf'
137
+
138
+ logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
139
+ create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
140
+ args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp)
141
+ logger.info("Successfully created Tonie file: %s", out_filename)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
@@ -0,0 +1,194 @@
1
+ """
2
+ Audio conversion functionality for the TonieToolbox package
3
+ """
4
+
5
+ import os
6
+ import glob
7
+ import subprocess
8
+ import tempfile
9
+ from .dependency_manager import get_ffmpeg_binary, get_opus_binary
10
+ from .logger import get_logger
11
+
12
+ logger = get_logger('audio_conversion')
13
+
14
+
15
+ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitrate=48, vbr=True, keep_temp=False):
16
+ """
17
+ Convert an audio file to Opus format and return a temporary file handle.
18
+
19
+ Args:
20
+ ffmpeg_binary: Path to the ffmpeg binary. If None, will be auto-detected or downloaded.
21
+ opus_binary: Path to the opusenc binary. If None, will be auto-detected or downloaded.
22
+ filename: Path to the input audio file
23
+ bitrate: Bitrate for the Opus encoding in kbps
24
+ vbr: Whether to use variable bitrate encoding
25
+ keep_temp: Whether to keep the temporary files for testing
26
+
27
+ Returns:
28
+ tuple: (file handle, temp_file_path) or (file handle, None) if keep_temp is False
29
+ """
30
+ logger.debug("Converting %s to Opus format (bitrate: %d kbps, vbr: %s)", filename, bitrate, vbr)
31
+
32
+ if ffmpeg_binary is None:
33
+ logger.debug("FFmpeg not specified, attempting to auto-detect")
34
+ ffmpeg_binary = get_ffmpeg_binary()
35
+ if ffmpeg_binary is None:
36
+ logger.error("Could not find or download FFmpeg binary")
37
+ raise RuntimeError("Could not find or download FFmpeg binary")
38
+ logger.debug("Found FFmpeg at: %s", ffmpeg_binary)
39
+
40
+ if opus_binary is None:
41
+ logger.debug("Opusenc not specified, attempting to auto-detect")
42
+ opus_binary = get_opus_binary()
43
+ if opus_binary is None:
44
+ logger.error("Could not find or download Opus binary")
45
+ raise RuntimeError("Could not find or download Opus binary")
46
+ logger.debug("Found opusenc at: %s", opus_binary)
47
+
48
+ vbr_parameter = "--vbr" if vbr else "--hard-cbr"
49
+ logger.debug("Using encoding parameter: %s", vbr_parameter)
50
+
51
+ temp_path = None
52
+ if keep_temp:
53
+ temp_dir = os.path.join(tempfile.gettempdir(), "tonie_toolbox_temp")
54
+ os.makedirs(temp_dir, exist_ok=True)
55
+ base_filename = os.path.basename(filename)
56
+ temp_path = os.path.join(temp_dir, f"{os.path.splitext(base_filename)[0]}_{bitrate}kbps.opus")
57
+ logger.info("Creating persistent temporary file: %s", temp_path)
58
+
59
+ logger.debug("Starting FFmpeg process")
60
+ ffmpeg_process = subprocess.Popen(
61
+ [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav",
62
+ "-ar", "48000", "-"], stdout=subprocess.PIPE)
63
+
64
+ logger.debug("Starting opusenc process")
65
+ opusenc_process = subprocess.Popen(
66
+ [opus_binary, "--quiet", vbr_parameter, "--bitrate", f"{bitrate:d}", "-", temp_path],
67
+ stdin=ffmpeg_process.stdout, stderr=subprocess.DEVNULL)
68
+
69
+ opusenc_process.communicate()
70
+
71
+ if opusenc_process.returncode != 0:
72
+ logger.error("Opus encoding failed with return code %d", opusenc_process.returncode)
73
+ raise RuntimeError(f"Opus encoding failed with return code {opusenc_process.returncode}")
74
+
75
+ logger.debug("Opening temporary file for reading")
76
+ tmp_file = open(temp_path, "rb")
77
+ return tmp_file, temp_path
78
+ else:
79
+ logger.debug("Using in-memory temporary file")
80
+
81
+ logger.debug("Starting FFmpeg process")
82
+ ffmpeg_process = subprocess.Popen(
83
+ [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav",
84
+ "-ar", "48000", "-"], stdout=subprocess.PIPE)
85
+
86
+ logger.debug("Starting opusenc process")
87
+ opusenc_process = subprocess.Popen(
88
+ [opus_binary, "--quiet", vbr_parameter, "--bitrate", f"{bitrate:d}", "-", "-"],
89
+ stdin=ffmpeg_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
90
+
91
+ tmp_file = tempfile.SpooledTemporaryFile()
92
+ bytes_written = 0
93
+
94
+ logger.debug("Reading opusenc output")
95
+ for chunk in iter(lambda: opusenc_process.stdout.read(4096), b""):
96
+ tmp_file.write(chunk)
97
+ bytes_written += len(chunk)
98
+
99
+ if opusenc_process.wait() != 0:
100
+ logger.error("Opus encoding failed with return code %d", opusenc_process.returncode)
101
+ raise RuntimeError(f"Opus encoding failed with return code {opusenc_process.returncode}")
102
+
103
+ logger.debug("Wrote %d bytes to temporary file", bytes_written)
104
+ tmp_file.seek(0)
105
+
106
+ return tmp_file, None
107
+
108
+
109
+ def filter_directories(glob_list):
110
+ """
111
+ Filter a list of glob results to include only audio files that can be handled by ffmpeg.
112
+
113
+ Args:
114
+ glob_list: List of path names from glob.glob()
115
+
116
+ Returns:
117
+ list: Filtered list containing only supported audio files
118
+ """
119
+ logger.debug("Filtering %d glob results for supported audio files", len(glob_list))
120
+
121
+ # Common audio file extensions supported by ffmpeg
122
+ supported_extensions = [
123
+ '.wav', '.mp3', '.aac', '.m4a', '.flac', '.ogg', '.opus',
124
+ '.ape', '.wma', '.aiff', '.mp2', '.mp4', '.webm', '.mka'
125
+ ]
126
+
127
+ filtered = []
128
+ for name in glob_list:
129
+ if os.path.isfile(name):
130
+ ext = os.path.splitext(name)[1].lower()
131
+ if ext in supported_extensions:
132
+ filtered.append(name)
133
+ logger.trace("Added supported audio file: %s", name)
134
+ else:
135
+ logger.trace("Skipping unsupported file: %s", name)
136
+
137
+ logger.debug("Found %d supported audio files after filtering", len(filtered))
138
+ return filtered
139
+
140
+
141
+ def get_input_files(input_filename):
142
+ """
143
+ Get a list of input files to process.
144
+
145
+ Supports direct file paths, directory paths, glob patterns, and .lst files.
146
+
147
+ Args:
148
+ input_filename: Input file pattern or list file path
149
+
150
+ Returns:
151
+ list: List of input file paths
152
+ """
153
+ logger.debug("Getting input files for pattern: %s", input_filename)
154
+
155
+ if input_filename.endswith(".lst"):
156
+ logger.debug("Processing list file: %s", input_filename)
157
+ list_dir = os.path.dirname(os.path.abspath(input_filename))
158
+ input_files = []
159
+ with open(input_filename) as file_list:
160
+ for line in file_list:
161
+ fname = line.rstrip()
162
+ full_path = os.path.join(list_dir, fname)
163
+ input_files.append(full_path)
164
+ logger.trace("Added file from list: %s", full_path)
165
+ logger.debug("Found %d files in list file", len(input_files))
166
+ else:
167
+ logger.debug("Processing glob pattern: %s", input_filename)
168
+ input_files = sorted(filter_directories(glob.glob(input_filename)))
169
+ logger.debug("Found %d files matching pattern", len(input_files))
170
+
171
+ return input_files
172
+
173
+
174
+ def append_to_filename(output_filename, tag):
175
+ """
176
+ Append a tag to a filename, preserving the extension.
177
+
178
+ Args:
179
+ output_filename: Original filename
180
+ tag: Tag to append (typically an 8-character hex value)
181
+
182
+ Returns:
183
+ str: Modified filename with tag
184
+ """
185
+ logger.debug("Appending tag '%s' to filename: %s", tag, output_filename)
186
+ pos = output_filename.rfind('.')
187
+ if pos == -1:
188
+ result = f"{output_filename}_{tag}"
189
+ logger.debug("No extension found, result: %s", result)
190
+ return result
191
+ else:
192
+ result = f"{output_filename[:pos]}_{tag}{output_filename[pos:]}"
193
+ logger.debug("Extension found, result: %s", result)
194
+ return result
@@ -0,0 +1,14 @@
1
+ """
2
+ Constants used throughout the TonieToolbox package
3
+ """
4
+ SAMPLE_RATE_KHZ = 48
5
+ ONLY_CONVERT_FRAMEPACKING = -1
6
+ OTHER_PACKET_NEEDED = -2
7
+ DO_NOTHING = -3
8
+ TOO_MANY_SEGMENTS = -4
9
+ OPUS_TAGS = [
10
+ bytearray(
11
+ b"\x4F\x70\x75\x73\x54\x61\x67\x73\x0D\x00\x00\x00\x4C\x61\x76\x66\x35\x38\x2E\x32\x30\x2E\x31\x30\x30\x03\x00\x00\x00\x26\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x3D\x6F\x70\x75\x73\x65\x6E\x63\x20\x66\x72\x6F\x6D\x20\x6F\x70\x75\x73\x2D\x74\x6F\x6F\x6C\x73\x20\x30\x2E\x31\x2E\x31\x30\x2A\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x5F\x6F\x70\x74\x69\x6F\x6E\x73\x3D\x2D\x2D\x71\x75\x69\x65\x74\x20\x2D\x2D\x62\x69\x74\x72\x61\x74\x65\x20\x39\x36\x20\x2D\x2D\x76\x62\x72\x3B\x01\x00\x00\x70\x61\x64\x3D\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30"),
12
+ bytearray(
13
+ b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30")
14
+ ]