piwave 2.1.10__py3-none-any.whl → 2.1.12__py3-none-any.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.
- piwave/backends/base.py +15 -1
- piwave/logger.py +11 -3
- piwave/piwave.py +32 -66
- {piwave-2.1.10.dist-info → piwave-2.1.12.dist-info}/METADATA +2 -4
- piwave-2.1.12.dist-info/RECORD +13 -0
- {piwave-2.1.10.dist-info → piwave-2.1.12.dist-info}/WHEEL +1 -1
- piwave-2.1.10.dist-info/RECORD +0 -13
- {piwave-2.1.10.dist-info → piwave-2.1.12.dist-info}/licenses/LICENSE +0 -0
- {piwave-2.1.10.dist-info → piwave-2.1.12.dist-info}/top_level.txt +0 -0
piwave/backends/base.py
CHANGED
|
@@ -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):
|
piwave/logger.py
CHANGED
|
@@ -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()
|
piwave/piwave.py
CHANGED
|
@@ -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,
|
|
@@ -407,6 +368,7 @@ class PiWave:
|
|
|
407
368
|
if self.stop_event.is_set():
|
|
408
369
|
break
|
|
409
370
|
if chunk:
|
|
371
|
+
Log.debug(f"Producer: sending chunk of {len(chunk)} bytes")
|
|
410
372
|
self.audio_queue.put(chunk, timeout=1)
|
|
411
373
|
|
|
412
374
|
elif callable(audio_source):
|
|
@@ -414,6 +376,7 @@ class PiWave:
|
|
|
414
376
|
chunk = audio_source()
|
|
415
377
|
if not chunk:
|
|
416
378
|
break
|
|
379
|
+
Log.debug(f"Producer: sending chunk of {len(chunk)} bytes")
|
|
417
380
|
self.audio_queue.put(chunk, timeout=1)
|
|
418
381
|
|
|
419
382
|
elif hasattr(audio_source, 'read'):
|
|
@@ -421,6 +384,7 @@ class PiWave:
|
|
|
421
384
|
chunk = audio_source.read(chunk_size)
|
|
422
385
|
if not chunk:
|
|
423
386
|
break
|
|
387
|
+
Log.debug(f"Producer: sending chunk of {len(chunk)} bytes")
|
|
424
388
|
self.audio_queue.put(chunk, timeout=1)
|
|
425
389
|
|
|
426
390
|
except Exception as e:
|
|
@@ -438,6 +402,8 @@ class PiWave:
|
|
|
438
402
|
chunk = self.audio_queue.get(timeout=0.1)
|
|
439
403
|
if chunk is None:
|
|
440
404
|
break
|
|
405
|
+
|
|
406
|
+
Log.debug(f"Consumer: queue size = {self.audio_queue.qsize()}")
|
|
441
407
|
|
|
442
408
|
if self.current_process and self.current_process.stdin:
|
|
443
409
|
self.current_process.stdin.write(chunk)
|
|
@@ -460,7 +426,7 @@ class PiWave:
|
|
|
460
426
|
self.is_live_streaming = False
|
|
461
427
|
|
|
462
428
|
def _playback_worker(self):
|
|
463
|
-
|
|
429
|
+
Log.debug("Playback worker started")
|
|
464
430
|
|
|
465
431
|
if not self.current_file:
|
|
466
432
|
Log.error("No file specified for playback")
|
|
@@ -483,7 +449,7 @@ class PiWave:
|
|
|
483
449
|
Log.error(f"Playback failed for {wav_file}")
|
|
484
450
|
|
|
485
451
|
self.is_playing = False
|
|
486
|
-
|
|
452
|
+
Log.debug("Playback worker finished")
|
|
487
453
|
|
|
488
454
|
|
|
489
455
|
def play(self, source, sample_rate: int = 44100, channels: int = 2, chunk_size: int = 4096, blocking: bool = False):
|
|
@@ -703,11 +669,11 @@ class PiWave:
|
|
|
703
669
|
updated_settings.append(f"PI: {self.pi}")
|
|
704
670
|
|
|
705
671
|
if debug is not None:
|
|
706
|
-
|
|
672
|
+
Log.config(silent=Log.SILENT, debug=debug)
|
|
707
673
|
updated_settings.append(f"debug: {debug}")
|
|
708
674
|
|
|
709
675
|
if silent is not None:
|
|
710
|
-
Log.config(silent=silent)
|
|
676
|
+
Log.config(silent=silent, debug=Log.DEBUG)
|
|
711
677
|
updated_settings.append(f"silent: {silent}")
|
|
712
678
|
|
|
713
679
|
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.12
|
|
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
|

