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 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)