pyMIDIspy 1.0.0__cp314-cp314-macosx_10_13_universal2.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.
- pymidispy-1.0.0.data/purelib/pyMIDIspy/__init__.py +168 -0
- pymidispy-1.0.0.data/purelib/pyMIDIspy/core.py +1205 -0
- pymidispy-1.0.0.data/purelib/pyMIDIspy/midi_utils.py +430 -0
- pymidispy-1.0.0.dist-info/METADATA +436 -0
- pymidispy-1.0.0.dist-info/RECORD +8 -0
- pymidispy-1.0.0.dist-info/WHEEL +5 -0
- pymidispy-1.0.0.dist-info/licenses/LICENSE +27 -0
- pymidispy-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyMIDIspy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python wrapper for SnoizeMIDISpy - capture outgoing MIDI on macOS
|
|
5
|
+
Home-page: https://github.com/gramster/pyMIDIspy
|
|
6
|
+
Author: gramster
|
|
7
|
+
License: BSD-3-Clause
|
|
8
|
+
Project-URL: Repository, https://github.com/gramster/pyMIDIspy
|
|
9
|
+
Project-URL: Homepage, https://github.com/gramster/pyMIDIspy
|
|
10
|
+
Keywords: midi,macos,coremidi,audio,music
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: pyobjc-core
|
|
27
|
+
Requires-Dist: pyobjc-framework-Cocoa
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy; extra == "dev"
|
|
31
|
+
Requires-Dist: build; extra == "dev"
|
|
32
|
+
Requires-Dist: twine; extra == "dev"
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
|
|
37
|
+
# pyMIDIspy - Python MIDI Spy for macOS
|
|
38
|
+
|
|
39
|
+
A Python library for MIDI capture on macOS, providing both:
|
|
40
|
+
- **Outgoing MIDI capture** - Spy on MIDI being sent TO destinations (via SnoizeMIDISpy)
|
|
41
|
+
- **Incoming MIDI capture** - Receive MIDI FROM sources (via standard CoreMIDI)
|
|
42
|
+
|
|
43
|
+
## Overview
|
|
44
|
+
|
|
45
|
+
This library enables Python applications to:
|
|
46
|
+
|
|
47
|
+
1. **Capture outgoing MIDI** (`MIDIOutputClient`) - Capture what other applications are *sending* to MIDI outputs. This uses the SnoizeMIDISpy driver and is not possible with normal MIDI APIs.
|
|
48
|
+
|
|
49
|
+
2. **Receive incoming MIDI** (`MIDIInputClient`) - Standard MIDI input from sources like keyboards and controllers.
|
|
50
|
+
|
|
51
|
+
Use cases:
|
|
52
|
+
- Debugging MIDI communication between apps and hardware
|
|
53
|
+
- Recording/logging MIDI output from DAWs and other applications
|
|
54
|
+
- Building MIDI monitoring and analysis tools
|
|
55
|
+
- Capturing both input and output for complete MIDI logging
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- **macOS only** - Uses macOS-specific CoreMIDI
|
|
60
|
+
- **Python 3.8+**
|
|
61
|
+
- **Xcode** - Required to build the SnoizeMIDISpy framework from source
|
|
62
|
+
- **PyObjC** (installed automatically) - Required for Objective-C block callbacks
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
### From Source (Recommended)
|
|
67
|
+
|
|
68
|
+
Clone the repository with submodules and build:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone --recursive https://github.com/gramster/pyMIDIspy.git
|
|
72
|
+
cd pyMIDIspy
|
|
73
|
+
|
|
74
|
+
# Build the framework and install the package
|
|
75
|
+
./build.sh
|
|
76
|
+
|
|
77
|
+
# Or install in development mode
|
|
78
|
+
pip install -e .
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### From Wheel (if available)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install pyMIDIspy
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Note: The wheel includes the pre-built SnoizeMIDISpy framework, so no Xcode is required.
|
|
88
|
+
|
|
89
|
+
### Manual Build
|
|
90
|
+
|
|
91
|
+
If you need more control over the build process:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 1. Clone with submodules
|
|
95
|
+
git clone --recursive https://github.com/gramster/pyMIDIspy.git
|
|
96
|
+
cd pyMIDIspy
|
|
97
|
+
|
|
98
|
+
# 2. Initialize submodules if you didn't use --recursive
|
|
99
|
+
git submodule update --init --recursive
|
|
100
|
+
|
|
101
|
+
# 3. Build using pip (this compiles the framework automatically)
|
|
102
|
+
pip install .
|
|
103
|
+
|
|
104
|
+
# Or build a wheel
|
|
105
|
+
python -m build
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Quick Start
|
|
109
|
+
|
|
110
|
+
### Install the MIDI Spy Driver (First Time Only)
|
|
111
|
+
|
|
112
|
+
The spy driver needs to be installed once to enable outgoing MIDI capture:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from pyMIDIspy import install_driver_if_necessary
|
|
116
|
+
|
|
117
|
+
# This installs the driver to ~/Library/Audio/MIDI Drivers/
|
|
118
|
+
error = install_driver_if_necessary()
|
|
119
|
+
if error:
|
|
120
|
+
print(f"Driver installation failed: {error}")
|
|
121
|
+
else:
|
|
122
|
+
print("Driver installed successfully!")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Note:** You may need to restart any running MIDI applications after driver installation.
|
|
126
|
+
|
|
127
|
+
## Usage
|
|
128
|
+
|
|
129
|
+
### Incoming MIDI (from sources)
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from pyMIDIspy import MIDIInputClient, get_sources
|
|
133
|
+
|
|
134
|
+
def on_midi(messages, source_id):
|
|
135
|
+
for msg in messages:
|
|
136
|
+
print(f"Received: {msg.data.hex()}")
|
|
137
|
+
|
|
138
|
+
# List sources
|
|
139
|
+
for src in get_sources():
|
|
140
|
+
print(f" {src.name} (ID: {src.unique_id})")
|
|
141
|
+
|
|
142
|
+
# Receive MIDI from a source
|
|
143
|
+
with MIDIInputClient(callback=on_midi) as client:
|
|
144
|
+
sources = get_sources()
|
|
145
|
+
if sources:
|
|
146
|
+
client.connect_source(sources[0])
|
|
147
|
+
|
|
148
|
+
import time
|
|
149
|
+
while True:
|
|
150
|
+
time.sleep(0.1)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Outgoing MIDI (to destinations)
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from pyMIDIspy import MIDIOutputClient, get_destinations, install_driver_if_necessary
|
|
157
|
+
|
|
158
|
+
# Install the spy driver (first time only)
|
|
159
|
+
install_driver_if_necessary()
|
|
160
|
+
|
|
161
|
+
def on_midi(messages, dest_id):
|
|
162
|
+
for msg in messages:
|
|
163
|
+
print(f"Captured outgoing: {msg.data.hex()}")
|
|
164
|
+
|
|
165
|
+
# Capture MIDI being sent to a destination
|
|
166
|
+
with MIDIOutputClient(callback=on_midi) as client:
|
|
167
|
+
destinations = get_destinations()
|
|
168
|
+
if destinations:
|
|
169
|
+
client.connect_destination(destinations[0])
|
|
170
|
+
|
|
171
|
+
import time
|
|
172
|
+
while True:
|
|
173
|
+
time.sleep(0.1)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Both directions
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from pyMIDIspy import MIDIOutputClient, MIDIInputClient, get_sources, get_destinations
|
|
180
|
+
|
|
181
|
+
def on_incoming(messages, source_id):
|
|
182
|
+
for msg in messages:
|
|
183
|
+
print(f"IN: {msg.data.hex()}")
|
|
184
|
+
|
|
185
|
+
def on_outgoing(messages, dest_id):
|
|
186
|
+
for msg in messages:
|
|
187
|
+
print(f"OUT: {msg.data.hex()}")
|
|
188
|
+
|
|
189
|
+
# Create both clients
|
|
190
|
+
with MIDIInputClient(callback=on_incoming) as input_client, \
|
|
191
|
+
MIDIOutputClient(callback=on_outgoing) as output_client:
|
|
192
|
+
|
|
193
|
+
# Connect to all sources and destinations
|
|
194
|
+
for src in get_sources():
|
|
195
|
+
input_client.connect_source(src)
|
|
196
|
+
for dest in get_destinations():
|
|
197
|
+
output_client.connect_destination(dest)
|
|
198
|
+
|
|
199
|
+
import time
|
|
200
|
+
while True:
|
|
201
|
+
time.sleep(0.1)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Filtering Messages
|
|
205
|
+
|
|
206
|
+
Use `MessageFilter` to filter MIDI messages before they reach your callback:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from pyMIDIspy import MIDIInputClient, MessageFilter
|
|
210
|
+
|
|
211
|
+
# Only receive note messages on channel 1
|
|
212
|
+
filter = MessageFilter(types=["note"], channels=[1])
|
|
213
|
+
|
|
214
|
+
client = MIDIInputClient(callback=on_midi, message_filter=filter)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Common filtering patterns:**
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# Exclude timing clock and active sensing (common noise)
|
|
221
|
+
filter = MessageFilter(exclude_types=["timing_clock", "active_sensing"])
|
|
222
|
+
|
|
223
|
+
# Only note on/off messages
|
|
224
|
+
filter = MessageFilter(types=["note"])
|
|
225
|
+
|
|
226
|
+
# Only control change messages for specific controllers (mod wheel, volume, pan)
|
|
227
|
+
filter = MessageFilter(types=["control_change"], controllers=[1, 7, 10])
|
|
228
|
+
|
|
229
|
+
# Only messages on channels 1-4
|
|
230
|
+
filter = MessageFilter(channels=[1, 2, 3, 4])
|
|
231
|
+
|
|
232
|
+
# Combine: notes on channel 1, excluding note-off
|
|
233
|
+
filter = MessageFilter(types=["note_on"], channels=[1])
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Change filter at runtime:**
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
client = MIDIInputClient(callback=on_midi)
|
|
240
|
+
client.connect_source(source)
|
|
241
|
+
|
|
242
|
+
# Later, add filtering
|
|
243
|
+
client.message_filter = MessageFilter(types=["note"])
|
|
244
|
+
|
|
245
|
+
# Remove filtering
|
|
246
|
+
client.message_filter = None
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Available message types for filtering:**
|
|
250
|
+
|
|
251
|
+
| Type | Description |
|
|
252
|
+
|------|-------------|
|
|
253
|
+
| `"note_off"` | Note Off messages |
|
|
254
|
+
| `"note_on"` | Note On messages (velocity > 0) |
|
|
255
|
+
| `"note"` | Both Note On and Note Off |
|
|
256
|
+
| `"control_change"` | Control Change (CC) messages |
|
|
257
|
+
| `"program_change"` | Program Change messages |
|
|
258
|
+
| `"pitch_bend"` | Pitch Bend messages |
|
|
259
|
+
| `"poly_pressure"` | Polyphonic Aftertouch |
|
|
260
|
+
| `"channel_pressure"` | Channel Aftertouch |
|
|
261
|
+
| `"sysex"` | System Exclusive messages |
|
|
262
|
+
| `"timing_clock"` | MIDI Clock (0xF8) |
|
|
263
|
+
| `"transport"` | Start, Stop, Continue |
|
|
264
|
+
| `"active_sensing"` | Active Sensing (0xFE) |
|
|
265
|
+
| `"realtime"` | All realtime (clock, transport, active sensing) |
|
|
266
|
+
| `"channel"` | All channel voice messages |
|
|
267
|
+
| `"system"` | All system messages |
|
|
268
|
+
|
|
269
|
+
### API Reference
|
|
270
|
+
|
|
271
|
+
#### Functions
|
|
272
|
+
|
|
273
|
+
##### `get_destinations() -> List[MIDIDestination]`
|
|
274
|
+
Get a list of all MIDI destinations (outputs) available on the system.
|
|
275
|
+
|
|
276
|
+
##### `get_destination_by_unique_id(unique_id: int) -> Optional[MIDIDestination]`
|
|
277
|
+
Find a specific MIDI destination by its unique identifier.
|
|
278
|
+
|
|
279
|
+
##### `get_sources() -> List[MIDISource]`
|
|
280
|
+
Get a list of all MIDI sources (inputs) available on the system.
|
|
281
|
+
|
|
282
|
+
##### `get_source_by_unique_id(unique_id: int) -> Optional[MIDISource]`
|
|
283
|
+
Find a specific MIDI source by its unique identifier.
|
|
284
|
+
|
|
285
|
+
##### `install_driver_if_necessary() -> Optional[str]`
|
|
286
|
+
Install the MIDI spy driver (for outgoing capture only). Returns `None` on success.
|
|
287
|
+
|
|
288
|
+
#### Classes
|
|
289
|
+
|
|
290
|
+
##### `MIDIInputClient`
|
|
291
|
+
|
|
292
|
+
Receives incoming MIDI from sources (standard CoreMIDI). No driver required.
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
client = MIDIInputClient(callback=my_callback, client_name="MyApp", message_filter=filter)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Methods:**
|
|
299
|
+
- `connect_source(source: MIDISource)` - Start receiving from a source
|
|
300
|
+
- `connect_source_by_id(unique_id: int)` - Connect by unique ID
|
|
301
|
+
- `disconnect_source(source: MIDISource)` - Stop receiving
|
|
302
|
+
- `disconnect_all()` - Disconnect from all sources
|
|
303
|
+
- `close()` - Release all resources
|
|
304
|
+
|
|
305
|
+
**Properties:**
|
|
306
|
+
- `connected_sources` - List of currently connected sources
|
|
307
|
+
- `message_filter` - Get/set the MessageFilter (or None)
|
|
308
|
+
|
|
309
|
+
##### `MIDIOutputClient`
|
|
310
|
+
|
|
311
|
+
Captures outgoing MIDI sent to destinations. Requires the spy driver.
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
client = MIDIOutputClient(callback=my_callback, message_filter=filter)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Methods:**
|
|
318
|
+
- `connect_destination(destination: MIDIDestination)` - Start capturing from a destination
|
|
319
|
+
- `connect_destination_by_id(unique_id: int)` - Connect by unique ID
|
|
320
|
+
- `disconnect_destination(destination: MIDIDestination)` - Stop capturing
|
|
321
|
+
- `disconnect_destination_by_id(unique_id: int)` - Disconnect by unique ID
|
|
322
|
+
- `disconnect_all()` - Disconnect from all destinations
|
|
323
|
+
- `close()` - Release all resources
|
|
324
|
+
|
|
325
|
+
**Properties:**
|
|
326
|
+
- `connected_destinations` - List of currently connected destinations
|
|
327
|
+
- `message_filter` - Get/set the MessageFilter (or None)
|
|
328
|
+
|
|
329
|
+
##### `MessageFilter`
|
|
330
|
+
|
|
331
|
+
Filters MIDI messages by type, channel, or other criteria.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
filter = MessageFilter(
|
|
335
|
+
types=["note", "control_change"], # Include only these types
|
|
336
|
+
exclude_types=["timing_clock"], # Exclude these types
|
|
337
|
+
channels=[1, 2], # Include only these channels (1-16)
|
|
338
|
+
exclude_channels=[10], # Exclude these channels
|
|
339
|
+
controllers=[1, 7, 10], # For CC: only these controller numbers
|
|
340
|
+
notes=[60, 62, 64], # For notes: only these note numbers
|
|
341
|
+
)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
##### `MIDISource`
|
|
345
|
+
|
|
346
|
+
Represents a MIDI source endpoint (input).
|
|
347
|
+
|
|
348
|
+
##### `MIDIDestination`
|
|
349
|
+
|
|
350
|
+
Represents a MIDI destination endpoint (output).
|
|
351
|
+
|
|
352
|
+
##### `MIDIMessage`
|
|
353
|
+
|
|
354
|
+
Represents a captured MIDI message.
|
|
355
|
+
|
|
356
|
+
**Attributes:**
|
|
357
|
+
- `timestamp: int` - Host time when the message was sent
|
|
358
|
+
- `data: bytes` - Raw MIDI bytes
|
|
359
|
+
|
|
360
|
+
**Properties:**
|
|
361
|
+
- `status` - The status byte (or None)
|
|
362
|
+
- `channel` - The MIDI channel 0-15 (for channel messages)
|
|
363
|
+
|
|
364
|
+
#### Exceptions
|
|
365
|
+
|
|
366
|
+
- `MIDISpyError` - Base exception class
|
|
367
|
+
- `DriverMissingError` - The MIDI spy driver is not installed
|
|
368
|
+
- `DriverCommunicationError` - Failed to communicate with the driver
|
|
369
|
+
- `ConnectionExistsError` - Already connected to this destination
|
|
370
|
+
- `ConnectionNotFoundError` - Not connected to this destination
|
|
371
|
+
|
|
372
|
+
## How It Works
|
|
373
|
+
|
|
374
|
+
The SnoizeMIDISpy framework consists of two parts:
|
|
375
|
+
|
|
376
|
+
1. **MIDI Driver** (`MIDI Monitor.plugin`) - Installed in `~/Library/Audio/MIDI Drivers/`. This is a CoreMIDI driver that intercepts MIDI data sent to destinations.
|
|
377
|
+
|
|
378
|
+
2. **Client Framework** - Communicates with the driver via Mach messages to receive the captured MIDI data.
|
|
379
|
+
|
|
380
|
+
When you connect to a destination, the driver starts forwarding copies of all MIDI messages sent to that destination to your callback.
|
|
381
|
+
|
|
382
|
+
## Troubleshooting
|
|
383
|
+
|
|
384
|
+
### "Could not find SnoizeMIDISpy.framework"
|
|
385
|
+
Make sure you've built the framework and either:
|
|
386
|
+
- Set `SNOIZE_MIDI_SPY_FRAMEWORK` environment variable
|
|
387
|
+
- Copied the framework to `/Library/Frameworks/` or `~/Library/Frameworks/`
|
|
388
|
+
|
|
389
|
+
### "MIDI spy driver is missing"
|
|
390
|
+
Call `install_driver_if_necessary()` to install the driver. You may need to restart your DAW or MIDI applications after installation.
|
|
391
|
+
|
|
392
|
+
### No MIDI messages received
|
|
393
|
+
- Make sure the driver is installed (for outgoing capture)
|
|
394
|
+
- Verify the endpoint exists with `get_destinations()` or `get_sources()`
|
|
395
|
+
- Check that MIDI is actually being sent/received
|
|
396
|
+
- The MIDI Monitor app from MIDIApps can help debug
|
|
397
|
+
|
|
398
|
+
## Technical Notes
|
|
399
|
+
|
|
400
|
+
### Why PyObjC is required
|
|
401
|
+
|
|
402
|
+
CoreMIDI's `MIDIReadBlock` callback is an Objective-C block type:
|
|
403
|
+
```c
|
|
404
|
+
void (^)(const MIDIPacketList *pktlist, void *srcConnRefCon)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Blocks are not simple C function pointers—they're closures with a special memory layout that the runtime can retain/release. The SnoizeMIDISpy framework calls `CFRetain()` on the callback, which would crash with a plain C function pointer. PyObjC's `objc.Block` creates properly-structured blocks that are ABI-compatible with what CoreMIDI and the framework expect.
|
|
408
|
+
|
|
409
|
+
## Publishing to PyPI
|
|
410
|
+
|
|
411
|
+
To publish a new version to PyPI:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# 1. Build the wheel (this compiles the SnoizeMIDISpy framework)
|
|
415
|
+
./build.sh
|
|
416
|
+
|
|
417
|
+
# 2. Verify the package metadata and contents
|
|
418
|
+
twine check dist/*
|
|
419
|
+
|
|
420
|
+
# 3. (Recommended) Test on TestPyPI first
|
|
421
|
+
twine upload --repository testpypi dist/*
|
|
422
|
+
pip install --index-url https://test.pypi.org/simple/ pyMIDIspy
|
|
423
|
+
|
|
424
|
+
# 4. Upload to PyPI
|
|
425
|
+
twine upload dist/*
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Notes:**
|
|
429
|
+
- The wheel is macOS-only and tagged as `macosx_10_13_universal2` (supports both arm64 and x86_64)
|
|
430
|
+
- Source distributions require Xcode to build the framework
|
|
431
|
+
- You'll need a PyPI account and API token (create at https://pypi.org/manage/account/token/)
|
|
432
|
+
- Store your token in `~/.pypirc` or use `TWINE_USERNAME=__token__` and `TWINE_PASSWORD=<your-token>`
|
|
433
|
+
|
|
434
|
+
## License
|
|
435
|
+
|
|
436
|
+
BSD License - see the LICENSE file.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pymidispy-1.0.0.data/purelib/pyMIDIspy/__init__.py,sha256=690ac3uHPf26F8jK3c5JcZmH_w4Dv5p2xv6v02Lb1sY,3858
|
|
2
|
+
pymidispy-1.0.0.data/purelib/pyMIDIspy/core.py,sha256=wmltYpceTqUe5dUsbVvS4zJRhm2lnNv6pBH0QeTjRU4,40631
|
|
3
|
+
pymidispy-1.0.0.data/purelib/pyMIDIspy/midi_utils.py,sha256=dF_Gis2jrmX1QUWgHzdFjpDvoLJrWKYi6qm5MnEbC04,14345
|
|
4
|
+
pymidispy-1.0.0.dist-info/licenses/LICENSE,sha256=4yQhIhnuGo-EYYXInCICmz15ZATx_J2MRz7HRyIfn_o,1493
|
|
5
|
+
pymidispy-1.0.0.dist-info/METADATA,sha256=6QxBDF0D0yQAQ0s3dERjeT89If7DQ38qFGK2q6gDfh4,13570
|
|
6
|
+
pymidispy-1.0.0.dist-info/WHEEL,sha256=k270JeYTEi7JZ22OBvUCY4w-JbllBANjHcahvUbWqko,116
|
|
7
|
+
pymidispy-1.0.0.dist-info/top_level.txt,sha256=3d4Gvqdgt9cIf__y4ODCvrYaKx8dWeAJhsxMoD6f13A,10
|
|
8
|
+
pymidispy-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2001-2023, Kurt Revis
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
* Neither the name of the copyright holder nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyMIDIspy
|