piwave 2.1.9__py3-none-any.whl → 2.1.11__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 +36 -67
- {piwave-2.1.9.dist-info → piwave-2.1.11.dist-info}/METADATA +2 -4
- piwave-2.1.11.dist-info/RECORD +13 -0
- {piwave-2.1.9.dist-info → piwave-2.1.11.dist-info}/WHEEL +1 -1
- piwave-2.1.9.dist-info/RECORD +0 -13
- {piwave-2.1.9.dist-info → piwave-2.1.11.dist-info}/licenses/LICENSE +0 -0
- {piwave-2.1.9.dist-info → piwave-2.1.11.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,11 +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
|
-
atexit.register(self._stop_curproc)
|
|
91
91
|
|
|
92
|
+
Log.debug(f"Discovering backends...")
|
|
92
93
|
discover_backends()
|
|
93
94
|
|
|
94
95
|
self.backend_use = used_for
|
|
@@ -119,18 +120,18 @@ class PiWave:
|
|
|
119
120
|
pi=self.pi
|
|
120
121
|
)
|
|
121
122
|
|
|
123
|
+
Log.debug(f"Selected backend: {backend_name}")
|
|
124
|
+
|
|
125
|
+
|
|
122
126
|
|
|
123
127
|
min_freq, max_freq = self.backend.frequency_range
|
|
124
128
|
rds_support = "with RDS" if self.backend.supports_rds else "no RDS"
|
|
125
129
|
Log.info(f"Using {self.backend.name} backend ({min_freq}-{max_freq}MHz, {rds_support})")
|
|
130
|
+
|
|
131
|
+
atexit.register(self._stop_curproc)
|
|
126
132
|
|
|
127
133
|
Log.info(f"PiWave initialized - Frequency: {frequency}MHz, PS: {ps}, Loop: {loop}")
|
|
128
134
|
|
|
129
|
-
def _log_debug(self, message: str):
|
|
130
|
-
if self.debug:
|
|
131
|
-
Log.print(f"[DEBUG] {message}", 'bright_cyan')
|
|
132
|
-
|
|
133
|
-
|
|
134
135
|
def _validate_environment(self):
|
|
135
136
|
|
|
136
137
|
#validate that we're running on a Raspberry Pi as root
|
|
@@ -152,57 +153,6 @@ class PiWave:
|
|
|
152
153
|
def _is_root(self) -> bool:
|
|
153
154
|
return os.geteuid() == 0
|
|
154
155
|
|
|
155
|
-
def _find_pi_fm_rds_path(self) -> str:
|
|
156
|
-
current_dir = Path(__file__).parent
|
|
157
|
-
cache_file = current_dir / "pi_fm_rds_path"
|
|
158
|
-
|
|
159
|
-
if cache_file.exists():
|
|
160
|
-
try:
|
|
161
|
-
cached_path = cache_file.read_text().strip()
|
|
162
|
-
if self._is_valid_executable(cached_path):
|
|
163
|
-
return cached_path
|
|
164
|
-
else:
|
|
165
|
-
cache_file.unlink()
|
|
166
|
-
except Exception as e:
|
|
167
|
-
Log.warning(f"Error reading cache file: {e}")
|
|
168
|
-
cache_file.unlink(missing_ok=True)
|
|
169
|
-
|
|
170
|
-
search_paths = ["/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
|
|
171
|
-
|
|
172
|
-
for search_path in search_paths:
|
|
173
|
-
if not Path(search_path).exists():
|
|
174
|
-
continue
|
|
175
|
-
|
|
176
|
-
try:
|
|
177
|
-
for root, dirs, files in os.walk(search_path):
|
|
178
|
-
if "pi_fm_rds" in files:
|
|
179
|
-
executable_path = Path(root) / "pi_fm_rds"
|
|
180
|
-
if self._is_valid_executable(str(executable_path)):
|
|
181
|
-
cache_file.write_text(str(executable_path))
|
|
182
|
-
return str(executable_path)
|
|
183
|
-
except (PermissionError, OSError):
|
|
184
|
-
continue
|
|
185
|
-
|
|
186
|
-
print("Could not automatically find `pi_fm_rds`. Please enter the full path manually.")
|
|
187
|
-
user_path = input("Enter the path to `pi_fm_rds`: ").strip()
|
|
188
|
-
|
|
189
|
-
if self._is_valid_executable(user_path):
|
|
190
|
-
cache_file.write_text(user_path)
|
|
191
|
-
return user_path
|
|
192
|
-
|
|
193
|
-
raise PiWaveError("Invalid pi_fm_rds path provided")
|
|
194
|
-
|
|
195
|
-
def _is_valid_executable(self, path: str) -> bool:
|
|
196
|
-
try:
|
|
197
|
-
result = subprocess.run(
|
|
198
|
-
[path, "--help"],
|
|
199
|
-
stdout=subprocess.PIPE,
|
|
200
|
-
stderr=subprocess.PIPE,
|
|
201
|
-
timeout=5
|
|
202
|
-
)
|
|
203
|
-
return result.returncode == 0
|
|
204
|
-
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
205
|
-
return False
|
|
206
156
|
|
|
207
157
|
def _is_wav_file(self, filepath: str) -> bool:
|
|
208
158
|
return filepath.lower().endswith('.wav')
|
|
@@ -210,6 +160,7 @@ class PiWave:
|
|
|
210
160
|
|
|
211
161
|
def _convert_to_wav(self, filepath: str) -> Optional[str]:
|
|
212
162
|
if self._is_wav_file(filepath):
|
|
163
|
+
Log.debug(f"File is already WAV, skipping conversion")
|
|
213
164
|
return filepath
|
|
214
165
|
|
|
215
166
|
Log.file(f"Converting {filepath} to WAV")
|
|
@@ -220,8 +171,15 @@ class PiWave:
|
|
|
220
171
|
'ffmpeg', '-i', filepath, '-acodec', 'pcm_s16le',
|
|
221
172
|
'-ar', '44100', '-ac', '2', '-y', output_file
|
|
222
173
|
]
|
|
174
|
+
|
|
175
|
+
if self.debug:
|
|
176
|
+
cmd.extend(['-v', 'debug'])
|
|
177
|
+
else:
|
|
178
|
+
cmd.extend(['-v', 'quiet'])
|
|
223
179
|
|
|
224
180
|
try:
|
|
181
|
+
Log.debug(f"Starting FFmpeg conversion: {' '.join(cmd)}")
|
|
182
|
+
|
|
225
183
|
subprocess.run(
|
|
226
184
|
cmd,
|
|
227
185
|
stdout=subprocess.PIPE,
|
|
@@ -230,7 +188,7 @@ class PiWave:
|
|
|
230
188
|
check=True
|
|
231
189
|
)
|
|
232
190
|
|
|
233
|
-
|
|
191
|
+
Log.debug(f"Conversion completed: {output_file}")
|
|
234
192
|
|
|
235
193
|
return output_file
|
|
236
194
|
|
|
@@ -245,11 +203,15 @@ class PiWave:
|
|
|
245
203
|
return None
|
|
246
204
|
|
|
247
205
|
def _get_file_duration(self, wav_file: str) -> float:
|
|
248
|
-
cmd = [
|
|
249
|
-
'ffprobe', '-i', wav_file, '-show_entries', 'format=duration',
|
|
250
|
-
'-v', 'quiet', '-of', 'csv=p=0'
|
|
251
|
-
]
|
|
206
|
+
cmd = ['ffprobe', '-i', wav_file, '-show_entries', 'format=duration', '-of', 'csv=p=0']
|
|
252
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
|
+
|
|
253
215
|
try:
|
|
254
216
|
result = subprocess.run(
|
|
255
217
|
cmd,
|
|
@@ -421,6 +383,8 @@ class PiWave:
|
|
|
421
383
|
if not chunk:
|
|
422
384
|
break
|
|
423
385
|
self.audio_queue.put(chunk, timeout=1)
|
|
386
|
+
|
|
387
|
+
Log.debug(f"Producer: sending chunk of {len(chunk)} bytes")
|
|
424
388
|
|
|
425
389
|
except Exception as e:
|
|
426
390
|
Log.error(f"Producer error: {e}")
|
|
@@ -437,6 +401,8 @@ class PiWave:
|
|
|
437
401
|
chunk = self.audio_queue.get(timeout=0.1)
|
|
438
402
|
if chunk is None:
|
|
439
403
|
break
|
|
404
|
+
|
|
405
|
+
Log.debug(f"Consumer: queue size = {self.audio_queue.qsize()}")
|
|
440
406
|
|
|
441
407
|
if self.current_process and self.current_process.stdin:
|
|
442
408
|
self.current_process.stdin.write(chunk)
|
|
@@ -459,7 +425,7 @@ class PiWave:
|
|
|
459
425
|
self.is_live_streaming = False
|
|
460
426
|
|
|
461
427
|
def _playback_worker(self):
|
|
462
|
-
|
|
428
|
+
Log.debug("Playback worker started")
|
|
463
429
|
|
|
464
430
|
if not self.current_file:
|
|
465
431
|
Log.error("No file specified for playback")
|
|
@@ -482,7 +448,7 @@ class PiWave:
|
|
|
482
448
|
Log.error(f"Playback failed for {wav_file}")
|
|
483
449
|
|
|
484
450
|
self.is_playing = False
|
|
485
|
-
|
|
451
|
+
Log.debug("Playback worker finished")
|
|
486
452
|
|
|
487
453
|
|
|
488
454
|
def play(self, source, sample_rate: int = 44100, channels: int = 2, chunk_size: int = 4096, blocking: bool = False):
|
|
@@ -562,6 +528,9 @@ class PiWave:
|
|
|
562
528
|
Log.success("Stopped")
|
|
563
529
|
|
|
564
530
|
def _stop_curproc(self):
|
|
531
|
+
if not hasattr(self, 'backend'):
|
|
532
|
+
return
|
|
533
|
+
|
|
565
534
|
if self.backend.current_process:
|
|
566
535
|
self.backend.stop()
|
|
567
536
|
elif self.current_process:
|
|
@@ -699,11 +668,11 @@ class PiWave:
|
|
|
699
668
|
updated_settings.append(f"PI: {self.pi}")
|
|
700
669
|
|
|
701
670
|
if debug is not None:
|
|
702
|
-
|
|
671
|
+
Log.config(silent=Log.SILENT, debug=debug)
|
|
703
672
|
updated_settings.append(f"debug: {debug}")
|
|
704
673
|
|
|
705
674
|
if silent is not None:
|
|
706
|
-
Log.config(silent=silent)
|
|
675
|
+
Log.config(silent=silent, debug=Log.DEBUG)
|
|
707
676
|
updated_settings.append(f"silent: {silent}")
|
|
708
677
|
|
|
709
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
|

|
|
@@ -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=Uu6KH03jOtsrgogswjxgPwvQtB51R_oJ539UunNedEQ,30873
|
|
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.11.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
10
|
+
piwave-2.1.11.dist-info/METADATA,sha256=EMd5DXvCMkjPCn9cl34y_PXJLLJrGYtNXFt2ndCjEh4,20821
|
|
11
|
+
piwave-2.1.11.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
12
|
+
piwave-2.1.11.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
|
|
13
|
+
piwave-2.1.11.dist-info/RECORD,,
|
piwave-2.1.9.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=6ns1VmQ8hZFwrI_8HTLwvSD_CwMCzzUWGqms0u1MKeQ,32293
|
|
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.9.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
10
|
-
piwave-2.1.9.dist-info/METADATA,sha256=0iHcdTApVbFqDrlJ4wurc-1d8O25T4R2ZzdiQxJz2jQ,20890
|
|
11
|
-
piwave-2.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
piwave-2.1.9.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
|
|
13
|
-
piwave-2.1.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|