riffy 0.1.0__tar.gz
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.
- riffy-0.1.0/.gitignore +207 -0
- riffy-0.1.0/LICENSE +21 -0
- riffy-0.1.0/PKG-INFO +364 -0
- riffy-0.1.0/README.md +314 -0
- riffy-0.1.0/pyproject.toml +68 -0
- riffy-0.1.0/src/riffy/__init__.py +56 -0
- riffy-0.1.0/src/riffy/exceptions.py +41 -0
- riffy-0.1.0/src/riffy/wav.py +181 -0
riffy-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
#poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
#pdm.lock
|
|
116
|
+
#pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
#pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# SageMath parsed files
|
|
135
|
+
*.sage.py
|
|
136
|
+
|
|
137
|
+
# Environments
|
|
138
|
+
.env
|
|
139
|
+
.envrc
|
|
140
|
+
.venv
|
|
141
|
+
env/
|
|
142
|
+
venv/
|
|
143
|
+
ENV/
|
|
144
|
+
env.bak/
|
|
145
|
+
venv.bak/
|
|
146
|
+
|
|
147
|
+
# Spyder project settings
|
|
148
|
+
.spyderproject
|
|
149
|
+
.spyproject
|
|
150
|
+
|
|
151
|
+
# Rope project settings
|
|
152
|
+
.ropeproject
|
|
153
|
+
|
|
154
|
+
# mkdocs documentation
|
|
155
|
+
/site
|
|
156
|
+
|
|
157
|
+
# mypy
|
|
158
|
+
.mypy_cache/
|
|
159
|
+
.dmypy.json
|
|
160
|
+
dmypy.json
|
|
161
|
+
|
|
162
|
+
# Pyre type checker
|
|
163
|
+
.pyre/
|
|
164
|
+
|
|
165
|
+
# pytype static type analyzer
|
|
166
|
+
.pytype/
|
|
167
|
+
|
|
168
|
+
# Cython debug symbols
|
|
169
|
+
cython_debug/
|
|
170
|
+
|
|
171
|
+
# PyCharm
|
|
172
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
173
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
174
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
175
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
176
|
+
#.idea/
|
|
177
|
+
|
|
178
|
+
# Abstra
|
|
179
|
+
# Abstra is an AI-powered process automation framework.
|
|
180
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
181
|
+
# Learn more at https://abstra.io/docs
|
|
182
|
+
.abstra/
|
|
183
|
+
|
|
184
|
+
# Visual Studio Code
|
|
185
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
186
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
188
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
189
|
+
# .vscode/
|
|
190
|
+
|
|
191
|
+
# Ruff stuff:
|
|
192
|
+
.ruff_cache/
|
|
193
|
+
|
|
194
|
+
# PyPI configuration file
|
|
195
|
+
.pypirc
|
|
196
|
+
|
|
197
|
+
# Cursor
|
|
198
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
199
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
200
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
201
|
+
.cursorignore
|
|
202
|
+
.cursorindexingignore
|
|
203
|
+
|
|
204
|
+
# Marimo
|
|
205
|
+
marimo/_static/
|
|
206
|
+
marimo/_lsp/
|
|
207
|
+
__marimo__/
|
riffy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 John McMeen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
riffy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: riffy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A low-dependency Python library for parsing and managing RIFF format files
|
|
5
|
+
Project-URL: Homepage, https://github.com/jmcmeen/riffy
|
|
6
|
+
Project-URL: Repository, https://github.com/jmcmeen/riffy
|
|
7
|
+
Project-URL: Issues, https://github.com/jmcmeen/riffy/issues
|
|
8
|
+
Author-email: John McMeen <johnmcmeen@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2025 John McMeen
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: audio,chunk,parser,riff,wav
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Requires-Python: >=3.8
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: black>=23.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
48
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# riffy
|
|
52
|
+
|
|
53
|
+
A low-dependency Python library for parsing and managing RIFF (Resource Interchange File Format) files.
|
|
54
|
+
|
|
55
|
+
Riffy provides a pure Python implementation for working with RIFF format files, with initial support for WAV audio files. The library is designed to have **zero external dependencies** while providing robust parsing capabilities for managing RIFF chunks.
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- **Zero Dependencies**: Pure Python implementation with no external dependencies
|
|
60
|
+
- **WAV File Support**: Complete WAV file parsing with format validation
|
|
61
|
+
- **RIFF Chunk Management**: Access and inspect individual RIFF chunks
|
|
62
|
+
- **Audio Metadata Extraction**: Extract sample rate, channels, bit depth, and duration
|
|
63
|
+
- **Format Validation**: Automatic validation of file format and integrity
|
|
64
|
+
- **Type Safety**: Full type hints for better IDE support and code quality
|
|
65
|
+
- **Lightweight**: Minimal footprint, perfect for embedded systems or restricted environments
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
### From PyPI
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install riffy
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### From Source
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Clone the repository
|
|
79
|
+
git clone https://github.com/jmcmeen/riffy.git
|
|
80
|
+
cd riffy
|
|
81
|
+
|
|
82
|
+
# Install in development mode
|
|
83
|
+
pip install -e .
|
|
84
|
+
|
|
85
|
+
# Or build and install
|
|
86
|
+
pip install build
|
|
87
|
+
python -m build
|
|
88
|
+
pip install dist/riffy-*.whl
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### For Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Install with development dependencies
|
|
95
|
+
pip install -e ".[dev]"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Quick Start
|
|
99
|
+
|
|
100
|
+
### Basic WAV File Parsing
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from riffy import WAVParser
|
|
104
|
+
|
|
105
|
+
# Parse a WAV file
|
|
106
|
+
parser = WAVParser("audio.wav")
|
|
107
|
+
info = parser.parse()
|
|
108
|
+
|
|
109
|
+
# Access format information
|
|
110
|
+
print(f"Sample Rate: {info['format']['sample_rate']} Hz")
|
|
111
|
+
print(f"Channels: {info['format']['channels']}")
|
|
112
|
+
print(f"Bit Depth: {info['format']['bits_per_sample']} bits")
|
|
113
|
+
print(f"Duration: {info['duration_seconds']:.2f} seconds")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Accessing RIFF Chunks
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from riffy import WAVParser
|
|
120
|
+
|
|
121
|
+
parser = WAVParser("audio.wav")
|
|
122
|
+
parser.parse()
|
|
123
|
+
|
|
124
|
+
# Access all chunks
|
|
125
|
+
for chunk_id, chunk in parser.chunks.items():
|
|
126
|
+
print(f"Chunk: {chunk_id}, Size: {chunk.size} bytes, Offset: {chunk.offset}")
|
|
127
|
+
|
|
128
|
+
# Access specific chunk
|
|
129
|
+
if 'fmt ' in parser.chunks:
|
|
130
|
+
fmt_chunk = parser.chunks['fmt ']
|
|
131
|
+
print(f"Format chunk size: {fmt_chunk.size}")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Working with Audio Data
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from riffy import WAVParser
|
|
138
|
+
|
|
139
|
+
parser = WAVParser("audio.wav")
|
|
140
|
+
parser.parse()
|
|
141
|
+
|
|
142
|
+
# Access raw audio data
|
|
143
|
+
audio_data = parser.audio_data
|
|
144
|
+
print(f"Audio data size: {len(audio_data)} bytes")
|
|
145
|
+
|
|
146
|
+
# Get sample count
|
|
147
|
+
info = parser.get_info()
|
|
148
|
+
print(f"Total samples: {info['sample_count']}")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Getting Detailed File Information
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from riffy import WAVParser
|
|
155
|
+
import json
|
|
156
|
+
|
|
157
|
+
parser = WAVParser("audio.wav")
|
|
158
|
+
info = parser.parse()
|
|
159
|
+
|
|
160
|
+
# Pretty print all information
|
|
161
|
+
print(json.dumps(info, indent=2))
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Output example:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"file_path": "/path/to/audio.wav",
|
|
169
|
+
"file_size": 1234567,
|
|
170
|
+
"format": {
|
|
171
|
+
"audio_format": 1,
|
|
172
|
+
"channels": 2,
|
|
173
|
+
"sample_rate": 44100,
|
|
174
|
+
"byte_rate": 176400,
|
|
175
|
+
"block_align": 4,
|
|
176
|
+
"bits_per_sample": 16,
|
|
177
|
+
"is_pcm": true
|
|
178
|
+
},
|
|
179
|
+
"duration_seconds": 3.5,
|
|
180
|
+
"audio_data_size": 617400,
|
|
181
|
+
"sample_count": 154350,
|
|
182
|
+
"chunks": {
|
|
183
|
+
"fmt ": 16,
|
|
184
|
+
"data": 617400
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## API Reference
|
|
190
|
+
|
|
191
|
+
### WAVParser
|
|
192
|
+
|
|
193
|
+
The main class for parsing WAV files.
|
|
194
|
+
|
|
195
|
+
#### Constructor
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
WAVParser(file_path: Union[str, Path])
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Parameters:**
|
|
202
|
+
|
|
203
|
+
- `file_path`: Path to the WAV file (string or `pathlib.Path`)
|
|
204
|
+
|
|
205
|
+
#### Methods
|
|
206
|
+
|
|
207
|
+
##### `parse() -> Dict`
|
|
208
|
+
|
|
209
|
+
Parse the WAV file and return comprehensive information.
|
|
210
|
+
|
|
211
|
+
**Returns:** Dictionary containing file information, format details, and chunk data
|
|
212
|
+
|
|
213
|
+
**Raises:**
|
|
214
|
+
|
|
215
|
+
- `FileNotFoundError`: If the file doesn't exist
|
|
216
|
+
- `InvalidWAVFormatError`: If the file is not a valid WAV file
|
|
217
|
+
- `CorruptedFileError`: If the file is corrupted or incomplete
|
|
218
|
+
|
|
219
|
+
##### `get_info() -> Dict`
|
|
220
|
+
|
|
221
|
+
Get comprehensive information about the parsed WAV file.
|
|
222
|
+
|
|
223
|
+
**Returns:** Dictionary with file metadata (must call `parse()` first)
|
|
224
|
+
|
|
225
|
+
#### Properties
|
|
226
|
+
|
|
227
|
+
- `format_info`: `WAVFormat` object containing format details
|
|
228
|
+
- `chunks`: Dictionary of `WAVChunk` objects keyed by chunk ID
|
|
229
|
+
- `audio_data`: Raw audio data as bytes
|
|
230
|
+
|
|
231
|
+
### WAVFormat
|
|
232
|
+
|
|
233
|
+
Dataclass containing WAV format information.
|
|
234
|
+
|
|
235
|
+
**Attributes:**
|
|
236
|
+
|
|
237
|
+
- `audio_format`: Audio format code (1 = PCM)
|
|
238
|
+
- `channels`: Number of audio channels
|
|
239
|
+
- `sample_rate`: Sample rate in Hz
|
|
240
|
+
- `byte_rate`: Bytes per second
|
|
241
|
+
- `block_align`: Block alignment in bytes
|
|
242
|
+
- `bits_per_sample`: Bits per sample
|
|
243
|
+
|
|
244
|
+
**Properties:**
|
|
245
|
+
|
|
246
|
+
- `is_pcm`: Boolean indicating if format is PCM (uncompressed)
|
|
247
|
+
- `duration_seconds`: Audio duration in seconds
|
|
248
|
+
|
|
249
|
+
### WAVChunk
|
|
250
|
+
|
|
251
|
+
Dataclass representing a RIFF chunk.
|
|
252
|
+
|
|
253
|
+
**Attributes:**
|
|
254
|
+
|
|
255
|
+
- `id`: Chunk identifier (e.g., "fmt ", "data")
|
|
256
|
+
- `size`: Chunk size in bytes
|
|
257
|
+
- `data`: Raw chunk data
|
|
258
|
+
- `offset`: Byte offset in the file
|
|
259
|
+
|
|
260
|
+
## Exception Hierarchy
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
RiffyError (base exception)
|
|
264
|
+
├── WAVError
|
|
265
|
+
│ ├── InvalidWAVFormatError
|
|
266
|
+
│ ├── CorruptedFileError
|
|
267
|
+
│ └── UnsupportedFormatError
|
|
268
|
+
└── ChunkError
|
|
269
|
+
├── InvalidChunkError
|
|
270
|
+
└── MissingChunkError
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Exception Handling
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from riffy import WAVParser, InvalidWAVFormatError, CorruptedFileError
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
parser = WAVParser("audio.wav")
|
|
280
|
+
info = parser.parse()
|
|
281
|
+
except InvalidWAVFormatError as e:
|
|
282
|
+
print(f"Invalid WAV format: {e}")
|
|
283
|
+
except CorruptedFileError as e:
|
|
284
|
+
print(f"File is corrupted: {e}")
|
|
285
|
+
except FileNotFoundError:
|
|
286
|
+
print("File not found")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Supported Formats
|
|
290
|
+
|
|
291
|
+
Currently, Riffy supports:
|
|
292
|
+
|
|
293
|
+
- **WAV Files**: PCM (uncompressed) audio only
|
|
294
|
+
- **RIFF Chunks**: Standard chunk parsing for any RIFF file
|
|
295
|
+
|
|
296
|
+
### Planned Support
|
|
297
|
+
|
|
298
|
+
- AVI files
|
|
299
|
+
- WebP images
|
|
300
|
+
- Additional WAV compression formats
|
|
301
|
+
- RIFF chunk writing/modification
|
|
302
|
+
|
|
303
|
+
## Requirements
|
|
304
|
+
|
|
305
|
+
- Python 3.8 or higher
|
|
306
|
+
- No external dependencies!
|
|
307
|
+
|
|
308
|
+
## Development
|
|
309
|
+
|
|
310
|
+
### Running Tests
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# Install development dependencies
|
|
314
|
+
pip install -e ".[dev]"
|
|
315
|
+
|
|
316
|
+
# Run tests
|
|
317
|
+
pytest
|
|
318
|
+
|
|
319
|
+
# Run tests with coverage
|
|
320
|
+
pytest --cov=riffy --cov-report=html
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Code Formatting
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
# Format code with black
|
|
327
|
+
black src/
|
|
328
|
+
|
|
329
|
+
# Lint with ruff
|
|
330
|
+
ruff check src/
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Contributing
|
|
334
|
+
|
|
335
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
336
|
+
|
|
337
|
+
1. Fork the repository
|
|
338
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
339
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
340
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
341
|
+
5. Open a Pull Request
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
346
|
+
|
|
347
|
+
## Changelog
|
|
348
|
+
|
|
349
|
+
### Version 0.1.0 (Initial Release)
|
|
350
|
+
|
|
351
|
+
- Pure Python WAV file parser
|
|
352
|
+
- RIFF chunk management
|
|
353
|
+
- Zero external dependencies
|
|
354
|
+
- Full type hints
|
|
355
|
+
- Comprehensive error handling
|
|
356
|
+
|
|
357
|
+
## Acknowledgments
|
|
358
|
+
|
|
359
|
+
- RIFF format specification: [Microsoft RIFF Specification](https://learn.microsoft.com/en-us/windows/win32/xaudio2/resource-interchange-file-format--riff-)
|
|
360
|
+
- WAV format specification: [WAV Audio File Format](http://soundfile.sapp.org/doc/WaveFormat/)
|
|
361
|
+
|
|
362
|
+
## Support
|
|
363
|
+
|
|
364
|
+
For bugs, feature requests, or questions, please [open an issue](https://github.com/jmcmeen/riffy/issues).
|
riffy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# riffy
|
|
2
|
+
|
|
3
|
+
A low-dependency Python library for parsing and managing RIFF (Resource Interchange File Format) files.
|
|
4
|
+
|
|
5
|
+
Riffy provides a pure Python implementation for working with RIFF format files, with initial support for WAV audio files. The library is designed to have **zero external dependencies** while providing robust parsing capabilities for managing RIFF chunks.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Zero Dependencies**: Pure Python implementation with no external dependencies
|
|
10
|
+
- **WAV File Support**: Complete WAV file parsing with format validation
|
|
11
|
+
- **RIFF Chunk Management**: Access and inspect individual RIFF chunks
|
|
12
|
+
- **Audio Metadata Extraction**: Extract sample rate, channels, bit depth, and duration
|
|
13
|
+
- **Format Validation**: Automatic validation of file format and integrity
|
|
14
|
+
- **Type Safety**: Full type hints for better IDE support and code quality
|
|
15
|
+
- **Lightweight**: Minimal footprint, perfect for embedded systems or restricted environments
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### From PyPI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install riffy
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### From Source
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Clone the repository
|
|
29
|
+
git clone https://github.com/jmcmeen/riffy.git
|
|
30
|
+
cd riffy
|
|
31
|
+
|
|
32
|
+
# Install in development mode
|
|
33
|
+
pip install -e .
|
|
34
|
+
|
|
35
|
+
# Or build and install
|
|
36
|
+
pip install build
|
|
37
|
+
python -m build
|
|
38
|
+
pip install dist/riffy-*.whl
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### For Development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Install with development dependencies
|
|
45
|
+
pip install -e ".[dev]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
### Basic WAV File Parsing
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from riffy import WAVParser
|
|
54
|
+
|
|
55
|
+
# Parse a WAV file
|
|
56
|
+
parser = WAVParser("audio.wav")
|
|
57
|
+
info = parser.parse()
|
|
58
|
+
|
|
59
|
+
# Access format information
|
|
60
|
+
print(f"Sample Rate: {info['format']['sample_rate']} Hz")
|
|
61
|
+
print(f"Channels: {info['format']['channels']}")
|
|
62
|
+
print(f"Bit Depth: {info['format']['bits_per_sample']} bits")
|
|
63
|
+
print(f"Duration: {info['duration_seconds']:.2f} seconds")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Accessing RIFF Chunks
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from riffy import WAVParser
|
|
70
|
+
|
|
71
|
+
parser = WAVParser("audio.wav")
|
|
72
|
+
parser.parse()
|
|
73
|
+
|
|
74
|
+
# Access all chunks
|
|
75
|
+
for chunk_id, chunk in parser.chunks.items():
|
|
76
|
+
print(f"Chunk: {chunk_id}, Size: {chunk.size} bytes, Offset: {chunk.offset}")
|
|
77
|
+
|
|
78
|
+
# Access specific chunk
|
|
79
|
+
if 'fmt ' in parser.chunks:
|
|
80
|
+
fmt_chunk = parser.chunks['fmt ']
|
|
81
|
+
print(f"Format chunk size: {fmt_chunk.size}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Working with Audio Data
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from riffy import WAVParser
|
|
88
|
+
|
|
89
|
+
parser = WAVParser("audio.wav")
|
|
90
|
+
parser.parse()
|
|
91
|
+
|
|
92
|
+
# Access raw audio data
|
|
93
|
+
audio_data = parser.audio_data
|
|
94
|
+
print(f"Audio data size: {len(audio_data)} bytes")
|
|
95
|
+
|
|
96
|
+
# Get sample count
|
|
97
|
+
info = parser.get_info()
|
|
98
|
+
print(f"Total samples: {info['sample_count']}")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Getting Detailed File Information
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from riffy import WAVParser
|
|
105
|
+
import json
|
|
106
|
+
|
|
107
|
+
parser = WAVParser("audio.wav")
|
|
108
|
+
info = parser.parse()
|
|
109
|
+
|
|
110
|
+
# Pretty print all information
|
|
111
|
+
print(json.dumps(info, indent=2))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Output example:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"file_path": "/path/to/audio.wav",
|
|
119
|
+
"file_size": 1234567,
|
|
120
|
+
"format": {
|
|
121
|
+
"audio_format": 1,
|
|
122
|
+
"channels": 2,
|
|
123
|
+
"sample_rate": 44100,
|
|
124
|
+
"byte_rate": 176400,
|
|
125
|
+
"block_align": 4,
|
|
126
|
+
"bits_per_sample": 16,
|
|
127
|
+
"is_pcm": true
|
|
128
|
+
},
|
|
129
|
+
"duration_seconds": 3.5,
|
|
130
|
+
"audio_data_size": 617400,
|
|
131
|
+
"sample_count": 154350,
|
|
132
|
+
"chunks": {
|
|
133
|
+
"fmt ": 16,
|
|
134
|
+
"data": 617400
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## API Reference
|
|
140
|
+
|
|
141
|
+
### WAVParser
|
|
142
|
+
|
|
143
|
+
The main class for parsing WAV files.
|
|
144
|
+
|
|
145
|
+
#### Constructor
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
WAVParser(file_path: Union[str, Path])
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Parameters:**
|
|
152
|
+
|
|
153
|
+
- `file_path`: Path to the WAV file (string or `pathlib.Path`)
|
|
154
|
+
|
|
155
|
+
#### Methods
|
|
156
|
+
|
|
157
|
+
##### `parse() -> Dict`
|
|
158
|
+
|
|
159
|
+
Parse the WAV file and return comprehensive information.
|
|
160
|
+
|
|
161
|
+
**Returns:** Dictionary containing file information, format details, and chunk data
|
|
162
|
+
|
|
163
|
+
**Raises:**
|
|
164
|
+
|
|
165
|
+
- `FileNotFoundError`: If the file doesn't exist
|
|
166
|
+
- `InvalidWAVFormatError`: If the file is not a valid WAV file
|
|
167
|
+
- `CorruptedFileError`: If the file is corrupted or incomplete
|
|
168
|
+
|
|
169
|
+
##### `get_info() -> Dict`
|
|
170
|
+
|
|
171
|
+
Get comprehensive information about the parsed WAV file.
|
|
172
|
+
|
|
173
|
+
**Returns:** Dictionary with file metadata (must call `parse()` first)
|
|
174
|
+
|
|
175
|
+
#### Properties
|
|
176
|
+
|
|
177
|
+
- `format_info`: `WAVFormat` object containing format details
|
|
178
|
+
- `chunks`: Dictionary of `WAVChunk` objects keyed by chunk ID
|
|
179
|
+
- `audio_data`: Raw audio data as bytes
|
|
180
|
+
|
|
181
|
+
### WAVFormat
|
|
182
|
+
|
|
183
|
+
Dataclass containing WAV format information.
|
|
184
|
+
|
|
185
|
+
**Attributes:**
|
|
186
|
+
|
|
187
|
+
- `audio_format`: Audio format code (1 = PCM)
|
|
188
|
+
- `channels`: Number of audio channels
|
|
189
|
+
- `sample_rate`: Sample rate in Hz
|
|
190
|
+
- `byte_rate`: Bytes per second
|
|
191
|
+
- `block_align`: Block alignment in bytes
|
|
192
|
+
- `bits_per_sample`: Bits per sample
|
|
193
|
+
|
|
194
|
+
**Properties:**
|
|
195
|
+
|
|
196
|
+
- `is_pcm`: Boolean indicating if format is PCM (uncompressed)
|
|
197
|
+
- `duration_seconds`: Audio duration in seconds
|
|
198
|
+
|
|
199
|
+
### WAVChunk
|
|
200
|
+
|
|
201
|
+
Dataclass representing a RIFF chunk.
|
|
202
|
+
|
|
203
|
+
**Attributes:**
|
|
204
|
+
|
|
205
|
+
- `id`: Chunk identifier (e.g., "fmt ", "data")
|
|
206
|
+
- `size`: Chunk size in bytes
|
|
207
|
+
- `data`: Raw chunk data
|
|
208
|
+
- `offset`: Byte offset in the file
|
|
209
|
+
|
|
210
|
+
## Exception Hierarchy
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
RiffyError (base exception)
|
|
214
|
+
├── WAVError
|
|
215
|
+
│ ├── InvalidWAVFormatError
|
|
216
|
+
│ ├── CorruptedFileError
|
|
217
|
+
│ └── UnsupportedFormatError
|
|
218
|
+
└── ChunkError
|
|
219
|
+
├── InvalidChunkError
|
|
220
|
+
└── MissingChunkError
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Exception Handling
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from riffy import WAVParser, InvalidWAVFormatError, CorruptedFileError
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
parser = WAVParser("audio.wav")
|
|
230
|
+
info = parser.parse()
|
|
231
|
+
except InvalidWAVFormatError as e:
|
|
232
|
+
print(f"Invalid WAV format: {e}")
|
|
233
|
+
except CorruptedFileError as e:
|
|
234
|
+
print(f"File is corrupted: {e}")
|
|
235
|
+
except FileNotFoundError:
|
|
236
|
+
print("File not found")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Supported Formats
|
|
240
|
+
|
|
241
|
+
Currently, Riffy supports:
|
|
242
|
+
|
|
243
|
+
- **WAV Files**: PCM (uncompressed) audio only
|
|
244
|
+
- **RIFF Chunks**: Standard chunk parsing for any RIFF file
|
|
245
|
+
|
|
246
|
+
### Planned Support
|
|
247
|
+
|
|
248
|
+
- AVI files
|
|
249
|
+
- WebP images
|
|
250
|
+
- Additional WAV compression formats
|
|
251
|
+
- RIFF chunk writing/modification
|
|
252
|
+
|
|
253
|
+
## Requirements
|
|
254
|
+
|
|
255
|
+
- Python 3.8 or higher
|
|
256
|
+
- No external dependencies!
|
|
257
|
+
|
|
258
|
+
## Development
|
|
259
|
+
|
|
260
|
+
### Running Tests
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Install development dependencies
|
|
264
|
+
pip install -e ".[dev]"
|
|
265
|
+
|
|
266
|
+
# Run tests
|
|
267
|
+
pytest
|
|
268
|
+
|
|
269
|
+
# Run tests with coverage
|
|
270
|
+
pytest --cov=riffy --cov-report=html
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Code Formatting
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# Format code with black
|
|
277
|
+
black src/
|
|
278
|
+
|
|
279
|
+
# Lint with ruff
|
|
280
|
+
ruff check src/
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Contributing
|
|
284
|
+
|
|
285
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
286
|
+
|
|
287
|
+
1. Fork the repository
|
|
288
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
289
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
290
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
291
|
+
5. Open a Pull Request
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
296
|
+
|
|
297
|
+
## Changelog
|
|
298
|
+
|
|
299
|
+
### Version 0.1.0 (Initial Release)
|
|
300
|
+
|
|
301
|
+
- Pure Python WAV file parser
|
|
302
|
+
- RIFF chunk management
|
|
303
|
+
- Zero external dependencies
|
|
304
|
+
- Full type hints
|
|
305
|
+
- Comprehensive error handling
|
|
306
|
+
|
|
307
|
+
## Acknowledgments
|
|
308
|
+
|
|
309
|
+
- RIFF format specification: [Microsoft RIFF Specification](https://learn.microsoft.com/en-us/windows/win32/xaudio2/resource-interchange-file-format--riff-)
|
|
310
|
+
- WAV format specification: [WAV Audio File Format](http://soundfile.sapp.org/doc/WaveFormat/)
|
|
311
|
+
|
|
312
|
+
## Support
|
|
313
|
+
|
|
314
|
+
For bugs, feature requests, or questions, please [open an issue](https://github.com/jmcmeen/riffy/issues).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "riffy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A low-dependency Python library for parsing and managing RIFF format files"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {file = "LICENSE"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "John McMeen", email = "johnmcmeen@gmail.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["riff", "wav", "audio", "parser", "chunk"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# No external dependencies - pure Python implementation
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"pytest-cov>=4.0",
|
|
37
|
+
"black>=23.0",
|
|
38
|
+
"ruff>=0.1.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/jmcmeen/riffy"
|
|
43
|
+
Repository = "https://github.com/jmcmeen/riffy"
|
|
44
|
+
Issues = "https://github.com/jmcmeen/riffy/issues"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/riffy"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.sdist]
|
|
50
|
+
include = [
|
|
51
|
+
"/src",
|
|
52
|
+
"/README.md",
|
|
53
|
+
"/LICENSE",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.black]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = ["py38", "py39", "py310", "py311"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py38"
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
python_files = ["test_*.py"]
|
|
67
|
+
python_classes = ["Test*"]
|
|
68
|
+
python_functions = ["test_*"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Riffy - A low-dependency Python library for parsing and managing RIFF format files.
|
|
3
|
+
|
|
4
|
+
Riffy provides a pure Python implementation for parsing RIFF (Resource Interchange File Format)
|
|
5
|
+
files, with initial support for WAV audio files. The library is designed to have minimal
|
|
6
|
+
dependencies while providing robust parsing capabilities.
|
|
7
|
+
|
|
8
|
+
Main Features:
|
|
9
|
+
- Pure Python implementation with zero external dependencies
|
|
10
|
+
- Parse WAV files and extract format information
|
|
11
|
+
- Access individual RIFF chunks
|
|
12
|
+
- Extract audio data and metadata
|
|
13
|
+
- Validate file format and integrity
|
|
14
|
+
|
|
15
|
+
Basic Usage:
|
|
16
|
+
>>> from riffy import WAVParser
|
|
17
|
+
>>> parser = WAVParser("audio.wav")
|
|
18
|
+
>>> info = parser.parse()
|
|
19
|
+
>>> print(f"Sample rate: {info['format']['sample_rate']} Hz")
|
|
20
|
+
>>> print(f"Duration: {info['duration_seconds']:.2f} seconds")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
__author__ = "John"
|
|
25
|
+
|
|
26
|
+
# Import main classes and functions
|
|
27
|
+
from .wav import WAVParser, WAVFormat, WAVChunk
|
|
28
|
+
from .exceptions import (
|
|
29
|
+
RiffyError,
|
|
30
|
+
WAVError,
|
|
31
|
+
InvalidWAVFormatError,
|
|
32
|
+
CorruptedFileError,
|
|
33
|
+
UnsupportedFormatError,
|
|
34
|
+
ChunkError,
|
|
35
|
+
InvalidChunkError,
|
|
36
|
+
MissingChunkError,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Version
|
|
41
|
+
"__version__",
|
|
42
|
+
"__author__",
|
|
43
|
+
# WAV parsing
|
|
44
|
+
"WAVParser",
|
|
45
|
+
"WAVFormat",
|
|
46
|
+
"WAVChunk",
|
|
47
|
+
# Exceptions
|
|
48
|
+
"RiffyError",
|
|
49
|
+
"WAVError",
|
|
50
|
+
"InvalidWAVFormatError",
|
|
51
|
+
"CorruptedFileError",
|
|
52
|
+
"UnsupportedFormatError",
|
|
53
|
+
"ChunkError",
|
|
54
|
+
"InvalidChunkError",
|
|
55
|
+
"MissingChunkError",
|
|
56
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Custom exceptions for the riffy RIFF parser library."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RiffyError(Exception):
|
|
5
|
+
"""Base exception for all riffy errors."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WAVError(RiffyError):
|
|
10
|
+
"""Base exception for WAV-related errors."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidWAVFormatError(WAVError):
|
|
15
|
+
"""Raised when a WAV file has an invalid format."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CorruptedFileError(WAVError):
|
|
20
|
+
"""Raised when a file is corrupted or incomplete."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnsupportedFormatError(WAVError):
|
|
25
|
+
"""Raised when a WAV format is not supported."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ChunkError(RiffyError):
|
|
30
|
+
"""Base exception for RIFF chunk-related errors."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InvalidChunkError(ChunkError):
|
|
35
|
+
"""Raised when a RIFF chunk is invalid."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MissingChunkError(ChunkError):
|
|
40
|
+
"""Raised when a required RIFF chunk is missing."""
|
|
41
|
+
pass
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
from typing import Dict, Optional, Union
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .exceptions import WAVError, InvalidWAVFormatError, CorruptedFileError
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class WAVFormat:
|
|
10
|
+
"""WAV format information."""
|
|
11
|
+
audio_format: int
|
|
12
|
+
channels: int
|
|
13
|
+
sample_rate: int
|
|
14
|
+
byte_rate: int
|
|
15
|
+
block_align: int
|
|
16
|
+
bits_per_sample: int
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def is_pcm(self) -> bool:
|
|
20
|
+
"""Check if format is PCM (uncompressed)."""
|
|
21
|
+
return self.audio_format == 1
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def duration_seconds(self) -> float:
|
|
25
|
+
"""Calculate duration based on byte rate (requires data size)."""
|
|
26
|
+
return getattr(self, '_duration', 0.0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class WAVChunk:
|
|
31
|
+
"""Represents a RIFF chunk in the WAV file."""
|
|
32
|
+
id: str
|
|
33
|
+
size: int
|
|
34
|
+
data: bytes
|
|
35
|
+
offset: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WAVParser:
|
|
39
|
+
"""Pure Python WAV file parser."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, file_path: Union[str, Path]):
|
|
42
|
+
"""Initialize parser with file path."""
|
|
43
|
+
self.file_path = Path(file_path)
|
|
44
|
+
self.format_info: Optional[WAVFormat] = None
|
|
45
|
+
self.chunks: Dict[str, WAVChunk] = {}
|
|
46
|
+
self.audio_data: Optional[bytes] = None
|
|
47
|
+
self._file_size = 0
|
|
48
|
+
|
|
49
|
+
def parse(self) -> Dict:
|
|
50
|
+
"""Parse the WAV file and return comprehensive information."""
|
|
51
|
+
if not self.file_path.exists():
|
|
52
|
+
raise FileNotFoundError(f"WAV file not found: {self.file_path}")
|
|
53
|
+
|
|
54
|
+
self._file_size = self.file_path.stat().st_size
|
|
55
|
+
|
|
56
|
+
with open(self.file_path, 'rb') as f:
|
|
57
|
+
self._parse_riff_header(f)
|
|
58
|
+
self._parse_chunks(f)
|
|
59
|
+
self._calculate_duration()
|
|
60
|
+
self._validate_format()
|
|
61
|
+
|
|
62
|
+
return self.get_info()
|
|
63
|
+
|
|
64
|
+
def _parse_riff_header(self, f) -> None:
|
|
65
|
+
"""Parse the RIFF header."""
|
|
66
|
+
riff_header = f.read(12)
|
|
67
|
+
if len(riff_header) != 12:
|
|
68
|
+
raise CorruptedFileError("File too small to be a valid WAV file")
|
|
69
|
+
|
|
70
|
+
riff_id, file_size, wave_id = struct.unpack('<4sI4s', riff_header)
|
|
71
|
+
|
|
72
|
+
if riff_id != b'RIFF':
|
|
73
|
+
raise InvalidWAVFormatError("Not a valid RIFF file")
|
|
74
|
+
|
|
75
|
+
if wave_id != b'WAVE':
|
|
76
|
+
raise InvalidWAVFormatError("Not a valid WAV file")
|
|
77
|
+
|
|
78
|
+
def _parse_chunks(self, f) -> None:
|
|
79
|
+
"""Parse all chunks in the WAV file."""
|
|
80
|
+
while True:
|
|
81
|
+
chunk_header = f.read(8)
|
|
82
|
+
if len(chunk_header) < 8:
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
chunk_id, chunk_size = struct.unpack('<4sI', chunk_header)
|
|
86
|
+
chunk_id = chunk_id.decode('ascii', errors='ignore')
|
|
87
|
+
|
|
88
|
+
chunk_offset = f.tell()
|
|
89
|
+
chunk_data = f.read(chunk_size)
|
|
90
|
+
if len(chunk_data) != chunk_size:
|
|
91
|
+
raise CorruptedFileError(f"Incomplete chunk: {chunk_id}")
|
|
92
|
+
|
|
93
|
+
self.chunks[chunk_id] = WAVChunk(
|
|
94
|
+
id=chunk_id,
|
|
95
|
+
size=chunk_size,
|
|
96
|
+
data=chunk_data,
|
|
97
|
+
offset=chunk_offset
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if chunk_id == 'fmt ':
|
|
101
|
+
self._parse_format_chunk(chunk_data)
|
|
102
|
+
elif chunk_id == 'data':
|
|
103
|
+
self.audio_data = chunk_data
|
|
104
|
+
|
|
105
|
+
if chunk_size % 2:
|
|
106
|
+
f.read(1)
|
|
107
|
+
|
|
108
|
+
def _parse_format_chunk(self, data: bytes) -> None:
|
|
109
|
+
"""Parse the format chunk."""
|
|
110
|
+
if len(data) < 16:
|
|
111
|
+
raise InvalidWAVFormatError("Format chunk too small")
|
|
112
|
+
|
|
113
|
+
fmt_data = struct.unpack('<HHIIHH', data[:16])
|
|
114
|
+
|
|
115
|
+
self.format_info = WAVFormat(
|
|
116
|
+
audio_format=fmt_data[0],
|
|
117
|
+
channels=fmt_data[1],
|
|
118
|
+
sample_rate=fmt_data[2],
|
|
119
|
+
byte_rate=fmt_data[3],
|
|
120
|
+
block_align=fmt_data[4],
|
|
121
|
+
bits_per_sample=fmt_data[5]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _calculate_duration(self) -> None:
|
|
125
|
+
"""Calculate audio duration after all chunks are parsed."""
|
|
126
|
+
if self.audio_data and self.format_info and self.format_info.byte_rate > 0:
|
|
127
|
+
duration = len(self.audio_data) / self.format_info.byte_rate
|
|
128
|
+
self.format_info._duration = duration
|
|
129
|
+
|
|
130
|
+
def _validate_format(self) -> None:
|
|
131
|
+
"""Validate the parsed format."""
|
|
132
|
+
if not self.format_info:
|
|
133
|
+
raise InvalidWAVFormatError("No format chunk found")
|
|
134
|
+
|
|
135
|
+
if not self.format_info.is_pcm:
|
|
136
|
+
raise InvalidWAVFormatError(
|
|
137
|
+
f"Unsupported audio format: {self.format_info.audio_format} "
|
|
138
|
+
"(only PCM is supported)"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if self.format_info.channels == 0:
|
|
142
|
+
raise InvalidWAVFormatError("Invalid number of channels")
|
|
143
|
+
|
|
144
|
+
if self.format_info.sample_rate == 0:
|
|
145
|
+
raise InvalidWAVFormatError("Invalid sample rate")
|
|
146
|
+
|
|
147
|
+
if self.audio_data is None:
|
|
148
|
+
raise InvalidWAVFormatError("No audio data found")
|
|
149
|
+
|
|
150
|
+
def get_info(self) -> Dict:
|
|
151
|
+
"""Get comprehensive information about the WAV file."""
|
|
152
|
+
if not self.format_info:
|
|
153
|
+
raise WAVError("File not parsed yet. Call parse() first.")
|
|
154
|
+
|
|
155
|
+
info = {
|
|
156
|
+
'file_path': str(self.file_path),
|
|
157
|
+
'file_size': self._file_size,
|
|
158
|
+
'format': {
|
|
159
|
+
'audio_format': self.format_info.audio_format,
|
|
160
|
+
'channels': self.format_info.channels,
|
|
161
|
+
'sample_rate': self.format_info.sample_rate,
|
|
162
|
+
'byte_rate': self.format_info.byte_rate,
|
|
163
|
+
'block_align': self.format_info.block_align,
|
|
164
|
+
'bits_per_sample': self.format_info.bits_per_sample,
|
|
165
|
+
'is_pcm': self.format_info.is_pcm
|
|
166
|
+
},
|
|
167
|
+
'duration_seconds': self.format_info.duration_seconds,
|
|
168
|
+
'audio_data_size': len(self.audio_data) if self.audio_data else 0,
|
|
169
|
+
'sample_count': self._calculate_sample_count(),
|
|
170
|
+
'chunks': {chunk_id: chunk.size for chunk_id, chunk in self.chunks.items()}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return info
|
|
174
|
+
|
|
175
|
+
def _calculate_sample_count(self) -> int:
|
|
176
|
+
"""Calculate total number of samples."""
|
|
177
|
+
if not self.audio_data or not self.format_info:
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
bytes_per_sample = self.format_info.bits_per_sample // 8
|
|
181
|
+
return len(self.audio_data) // (bytes_per_sample * self.format_info.channels)
|