piwave 2.0.6__py3-none-any.whl → 2.0.8__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/piwave.py +141 -186
- piwave-2.0.8.dist-info/METADATA +419 -0
- piwave-2.0.8.dist-info/RECORD +7 -0
- piwave-2.0.6.dist-info/METADATA +0 -205
- piwave-2.0.6.dist-info/RECORD +0 -7
- {piwave-2.0.6.dist-info → piwave-2.0.8.dist-info}/WHEEL +0 -0
- {piwave-2.0.6.dist-info → piwave-2.0.8.dist-info}/licenses/LICENSE +0 -0
- {piwave-2.0.6.dist-info → piwave-2.0.8.dist-info}/top_level.txt +0 -0
piwave/piwave.py
CHANGED
|
@@ -6,11 +6,11 @@ import subprocess
|
|
|
6
6
|
import signal
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
import tempfile
|
|
11
11
|
import shutil
|
|
12
12
|
import sys
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import Optional, Callable
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from urllib.parse import urlparse
|
|
16
16
|
import atexit
|
|
@@ -49,8 +49,17 @@ class Log:
|
|
|
49
49
|
'update': 'UPD',
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
SILENT = False
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def config(cls, silent: bool = False):
|
|
56
|
+
cls.SILENT = silent
|
|
57
|
+
|
|
52
58
|
@classmethod
|
|
53
59
|
def print(cls, message: str, style: str = '', icon: str = '', end: str = '\n'):
|
|
60
|
+
|
|
61
|
+
if cls.SILENT: return
|
|
62
|
+
|
|
54
63
|
color = cls.COLORS.get(style, '')
|
|
55
64
|
icon_char = cls.ICONS.get(icon, '')
|
|
56
65
|
if icon_char:
|
|
@@ -109,8 +118,8 @@ class PiWave:
|
|
|
109
118
|
ps: str = "PiWave",
|
|
110
119
|
rt: str = "PiWave: The best python module for managing your pi radio",
|
|
111
120
|
pi: str = "FFFF",
|
|
112
|
-
loop: bool = False,
|
|
113
121
|
debug: bool = False,
|
|
122
|
+
silent: bool = False,
|
|
114
123
|
on_track_change: Optional[Callable] = None,
|
|
115
124
|
on_error: Optional[Callable] = None):
|
|
116
125
|
"""Initialize PiWave FM transmitter.
|
|
@@ -123,10 +132,10 @@ class PiWave:
|
|
|
123
132
|
:type rt: str
|
|
124
133
|
:param pi: Program Identification code (4 hex digits)
|
|
125
134
|
:type pi: str
|
|
126
|
-
:param loop: Whether to loop the playlist when it ends
|
|
127
|
-
:type loop: bool
|
|
128
135
|
:param debug: Enable debug logging
|
|
129
136
|
:type debug: bool
|
|
137
|
+
:param silent: Removes every output log
|
|
138
|
+
:type silent: bool
|
|
130
139
|
:param on_track_change: Callback function called when track changes
|
|
131
140
|
:type on_track_change: Optional[Callable]
|
|
132
141
|
:param on_error: Callback function called when an error occurs
|
|
@@ -143,13 +152,10 @@ class PiWave:
|
|
|
143
152
|
self.ps = str(ps)[:8]
|
|
144
153
|
self.rt = str(rt)[:64]
|
|
145
154
|
self.pi = str(pi).upper()[:4]
|
|
146
|
-
self.loop = loop
|
|
147
155
|
self.on_track_change = on_track_change
|
|
148
156
|
self.on_error = on_error
|
|
149
157
|
|
|
150
|
-
self.
|
|
151
|
-
self.converted_files: dict[str, str] = {}
|
|
152
|
-
self.current_index = 0
|
|
158
|
+
self.current_file: Optional[str] = None
|
|
153
159
|
self.is_playing = False
|
|
154
160
|
self.is_stopped = False
|
|
155
161
|
|
|
@@ -158,19 +164,21 @@ class PiWave:
|
|
|
158
164
|
self.stop_event = threading.Event()
|
|
159
165
|
|
|
160
166
|
self.temp_dir = tempfile.mkdtemp(prefix="piwave_")
|
|
161
|
-
|
|
167
|
+
|
|
168
|
+
Log.config(silent=silent)
|
|
162
169
|
|
|
163
170
|
self.pi_fm_rds_path = self._find_pi_fm_rds_path()
|
|
164
171
|
|
|
165
172
|
self._validate_environment()
|
|
166
173
|
|
|
167
174
|
atexit.register(self.cleanup)
|
|
175
|
+
|
|
168
176
|
|
|
169
|
-
Log.info(f"PiWave initialized - Frequency: {frequency}MHz, PS: {ps}
|
|
177
|
+
Log.info(f"PiWave initialized - Frequency: {frequency}MHz, PS: {ps}")
|
|
170
178
|
|
|
171
179
|
def _log_debug(self, message: str):
|
|
172
180
|
if self.debug:
|
|
173
|
-
Log.print(f"[DEBUG] {message}", '
|
|
181
|
+
Log.print(f"[DEBUG] {message}", 'bright_cyan')
|
|
174
182
|
|
|
175
183
|
|
|
176
184
|
def _validate_environment(self):
|
|
@@ -185,10 +193,10 @@ class PiWave:
|
|
|
185
193
|
|
|
186
194
|
def _is_raspberry_pi(self) -> bool:
|
|
187
195
|
try:
|
|
188
|
-
with open(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
except
|
|
196
|
+
with open('/proc/cpuinfo', 'r') as f:
|
|
197
|
+
cpuinfo = f.read()
|
|
198
|
+
return 'raspberry' in cpuinfo.lower()
|
|
199
|
+
except:
|
|
192
200
|
return False
|
|
193
201
|
|
|
194
202
|
def _is_root(self) -> bool:
|
|
@@ -246,52 +254,17 @@ class PiWave:
|
|
|
246
254
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
247
255
|
return False
|
|
248
256
|
|
|
249
|
-
def _is_url(self, path: str) -> bool:
|
|
250
|
-
parsed = urlparse(path)
|
|
251
|
-
return parsed.scheme in ('http', 'https', 'ftp')
|
|
252
|
-
|
|
253
257
|
def _is_wav_file(self, filepath: str) -> bool:
|
|
254
258
|
return filepath.lower().endswith('.wav')
|
|
255
259
|
|
|
256
|
-
async def _download_stream_chunk(self, url: str, output_file: str, duration: int = 30) -> bool:
|
|
257
|
-
#Download a chunk of stream for specified duration
|
|
258
|
-
try:
|
|
259
|
-
cmd = [
|
|
260
|
-
'ffmpeg', '-i', url, '-t', str(duration),
|
|
261
|
-
'-acodec', 'pcm_s16le', '-ar', '44100', '-ac', '2',
|
|
262
|
-
'-y', output_file
|
|
263
|
-
]
|
|
264
|
-
|
|
265
|
-
process = await asyncio.create_subprocess_exec(
|
|
266
|
-
*cmd,
|
|
267
|
-
stdout=asyncio.subprocess.PIPE,
|
|
268
|
-
stderr=asyncio.subprocess.PIPE
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=duration + 10)
|
|
272
|
-
return process.returncode == 0
|
|
273
|
-
|
|
274
|
-
except asyncio.TimeoutError:
|
|
275
|
-
Log.error(f"Timeout downloading stream chunk from {url}")
|
|
276
|
-
return False
|
|
277
|
-
except Exception as e:
|
|
278
|
-
Log.error(f"Error downloading stream: {e}")
|
|
279
|
-
return False
|
|
280
260
|
|
|
281
261
|
def _convert_to_wav(self, filepath: str) -> Optional[str]:
|
|
282
|
-
if
|
|
283
|
-
return self.converted_files[filepath]
|
|
284
|
-
|
|
285
|
-
if self._is_wav_file(filepath) and not self._is_url(filepath):
|
|
286
|
-
self.converted_files[filepath] = filepath
|
|
262
|
+
if self._is_wav_file(filepath):
|
|
287
263
|
return filepath
|
|
288
264
|
|
|
289
265
|
Log.file_message(f"Converting {filepath} to WAV")
|
|
290
266
|
|
|
291
|
-
|
|
292
|
-
output_file = os.path.join(self.temp_dir, f"stream_{int(time.time())}.wav")
|
|
293
|
-
else:
|
|
294
|
-
output_file = f"{os.path.splitext(filepath)[0]}_converted.wav"
|
|
267
|
+
output_file = f"{os.path.splitext(filepath)[0]}_converted.wav"
|
|
295
268
|
|
|
296
269
|
cmd = [
|
|
297
270
|
'ffmpeg', '-i', filepath, '-acodec', 'pcm_s16le',
|
|
@@ -299,7 +272,7 @@ class PiWave:
|
|
|
299
272
|
]
|
|
300
273
|
|
|
301
274
|
try:
|
|
302
|
-
|
|
275
|
+
subprocess.run(
|
|
303
276
|
cmd,
|
|
304
277
|
stdout=subprocess.PIPE,
|
|
305
278
|
stderr=subprocess.PIPE,
|
|
@@ -309,7 +282,6 @@ class PiWave:
|
|
|
309
282
|
|
|
310
283
|
self._log_debug(f"FFmpeg conversion successful for {filepath}")
|
|
311
284
|
|
|
312
|
-
self.converted_files[filepath] = output_file
|
|
313
285
|
return output_file
|
|
314
286
|
|
|
315
287
|
except subprocess.TimeoutExpired:
|
|
@@ -368,7 +340,7 @@ class PiWave:
|
|
|
368
340
|
)
|
|
369
341
|
|
|
370
342
|
if self.on_track_change:
|
|
371
|
-
self.on_track_change(wav_file
|
|
343
|
+
self.on_track_change(wav_file)
|
|
372
344
|
|
|
373
345
|
# wait for either
|
|
374
346
|
# The duration to elapse (then kill the process), or
|
|
@@ -413,28 +385,25 @@ class PiWave:
|
|
|
413
385
|
def _playback_worker(self):
|
|
414
386
|
self._log_debug("Playback worker started")
|
|
415
387
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
continue
|
|
421
|
-
else:
|
|
422
|
-
break
|
|
423
|
-
|
|
424
|
-
if self.current_index < len(self.playlist):
|
|
425
|
-
wav_file = self.playlist[self.current_index]
|
|
388
|
+
if not self.current_file:
|
|
389
|
+
Log.error("No file specified for playback")
|
|
390
|
+
self.is_playing = False
|
|
391
|
+
return
|
|
426
392
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
393
|
+
wav_file = self._convert_to_wav(self.current_file)
|
|
394
|
+
if not wav_file:
|
|
395
|
+
Log.error(f"Failed to convert {self.current_file}")
|
|
396
|
+
self.is_playing = False
|
|
397
|
+
return
|
|
431
398
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
399
|
+
if not os.path.exists(wav_file):
|
|
400
|
+
Log.error(f"File not found: {wav_file}")
|
|
401
|
+
self.is_playing = False
|
|
402
|
+
return
|
|
436
403
|
|
|
437
|
-
|
|
404
|
+
if not self._play_wav(wav_file):
|
|
405
|
+
if not self.stop_event.is_set():
|
|
406
|
+
Log.error(f"Playback failed for {wav_file}")
|
|
438
407
|
|
|
439
408
|
self.is_playing = False
|
|
440
409
|
self._log_debug("Playback worker finished")
|
|
@@ -445,70 +414,27 @@ class PiWave:
|
|
|
445
414
|
self.stop()
|
|
446
415
|
os._exit(0)
|
|
447
416
|
|
|
448
|
-
def
|
|
449
|
-
"""
|
|
450
|
-
|
|
451
|
-
:param files: List of file paths or URLs to add to the playlist
|
|
452
|
-
:type files: List[str]
|
|
453
|
-
:return: True if at least one file was successfully added, False otherwise
|
|
454
|
-
:rtype: bool
|
|
455
|
-
|
|
456
|
-
.. note::
|
|
457
|
-
Files are automatically converted to WAV format if needed.
|
|
458
|
-
URLs are supported for streaming audio.
|
|
459
|
-
|
|
460
|
-
Example:
|
|
461
|
-
>>> pw.add_files(['song1.mp3', 'song2.wav', 'http://stream.url'])
|
|
462
|
-
"""
|
|
463
|
-
|
|
464
|
-
converted_files = []
|
|
465
|
-
|
|
466
|
-
for file_path in files:
|
|
467
|
-
if self._is_url(file_path):
|
|
468
|
-
converted_files.append(file_path)
|
|
469
|
-
else:
|
|
470
|
-
wav_file = self._convert_to_wav(file_path)
|
|
471
|
-
if wav_file:
|
|
472
|
-
converted_files.append(wav_file)
|
|
473
|
-
else:
|
|
474
|
-
Log.warning(f"Failed to convert {file_path}")
|
|
475
|
-
|
|
476
|
-
if converted_files:
|
|
477
|
-
self.playlist.extend(converted_files)
|
|
478
|
-
Log.success(f"Added {len(converted_files)} files to playlist")
|
|
479
|
-
return True
|
|
480
|
-
|
|
481
|
-
return False
|
|
482
|
-
|
|
483
|
-
def play(self, files: Optional[List[str]] = None) -> bool:
|
|
484
|
-
"""Start playing the playlist or specified files.
|
|
417
|
+
def play(self, file_path: str) -> bool:
|
|
418
|
+
"""Start playing the specified audio file.
|
|
485
419
|
|
|
486
|
-
:param
|
|
487
|
-
:type
|
|
420
|
+
:param file_path: Path to local audio file
|
|
421
|
+
:type file_path: str
|
|
488
422
|
:return: True if playback started successfully, False otherwise
|
|
489
423
|
:rtype: bool
|
|
490
424
|
|
|
491
425
|
.. note::
|
|
492
|
-
|
|
493
|
-
|
|
426
|
+
Files are automatically converted to WAV format if needed.
|
|
427
|
+
Only local files are supported.
|
|
494
428
|
|
|
495
429
|
Example:
|
|
496
|
-
>>> pw.play(
|
|
497
|
-
>>> pw.play()
|
|
430
|
+
>>> pw.play('song.mp3')
|
|
431
|
+
>>> pw.play('audio.wav')
|
|
498
432
|
"""
|
|
499
|
-
if files:
|
|
500
|
-
self.playlist.clear()
|
|
501
|
-
self.current_index = 0
|
|
502
|
-
if not self.add_files(files):
|
|
503
|
-
return False
|
|
504
|
-
|
|
505
|
-
if not self.playlist:
|
|
506
|
-
Log.warning("No files in playlist")
|
|
507
|
-
return False
|
|
508
433
|
|
|
509
434
|
if self.is_playing:
|
|
510
435
|
self.stop()
|
|
511
436
|
|
|
437
|
+
self.current_file = file_path
|
|
512
438
|
self.stop_event.clear()
|
|
513
439
|
self.is_stopped = False
|
|
514
440
|
self.is_playing = True
|
|
@@ -546,15 +472,6 @@ class PiWave:
|
|
|
546
472
|
finally:
|
|
547
473
|
self.current_process = None
|
|
548
474
|
|
|
549
|
-
if self.stream_process:
|
|
550
|
-
try:
|
|
551
|
-
os.killpg(os.getpgid(self.stream_process.pid), signal.SIGTERM)
|
|
552
|
-
self.stream_process.wait(timeout=5)
|
|
553
|
-
except Exception:
|
|
554
|
-
pass
|
|
555
|
-
finally:
|
|
556
|
-
self.stream_process = None
|
|
557
|
-
|
|
558
475
|
if self.playback_thread and self.playback_thread.is_alive():
|
|
559
476
|
self.playback_thread.join(timeout=5)
|
|
560
477
|
|
|
@@ -564,7 +481,7 @@ class PiWave:
|
|
|
564
481
|
def pause(self):
|
|
565
482
|
"""Pause the current playback.
|
|
566
483
|
|
|
567
|
-
Stops the current track but maintains the
|
|
484
|
+
Stops the current track but maintains the file reference.
|
|
568
485
|
Use :meth:`resume` to continue playback.
|
|
569
486
|
|
|
570
487
|
Example:
|
|
@@ -575,40 +492,89 @@ class PiWave:
|
|
|
575
492
|
Log.info("Playback paused")
|
|
576
493
|
|
|
577
494
|
def resume(self):
|
|
578
|
-
"""Resume playback from the current
|
|
495
|
+
"""Resume playback from the current file.
|
|
579
496
|
|
|
580
|
-
Continues playback
|
|
497
|
+
Continues playback of the current file.
|
|
581
498
|
|
|
582
499
|
Example:
|
|
583
500
|
>>> pw.resume()
|
|
584
501
|
"""
|
|
585
|
-
if not self.is_playing and self.
|
|
586
|
-
self.play()
|
|
587
|
-
|
|
588
|
-
def
|
|
589
|
-
|
|
502
|
+
if not self.is_playing and self.current_file:
|
|
503
|
+
self.play(self.current_file)
|
|
504
|
+
|
|
505
|
+
def update(self,
|
|
506
|
+
frequency: Optional[float] = None,
|
|
507
|
+
ps: Optional[str] = None,
|
|
508
|
+
rt: Optional[str] = None,
|
|
509
|
+
pi: Optional[str] = None,
|
|
510
|
+
debug: Optional[bool] = None,
|
|
511
|
+
silent: Optional[bool] = None,
|
|
512
|
+
on_track_change: Optional[Callable] = None,
|
|
513
|
+
on_error: Optional[Callable] = None):
|
|
514
|
+
"""Update PiWave settings.
|
|
590
515
|
|
|
591
|
-
|
|
516
|
+
:param frequency: FM frequency to broadcast on (80.0-108.0 MHz)
|
|
517
|
+
:type frequency: Optional[float]
|
|
518
|
+
:param ps: Program Service name (max 8 characters)
|
|
519
|
+
:type ps: Optional[str]
|
|
520
|
+
:param rt: Radio Text message (max 64 characters)
|
|
521
|
+
:type rt: Optional[str]
|
|
522
|
+
:param pi: Program Identification code (4 hex digits)
|
|
523
|
+
:type pi: Optional[str]
|
|
524
|
+
:param debug: Enable debug logging
|
|
525
|
+
:type debug: Optional[bool]
|
|
526
|
+
:param silent: Remove every output log
|
|
527
|
+
:type silent: Optional[bool]
|
|
528
|
+
:param on_track_change: Callback function called when track changes
|
|
529
|
+
:type on_track_change: Optional[Callable]
|
|
530
|
+
:param on_error: Callback function called when an error occurs
|
|
531
|
+
:type on_error: Optional[Callable]
|
|
592
532
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
"""
|
|
596
|
-
if self.is_playing:
|
|
597
|
-
self._stop_current_process()
|
|
598
|
-
self.current_index += 1
|
|
599
|
-
|
|
600
|
-
def previous_track(self):
|
|
601
|
-
"""Go back to the previous track in the playlist.
|
|
602
|
-
|
|
603
|
-
If currently playing, stops the current track and goes to the previous one.
|
|
604
|
-
Cannot go before the first track.
|
|
533
|
+
.. note::
|
|
534
|
+
Only non-None parameters will be updated. Changes take effect immediately, except for broadcast related changes, where you will have to start a new broadcast to apply them.
|
|
605
535
|
|
|
606
536
|
Example:
|
|
607
|
-
>>> pw.
|
|
537
|
+
>>> pw.update(frequency=101.5, ps="NewName")
|
|
538
|
+
>>> pw.update(rt="Updated radio text", debug=True)
|
|
608
539
|
"""
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
540
|
+
updated_settings = []
|
|
541
|
+
|
|
542
|
+
if frequency is not None:
|
|
543
|
+
self.frequency = frequency
|
|
544
|
+
updated_settings.append(f"frequency: {frequency}MHz")
|
|
545
|
+
|
|
546
|
+
if ps is not None:
|
|
547
|
+
self.ps = str(ps)[:8]
|
|
548
|
+
updated_settings.append(f"PS: {self.ps}")
|
|
549
|
+
|
|
550
|
+
if rt is not None:
|
|
551
|
+
self.rt = str(rt)[:64]
|
|
552
|
+
updated_settings.append(f"RT: {self.rt}")
|
|
553
|
+
|
|
554
|
+
if pi is not None:
|
|
555
|
+
self.pi = str(pi).upper()[:4]
|
|
556
|
+
updated_settings.append(f"PI: {self.pi}")
|
|
557
|
+
|
|
558
|
+
if debug is not None:
|
|
559
|
+
self.debug = debug
|
|
560
|
+
updated_settings.append(f"debug: {debug}")
|
|
561
|
+
|
|
562
|
+
if silent is not None:
|
|
563
|
+
Log.config(silent=silent)
|
|
564
|
+
updated_settings.append(f"silent: {silent}")
|
|
565
|
+
|
|
566
|
+
if on_track_change is not None:
|
|
567
|
+
self.on_track_change = on_track_change
|
|
568
|
+
updated_settings.append("on_track_change callback updated")
|
|
569
|
+
|
|
570
|
+
if on_error is not None:
|
|
571
|
+
self.on_error = on_error
|
|
572
|
+
updated_settings.append("on_error callback updated")
|
|
573
|
+
|
|
574
|
+
if updated_settings:
|
|
575
|
+
Log.success(f"Updated settings: {', '.join(updated_settings)}")
|
|
576
|
+
else:
|
|
577
|
+
Log.info("No settings updated")
|
|
612
578
|
|
|
613
579
|
def set_frequency(self, frequency: float):
|
|
614
580
|
"""Change the FM broadcast frequency.
|
|
@@ -617,7 +583,7 @@ class PiWave:
|
|
|
617
583
|
:type frequency: float
|
|
618
584
|
|
|
619
585
|
.. note::
|
|
620
|
-
The frequency change will take effect on the next
|
|
586
|
+
The frequency change will take effect on the next broadcast.
|
|
621
587
|
|
|
622
588
|
Example:
|
|
623
589
|
>>> pw.set_frequency(101.5)
|
|
@@ -634,22 +600,24 @@ class PiWave:
|
|
|
634
600
|
The returned dictionary contains:
|
|
635
601
|
|
|
636
602
|
- **is_playing** (bool): Whether playback is active
|
|
637
|
-
- **current_index** (int): Current position in playlist
|
|
638
|
-
- **playlist_length** (int): Total number of items in playlist
|
|
639
603
|
- **frequency** (float): Current broadcast frequency
|
|
640
604
|
- **current_file** (str|None): Path of currently playing file
|
|
605
|
+
- **ps** (str): Program Service name
|
|
606
|
+
- **rt** (str): Radio Text message
|
|
607
|
+
- **pi** (str): Program Identification code
|
|
641
608
|
|
|
642
609
|
Example:
|
|
643
610
|
>>> status = pw.get_status()
|
|
644
611
|
>>> print(f"Playing: {status['is_playing']}")
|
|
645
|
-
>>> print(f"
|
|
612
|
+
>>> print(f"Current file: {status['current_file']}")
|
|
646
613
|
"""
|
|
647
614
|
return {
|
|
648
615
|
'is_playing': self.is_playing,
|
|
649
|
-
'current_index': self.current_index,
|
|
650
|
-
'playlist_length': len(self.playlist),
|
|
651
616
|
'frequency': self.frequency,
|
|
652
|
-
'current_file': self.
|
|
617
|
+
'current_file': self.current_file,
|
|
618
|
+
'ps': self.ps,
|
|
619
|
+
'rt': self.rt,
|
|
620
|
+
'pi': self.pi
|
|
653
621
|
}
|
|
654
622
|
|
|
655
623
|
def cleanup(self):
|
|
@@ -671,11 +639,11 @@ class PiWave:
|
|
|
671
639
|
def __del__(self):
|
|
672
640
|
self.cleanup()
|
|
673
641
|
|
|
674
|
-
def send(self,
|
|
642
|
+
def send(self, file_path: str):
|
|
675
643
|
"""Alias for the play method.
|
|
676
644
|
|
|
677
|
-
:param
|
|
678
|
-
:type
|
|
645
|
+
:param file_path: Path to local audio file
|
|
646
|
+
:type file_path: str
|
|
679
647
|
:return: True if playback started successfully, False otherwise
|
|
680
648
|
:rtype: bool
|
|
681
649
|
|
|
@@ -683,22 +651,9 @@ class PiWave:
|
|
|
683
651
|
This is an alias for :meth:`play` for backward compatibility.
|
|
684
652
|
|
|
685
653
|
Example:
|
|
686
|
-
>>> pw.send(
|
|
687
|
-
"""
|
|
688
|
-
return self.play(files)
|
|
689
|
-
|
|
690
|
-
def restart(self):
|
|
691
|
-
"""Restart playback from the beginning of the playlist.
|
|
692
|
-
|
|
693
|
-
Resets the current position to the first track and starts playback.
|
|
694
|
-
Only works if there are files in the playlist.
|
|
695
|
-
|
|
696
|
-
Example:
|
|
697
|
-
>>> pw.restart()
|
|
654
|
+
>>> pw.send('song.mp3')
|
|
698
655
|
"""
|
|
699
|
-
|
|
700
|
-
self.current_index = 0
|
|
701
|
-
self.play()
|
|
656
|
+
return self.play(file_path)
|
|
702
657
|
|
|
703
658
|
if __name__ == "__main__":
|
|
704
659
|
Log.header("PiWave Radio Module")
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: piwave
|
|
3
|
+
Version: 2.0.8
|
|
4
|
+
Summary: A python module to broadcast radio waves with your Raspberry Pi.
|
|
5
|
+
Home-page: https://github.com/douxxtech/piwave
|
|
6
|
+
Author: Douxx
|
|
7
|
+
Author-email: douxx@douxx.tech
|
|
8
|
+
License: GPL-3.0-or-later
|
|
9
|
+
Project-URL: Bug Reports, https://github.com/douxxtech/piwave/issues
|
|
10
|
+
Project-URL: Source, https://github.com/douxxtech/piwave
|
|
11
|
+
Keywords: raspberry pi,radio,fm,rds,streaming,audio,broadcast
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Requires-Python: >=3.7
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
25
|
+
Requires-Dist: flake8>=4.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
<div align=center>
|
|
29
|
+
<img alt="PiWave image" src="https://piwave.xyz/static/img/logo.png"/>
|
|
30
|
+
<h1>PiWave</h1>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
**PiWave** is a Python module designed to manage and control your Raspberry Pi radio using the `pi_fm_rds` utility. It allows you to easily convert audio files to WAV format and broadcast them at a specified frequency with RDS (Radio Data System) support.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- Supports most audio file formats (MP3, FLAC, M4A, etc.)
|
|
38
|
+
- Configurable broadcast frequency, PS (Program Service), RT (Radio Text), and PI (Program Identifier)
|
|
39
|
+
- Real-time settings updates without restart
|
|
40
|
+
- Detailed logging with debug mode
|
|
41
|
+
- Error handling and event callbacks
|
|
42
|
+
- Non-blocking playback with threading
|
|
43
|
+
- Simple streaming-focused design
|
|
44
|
+
|
|
45
|
+
## Hardware Installation
|
|
46
|
+
|
|
47
|
+
To use PiWave for broadcasting, you need to set up the hardware correctly. This involves connecting an antenna or cable to the Raspberry Pi's GPIO pin.
|
|
48
|
+
|
|
49
|
+
1. **Connect the Cable or Antenna**:
|
|
50
|
+
- Attach a cable or an antenna to GPIO 4 (Pin 7) on the Raspberry Pi.
|
|
51
|
+
- Ensure the connection is secure to avoid any broadcasting issues.
|
|
52
|
+
|
|
53
|
+
2. **GPIO Pinout**:
|
|
54
|
+
- GPIO 4 (Pin 7) is used for the broadcasting signal.
|
|
55
|
+
- Ensure that the cable or antenna is properly connected to this pin for optimal performance.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
> [!WARNING]
|
|
60
|
+
> **Warning**: Using PiWave involves broadcasting signals which may be subject to local regulations and laws. It is your responsibility to ensure that your use of PiWave complies with all applicable legal requirements and regulations in your area. Unauthorized use of broadcasting equipment may result in legal consequences, including fines or penalties.
|
|
61
|
+
>
|
|
62
|
+
> **Liability**: The author of PiWave is not responsible for any damage, loss, or legal issues that may arise from the use of this software. By using PiWave, you agree to accept all risks and liabilities associated with its operation and broadcasting capabilities.
|
|
63
|
+
>
|
|
64
|
+
> Please exercise caution and ensure you have the proper permissions and knowledge of the regulations before using PiWave for broadcasting purposes.
|
|
65
|
+
|
|
66
|
+
### Auto Installer
|
|
67
|
+
|
|
68
|
+
For a quick and easy installation, you can use the auto installer script. Open a terminal and run:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl -sL https://setup.piwave.xyz/ | sudo bash
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This command will download and execute the installation script, setting up PiWave and its dependencies automatically.
|
|
75
|
+
|
|
76
|
+
> [!NOTE]
|
|
77
|
+
> To uninstall, use the following command:
|
|
78
|
+
> ```bash
|
|
79
|
+
> curl -sL https://setup.piwave.xyz/uninstall | sudo bash
|
|
80
|
+
> ```
|
|
81
|
+
|
|
82
|
+
### Manual Installation
|
|
83
|
+
|
|
84
|
+
To install PiWave manually, follow these steps:
|
|
85
|
+
|
|
86
|
+
1. **Clone the repository and install**:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install git+https://github.com/douxxtech/piwave.git
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. **Dependencies**:
|
|
93
|
+
|
|
94
|
+
PiWave requires the `ffmpeg` and `ffprobe` utilities for file conversion and duration extraction. Install them using:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
sudo apt-get install ffmpeg
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
3. **PiFmRds**:
|
|
101
|
+
|
|
102
|
+
PiWave uses [PiFmRds](https://github.com/ChristopheJacquet/PiFmRds) to work. Make sure you have installed it before running PiWave.
|
|
103
|
+
|
|
104
|
+
## Quick Start
|
|
105
|
+
|
|
106
|
+
### Basic Usage
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from piwave import PiWave
|
|
110
|
+
|
|
111
|
+
# Create PiWave instance
|
|
112
|
+
pw = PiWave(
|
|
113
|
+
frequency=90.0,
|
|
114
|
+
ps="MyRadio",
|
|
115
|
+
rt="Playing great music",
|
|
116
|
+
pi="ABCD",
|
|
117
|
+
debug=True
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Play a single audio file
|
|
121
|
+
pw.play("song.mp3")
|
|
122
|
+
|
|
123
|
+
# Stop playback
|
|
124
|
+
pw.stop()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Real-time Settings Updates
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from piwave import PiWave
|
|
131
|
+
|
|
132
|
+
pw = PiWave()
|
|
133
|
+
|
|
134
|
+
# Update multiple settings at once
|
|
135
|
+
pw.update(
|
|
136
|
+
frequency=101.5,
|
|
137
|
+
ps="NewName",
|
|
138
|
+
rt="Updated radio text",
|
|
139
|
+
debug=True
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Update individual settings
|
|
143
|
+
pw.update(frequency=102.1)
|
|
144
|
+
pw.update(ps="Radio2024")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Control Playback
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from piwave import PiWave
|
|
151
|
+
|
|
152
|
+
pw = PiWave(frequency=95.0)
|
|
153
|
+
|
|
154
|
+
# Play, pause, resume
|
|
155
|
+
pw.play("music.mp3")
|
|
156
|
+
pw.pause()
|
|
157
|
+
pw.resume()
|
|
158
|
+
|
|
159
|
+
# Check status
|
|
160
|
+
status = pw.get_status()
|
|
161
|
+
print(f"Playing: {status['is_playing']}")
|
|
162
|
+
print(f"Current file: {status['current_file']}")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Examples
|
|
166
|
+
|
|
167
|
+
### Text-to-Speech Radio
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from gtts import gTTS
|
|
171
|
+
from piwave import PiWave
|
|
172
|
+
from pydub import AudioSegment
|
|
173
|
+
import os
|
|
174
|
+
import sys
|
|
175
|
+
import time
|
|
176
|
+
|
|
177
|
+
wav_file = sys.argv[1] if len(sys.argv) > 1 else 'tts.wav'
|
|
178
|
+
|
|
179
|
+
def tts(text, wav_file):
|
|
180
|
+
mp3_file = "tts.mp3"
|
|
181
|
+
tts = gTTS(text=text, lang="en", slow=False)
|
|
182
|
+
tts.save(mp3_file)
|
|
183
|
+
sound = AudioSegment.from_mp3(mp3_file)
|
|
184
|
+
sound.export(wav_file, format="wav")
|
|
185
|
+
os.remove(mp3_file)
|
|
186
|
+
|
|
187
|
+
def main():
|
|
188
|
+
pw = None
|
|
189
|
+
|
|
190
|
+
print("=" * 50)
|
|
191
|
+
print("Text Broadcast by https://douxx.tech")
|
|
192
|
+
print("""You need PiWave and a raspberry pi with root
|
|
193
|
+
access to run this tool !""")
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
while True:
|
|
197
|
+
print("=" * 50)
|
|
198
|
+
text = input("Text to broadcast: ").strip()
|
|
199
|
+
if not text:
|
|
200
|
+
print("No text entered, skipping...\n")
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
freq = float(input("Frequency to broadcast (MHz): "))
|
|
205
|
+
except ValueError:
|
|
206
|
+
print("Invalid frequency, please enter a number.\n")
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
tts(text, wav_file)
|
|
210
|
+
pw = PiWave(silent=True, frequency=freq)
|
|
211
|
+
|
|
212
|
+
print("=" * 50)
|
|
213
|
+
print("Ready to play!")
|
|
214
|
+
print(f"Frequency : {freq} MHz")
|
|
215
|
+
print(f"Text : {text}")
|
|
216
|
+
print(f"WAV file : {os.path.abspath(wav_file)}")
|
|
217
|
+
print("=" * 50)
|
|
218
|
+
|
|
219
|
+
pw.play(wav_file)
|
|
220
|
+
print("Playing! Press Ctrl+C to stop or wait for completion...\n")
|
|
221
|
+
|
|
222
|
+
# Wait for playback to complete
|
|
223
|
+
while pw.get_status()['is_playing']:
|
|
224
|
+
time.sleep(0.5)
|
|
225
|
+
|
|
226
|
+
print("Playback completed!\n")
|
|
227
|
+
|
|
228
|
+
except KeyboardInterrupt:
|
|
229
|
+
print("\nStopped by user.")
|
|
230
|
+
finally:
|
|
231
|
+
# Cleanup PiWave
|
|
232
|
+
if pw:
|
|
233
|
+
pw.stop()
|
|
234
|
+
pw.cleanup()
|
|
235
|
+
|
|
236
|
+
# Remove temp file
|
|
237
|
+
if os.path.exists(wav_file):
|
|
238
|
+
os.remove(wav_file)
|
|
239
|
+
|
|
240
|
+
print("Cleanup done. Exiting...")
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
main()
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Music Player with Callbacks
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from piwave import PiWave
|
|
250
|
+
import os
|
|
251
|
+
import time
|
|
252
|
+
|
|
253
|
+
def on_track_change(filename):
|
|
254
|
+
print(f"🎵 Now playing: {os.path.basename(filename)}")
|
|
255
|
+
|
|
256
|
+
def on_error(error):
|
|
257
|
+
print(f"❌ Error occurred: {error}")
|
|
258
|
+
|
|
259
|
+
def main():
|
|
260
|
+
# Create player with callbacks
|
|
261
|
+
pw = PiWave(
|
|
262
|
+
frequency=101.5,
|
|
263
|
+
ps="MyMusic",
|
|
264
|
+
rt="Your favorite tunes",
|
|
265
|
+
on_track_change=on_track_change,
|
|
266
|
+
on_error=on_error
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
# Play different formats
|
|
271
|
+
audio_files = ["song1.mp3", "song2.flac", "song3.m4a"]
|
|
272
|
+
|
|
273
|
+
for audio_file in audio_files:
|
|
274
|
+
if os.path.exists(audio_file):
|
|
275
|
+
print(f"Playing {audio_file}...")
|
|
276
|
+
pw.play(audio_file)
|
|
277
|
+
|
|
278
|
+
# Wait for playback to complete
|
|
279
|
+
while pw.get_status()['is_playing']:
|
|
280
|
+
time.sleep(0.5)
|
|
281
|
+
|
|
282
|
+
print("Track completed. Press Enter for next song or Ctrl+C to quit...")
|
|
283
|
+
input()
|
|
284
|
+
else:
|
|
285
|
+
print(f"File {audio_file} not found, skipping...")
|
|
286
|
+
|
|
287
|
+
print("All tracks completed!")
|
|
288
|
+
|
|
289
|
+
except KeyboardInterrupt:
|
|
290
|
+
print("\nPlayback interrupted by user.")
|
|
291
|
+
finally:
|
|
292
|
+
# Cleanup
|
|
293
|
+
pw.stop()
|
|
294
|
+
pw.cleanup()
|
|
295
|
+
print("Cleanup completed.")
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
main()
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Simple FM Transmitter
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from piwave import PiWave
|
|
305
|
+
import os
|
|
306
|
+
import time
|
|
307
|
+
|
|
308
|
+
def simple_broadcast():
|
|
309
|
+
pw = None
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# Initialize with custom settings
|
|
313
|
+
pw = PiWave(
|
|
314
|
+
frequency=88.5,
|
|
315
|
+
ps="Pi-FM",
|
|
316
|
+
rt="Broadcasting from Raspberry Pi",
|
|
317
|
+
pi="RAPI"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
audio_file = input("Enter audio file path: ")
|
|
321
|
+
|
|
322
|
+
if not os.path.exists(audio_file):
|
|
323
|
+
print("File not found!")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
print(f"Broadcasting {audio_file} on 88.5 MHz")
|
|
327
|
+
print("Press Ctrl+C to stop...")
|
|
328
|
+
|
|
329
|
+
pw.play(audio_file)
|
|
330
|
+
|
|
331
|
+
# Keep program running and show status
|
|
332
|
+
while pw.get_status()['is_playing']:
|
|
333
|
+
time.sleep(1)
|
|
334
|
+
|
|
335
|
+
print("\nPlayback completed!")
|
|
336
|
+
|
|
337
|
+
except KeyboardInterrupt:
|
|
338
|
+
print("\nStopping broadcast...")
|
|
339
|
+
except Exception as e:
|
|
340
|
+
print(f"Error: {e}")
|
|
341
|
+
finally:
|
|
342
|
+
if pw:
|
|
343
|
+
pw.stop()
|
|
344
|
+
pw.cleanup()
|
|
345
|
+
print("Broadcast stopped and cleaned up.")
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
simple_broadcast()
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## API Reference
|
|
352
|
+
|
|
353
|
+
### PiWave Class
|
|
354
|
+
|
|
355
|
+
#### Initialization
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
PiWave(
|
|
359
|
+
frequency=90.0, # Broadcast frequency (80.0-108.0 MHz)
|
|
360
|
+
ps="PiWave", # Program Service name (max 8 chars)
|
|
361
|
+
rt="PiWave: ...", # Radio Text (max 64 chars)
|
|
362
|
+
pi="FFFF", # Program Identifier (4 hex digits)
|
|
363
|
+
debug=False, # Enable debug logging
|
|
364
|
+
silent=False, # Disable all logging
|
|
365
|
+
on_track_change=None, # Callback for track changes
|
|
366
|
+
on_error=None # Callback for errors
|
|
367
|
+
)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### Methods
|
|
371
|
+
|
|
372
|
+
- **`play(file_path)`** - Play an audio file
|
|
373
|
+
- **`stop()`** - Stop playback
|
|
374
|
+
- **`pause()`** - Pause current playback
|
|
375
|
+
- **`resume()`** - Resume playback
|
|
376
|
+
- **`update(**kwargs)`** - Update any settings in real-time
|
|
377
|
+
- **`set_frequency(freq)`** - Change broadcast frequency
|
|
378
|
+
- **`get_status()`** - Get current player status
|
|
379
|
+
- **`cleanup()`** - Clean up resources
|
|
380
|
+
|
|
381
|
+
#### Properties
|
|
382
|
+
|
|
383
|
+
Access current settings through `get_status()`:
|
|
384
|
+
- `is_playing` - Whether audio is currently playing
|
|
385
|
+
- `frequency` - Current broadcast frequency
|
|
386
|
+
- `current_file` - Currently loaded file
|
|
387
|
+
- `ps` - Program Service name
|
|
388
|
+
- `rt` - Radio Text
|
|
389
|
+
- `pi` - Program Identifier
|
|
390
|
+
|
|
391
|
+
## Error Handling
|
|
392
|
+
|
|
393
|
+
- **Raspberry Pi Check**: Verifies the program is running on a Raspberry Pi
|
|
394
|
+
- **Root User Check**: Requires root privileges for GPIO access
|
|
395
|
+
- **Executable Check**: Automatically finds `pi_fm_rds` or prompts for path
|
|
396
|
+
- **File Validation**: Checks file existence and conversion capability
|
|
397
|
+
- **Process Management**: Handles cleanup of broadcasting processes
|
|
398
|
+
|
|
399
|
+
## Requirements
|
|
400
|
+
|
|
401
|
+
- Raspberry Pi (any model with GPIO)
|
|
402
|
+
- Root access (`sudo`)
|
|
403
|
+
- Python 3.6+
|
|
404
|
+
- FFmpeg for audio conversion
|
|
405
|
+
- PiFmRds for FM transmission
|
|
406
|
+
|
|
407
|
+
## License
|
|
408
|
+
|
|
409
|
+
PiWave is licensed under the GNU General Public License (GPL) v3.0. See the [LICENSE](LICENSE) file for more details.
|
|
410
|
+
|
|
411
|
+
## Contributing
|
|
412
|
+
|
|
413
|
+
Contributions are welcome! Please submit a pull request or open an issue on [GitHub](https://github.com/douxxtech/piwave/issues) for any bugs or feature requests.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
Thank you for using PiWave!
|
|
418
|
+
|
|
419
|
+

|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
piwave/__init__.py,sha256=tAmruZvneieh6fgkf7chKzOX9Q6fEB-5Jt9FJ7Fl5xQ,74
|
|
2
|
+
piwave/piwave.py,sha256=NG8zDub2qCcy88_zOHL8nD3Jfw9jzlUQyGyXBseLhmI,22249
|
|
3
|
+
piwave-2.0.8.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
4
|
+
piwave-2.0.8.dist-info/METADATA,sha256=GJ5BGL6jLHpAiFlSbQQl1OHunLc-jHx0GEudMKs6wao,11697
|
|
5
|
+
piwave-2.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
piwave-2.0.8.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
|
|
7
|
+
piwave-2.0.8.dist-info/RECORD,,
|
piwave-2.0.6.dist-info/METADATA
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: piwave
|
|
3
|
-
Version: 2.0.6
|
|
4
|
-
Summary: A python module to broadcast radio waves with your Raspberry Pi.
|
|
5
|
-
Home-page: https://github.com/douxxtech/piwave
|
|
6
|
-
Author: Douxx
|
|
7
|
-
Author-email: douxx@douxx.tech
|
|
8
|
-
License: GPL-3.0-or-later
|
|
9
|
-
Project-URL: Bug Reports, https://github.com/douxxtech/piwave/issues
|
|
10
|
-
Project-URL: Source, https://github.com/douxxtech/piwave
|
|
11
|
-
Keywords: raspberry pi,radio,fm,rds,streaming,audio,broadcast
|
|
12
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
17
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
-
Requires-Python: >=3.7
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
License-File: LICENSE
|
|
21
|
-
Provides-Extra: dev
|
|
22
|
-
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
23
|
-
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
|
24
|
-
Requires-Dist: black>=22.0; extra == "dev"
|
|
25
|
-
Requires-Dist: flake8>=4.0; extra == "dev"
|
|
26
|
-
Dynamic: license-file
|
|
27
|
-
|
|
28
|
-
<div align=center>
|
|
29
|
-
<img alt="PiWave image" src="https://piwave.xyz/static/img/logo.png"/>
|
|
30
|
-
<h1>PiWave</h1>
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
**PiWave** is a Python module designed to manage and control your Raspberry Pi radio using the `pi_fm_rds` utility. It allows you to easily convert audio files to WAV format and broadcast them at a specified frequency with RDS (Radio Data System) support.
|
|
34
|
-
|
|
35
|
-
## Features
|
|
36
|
-
|
|
37
|
-
- Converts audio files to WAV format.
|
|
38
|
-
- Broadcasts WAV files using the `pi_fm_rds` utility.
|
|
39
|
-
- Configurable broadcast frequency, PS (Program Service), RT (Radio Text), and PI (Program Identifier).
|
|
40
|
-
- Supports looping of playback.
|
|
41
|
-
- Detailed logging for debug mode.
|
|
42
|
-
- Supports streaming from URLs.
|
|
43
|
-
- Better error handling and event callbacks.
|
|
44
|
-
- Non-blocking playback with threading.
|
|
45
|
-
|
|
46
|
-
## Hardware Installation
|
|
47
|
-
|
|
48
|
-
To use PiWave for broadcasting, you need to set up the hardware correctly. This involves connecting an antenna or cable to the Raspberry Pi's GPIO pin.
|
|
49
|
-
|
|
50
|
-
1. **Connect the Cable or Antenna**:
|
|
51
|
-
- Attach a cable or an antenna to GPIO 4 (Pin 7) on the Raspberry Pi.
|
|
52
|
-
- Ensure the connection is secure to avoid any broadcasting issues.
|
|
53
|
-
|
|
54
|
-
2. **GPIO Pinout**:
|
|
55
|
-
- GPIO 4 (Pin 7) is used for the broadcasting signal.
|
|
56
|
-
- Ensure that the cable or antenna is properly connected to this pin for optimal performance.
|
|
57
|
-
|
|
58
|
-
## Installation
|
|
59
|
-
|
|
60
|
-
> [!WARNING]
|
|
61
|
-
> **Warning**: Using PiWave involves broadcasting signals which may be subject to local regulations and laws. It is your responsibility to ensure that your use of PiWave complies with all applicable legal requirements and regulations in your area. Unauthorized use of broadcasting equipment may result in legal consequences, including fines or penalties.
|
|
62
|
-
>
|
|
63
|
-
> **Liability**: The author of PiWave is not responsible for any damage, loss, or legal issues that may arise from the use of this software. By using PiWave, you agree to accept all risks and liabilities associated with its operation and broadcasting capabilities.
|
|
64
|
-
>
|
|
65
|
-
> Please exercise caution and ensure you have the proper permissions and knowledge of the regulations before using PiWave for broadcasting purposes.
|
|
66
|
-
|
|
67
|
-
### Auto Installer
|
|
68
|
-
|
|
69
|
-
For a quick and easy installation, you can use the auto installer script. Open a terminal and run:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
curl -sL https://setup.piwave.xyz/ | sudo bash
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
This command will download and execute the installation script, setting up PiWave and its dependencies automatically.
|
|
76
|
-
|
|
77
|
-
> [!NOTE]
|
|
78
|
-
> To uninstall, use the following command:
|
|
79
|
-
> ```bash
|
|
80
|
-
> curl -sL https://setup.piwave.xyz/uninstall | sudo bash
|
|
81
|
-
> ```
|
|
82
|
-
|
|
83
|
-
### Manual Installation
|
|
84
|
-
|
|
85
|
-
To install PiWave manually, follow these steps:
|
|
86
|
-
|
|
87
|
-
1. **Clone the repository and install**:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
pip install git+https://github.com/douxxtech/piwave.git --break-system-packages
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
2. **Dependencies**:
|
|
94
|
-
|
|
95
|
-
PiWave requires the `ffmpeg` and `ffprobe` utilities for file conversion and duration extraction. Install them using:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
sudo apt-get install ffmpeg
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
3. **PiFmRds**:
|
|
102
|
-
|
|
103
|
-
PiWave uses [PiFmRds](https://github.com/ChristopheJacquet/PiFmRds) to work. Make sure you have installed it before running PiWave.
|
|
104
|
-
|
|
105
|
-
## Usage
|
|
106
|
-
|
|
107
|
-
### Basic Usage
|
|
108
|
-
|
|
109
|
-
1. **Importing the module**:
|
|
110
|
-
|
|
111
|
-
```python
|
|
112
|
-
from piwave import PiWave
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
2. **Creating an instance**:
|
|
116
|
-
|
|
117
|
-
```python
|
|
118
|
-
piwave = PiWave(
|
|
119
|
-
frequency=90.0,
|
|
120
|
-
ps="MyRadio",
|
|
121
|
-
rt="Playing great music",
|
|
122
|
-
pi="ABCD",
|
|
123
|
-
loop=True,
|
|
124
|
-
debug=True,
|
|
125
|
-
on_track_change=lambda file, index: print(f"Now playing: {file}"),
|
|
126
|
-
on_error=lambda error: print(f"Error occurred: {error}")
|
|
127
|
-
)
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
3. **Adding files to the playlist**:
|
|
131
|
-
|
|
132
|
-
```python
|
|
133
|
-
files = ["path/to/your/audiofile.mp3", "http://example.com/stream.mp3"]
|
|
134
|
-
piwave.add_files(files)
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
4. **Starting playback**:
|
|
138
|
-
|
|
139
|
-
```python
|
|
140
|
-
piwave.play() # or files = ["path/to/your/audiofile.mp3", "http://example.com/stream.mp3"]; piwave.play(files)
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
5. **Stopping playback**:
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
piwave.stop()
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
6. **Pausing and resuming playback**:
|
|
150
|
-
|
|
151
|
-
```python
|
|
152
|
-
piwave.pause()
|
|
153
|
-
piwave.resume()
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
7. **Skipping tracks**:
|
|
157
|
-
|
|
158
|
-
```python
|
|
159
|
-
piwave.next_track()
|
|
160
|
-
piwave.previous_track()
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
8. **Changing frequency**:
|
|
164
|
-
|
|
165
|
-
```python
|
|
166
|
-
piwave.set_frequency(95.0)
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
9. **Getting status**:
|
|
170
|
-
|
|
171
|
-
```python
|
|
172
|
-
status = piwave.get_status()
|
|
173
|
-
print(status)
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Configuration
|
|
177
|
-
|
|
178
|
-
- `frequency`: The broadcast frequency in MHz (default: 90.0).
|
|
179
|
-
- `ps`: Program Service name (up to 8 characters, default: "PiWave").
|
|
180
|
-
- `rt`: Radio Text (up to 64 characters, default: "PiWave: The best python module for managing your pi radio").
|
|
181
|
-
- `pi`: Program Identifier (up to 4 characters, default: "FFFF").
|
|
182
|
-
- `loop`: Whether to loop playback of files (default: False).
|
|
183
|
-
- `debug`: Enable detailed debug logging (default: False).
|
|
184
|
-
- `on_track_change`: Callback function when the track changes (default: None).
|
|
185
|
-
- `on_error`: Callback function when an error occurs (default: None).
|
|
186
|
-
|
|
187
|
-
## Error Handling
|
|
188
|
-
|
|
189
|
-
- **Raspberry Pi Check**: The program verifies if it is running on a Raspberry Pi. It exits with an error message if not.
|
|
190
|
-
- **Root User Check**: The program requires root privileges to run. It exits with an error message if not run as root.
|
|
191
|
-
- **Executable Check**: The program automatically finds the `pi_fm_rds` executable or prompts the user to manually provide its path if it cannot be found.
|
|
192
|
-
|
|
193
|
-
## License
|
|
194
|
-
|
|
195
|
-
PiWave is licensed under the GNU General Public License (GPL) v3.0. See the [LICENSE](LICENSE) file for more details.
|
|
196
|
-
|
|
197
|
-
## Contributing
|
|
198
|
-
|
|
199
|
-
Contributions are welcome! Please submit a pull request or open an issue on [GitHub](https://github.com/douxxtech/piwave/issues) for any bugs or feature requests.
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
Thank you for using PiWave!
|
|
204
|
-
|
|
205
|
-
Made with <3 by Douxx
|
piwave-2.0.6.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
piwave/__init__.py,sha256=tAmruZvneieh6fgkf7chKzOX9Q6fEB-5Jt9FJ7Fl5xQ,74
|
|
2
|
-
piwave/piwave.py,sha256=SmBZmTW3NtJnHwCJcg1rLimSoYEOCZXRNrV7_3W5fPA,24307
|
|
3
|
-
piwave-2.0.6.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
4
|
-
piwave-2.0.6.dist-info/METADATA,sha256=ZLODMyeli7CeqRtkecDcDpBRXnXmZ5K8xaEVjc9NGmw,6768
|
|
5
|
-
piwave-2.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
piwave-2.0.6.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
|
|
7
|
-
piwave-2.0.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|