|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
piwave/__init__.py,sha256=jz2r-qclltKTxJjlGnqhAwIlUgjvRTR31f4hjTHP5NA,230
|
|
2
|
+
piwave/__main__.py,sha256=-Z3RI7TiicE5UwWc4dZaHX82-MpwVXWkfMZXOC457_Q,4089
|
|
3
|
+
piwave/logger.py,sha256=jpsM3tTPTxuhojtSJbfLq4nDFxOjojSSOrtYDGl_oTw,1353
|
|
4
|
+
piwave/piwave.py,sha256=ZXs2Ay1IY1jvW6Zu-QDdjGjsHOD_yp1Fnrf_qEFBW9I,31029
|
|
5
|
+
piwave/backends/__init__.py,sha256=DUbdyYf2V2XcDB05vmFWEkuJ292YTNiNJjzh1raJ5Cg,3756
|
|
6
|
+
piwave/backends/base.py,sha256=GI8IBinGii09F3AAuy9n9t59LlSEJAIkRiwwTcupWtM,8258
|
|
7
|
+
piwave/backends/fm_transmitter.py,sha256=Wzsoyi5hqLLZF5eNPmZ7WFvP27OMs2ywJlwrJVut-8w,1403
|
|
8
|
+
piwave/backends/pi_fm_rds.py,sha256=VjcbFeje8LHVr4cBjlL36IcvA3WRfI1hHSGG2ui8Pb0,1467
|
|
9
|
+
piwave-2.1.12.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
10
|
+
piwave-2.1.12.dist-info/METADATA,sha256=xeVahhhc_mYAr56tIJFa-QsBtiSYA4K0JlNLpC6pxJ4,20821
|
|
11
|
+
piwave-2.1.12.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
+
piwave-2.1.12.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
|
|
13
|
+
piwave-2.1.12.dist-info/RECORD,,
|
piwave-2.1.10.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
piwave/__init__.py,sha256=jz2r-qclltKTxJjlGnqhAwIlUgjvRTR31f4hjTHP5NA,230
|
|
2
|
-
piwave/__main__.py,sha256=-Z3RI7TiicE5UwWc4dZaHX82-MpwVXWkfMZXOC457_Q,4089
|
|
3
|
-
piwave/logger.py,sha256=ipWzdNcEYob0xjXP8bbTq9enA3nryU-GKzAOKayRqlg,1149
|
|
4
|
-
piwave/piwave.py,sha256=7orqxKjqRrx-s4T3jK_zjmIeFTn-KE07AhG25K14fwU,32359
|
|
5
|
-
piwave/backends/__init__.py,sha256=DUbdyYf2V2XcDB05vmFWEkuJ292YTNiNJjzh1raJ5Cg,3756
|
|
6
|
-
piwave/backends/base.py,sha256=PJNXEEXZ8wgc343SO5FJ1oGOop--Y-7t01PUWSa5GuE,7818
|
|
7
|
-
piwave/backends/fm_transmitter.py,sha256=Wzsoyi5hqLLZF5eNPmZ7WFvP27OMs2ywJlwrJVut-8w,1403
|
|
8
|
-
piwave/backends/pi_fm_rds.py,sha256=VjcbFeje8LHVr4cBjlL36IcvA3WRfI1hHSGG2ui8Pb0,1467
|
|
9
|
-
piwave-2.1.10.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
10
|
-
piwave-2.1.10.dist-info/METADATA,sha256=SERr-6sbeTFE1IYjefV6ADq6F2h2pPil_hBSyROrL4g,20891
|
|
11
|
-
piwave-2.1.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
piwave-2.1.10.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
|
|
13
|
-
piwave-2.1.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|