piwave 2.1.10__tar.gz → 2.1.11__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.10 → piwave-2.1.11}/PKG-INFO +2 -4
- {piwave-2.1.10 → piwave-2.1.11}/README.md +2 -4
- {piwave-2.1.10 → piwave-2.1.11}/piwave/backends/base.py +15 -1
- {piwave-2.1.10 → piwave-2.1.11}/piwave/logger.py +11 -3
- {piwave-2.1.10 → piwave-2.1.11}/piwave/piwave.py +31 -66
- {piwave-2.1.10 → piwave-2.1.11}/piwave.egg-info/PKG-INFO +2 -4
- {piwave-2.1.10 → piwave-2.1.11}/setup.cfg +1 -1
- {piwave-2.1.10 → piwave-2.1.11}/LICENSE +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave/__init__.py +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave/__main__.py +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave/backends/__init__.py +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave/backends/fm_transmitter.py +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave/backends/pi_fm_rds.py +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave.egg-info/SOURCES.txt +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave.egg-info/dependency_links.txt +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave.egg-info/requires.txt +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/piwave.egg-info/top_level.txt +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/pyproject.toml +0 -0
- {piwave-2.1.10 → piwave-2.1.11}/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.11
|
|
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
|
|
@@ -31,9 +31,6 @@ Dynamic: license-file
|
|
|
31
31
|
<h1>PiWave</h1>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
> [!CAUTION]
|
|
35
|
-
> `piwave==2.1.6` is broken ! Please use `piwave==2.1.5`
|
|
36
|
-
|
|
37
34
|
**PiWave** is a Python module designed to manage and control your Raspberry Pi radio using multiple FM transmission backends. It provides a unified interface for broadcasting audio files with multiple backends support and RDS (Radio Data System) support.
|
|
38
35
|
|
|
39
36
|
## Features
|
|
@@ -761,4 +758,5 @@ Please submit pull requests or open issues on [GitHub](https://github.com/douxxt
|
|
|
761
758
|
|
|
762
759
|
**PiWave** - FM Broadcasting module for Raspberry Pi
|
|
763
760
|
|
|
761
|
+
|
|
764
762
|

|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
<h1>PiWave</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
> [!CAUTION]
|
|
7
|
-
> `piwave==2.1.6` is broken ! Please use `piwave==2.1.5`
|
|
8
|
-
|
|
9
6
|
**PiWave** is a Python module designed to manage and control your Raspberry Pi radio using multiple FM transmission backends. It provides a unified interface for broadcasting audio files with multiple backends support and RDS (Radio Data System) support.
|
|
10
7
|
|
|
11
8
|
## Features
|
|
@@ -733,4 +730,5 @@ Please submit pull requests or open issues on [GitHub](https://github.com/douxxt
|
|
|
733
730
|
|
|
734
731
|
**PiWave** - FM Broadcasting module for Raspberry Pi
|
|
735
732
|
|
|
736
|
-
|
|
733
|
+
|
|
734
|
+

|
|
@@ -6,10 +6,11 @@ from abc import ABC, abstractmethod
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
import subprocess
|
|
8
8
|
import os
|
|
9
|
-
import signal
|
|
10
9
|
import shutil
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
|
|
12
|
+
from ..logger import Log
|
|
13
|
+
|
|
13
14
|
class BackendError(Exception):
|
|
14
15
|
pass
|
|
15
16
|
|
|
@@ -63,18 +64,25 @@ class Backend(ABC):
|
|
|
63
64
|
|
|
64
65
|
def _find_executable(self) -> str:
|
|
65
66
|
# Check cache first
|
|
67
|
+
Log.debug(f"Checking cache at {self.cache_file}")
|
|
68
|
+
|
|
66
69
|
if self.cache_file.exists():
|
|
67
70
|
try:
|
|
68
71
|
cached_path = self.cache_file.read_text().strip()
|
|
69
72
|
if self._is_valid_executable(cached_path):
|
|
73
|
+
Log.debug(f"Cache hit: {cached_path}")
|
|
70
74
|
return cached_path
|
|
71
75
|
else:
|
|
72
76
|
self.cache_file.unlink()
|
|
73
77
|
except Exception:
|
|
74
78
|
self.cache_file.unlink(missing_ok=True)
|
|
79
|
+
|
|
80
|
+
Log.debug(f"Cache miss, searching filesystem...")
|
|
75
81
|
|
|
76
82
|
# Only then search for it
|
|
77
83
|
found_path = self._search_executable()
|
|
84
|
+
Log.debug(f"Found executable at: {found_path}")
|
|
85
|
+
|
|
78
86
|
if found_path:
|
|
79
87
|
try:
|
|
80
88
|
self.cache_file.write_text(found_path)
|
|
@@ -82,6 +90,8 @@ class Backend(ABC):
|
|
|
82
90
|
pass
|
|
83
91
|
return found_path
|
|
84
92
|
|
|
93
|
+
Log.debug(f"Failed to find {self.name} on the filesystem.")
|
|
94
|
+
|
|
85
95
|
raise BackendError(f"Could not find path for {self.name}. Please manually add one with python3 -m piwave add {self.name} <path>")
|
|
86
96
|
|
|
87
97
|
@abstractmethod
|
|
@@ -230,12 +240,16 @@ class Backend(ABC):
|
|
|
230
240
|
raise BackendError(f"{self.name} supports {min_freq}-{max_freq}MHz, got {self.frequency}MHz")
|
|
231
241
|
|
|
232
242
|
cmd = self.build_command(wav_file, loop)
|
|
243
|
+
Log.debug(f"Build command: {' '.join(cmd)}")
|
|
233
244
|
self.current_process = subprocess.Popen(
|
|
234
245
|
cmd,
|
|
235
246
|
stdout=subprocess.DEVNULL,
|
|
236
247
|
stderr=subprocess.DEVNULL,
|
|
237
248
|
stdin=subprocess.DEVNULL
|
|
238
249
|
)
|
|
250
|
+
|
|
251
|
+
Log.debug(f"Process PID: {self.current_process.pid}")
|
|
252
|
+
|
|
239
253
|
return self.current_process
|
|
240
254
|
|
|
241
255
|
def stop(self):
|
|
@@ -12,7 +12,8 @@ class Logger(DLogger):
|
|
|
12
12
|
'warning': 'WARN',
|
|
13
13
|
'info': 'INFO',
|
|
14
14
|
'file': 'FILE',
|
|
15
|
-
'broadcast': 'BCAST'
|
|
15
|
+
'broadcast': 'BCAST',
|
|
16
|
+
'debug': 'DEBUG'
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
STYLES = {
|
|
@@ -21,10 +22,12 @@ class Logger(DLogger):
|
|
|
21
22
|
'warning': 'bright_yellow',
|
|
22
23
|
'info': 'bright_cyan',
|
|
23
24
|
'file': 'yellow',
|
|
24
|
-
'broadcast': 'bright_magenta'
|
|
25
|
+
'broadcast': 'bright_magenta',
|
|
26
|
+
'debug': 'orange'
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
SILENT = False
|
|
30
|
+
DEBUG = False
|
|
28
31
|
|
|
29
32
|
def __init__(self):
|
|
30
33
|
# Initialize with prebuilt icons & styles and silent support.
|
|
@@ -35,12 +38,17 @@ class Logger(DLogger):
|
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
@classmethod
|
|
38
|
-
def config(self, silent: bool = False):
|
|
41
|
+
def config(self, silent: bool = False, debug: bool = False):
|
|
39
42
|
self.SILENT = silent
|
|
43
|
+
self.DEBUG = debug
|
|
40
44
|
|
|
41
45
|
def print(self, message: str, style: str = '', icon: str = '', end: str = '\n'):
|
|
42
46
|
if self.SILENT:
|
|
43
47
|
return
|
|
48
|
+
|
|
49
|
+
if icon == "DEBUG" and not self.DEBUG:
|
|
50
|
+
return
|
|
51
|
+
|
|
44
52
|
super().print(message, style, icon, end)
|
|
45
53
|
|
|
46
54
|
Log = Logger()
|
|
@@ -84,10 +84,12 @@ class PiWave:
|
|
|
84
84
|
self.live_thread: Optional[threading.Thread] = None
|
|
85
85
|
self.audio_queue: Optional[queue.Queue] = None
|
|
86
86
|
|
|
87
|
-
Log.config(silent=silent)
|
|
87
|
+
Log.config(silent=silent, debug=debug)
|
|
88
88
|
|
|
89
|
+
Log.debug(f"Validating environment...")
|
|
89
90
|
self._validate_environment()
|
|
90
91
|
|
|
92
|
+
Log.debug(f"Discovering backends...")
|
|
91
93
|
discover_backends()
|
|
92
94
|
|
|
93
95
|
self.backend_use = used_for
|
|
@@ -118,6 +120,9 @@ class PiWave:
|
|
|
118
120
|
pi=self.pi
|
|
119
121
|
)
|
|
120
122
|
|
|
123
|
+
Log.debug(f"Selected backend: {backend_name}")
|
|
124
|
+
|
|
125
|
+
|
|
121
126
|
|
|
122
127
|
min_freq, max_freq = self.backend.frequency_range
|
|
123
128
|
rds_support = "with RDS" if self.backend.supports_rds else "no RDS"
|
|
@@ -127,11 +132,6 @@ class PiWave:
|
|
|
127
132
|
|
|
128
133
|
Log.info(f"PiWave initialized - Frequency: {frequency}MHz, PS: {ps}, Loop: {loop}")
|
|
129
134
|
|
|
130
|
-
def _log_debug(self, message: str):
|
|
131
|
-
if self.debug:
|
|
132
|
-
Log.print(f"[DEBUG] {message}", 'bright_cyan')
|
|
133
|
-
|
|
134
|
-
|
|
135
135
|
def _validate_environment(self):
|
|
136
136
|
|
|
137
137
|
#validate that we're running on a Raspberry Pi as root
|
|
@@ -153,57 +153,6 @@ class PiWave:
|
|
|
153
153
|
def _is_root(self) -> bool:
|
|
154
154
|
return os.geteuid() == 0
|
|
155
155
|
|
|
156
|
-
def _find_pi_fm_rds_path(self) -> str:
|
|
157
|
-
current_dir = Path(__file__).parent
|
|
158
|
-
cache_file = current_dir / "pi_fm_rds_path"
|
|
159
|
-
|
|
160
|
-
if cache_file.exists():
|
|
161
|
-
try:
|
|
162
|
-
cached_path = cache_file.read_text().strip()
|
|
163
|
-
if self._is_valid_executable(cached_path):
|
|
164
|
-
return cached_path
|
|
165
|
-
else:
|
|
166
|
-
cache_file.unlink()
|
|
167
|
-
except Exception as e:
|
|
168
|
-
Log.warning(f"Error reading cache file: {e}")
|
|
169
|
-
cache_file.unlink(missing_ok=True)
|
|
170
|
-
|
|
171
|
-
search_paths = ["/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
|
|
172
|
-
|
|
173
|
-
for search_path in search_paths:
|
|
174
|
-
if not Path(search_path).exists():
|
|
175
|
-
continue
|
|
176
|
-
|
|
177
|
-
try:
|
|
178
|
-
for root, dirs, files in os.walk(search_path):
|
|
179
|
-
if "pi_fm_rds" in files:
|
|
180
|
-
executable_path = Path(root) / "pi_fm_rds"
|
|
181
|
-
if self._is_valid_executable(str(executable_path)):
|
|
182
|
-
cache_file.write_text(str(executable_path))
|
|
183
|
-
return str(executable_path)
|
|
184
|
-
except (PermissionError, OSError):
|
|
185
|
-
continue
|
|
186
|
-
|
|
187
|
-
print("Could not automatically find `pi_fm_rds`. Please enter the full path manually.")
|
|
188
|
-
user_path = input("Enter the path to `pi_fm_rds`: ").strip()
|
|
189
|
-
|
|
190
|
-
if self._is_valid_executable(user_path):
|
|
191
|
-
cache_file.write_text(user_path)
|
|
192
|
-
return user_path
|
|
193
|
-
|
|
194
|
-
raise PiWaveError("Invalid pi_fm_rds path provided")
|
|
195
|
-
|
|
196
|
-
def _is_valid_executable(self, path: str) -> bool:
|
|
197
|
-
try:
|
|
198
|
-
result = subprocess.run(
|
|
199
|
-
[path, "--help"],
|
|
200
|
-
stdout=subprocess.PIPE,
|
|
201
|
-
stderr=subprocess.PIPE,
|
|
202
|
-
timeout=5
|
|
203
|
-
)
|
|
204
|
-
return result.returncode == 0
|
|
205
|
-
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
206
|
-
return False
|
|
207
156
|
|
|
208
157
|
def _is_wav_file(self, filepath: str) -> bool:
|
|
209
158
|
return filepath.lower().endswith('.wav')
|
|
@@ -211,6 +160,7 @@ class PiWave:
|
|
|
211
160
|
|
|
212
161
|
def _convert_to_wav(self, filepath: str) -> Optional[str]:
|
|
213
162
|
if self._is_wav_file(filepath):
|
|
163
|
+
Log.debug(f"File is already WAV, skipping conversion")
|
|
214
164
|
return filepath
|
|
215
165
|
|
|
216
166
|
Log.file(f"Converting {filepath} to WAV")
|
|
@@ -221,8 +171,15 @@ class PiWave:
|
|
|
221
171
|
'ffmpeg', '-i', filepath, '-acodec', 'pcm_s16le',
|
|
222
172
|
'-ar', '44100', '-ac', '2', '-y', output_file
|
|
223
173
|
]
|
|
174
|
+
|
|
175
|
+
if self.debug:
|
|
176
|
+
cmd.extend(['-v', 'debug'])
|
|
177
|
+
else:
|
|
178
|
+
cmd.extend(['-v', 'quiet'])
|
|
224
179
|
|
|
225
180
|
try:
|
|
181
|
+
Log.debug(f"Starting FFmpeg conversion: {' '.join(cmd)}")
|
|
182
|
+
|
|
226
183
|
subprocess.run(
|
|
227
184
|
cmd,
|
|
228
185
|
stdout=subprocess.PIPE,
|
|
@@ -231,7 +188,7 @@ class PiWave:
|
|
|
231
188
|
check=True
|
|
232
189
|
)
|
|
233
190
|
|
|
234
|
-
|
|
191
|
+
Log.debug(f"Conversion completed: {output_file}")
|
|
235
192
|
|
|
236
193
|
return output_file
|
|
237
194
|
|
|
@@ -246,11 +203,15 @@ class PiWave:
|
|
|
246
203
|
return None
|
|
247
204
|
|
|
248
205
|
def _get_file_duration(self, wav_file: str) -> float:
|
|
249
|
-
cmd = [
|
|
250
|
-
'ffprobe', '-i', wav_file, '-show_entries', 'format=duration',
|
|
251
|
-
'-v', 'quiet', '-of', 'csv=p=0'
|
|
252
|
-
]
|
|
206
|
+
cmd = ['ffprobe', '-i', wav_file, '-show_entries', 'format=duration', '-of', 'csv=p=0']
|
|
253
207
|
|
|
208
|
+
if self.debug:
|
|
209
|
+
cmd.extend(['-v', 'debug'])
|
|
210
|
+
else:
|
|
211
|
+
cmd.extend(['-v', 'quiet'])
|
|
212
|
+
|
|
213
|
+
Log.debug(f"Running command: {' '.join(cmd)}")
|
|
214
|
+
|
|
254
215
|
try:
|
|
255
216
|
result = subprocess.run(
|
|
256
217
|
cmd,
|
|
@@ -422,6 +383,8 @@ class PiWave:
|
|
|
422
383
|
if not chunk:
|
|
423
384
|
break
|
|
424
385
|
self.audio_queue.put(chunk, timeout=1)
|
|
386
|
+
|
|
387
|
+
Log.debug(f"Producer: sending chunk of {len(chunk)} bytes")
|
|
425
388
|
|
|
426
389
|
except Exception as e:
|
|
427
390
|
Log.error(f"Producer error: {e}")
|
|
@@ -438,6 +401,8 @@ class PiWave:
|
|
|
438
401
|
chunk = self.audio_queue.get(timeout=0.1)
|
|
439
402
|
if chunk is None:
|
|
440
403
|
break
|
|
404
|
+
|
|
405
|
+
Log.debug(f"Consumer: queue size = {self.audio_queue.qsize()}")
|
|
441
406
|
|
|
442
407
|
if self.current_process and self.current_process.stdin:
|
|
443
408
|
self.current_process.stdin.write(chunk)
|
|
@@ -460,7 +425,7 @@ class PiWave:
|
|
|
460
425
|
self.is_live_streaming = False
|
|
461
426
|
|
|
462
427
|
def _playback_worker(self):
|
|
463
|
-
|
|
428
|
+
Log.debug("Playback worker started")
|
|
464
429
|
|
|
465
430
|
if not self.current_file:
|
|
466
431
|
Log.error("No file specified for playback")
|
|
@@ -483,7 +448,7 @@ class PiWave:
|
|
|
483
448
|
Log.error(f"Playback failed for {wav_file}")
|
|
484
449
|
|
|
485
450
|
self.is_playing = False
|
|
486
|
-
|
|
451
|
+
Log.debug("Playback worker finished")
|
|
487
452
|
|
|
488
453
|
|
|
489
454
|
def play(self, source, sample_rate: int = 44100, channels: int = 2, chunk_size: int = 4096, blocking: bool = False):
|
|
@@ -703,11 +668,11 @@ class PiWave:
|
|
|
703
668
|
updated_settings.append(f"PI: {self.pi}")
|
|
704
669
|
|
|
705
670
|
if debug is not None:
|
|
706
|
-
|
|
671
|
+
Log.config(silent=Log.SILENT, debug=debug)
|
|
707
672
|
updated_settings.append(f"debug: {debug}")
|
|
708
673
|
|
|
709
674
|
if silent is not None:
|
|
710
|
-
Log.config(silent=silent)
|
|
675
|
+
Log.config(silent=silent, debug=Log.DEBUG)
|
|
711
676
|
updated_settings.append(f"silent: {silent}")
|
|
712
677
|
|
|
713
678
|
if loop is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: piwave
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.11
|
|
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
|
|
@@ -31,9 +31,6 @@ Dynamic: license-file
|
|
|
31
31
|
<h1>PiWave</h1>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
> [!CAUTION]
|
|
35
|
-
> `piwave==2.1.6` is broken ! Please use `piwave==2.1.5`
|
|
36
|
-
|
|
37
34
|
**PiWave** is a Python module designed to manage and control your Raspberry Pi radio using multiple FM transmission backends. It provides a unified interface for broadcasting audio files with multiple backends support and RDS (Radio Data System) support.
|
|
38
35
|
|
|
39
36
|
## Features
|
|
@@ -761,4 +758,5 @@ Please submit pull requests or open issues on [GitHub](https://github.com/douxxt
|
|
|
761
758
|
|
|
762
759
|
**PiWave** - FM Broadcasting module for Raspberry Pi
|
|
763
760
|
|
|
761
|
+
|
|
764
762
|

|
|
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
|
|
File without changes
|
|
File without changes
|