piwave 2.1.0__tar.gz → 2.1.2__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.
- {piwave-2.1.0 → piwave-2.1.2}/PKG-INFO +54 -47
- {piwave-2.1.0 → piwave-2.1.2}/README.md +53 -46
- {piwave-2.1.0 → piwave-2.1.2}/piwave/backends/__init__.py +8 -1
- {piwave-2.1.0 → piwave-2.1.2}/piwave/backends/base.py +9 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave/backends/fm_transmitter.py +12 -1
- {piwave-2.1.0 → piwave-2.1.2}/piwave/backends/pi_fm_rds.py +7 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave/piwave.py +231 -61
- {piwave-2.1.0 → piwave-2.1.2}/piwave.egg-info/PKG-INFO +54 -47
- {piwave-2.1.0 → piwave-2.1.2}/setup.cfg +1 -1
- {piwave-2.1.0 → piwave-2.1.2}/LICENSE +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave/__init__.py +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave/__main__.py +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave/logger.py +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave.egg-info/SOURCES.txt +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave.egg-info/dependency_links.txt +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave.egg-info/requires.txt +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/piwave.egg-info/top_level.txt +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/pyproject.toml +0 -0
- {piwave-2.1.0 → piwave-2.1.2}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: piwave
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: A python module to broadcast radio waves with your Raspberry Pi.
|
|
5
5
|
Home-page: https://github.com/douxxtech/piwave
|
|
6
6
|
Author: Douxx
|
|
@@ -37,6 +37,7 @@ Dynamic: license-file
|
|
|
37
37
|
- **Multi-Backend Architecture**: Supports multiple backends for different actions
|
|
38
38
|
- **Wide Frequency Support**: 1-250 MHz coverage through different backends
|
|
39
39
|
- **RDS Support**: Program Service, Radio Text, and Program Identifier broadcasting
|
|
40
|
+
- **Live Stream Support**: Broadcast live from a stream source.
|
|
40
41
|
- **Smart Backend Selection**: Automatically chooses the best backend to suit your needs
|
|
41
42
|
- **Audio Format Support**: Converts most audio formats (MP3, FLAC, M4A, etc.) to WAV
|
|
42
43
|
- **Real-time Settings Updates**: Change frequency, RDS data, and settings without restart
|
|
@@ -51,12 +52,14 @@ Dynamic: license-file
|
|
|
51
52
|
### PiFmRds Backend
|
|
52
53
|
- **Frequency Range**: 80.0 - 108.0 MHz (Standard FM band)
|
|
53
54
|
- **RDS Support**: ✅ Full support (PS, RT, PI)
|
|
55
|
+
- **Live Support**: ❌ No live support
|
|
54
56
|
- **Repository**: [ChristopheJacquet/PiFmRds](https://github.com/ChristopheJacquet/PiFmRds)
|
|
55
57
|
- **Best For**: Standard FM broadcasting with RDS features
|
|
56
58
|
|
|
57
59
|
### FmTransmitter Backend
|
|
58
60
|
- **Frequency Range**: 1.0 - 250.0 MHz (Extended range)
|
|
59
61
|
- **RDS Support**: ❌ No RDS support
|
|
62
|
+
- **Live Support**: ✅ Experimental support
|
|
60
63
|
- **Repository**: [markondej/fm_transmitter](https://github.com/markondej/fm_transmitter)
|
|
61
64
|
- **Best For**: Non-standard frequencies and experimental broadcasting
|
|
62
65
|
|
|
@@ -141,42 +144,6 @@ curl -sL https://setup.piwave.xyz/uninstall | sudo bash
|
|
|
141
144
|
make
|
|
142
145
|
```
|
|
143
146
|
|
|
144
|
-
## Backend Management
|
|
145
|
-
|
|
146
|
-
### CLI Commands
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
# Search for available backends on system
|
|
150
|
-
python3 -m piwave search
|
|
151
|
-
|
|
152
|
-
# List cached backends
|
|
153
|
-
python3 -m piwave list
|
|
154
|
-
|
|
155
|
-
# Manually add backend executable path
|
|
156
|
-
python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
|
|
157
|
-
|
|
158
|
-
# Show package information
|
|
159
|
-
python3 -m piwave info
|
|
160
|
-
|
|
161
|
-
# Broadcast a file directly
|
|
162
|
-
python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Programmatic Backend Discovery
|
|
166
|
-
|
|
167
|
-
```python
|
|
168
|
-
from piwave.backends import discover_backends, list_backends, search_backends
|
|
169
|
-
|
|
170
|
-
# Load cached backends
|
|
171
|
-
discover_backends()
|
|
172
|
-
|
|
173
|
-
# Search for new backends (ignores cache)
|
|
174
|
-
search_backends()
|
|
175
|
-
|
|
176
|
-
# List available backends with details
|
|
177
|
-
backends_info = list_backends()
|
|
178
|
-
```
|
|
179
|
-
|
|
180
147
|
## Quick Start
|
|
181
148
|
|
|
182
149
|
### Basic Usage
|
|
@@ -256,6 +223,44 @@ print(f"Backend supports RDS: {status['backend_supports_rds']}")
|
|
|
256
223
|
print(f"Frequency range: {status['backend_frequency_range']}")
|
|
257
224
|
```
|
|
258
225
|
|
|
226
|
+
|
|
227
|
+
## Backend Management
|
|
228
|
+
|
|
229
|
+
### CLI Commands
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Search for available backends on system
|
|
233
|
+
python3 -m piwave search
|
|
234
|
+
|
|
235
|
+
# List cached backends
|
|
236
|
+
python3 -m piwave list
|
|
237
|
+
|
|
238
|
+
# Manually add backend executable path
|
|
239
|
+
python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
|
|
240
|
+
|
|
241
|
+
# Show package information
|
|
242
|
+
python3 -m piwave info
|
|
243
|
+
|
|
244
|
+
# Broadcast a file directly
|
|
245
|
+
python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Programmatic Backend Discovery
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from piwave.backends import discover_backends, list_backends, search_backends
|
|
252
|
+
|
|
253
|
+
# Load cached backends
|
|
254
|
+
discover_backends()
|
|
255
|
+
|
|
256
|
+
# Search for new backends (ignores cache)
|
|
257
|
+
search_backends()
|
|
258
|
+
|
|
259
|
+
# List available backends with details
|
|
260
|
+
backends_info = list_backends()
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
|
|
259
264
|
## Complete Examples
|
|
260
265
|
|
|
261
266
|
<details>
|
|
@@ -443,16 +448,17 @@ if __name__ == "__main__":
|
|
|
443
448
|
|
|
444
449
|
```python
|
|
445
450
|
PiWave(
|
|
446
|
-
frequency=90.0,
|
|
447
|
-
ps="PiWave",
|
|
448
|
-
rt="PiWave: ...",
|
|
449
|
-
pi="FFFF",
|
|
450
|
-
debug=False,
|
|
451
|
-
silent=False,
|
|
452
|
-
loop=False,
|
|
453
|
-
backend="auto",
|
|
454
|
-
|
|
455
|
-
|
|
451
|
+
frequency=90.0, # Broadcast frequency (1.0-250.0 MHz)
|
|
452
|
+
ps="PiWave", # Program Service name (max 8 chars)
|
|
453
|
+
rt="PiWave: ...", # Radio Text (max 64 chars)
|
|
454
|
+
pi="FFFF", # Program Identifier (4 hex digits)
|
|
455
|
+
debug=False, # Enable debug logging
|
|
456
|
+
silent=False, # Disable all logging
|
|
457
|
+
loop=False, # Loop current track continuously
|
|
458
|
+
backend="auto", # Backend selection ("auto", "pi_fm_rds", "fm_transmitter")
|
|
459
|
+
used_for="file_broadcast", # Backend main purpose, used if backend = auto ("file_broadcast", "live_broadcast")
|
|
460
|
+
on_track_change=None, # Callback for track changes
|
|
461
|
+
on_error=None # Callback for errors
|
|
456
462
|
)
|
|
457
463
|
```
|
|
458
464
|
|
|
@@ -495,6 +501,7 @@ status = pw.get_status()
|
|
|
495
501
|
# Returns:
|
|
496
502
|
{
|
|
497
503
|
'is_playing': bool,
|
|
504
|
+
'is_live_streaming': bool,
|
|
498
505
|
'frequency': float,
|
|
499
506
|
'current_file': str|None,
|
|
500
507
|
'current_backend': str,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- **Multi-Backend Architecture**: Supports multiple backends for different actions
|
|
11
11
|
- **Wide Frequency Support**: 1-250 MHz coverage through different backends
|
|
12
12
|
- **RDS Support**: Program Service, Radio Text, and Program Identifier broadcasting
|
|
13
|
+
- **Live Stream Support**: Broadcast live from a stream source.
|
|
13
14
|
- **Smart Backend Selection**: Automatically chooses the best backend to suit your needs
|
|
14
15
|
- **Audio Format Support**: Converts most audio formats (MP3, FLAC, M4A, etc.) to WAV
|
|
15
16
|
- **Real-time Settings Updates**: Change frequency, RDS data, and settings without restart
|
|
@@ -24,12 +25,14 @@
|
|
|
24
25
|
### PiFmRds Backend
|
|
25
26
|
- **Frequency Range**: 80.0 - 108.0 MHz (Standard FM band)
|
|
26
27
|
- **RDS Support**: ✅ Full support (PS, RT, PI)
|
|
28
|
+
- **Live Support**: ❌ No live support
|
|
27
29
|
- **Repository**: [ChristopheJacquet/PiFmRds](https://github.com/ChristopheJacquet/PiFmRds)
|
|
28
30
|
- **Best For**: Standard FM broadcasting with RDS features
|
|
29
31
|
|
|
30
32
|
### FmTransmitter Backend
|
|
31
33
|
- **Frequency Range**: 1.0 - 250.0 MHz (Extended range)
|
|
32
34
|
- **RDS Support**: ❌ No RDS support
|
|
35
|
+
- **Live Support**: ✅ Experimental support
|
|
33
36
|
- **Repository**: [markondej/fm_transmitter](https://github.com/markondej/fm_transmitter)
|
|
34
37
|
- **Best For**: Non-standard frequencies and experimental broadcasting
|
|
35
38
|
|
|
@@ -114,42 +117,6 @@ curl -sL https://setup.piwave.xyz/uninstall | sudo bash
|
|
|
114
117
|
make
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
## Backend Management
|
|
118
|
-
|
|
119
|
-
### CLI Commands
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
# Search for available backends on system
|
|
123
|
-
python3 -m piwave search
|
|
124
|
-
|
|
125
|
-
# List cached backends
|
|
126
|
-
python3 -m piwave list
|
|
127
|
-
|
|
128
|
-
# Manually add backend executable path
|
|
129
|
-
python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
|
|
130
|
-
|
|
131
|
-
# Show package information
|
|
132
|
-
python3 -m piwave info
|
|
133
|
-
|
|
134
|
-
# Broadcast a file directly
|
|
135
|
-
python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Programmatic Backend Discovery
|
|
139
|
-
|
|
140
|
-
```python
|
|
141
|
-
from piwave.backends import discover_backends, list_backends, search_backends
|
|
142
|
-
|
|
143
|
-
# Load cached backends
|
|
144
|
-
discover_backends()
|
|
145
|
-
|
|
146
|
-
# Search for new backends (ignores cache)
|
|
147
|
-
search_backends()
|
|
148
|
-
|
|
149
|
-
# List available backends with details
|
|
150
|
-
backends_info = list_backends()
|
|
151
|
-
```
|
|
152
|
-
|
|
153
120
|
## Quick Start
|
|
154
121
|
|
|
155
122
|
### Basic Usage
|
|
@@ -229,6 +196,44 @@ print(f"Backend supports RDS: {status['backend_supports_rds']}")
|
|
|
229
196
|
print(f"Frequency range: {status['backend_frequency_range']}")
|
|
230
197
|
```
|
|
231
198
|
|
|
199
|
+
|
|
200
|
+
## Backend Management
|
|
201
|
+
|
|
202
|
+
### CLI Commands
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Search for available backends on system
|
|
206
|
+
python3 -m piwave search
|
|
207
|
+
|
|
208
|
+
# List cached backends
|
|
209
|
+
python3 -m piwave list
|
|
210
|
+
|
|
211
|
+
# Manually add backend executable path
|
|
212
|
+
python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
|
|
213
|
+
|
|
214
|
+
# Show package information
|
|
215
|
+
python3 -m piwave info
|
|
216
|
+
|
|
217
|
+
# Broadcast a file directly
|
|
218
|
+
python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Programmatic Backend Discovery
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from piwave.backends import discover_backends, list_backends, search_backends
|
|
225
|
+
|
|
226
|
+
# Load cached backends
|
|
227
|
+
discover_backends()
|
|
228
|
+
|
|
229
|
+
# Search for new backends (ignores cache)
|
|
230
|
+
search_backends()
|
|
231
|
+
|
|
232
|
+
# List available backends with details
|
|
233
|
+
backends_info = list_backends()
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
|
|
232
237
|
## Complete Examples
|
|
233
238
|
|
|
234
239
|
<details>
|
|
@@ -416,16 +421,17 @@ if __name__ == "__main__":
|
|
|
416
421
|
|
|
417
422
|
```python
|
|
418
423
|
PiWave(
|
|
419
|
-
frequency=90.0,
|
|
420
|
-
ps="PiWave",
|
|
421
|
-
rt="PiWave: ...",
|
|
422
|
-
pi="FFFF",
|
|
423
|
-
debug=False,
|
|
424
|
-
silent=False,
|
|
425
|
-
loop=False,
|
|
426
|
-
backend="auto",
|
|
427
|
-
|
|
428
|
-
|
|
424
|
+
frequency=90.0, # Broadcast frequency (1.0-250.0 MHz)
|
|
425
|
+
ps="PiWave", # Program Service name (max 8 chars)
|
|
426
|
+
rt="PiWave: ...", # Radio Text (max 64 chars)
|
|
427
|
+
pi="FFFF", # Program Identifier (4 hex digits)
|
|
428
|
+
debug=False, # Enable debug logging
|
|
429
|
+
silent=False, # Disable all logging
|
|
430
|
+
loop=False, # Loop current track continuously
|
|
431
|
+
backend="auto", # Backend selection ("auto", "pi_fm_rds", "fm_transmitter")
|
|
432
|
+
used_for="file_broadcast", # Backend main purpose, used if backend = auto ("file_broadcast", "live_broadcast")
|
|
433
|
+
on_track_change=None, # Callback for track changes
|
|
434
|
+
on_error=None # Callback for errors
|
|
429
435
|
)
|
|
430
436
|
```
|
|
431
437
|
|
|
@@ -468,6 +474,7 @@ status = pw.get_status()
|
|
|
468
474
|
# Returns:
|
|
469
475
|
{
|
|
470
476
|
'is_playing': bool,
|
|
477
|
+
'is_live_streaming': bool,
|
|
471
478
|
'frequency': float,
|
|
472
479
|
'current_file': str|None,
|
|
473
480
|
'current_backend': str,
|
|
@@ -21,13 +21,20 @@ def get_best_backend(mode: str, frequency: float):
|
|
|
21
21
|
return "pi_fm_rds"
|
|
22
22
|
elif "fm_transmitter" in backends:
|
|
23
23
|
return "fm_transmitter"
|
|
24
|
-
|
|
25
24
|
else:
|
|
26
25
|
if "fm_transmitter" in backends:
|
|
27
26
|
return "fm_transmitter"
|
|
28
27
|
elif "pi_fm_rds" in backends:
|
|
29
28
|
return "pi_fm_rds"
|
|
30
29
|
|
|
30
|
+
elif mode == "live_broadcast":
|
|
31
|
+
if "fm_transmitter" in backends:
|
|
32
|
+
backend = backends["fm_transmitter"]()
|
|
33
|
+
min_freq, max_freq = backend.frequency_range
|
|
34
|
+
if min_freq <= frequency <= max_freq:
|
|
35
|
+
return "fm_transmitter"
|
|
36
|
+
|
|
37
|
+
return None
|
|
31
38
|
|
|
32
39
|
return None
|
|
33
40
|
|
|
@@ -38,6 +38,11 @@ class Backend(ABC):
|
|
|
38
38
|
@abstractmethod
|
|
39
39
|
def supports_rds(self):
|
|
40
40
|
pass
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def supports_live_streaming(self):
|
|
45
|
+
pass
|
|
41
46
|
|
|
42
47
|
@property
|
|
43
48
|
def cache_file(self):
|
|
@@ -205,6 +210,10 @@ class Backend(ABC):
|
|
|
205
210
|
@abstractmethod
|
|
206
211
|
def build_command(self, wav_file: str):
|
|
207
212
|
pass
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
def build_live_command(self):
|
|
216
|
+
pass
|
|
208
217
|
|
|
209
218
|
def validate_settings(self):
|
|
210
219
|
min_freq, max_freq = self.frequency_range
|
|
@@ -19,15 +19,26 @@ class FmTransmitterBackend(Backend):
|
|
|
19
19
|
def supports_rds(self):
|
|
20
20
|
return False
|
|
21
21
|
|
|
22
|
+
@property
|
|
23
|
+
def supports_live_streaming(self):
|
|
24
|
+
return True
|
|
25
|
+
|
|
22
26
|
def _get_executable_name(self):
|
|
23
27
|
return "fm_transmitter"
|
|
24
28
|
|
|
25
29
|
def _get_search_paths(self):
|
|
26
|
-
return ["/opt/PiWave/fm_transmitter", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"
|
|
30
|
+
return ["/opt/PiWave/fm_transmitter", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
|
|
27
31
|
|
|
28
32
|
def build_command(self, wav_file: str):
|
|
29
33
|
return [
|
|
30
34
|
'sudo', self.required_executable,
|
|
31
35
|
'-f', str(self.frequency),
|
|
32
36
|
wav_file
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def build_live_command(self):
|
|
40
|
+
return [
|
|
41
|
+
'sudo', self.required_executable,
|
|
42
|
+
'-f', str(self.frequency),
|
|
43
|
+
'-'
|
|
33
44
|
]
|
|
@@ -19,6 +19,10 @@ class PiFmRdsBackend(Backend):
|
|
|
19
19
|
def supports_rds(self):
|
|
20
20
|
return True
|
|
21
21
|
|
|
22
|
+
@property
|
|
23
|
+
def supports_live_streaming(self):
|
|
24
|
+
return False
|
|
25
|
+
|
|
22
26
|
def _get_executable_name(self):
|
|
23
27
|
return "pi_fm_rds"
|
|
24
28
|
|
|
@@ -40,3 +44,6 @@ class PiFmRdsBackend(Backend):
|
|
|
40
44
|
cmd.extend(['-pi', self.pi])
|
|
41
45
|
|
|
42
46
|
return cmd
|
|
47
|
+
|
|
48
|
+
def build_live_command(self):
|
|
49
|
+
return None # not supported sadly
|
|
@@ -10,7 +10,7 @@ import time
|
|
|
10
10
|
|
|
11
11
|
import tempfile
|
|
12
12
|
import shutil
|
|
13
|
-
import
|
|
13
|
+
import queue
|
|
14
14
|
from typing import Optional, Callable
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from urllib.parse import urlparse
|
|
@@ -24,16 +24,17 @@ class PiWaveError(Exception):
|
|
|
24
24
|
|
|
25
25
|
class PiWave:
|
|
26
26
|
def __init__(self,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
frequency: float = 90.0,
|
|
28
|
+
ps: str = "PiWave",
|
|
29
|
+
rt: str = "PiWave: The best python module for managing your pi radio",
|
|
30
|
+
pi: str = "FFFF",
|
|
31
|
+
debug: bool = False,
|
|
32
|
+
silent: bool = False,
|
|
33
|
+
loop: bool = False,
|
|
34
|
+
backend: str = "auto",
|
|
35
|
+
used_for: str = "file_broadcast",
|
|
36
|
+
on_track_change: Optional[Callable] = None,
|
|
37
|
+
on_error: Optional[Callable] = None):
|
|
37
38
|
"""Initialize PiWave FM transmitter.
|
|
38
39
|
|
|
39
40
|
:param frequency: FM frequency to broadcast on (80.0-108.0 MHz)
|
|
@@ -50,7 +51,9 @@ class PiWave:
|
|
|
50
51
|
:type silent: bool
|
|
51
52
|
:param loop: Loop the current track continuously (default: False)
|
|
52
53
|
:type loop: bool
|
|
53
|
-
:param backend: Chose a specific backend to handle the broadcast (default: auto)
|
|
54
|
+
:param backend: Chose a specific backend to handle the broadcast (default: auto). Supports `pi_fm_rds`, `fm_transmitter` and `auto`
|
|
55
|
+
:type backend: str
|
|
56
|
+
:param backend: Give the main use for the current instance, will be used if backend: auto (default: file_broadcast). Supports `file_broadcast` and `live_broadcast`
|
|
54
57
|
:type backend: str
|
|
55
58
|
:param on_track_change: Callback function called when track changes
|
|
56
59
|
:type on_track_change: Optional[Callable]
|
|
@@ -59,7 +62,7 @@ class PiWave:
|
|
|
59
62
|
:raises PiWaveError: If not running on Raspberry Pi or without root privileges
|
|
60
63
|
|
|
61
64
|
.. note::
|
|
62
|
-
This class requires pi_fm_rds to be installed and accessible.
|
|
65
|
+
This class requires pi_fm_rds or fm_transmitter to be installed and accessible.
|
|
63
66
|
Must be run on a Raspberry Pi with root privileges.
|
|
64
67
|
"""
|
|
65
68
|
|
|
@@ -79,6 +82,10 @@ class PiWave:
|
|
|
79
82
|
self.playback_thread: Optional[threading.Thread] = None
|
|
80
83
|
self.stop_event = threading.Event()
|
|
81
84
|
self.temp_dir = tempfile.mkdtemp(prefix="piwave_")
|
|
85
|
+
|
|
86
|
+
self.is_live_streaming = False
|
|
87
|
+
self.live_thread: Optional[threading.Thread] = None
|
|
88
|
+
self.audio_queue: Optional[queue.Queue] = None
|
|
82
89
|
|
|
83
90
|
Log.config(silent=silent)
|
|
84
91
|
|
|
@@ -87,11 +94,13 @@ class PiWave:
|
|
|
87
94
|
|
|
88
95
|
discover_backends()
|
|
89
96
|
|
|
97
|
+
self.backend_use = used_for
|
|
98
|
+
|
|
90
99
|
if backend == "auto":
|
|
91
|
-
backend_name = get_best_backend(
|
|
100
|
+
backend_name = get_best_backend(self.backend_use, self.frequency)
|
|
92
101
|
if not backend_name:
|
|
93
102
|
available = list(backends.keys())
|
|
94
|
-
raise PiWaveError(f"No suitable backend found for {self.frequency}MHz. Available backends: {available}")
|
|
103
|
+
raise PiWaveError(f"No suitable backend found for {self.frequency}MHz and {self.backend_use} mode. Available backends: {available}")
|
|
95
104
|
else:
|
|
96
105
|
if backend not in backends:
|
|
97
106
|
available = list(backends.keys())
|
|
@@ -113,6 +122,7 @@ class PiWave:
|
|
|
113
122
|
pi=self.pi
|
|
114
123
|
)
|
|
115
124
|
|
|
125
|
+
|
|
116
126
|
min_freq, max_freq = self.backend.frequency_range
|
|
117
127
|
rds_support = "with RDS" if self.backend.supports_rds else "no RDS"
|
|
118
128
|
Log.info(f"Using {self.backend.name} backend ({min_freq}-{max_freq}MHz, {rds_support})")
|
|
@@ -255,7 +265,7 @@ class PiWave:
|
|
|
255
265
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, ValueError):
|
|
256
266
|
return 0.0
|
|
257
267
|
|
|
258
|
-
def
|
|
268
|
+
def _play_file(self, wav_file: str) -> bool:
|
|
259
269
|
if self.stop_event.is_set():
|
|
260
270
|
return False
|
|
261
271
|
|
|
@@ -315,6 +325,141 @@ class PiWave:
|
|
|
315
325
|
self.on_error(e)
|
|
316
326
|
self._stop_current_process()
|
|
317
327
|
return False
|
|
328
|
+
|
|
329
|
+
def _playback_worker_wrapper(self):
|
|
330
|
+
# wrapper for non-blocking playback
|
|
331
|
+
try:
|
|
332
|
+
wav_file = self._convert_to_wav(self.current_file)
|
|
333
|
+
if not wav_file:
|
|
334
|
+
Log.error(f"Failed to convert {self.current_file}")
|
|
335
|
+
self.is_playing = False
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
if not os.path.exists(wav_file):
|
|
339
|
+
Log.error(f"File not found: {wav_file}")
|
|
340
|
+
self.is_playing = False
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
self._play_file(wav_file)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
Log.error(f"Playback error: {e}")
|
|
346
|
+
if self.on_error:
|
|
347
|
+
self.on_error(e)
|
|
348
|
+
finally:
|
|
349
|
+
self.is_playing = False
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _play_live(self, audio_source, sample_rate: int, channels: int, chunk_size: int) -> bool:
|
|
353
|
+
if self.is_playing or self.is_live_streaming:
|
|
354
|
+
self.stop()
|
|
355
|
+
|
|
356
|
+
if not self.backend.supports_live_streaming:
|
|
357
|
+
raise PiWaveError(
|
|
358
|
+
f"Backend '{self.backend_name}' doesn't support live streaming. Try using fm_transmitter instead.")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
min_freq, max_freq = self.backend.frequency_range
|
|
362
|
+
if not (min_freq <= self.frequency <= max_freq):
|
|
363
|
+
raise PiWaveError(
|
|
364
|
+
f"Backend '{self.backend_name}' doesn't support {self.frequency}MHz"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
self.stop_event.clear()
|
|
368
|
+
self.is_live_streaming = True
|
|
369
|
+
self.audio_queue = queue.Queue(maxsize=20)
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
cmd = self.backend.build_live_command()
|
|
373
|
+
if not cmd:
|
|
374
|
+
raise PiWaveError(f"Backend doesn't support live streaming") # since we checked before, we shouldnt get this but meh
|
|
375
|
+
|
|
376
|
+
self.current_process = subprocess.Popen(
|
|
377
|
+
cmd,
|
|
378
|
+
stdin=subprocess.PIPE,
|
|
379
|
+
stdout=subprocess.PIPE,
|
|
380
|
+
stderr=subprocess.PIPE,
|
|
381
|
+
preexec_fn=os.setsid
|
|
382
|
+
)
|
|
383
|
+
except Exception as e:
|
|
384
|
+
Log.error(f"Failed to start live stream: {e}")
|
|
385
|
+
self.is_live_streaming = False
|
|
386
|
+
if self.on_error:
|
|
387
|
+
self.on_error(e)
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
self.live_thread = threading.Thread(
|
|
391
|
+
target=self._live_producer_worker,
|
|
392
|
+
args=(audio_source, chunk_size)
|
|
393
|
+
)
|
|
394
|
+
self.live_thread.daemon = True
|
|
395
|
+
self.live_thread.start()
|
|
396
|
+
|
|
397
|
+
consumer_thread = threading.Thread(target=self._live_consumer_worker)
|
|
398
|
+
consumer_thread.daemon = True
|
|
399
|
+
consumer_thread.start()
|
|
400
|
+
|
|
401
|
+
Log.broadcast_message(f"Live streaming at {self.frequency}MHz ({sample_rate}Hz, {channels}ch)")
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
def _live_producer_worker(self, audio_source, chunk_size: int):
|
|
405
|
+
# producer: reads from audio source, puts in queue; consumer will play it
|
|
406
|
+
try:
|
|
407
|
+
if hasattr(audio_source, '__iter__') and not isinstance(audio_source, (str, bytes)):
|
|
408
|
+
for chunk in audio_source:
|
|
409
|
+
if self.stop_event.is_set():
|
|
410
|
+
break
|
|
411
|
+
if chunk:
|
|
412
|
+
self.audio_queue.put(chunk, timeout=1)
|
|
413
|
+
|
|
414
|
+
elif callable(audio_source):
|
|
415
|
+
while not self.stop_event.is_set():
|
|
416
|
+
chunk = audio_source()
|
|
417
|
+
if not chunk:
|
|
418
|
+
break
|
|
419
|
+
self.audio_queue.put(chunk, timeout=1)
|
|
420
|
+
|
|
421
|
+
elif hasattr(audio_source, 'read'):
|
|
422
|
+
while not self.stop_event.is_set():
|
|
423
|
+
chunk = audio_source.read(chunk_size)
|
|
424
|
+
if not chunk:
|
|
425
|
+
break
|
|
426
|
+
self.audio_queue.put(chunk, timeout=1)
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
Log.error(f"Producer error: {e}")
|
|
430
|
+
if self.on_error:
|
|
431
|
+
self.on_error(e)
|
|
432
|
+
finally:
|
|
433
|
+
self.audio_queue.put(None)
|
|
434
|
+
|
|
435
|
+
def _live_consumer_worker(self):
|
|
436
|
+
# consumer reads from queue and puts in process
|
|
437
|
+
try:
|
|
438
|
+
while not self.stop_event.is_set():
|
|
439
|
+
try:
|
|
440
|
+
chunk = self.audio_queue.get(timeout=0.1)
|
|
441
|
+
if chunk is None:
|
|
442
|
+
break
|
|
443
|
+
|
|
444
|
+
if self.current_process and self.current_process.stdin:
|
|
445
|
+
self.current_process.stdin.write(chunk)
|
|
446
|
+
self.current_process.stdin.flush()
|
|
447
|
+
|
|
448
|
+
except queue.Empty:
|
|
449
|
+
continue
|
|
450
|
+
except BrokenPipeError:
|
|
451
|
+
Log.error(f"Stream process terminated: make sure that the stream you provided compiles with {self.backend_name} stdin support.")
|
|
452
|
+
break
|
|
453
|
+
except Exception as e:
|
|
454
|
+
Log.error(f"Write error: {e}")
|
|
455
|
+
break
|
|
456
|
+
finally:
|
|
457
|
+
if self.current_process and self.current_process.stdin:
|
|
458
|
+
try:
|
|
459
|
+
self.current_process.stdin.close()
|
|
460
|
+
except:
|
|
461
|
+
pass
|
|
462
|
+
self.is_live_streaming = False
|
|
318
463
|
|
|
319
464
|
|
|
320
465
|
def _stop_current_process(self):
|
|
@@ -365,38 +510,45 @@ class PiWave:
|
|
|
365
510
|
self.stop()
|
|
366
511
|
os._exit(0)
|
|
367
512
|
|
|
368
|
-
def play(self,
|
|
369
|
-
"""
|
|
370
|
-
|
|
371
|
-
:param file_path: Path to local audio file
|
|
372
|
-
:type file_path: str
|
|
373
|
-
:return: True if playback started successfully, False otherwise
|
|
374
|
-
:rtype: bool
|
|
513
|
+
def play(self, source, sample_rate: int = 44100, channels: int = 2, chunk_size: int = 4096, blocking: bool = False):
|
|
514
|
+
"""Play audio from file or live source.
|
|
375
515
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
516
|
+
:param source: Either a file path (str) or live audio source (generator/callable/file-like)
|
|
517
|
+
:param sample_rate: Sample rate for live audio (ignored for files)
|
|
518
|
+
:param channels: Channels for live audio (ignored for files)
|
|
519
|
+
:param chunk_size: Chunk size for live audio (ignored for files)
|
|
520
|
+
:param blocking: If the playback should be blocking or not (ignored for live, always non-blocking)
|
|
521
|
+
:return: True if playback/streaming started successfully
|
|
522
|
+
:rtype: bool
|
|
380
523
|
|
|
381
524
|
Example:
|
|
382
|
-
>>> pw.play('song.mp3')
|
|
383
|
-
>>> pw.play(
|
|
525
|
+
>>> pw.play('song.mp3') # File playback
|
|
526
|
+
>>> pw.play(mic_generator()) # Live streaming
|
|
384
527
|
"""
|
|
528
|
+
|
|
529
|
+
# autodetect if source is live or file
|
|
530
|
+
if isinstance(source, str):
|
|
531
|
+
# file (string)
|
|
532
|
+
if blocking:
|
|
533
|
+
return self._play_file(source)
|
|
534
|
+
else:
|
|
535
|
+
if self.is_playing:
|
|
536
|
+
self.stop()
|
|
537
|
+
|
|
538
|
+
self.current_file = source
|
|
539
|
+
self.is_playing = True
|
|
540
|
+
self.stop_event.clear()
|
|
541
|
+
|
|
542
|
+
self.playback_thread = threading.Thread(
|
|
543
|
+
target=self._playback_worker_wrapper,
|
|
544
|
+
daemon=True
|
|
545
|
+
)
|
|
546
|
+
self.playback_thread.start()
|
|
547
|
+
return True
|
|
385
548
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
self.current_file = file_path
|
|
390
|
-
self.stop_event.clear()
|
|
391
|
-
self.is_stopped = False
|
|
392
|
-
self.is_playing = True
|
|
393
|
-
|
|
394
|
-
self.playback_thread = threading.Thread(target=self._playback_worker)
|
|
395
|
-
self.playback_thread.daemon = True
|
|
396
|
-
self.playback_thread.start()
|
|
397
|
-
|
|
398
|
-
Log.success("Playback started")
|
|
399
|
-
return True
|
|
549
|
+
else:
|
|
550
|
+
# live
|
|
551
|
+
return self._play_live(source, sample_rate, channels, chunk_size)
|
|
400
552
|
|
|
401
553
|
def stop(self):
|
|
402
554
|
"""Stop all playback and streaming.
|
|
@@ -407,14 +559,21 @@ class PiWave:
|
|
|
407
559
|
Example:
|
|
408
560
|
>>> pw.stop()
|
|
409
561
|
"""
|
|
410
|
-
if not self.is_playing:
|
|
562
|
+
if not self.is_playing and not self.is_live_streaming:
|
|
411
563
|
return
|
|
412
564
|
|
|
413
|
-
Log.warning("Stopping
|
|
414
|
-
|
|
565
|
+
Log.warning("Stopping...")
|
|
566
|
+
|
|
415
567
|
self.is_stopped = True
|
|
416
568
|
self.stop_event.set()
|
|
417
|
-
|
|
569
|
+
|
|
570
|
+
if self.audio_queue:
|
|
571
|
+
while not self.audio_queue.empty():
|
|
572
|
+
try:
|
|
573
|
+
self.audio_queue.get_nowait()
|
|
574
|
+
except queue.Empty:
|
|
575
|
+
break
|
|
576
|
+
|
|
418
577
|
if self.current_process:
|
|
419
578
|
try:
|
|
420
579
|
os.killpg(os.getpgid(self.current_process.pid), signal.SIGTERM)
|
|
@@ -423,12 +582,15 @@ class PiWave:
|
|
|
423
582
|
pass
|
|
424
583
|
finally:
|
|
425
584
|
self.current_process = None
|
|
426
|
-
|
|
585
|
+
|
|
427
586
|
if self.playback_thread and self.playback_thread.is_alive():
|
|
428
587
|
self.playback_thread.join(timeout=5)
|
|
429
|
-
|
|
588
|
+
if self.live_thread and self.live_thread.is_alive():
|
|
589
|
+
self.live_thread.join(timeout=3)
|
|
590
|
+
|
|
430
591
|
self.is_playing = False
|
|
431
|
-
|
|
592
|
+
self.is_live_streaming = False
|
|
593
|
+
Log.success("Stopped")
|
|
432
594
|
|
|
433
595
|
def pause(self):
|
|
434
596
|
"""Pause the current playback.
|
|
@@ -455,16 +617,17 @@ class PiWave:
|
|
|
455
617
|
self.play(self.current_file)
|
|
456
618
|
|
|
457
619
|
def update(self,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
620
|
+
frequency: Optional[float] = None,
|
|
621
|
+
ps: Optional[str] = None,
|
|
622
|
+
rt: Optional[str] = None,
|
|
623
|
+
pi: Optional[str] = None,
|
|
624
|
+
debug: Optional[bool] = None,
|
|
625
|
+
silent: Optional[bool] = None,
|
|
626
|
+
loop: Optional[bool] = None,
|
|
627
|
+
backend: Optional[str] = None,
|
|
628
|
+
used_for: Optional[str] = None,
|
|
629
|
+
on_track_change: Optional[Callable] = None,
|
|
630
|
+
on_error: Optional[Callable] = None):
|
|
468
631
|
"""Update PiWave settings.
|
|
469
632
|
|
|
470
633
|
:param frequency: FM frequency to broadcast on (80.0-108.0 MHz)
|
|
@@ -483,6 +646,8 @@ class PiWave:
|
|
|
483
646
|
:type loop: Optional[bool]
|
|
484
647
|
:param backend: Backend used to broadcast
|
|
485
648
|
:type backend: Optional[str]
|
|
649
|
+
:param backend: Give the main use for the current instance, will be used if backend: auto. Supports `file_broadcast` and `live_broadcast`
|
|
650
|
+
:type backend: Optional[str]
|
|
486
651
|
:param on_track_change: Callback function called when track changes
|
|
487
652
|
:type on_track_change: Optional[Callable]
|
|
488
653
|
:param on_error: Callback function called when an error occurs
|
|
@@ -499,9 +664,12 @@ class PiWave:
|
|
|
499
664
|
|
|
500
665
|
freq_to_use = frequency if frequency is not None else self.frequency
|
|
501
666
|
|
|
667
|
+
if used_for is not None:
|
|
668
|
+
self.backend_use = used_for
|
|
669
|
+
|
|
502
670
|
if backend is not None:
|
|
503
671
|
if backend == "auto":
|
|
504
|
-
backend_name = get_best_backend(
|
|
672
|
+
backend_name = get_best_backend(self.backend_use, freq_to_use)
|
|
505
673
|
if not backend_name:
|
|
506
674
|
available = list(backends.keys())
|
|
507
675
|
raise PiWaveError(f"No suitable backend found for {freq_to_use}MHz. Available: {available}")
|
|
@@ -608,6 +776,7 @@ class PiWave:
|
|
|
608
776
|
The returned dictionary contains:
|
|
609
777
|
|
|
610
778
|
- **is_playing** (bool): Whether playback is active
|
|
779
|
+
- **is_live_streaming** (bool): Whether live playback is active
|
|
611
780
|
- **frequency** (float): Current broadcast frequency
|
|
612
781
|
- **current_file** (str|None): Path of currently playing file
|
|
613
782
|
- **current_backend** (str): Currently used backend
|
|
@@ -627,6 +796,7 @@ class PiWave:
|
|
|
627
796
|
"""
|
|
628
797
|
return {
|
|
629
798
|
'is_playing': self.is_playing,
|
|
799
|
+
'is_live_streaming': self.is_live_streaming,
|
|
630
800
|
'frequency': self.frequency,
|
|
631
801
|
'current_file': self.current_file,
|
|
632
802
|
'current_backend': self.backend_name,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: piwave
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: A python module to broadcast radio waves with your Raspberry Pi.
|
|
5
5
|
Home-page: https://github.com/douxxtech/piwave
|
|
6
6
|
Author: Douxx
|
|
@@ -37,6 +37,7 @@ Dynamic: license-file
|
|
|
37
37
|
- **Multi-Backend Architecture**: Supports multiple backends for different actions
|
|
38
38
|
- **Wide Frequency Support**: 1-250 MHz coverage through different backends
|
|
39
39
|
- **RDS Support**: Program Service, Radio Text, and Program Identifier broadcasting
|
|
40
|
+
- **Live Stream Support**: Broadcast live from a stream source.
|
|
40
41
|
- **Smart Backend Selection**: Automatically chooses the best backend to suit your needs
|
|
41
42
|
- **Audio Format Support**: Converts most audio formats (MP3, FLAC, M4A, etc.) to WAV
|
|
42
43
|
- **Real-time Settings Updates**: Change frequency, RDS data, and settings without restart
|
|
@@ -51,12 +52,14 @@ Dynamic: license-file
|
|
|
51
52
|
### PiFmRds Backend
|
|
52
53
|
- **Frequency Range**: 80.0 - 108.0 MHz (Standard FM band)
|
|
53
54
|
- **RDS Support**: ✅ Full support (PS, RT, PI)
|
|
55
|
+
- **Live Support**: ❌ No live support
|
|
54
56
|
- **Repository**: [ChristopheJacquet/PiFmRds](https://github.com/ChristopheJacquet/PiFmRds)
|
|
55
57
|
- **Best For**: Standard FM broadcasting with RDS features
|
|
56
58
|
|
|
57
59
|
### FmTransmitter Backend
|
|
58
60
|
- **Frequency Range**: 1.0 - 250.0 MHz (Extended range)
|
|
59
61
|
- **RDS Support**: ❌ No RDS support
|
|
62
|
+
- **Live Support**: ✅ Experimental support
|
|
60
63
|
- **Repository**: [markondej/fm_transmitter](https://github.com/markondej/fm_transmitter)
|
|
61
64
|
- **Best For**: Non-standard frequencies and experimental broadcasting
|
|
62
65
|
|
|
@@ -141,42 +144,6 @@ curl -sL https://setup.piwave.xyz/uninstall | sudo bash
|
|
|
141
144
|
make
|
|
142
145
|
```
|
|
143
146
|
|
|
144
|
-
## Backend Management
|
|
145
|
-
|
|
146
|
-
### CLI Commands
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
# Search for available backends on system
|
|
150
|
-
python3 -m piwave search
|
|
151
|
-
|
|
152
|
-
# List cached backends
|
|
153
|
-
python3 -m piwave list
|
|
154
|
-
|
|
155
|
-
# Manually add backend executable path
|
|
156
|
-
python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
|
|
157
|
-
|
|
158
|
-
# Show package information
|
|
159
|
-
python3 -m piwave info
|
|
160
|
-
|
|
161
|
-
# Broadcast a file directly
|
|
162
|
-
python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Programmatic Backend Discovery
|
|
166
|
-
|
|
167
|
-
```python
|
|
168
|
-
from piwave.backends import discover_backends, list_backends, search_backends
|
|
169
|
-
|
|
170
|
-
# Load cached backends
|
|
171
|
-
discover_backends()
|
|
172
|
-
|
|
173
|
-
# Search for new backends (ignores cache)
|
|
174
|
-
search_backends()
|
|
175
|
-
|
|
176
|
-
# List available backends with details
|
|
177
|
-
backends_info = list_backends()
|
|
178
|
-
```
|
|
179
|
-
|
|
180
147
|
## Quick Start
|
|
181
148
|
|
|
182
149
|
### Basic Usage
|
|
@@ -256,6 +223,44 @@ print(f"Backend supports RDS: {status['backend_supports_rds']}")
|
|
|
256
223
|
print(f"Frequency range: {status['backend_frequency_range']}")
|
|
257
224
|
```
|
|
258
225
|
|
|
226
|
+
|
|
227
|
+
## Backend Management
|
|
228
|
+
|
|
229
|
+
### CLI Commands
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Search for available backends on system
|
|
233
|
+
python3 -m piwave search
|
|
234
|
+
|
|
235
|
+
# List cached backends
|
|
236
|
+
python3 -m piwave list
|
|
237
|
+
|
|
238
|
+
# Manually add backend executable path
|
|
239
|
+
python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
|
|
240
|
+
|
|
241
|
+
# Show package information
|
|
242
|
+
python3 -m piwave info
|
|
243
|
+
|
|
244
|
+
# Broadcast a file directly
|
|
245
|
+
python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Programmatic Backend Discovery
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from piwave.backends import discover_backends, list_backends, search_backends
|
|
252
|
+
|
|
253
|
+
# Load cached backends
|
|
254
|
+
discover_backends()
|
|
255
|
+
|
|
256
|
+
# Search for new backends (ignores cache)
|
|
257
|
+
search_backends()
|
|
258
|
+
|
|
259
|
+
# List available backends with details
|
|
260
|
+
backends_info = list_backends()
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
|
|
259
264
|
## Complete Examples
|
|
260
265
|
|
|
261
266
|
<details>
|
|
@@ -443,16 +448,17 @@ if __name__ == "__main__":
|
|
|
443
448
|
|
|
444
449
|
```python
|
|
445
450
|
PiWave(
|
|
446
|
-
frequency=90.0,
|
|
447
|
-
ps="PiWave",
|
|
448
|
-
rt="PiWave: ...",
|
|
449
|
-
pi="FFFF",
|
|
450
|
-
debug=False,
|
|
451
|
-
silent=False,
|
|
452
|
-
loop=False,
|
|
453
|
-
backend="auto",
|
|
454
|
-
|
|
455
|
-
|
|
451
|
+
frequency=90.0, # Broadcast frequency (1.0-250.0 MHz)
|
|
452
|
+
ps="PiWave", # Program Service name (max 8 chars)
|
|
453
|
+
rt="PiWave: ...", # Radio Text (max 64 chars)
|
|
454
|
+
pi="FFFF", # Program Identifier (4 hex digits)
|
|
455
|
+
debug=False, # Enable debug logging
|
|
456
|
+
silent=False, # Disable all logging
|
|
457
|
+
loop=False, # Loop current track continuously
|
|
458
|
+
backend="auto", # Backend selection ("auto", "pi_fm_rds", "fm_transmitter")
|
|
459
|
+
used_for="file_broadcast", # Backend main purpose, used if backend = auto ("file_broadcast", "live_broadcast")
|
|
460
|
+
on_track_change=None, # Callback for track changes
|
|
461
|
+
on_error=None # Callback for errors
|
|
456
462
|
)
|
|
457
463
|
```
|
|
458
464
|
|
|
@@ -495,6 +501,7 @@ status = pw.get_status()
|
|
|
495
501
|
# Returns:
|
|
496
502
|
{
|
|
497
503
|
'is_playing': bool,
|
|
504
|
+
'is_live_streaming': bool,
|
|
498
505
|
'frequency': float,
|
|
499
506
|
'current_file': str|None,
|
|
500
507
|
'current_backend': str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|