piwave 2.0.7__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 +126 -186
- piwave-2.0.8.dist-info/METADATA +419 -0
- piwave-2.0.8.dist-info/RECORD +7 -0
- piwave-2.0.7.dist-info/METADATA +0 -207
- piwave-2.0.7.dist-info/RECORD +0 -7
- {piwave-2.0.7.dist-info → piwave-2.0.8.dist-info}/WHEEL +0 -0
- {piwave-2.0.7.dist-info → piwave-2.0.8.dist-info}/licenses/LICENSE +0 -0
- {piwave-2.0.7.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
|
|
@@ -118,7 +118,6 @@ class PiWave:
|
|
|
118
118
|
ps: str = "PiWave",
|
|
119
119
|
rt: str = "PiWave: The best python module for managing your pi radio",
|
|
120
120
|
pi: str = "FFFF",
|
|
121
|
-
loop: bool = False,
|
|
122
121
|
debug: bool = False,
|
|
123
122
|
silent: bool = False,
|
|
124
123
|
on_track_change: Optional[Callable] = None,
|
|
@@ -133,8 +132,6 @@ class PiWave:
|
|
|
133
132
|
:type rt: str
|
|
134
133
|
:param pi: Program Identification code (4 hex digits)
|
|
135
134
|
:type pi: str
|
|
136
|
-
:param loop: Whether to loop the playlist when it ends
|
|
137
|
-
:type loop: bool
|
|
138
135
|
:param debug: Enable debug logging
|
|
139
136
|
:type debug: bool
|
|
140
137
|
:param silent: Removes every output log
|
|
@@ -155,13 +152,10 @@ class PiWave:
|
|
|
155
152
|
self.ps = str(ps)[:8]
|
|
156
153
|
self.rt = str(rt)[:64]
|
|
157
154
|
self.pi = str(pi).upper()[:4]
|
|
158
|
-
self.loop = loop
|
|
159
155
|
self.on_track_change = on_track_change
|
|
160
156
|
self.on_error = on_error
|
|
161
157
|
|
|
162
|
-
self.
|
|
163
|
-
self.converted_files: dict[str, str] = {}
|
|
164
|
-
self.current_index = 0
|
|
158
|
+
self.current_file: Optional[str] = None
|
|
165
159
|
self.is_playing = False
|
|
166
160
|
self.is_stopped = False
|
|
167
161
|
|
|
@@ -170,7 +164,6 @@ class PiWave:
|
|
|
170
164
|
self.stop_event = threading.Event()
|
|
171
165
|
|
|
172
166
|
self.temp_dir = tempfile.mkdtemp(prefix="piwave_")
|
|
173
|
-
self.stream_process: Optional[subprocess.Popen] = None
|
|
174
167
|
|
|
175
168
|
Log.config(silent=silent)
|
|
176
169
|
|
|
@@ -181,11 +174,11 @@ class PiWave:
|
|
|
181
174
|
atexit.register(self.cleanup)
|
|
182
175
|
|
|
183
176
|
|
|
184
|
-
Log.info(f"PiWave initialized - Frequency: {frequency}MHz, PS: {ps}
|
|
177
|
+
Log.info(f"PiWave initialized - Frequency: {frequency}MHz, PS: {ps}")
|
|
185
178
|
|
|
186
179
|
def _log_debug(self, message: str):
|
|
187
180
|
if self.debug:
|
|
188
|
-
Log.print(f"[DEBUG] {message}", '
|
|
181
|
+
Log.print(f"[DEBUG] {message}", 'bright_cyan')
|
|
189
182
|
|
|
190
183
|
|
|
191
184
|
def _validate_environment(self):
|
|
@@ -200,10 +193,10 @@ class PiWave:
|
|
|
200
193
|
|
|
201
194
|
def _is_raspberry_pi(self) -> bool:
|
|
202
195
|
try:
|
|
203
|
-
with open(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
except
|
|
196
|
+
with open('/proc/cpuinfo', 'r') as f:
|
|
197
|
+
cpuinfo = f.read()
|
|
198
|
+
return 'raspberry' in cpuinfo.lower()
|
|
199
|
+
except:
|
|
207
200
|
return False
|
|
208
201
|
|
|
209
202
|
def _is_root(self) -> bool:
|
|
@@ -261,52 +254,17 @@ class PiWave:
|
|
|
261
254
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
262
255
|
return False
|
|
263
256
|
|
|
264
|
-
def _is_url(self, path: str) -> bool:
|
|
265
|
-
parsed = urlparse(path)
|
|
266
|
-
return parsed.scheme in ('http', 'https', 'ftp')
|
|
267
|
-
|
|
268
257
|
def _is_wav_file(self, filepath: str) -> bool:
|
|
269
258
|
return filepath.lower().endswith('.wav')
|
|
270
259
|
|
|
271
|
-
async def _download_stream_chunk(self, url: str, output_file: str, duration: int = 30) -> bool:
|
|
272
|
-
#Download a chunk of stream for specified duration
|
|
273
|
-
try:
|
|
274
|
-
cmd = [
|
|
275
|
-
'ffmpeg', '-i', url, '-t', str(duration),
|
|
276
|
-
'-acodec', 'pcm_s16le', '-ar', '44100', '-ac', '2',
|
|
277
|
-
'-y', output_file
|
|
278
|
-
]
|
|
279
|
-
|
|
280
|
-
process = await asyncio.create_subprocess_exec(
|
|
281
|
-
*cmd,
|
|
282
|
-
stdout=asyncio.subprocess.PIPE,
|
|
283
|
-
stderr=asyncio.subprocess.PIPE
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=duration + 10)
|
|
287
|
-
return process.returncode == 0
|
|
288
|
-
|
|
289
|
-
except asyncio.TimeoutError:
|
|
290
|
-
Log.error(f"Timeout downloading stream chunk from {url}")
|
|
291
|
-
return False
|
|
292
|
-
except Exception as e:
|
|
293
|
-
Log.error(f"Error downloading stream: {e}")
|
|
294
|
-
return False
|
|
295
260
|
|
|
296
261
|
def _convert_to_wav(self, filepath: str) -> Optional[str]:
|
|
297
|
-
if
|
|
298
|
-
return self.converted_files[filepath]
|
|
299
|
-
|
|
300
|
-
if self._is_wav_file(filepath) and not self._is_url(filepath):
|
|
301
|
-
self.converted_files[filepath] = filepath
|
|
262
|
+
if self._is_wav_file(filepath):
|
|
302
263
|
return filepath
|
|
303
264
|
|
|
304
265
|
Log.file_message(f"Converting {filepath} to WAV")
|
|
305
266
|
|
|
306
|
-
|
|
307
|
-
output_file = os.path.join(self.temp_dir, f"stream_{int(time.time())}.wav")
|
|
308
|
-
else:
|
|
309
|
-
output_file = f"{os.path.splitext(filepath)[0]}_converted.wav"
|
|
267
|
+
output_file = f"{os.path.splitext(filepath)[0]}_converted.wav"
|
|
310
268
|
|
|
311
269
|
cmd = [
|
|
312
270
|
'ffmpeg', '-i', filepath, '-acodec', 'pcm_s16le',
|
|
@@ -314,7 +272,7 @@ class PiWave:
|
|
|
314
272
|
]
|
|
315
273
|
|
|
316
274
|
try:
|
|
317
|
-
|
|
275
|
+
subprocess.run(
|
|
318
276
|
cmd,
|
|
319
277
|
stdout=subprocess.PIPE,
|
|
320
278
|
stderr=subprocess.PIPE,
|
|
@@ -324,7 +282,6 @@ class PiWave:
|
|
|
324
282
|
|
|
325
283
|
self._log_debug(f"FFmpeg conversion successful for {filepath}")
|
|
326
284
|
|
|
327
|
-
self.converted_files[filepath] = output_file
|
|
328
285
|
return output_file
|
|
329
286
|
|
|
330
287
|
except subprocess.TimeoutExpired:
|
|
@@ -383,7 +340,7 @@ class PiWave:
|
|
|
383
340
|
)
|
|
384
341
|
|
|
385
342
|
if self.on_track_change:
|
|
386
|
-
self.on_track_change(wav_file
|
|
343
|
+
self.on_track_change(wav_file)
|
|
387
344
|
|
|
388
345
|
# wait for either
|
|
389
346
|
# The duration to elapse (then kill the process), or
|
|
@@ -428,28 +385,25 @@ class PiWave:
|
|
|
428
385
|
def _playback_worker(self):
|
|
429
386
|
self._log_debug("Playback worker started")
|
|
430
387
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
continue
|
|
436
|
-
else:
|
|
437
|
-
break
|
|
438
|
-
|
|
439
|
-
if self.current_index < len(self.playlist):
|
|
440
|
-
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
|
|
441
392
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
446
398
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
399
|
+
if not os.path.exists(wav_file):
|
|
400
|
+
Log.error(f"File not found: {wav_file}")
|
|
401
|
+
self.is_playing = False
|
|
402
|
+
return
|
|
451
403
|
|
|
452
|
-
|
|
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}")
|
|
453
407
|
|
|
454
408
|
self.is_playing = False
|
|
455
409
|
self._log_debug("Playback worker finished")
|
|
@@ -460,70 +414,27 @@ class PiWave:
|
|
|
460
414
|
self.stop()
|
|
461
415
|
os._exit(0)
|
|
462
416
|
|
|
463
|
-
def
|
|
464
|
-
"""
|
|
417
|
+
def play(self, file_path: str) -> bool:
|
|
418
|
+
"""Start playing the specified audio file.
|
|
465
419
|
|
|
466
|
-
:param
|
|
467
|
-
:type
|
|
468
|
-
:return: True if at least one file was successfully added, False otherwise
|
|
469
|
-
:rtype: bool
|
|
470
|
-
|
|
471
|
-
.. note::
|
|
472
|
-
Files are automatically converted to WAV format if needed.
|
|
473
|
-
URLs are supported for streaming audio.
|
|
474
|
-
|
|
475
|
-
Example:
|
|
476
|
-
>>> pw.add_files(['song1.mp3', 'song2.wav', 'http://stream.url'])
|
|
477
|
-
"""
|
|
478
|
-
|
|
479
|
-
converted_files = []
|
|
480
|
-
|
|
481
|
-
for file_path in files:
|
|
482
|
-
if self._is_url(file_path):
|
|
483
|
-
converted_files.append(file_path)
|
|
484
|
-
else:
|
|
485
|
-
wav_file = self._convert_to_wav(file_path)
|
|
486
|
-
if wav_file:
|
|
487
|
-
converted_files.append(wav_file)
|
|
488
|
-
else:
|
|
489
|
-
Log.warning(f"Failed to convert {file_path}")
|
|
490
|
-
|
|
491
|
-
if converted_files:
|
|
492
|
-
self.playlist.extend(converted_files)
|
|
493
|
-
Log.success(f"Added {len(converted_files)} files to playlist")
|
|
494
|
-
return True
|
|
495
|
-
|
|
496
|
-
return False
|
|
497
|
-
|
|
498
|
-
def play(self, files: Optional[List[str]] = None) -> bool:
|
|
499
|
-
"""Start playing the playlist or specified files.
|
|
500
|
-
|
|
501
|
-
:param files: Optional list of files to play. If provided, replaces current playlist
|
|
502
|
-
:type files: Optional[List[str]]
|
|
420
|
+
:param file_path: Path to local audio file
|
|
421
|
+
:type file_path: str
|
|
503
422
|
:return: True if playback started successfully, False otherwise
|
|
504
423
|
:rtype: bool
|
|
505
424
|
|
|
506
425
|
.. note::
|
|
507
|
-
|
|
508
|
-
|
|
426
|
+
Files are automatically converted to WAV format if needed.
|
|
427
|
+
Only local files are supported.
|
|
509
428
|
|
|
510
429
|
Example:
|
|
511
|
-
>>> pw.play(
|
|
512
|
-
>>> pw.play()
|
|
430
|
+
>>> pw.play('song.mp3')
|
|
431
|
+
>>> pw.play('audio.wav')
|
|
513
432
|
"""
|
|
514
|
-
if files:
|
|
515
|
-
self.playlist.clear()
|
|
516
|
-
self.current_index = 0
|
|
517
|
-
if not self.add_files(files):
|
|
518
|
-
return False
|
|
519
|
-
|
|
520
|
-
if not self.playlist:
|
|
521
|
-
Log.warning("No files in playlist")
|
|
522
|
-
return False
|
|
523
433
|
|
|
524
434
|
if self.is_playing:
|
|
525
435
|
self.stop()
|
|
526
436
|
|
|
437
|
+
self.current_file = file_path
|
|
527
438
|
self.stop_event.clear()
|
|
528
439
|
self.is_stopped = False
|
|
529
440
|
self.is_playing = True
|
|
@@ -561,15 +472,6 @@ class PiWave:
|
|
|
561
472
|
finally:
|
|
562
473
|
self.current_process = None
|
|
563
474
|
|
|
564
|
-
if self.stream_process:
|
|
565
|
-
try:
|
|
566
|
-
os.killpg(os.getpgid(self.stream_process.pid), signal.SIGTERM)
|
|
567
|
-
self.stream_process.wait(timeout=5)
|
|
568
|
-
except Exception:
|
|
569
|
-
pass
|
|
570
|
-
finally:
|
|
571
|
-
self.stream_process = None
|
|
572
|
-
|
|
573
475
|
if self.playback_thread and self.playback_thread.is_alive():
|
|
574
476
|
self.playback_thread.join(timeout=5)
|
|
575
477
|
|
|
@@ -579,7 +481,7 @@ class PiWave:
|
|
|
579
481
|
def pause(self):
|
|
580
482
|
"""Pause the current playback.
|
|
581
483
|
|
|
582
|
-
Stops the current track but maintains the
|
|
484
|
+
Stops the current track but maintains the file reference.
|
|
583
485
|
Use :meth:`resume` to continue playback.
|
|
584
486
|
|
|
585
487
|
Example:
|
|
@@ -590,40 +492,89 @@ class PiWave:
|
|
|
590
492
|
Log.info("Playback paused")
|
|
591
493
|
|
|
592
494
|
def resume(self):
|
|
593
|
-
"""Resume playback from the current
|
|
495
|
+
"""Resume playback from the current file.
|
|
594
496
|
|
|
595
|
-
Continues playback
|
|
497
|
+
Continues playback of the current file.
|
|
596
498
|
|
|
597
499
|
Example:
|
|
598
500
|
>>> pw.resume()
|
|
599
501
|
"""
|
|
600
|
-
if not self.is_playing and self.
|
|
601
|
-
self.play()
|
|
602
|
-
|
|
603
|
-
def
|
|
604
|
-
|
|
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.
|
|
605
515
|
|
|
606
|
-
|
|
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]
|
|
607
532
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
"""
|
|
611
|
-
if self.is_playing:
|
|
612
|
-
self._stop_current_process()
|
|
613
|
-
self.current_index += 1
|
|
614
|
-
|
|
615
|
-
def previous_track(self):
|
|
616
|
-
"""Go back to the previous track in the playlist.
|
|
617
|
-
|
|
618
|
-
If currently playing, stops the current track and goes to the previous one.
|
|
619
|
-
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.
|
|
620
535
|
|
|
621
536
|
Example:
|
|
622
|
-
>>> pw.
|
|
537
|
+
>>> pw.update(frequency=101.5, ps="NewName")
|
|
538
|
+
>>> pw.update(rt="Updated radio text", debug=True)
|
|
623
539
|
"""
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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")
|
|
627
578
|
|
|
628
579
|
def set_frequency(self, frequency: float):
|
|
629
580
|
"""Change the FM broadcast frequency.
|
|
@@ -632,7 +583,7 @@ class PiWave:
|
|
|
632
583
|
:type frequency: float
|
|
633
584
|
|
|
634
585
|
.. note::
|
|
635
|
-
The frequency change will take effect on the next
|
|
586
|
+
The frequency change will take effect on the next broadcast.
|
|
636
587
|
|
|
637
588
|
Example:
|
|
638
589
|
>>> pw.set_frequency(101.5)
|
|
@@ -649,22 +600,24 @@ class PiWave:
|
|
|
649
600
|
The returned dictionary contains:
|
|
650
601
|
|
|
651
602
|
- **is_playing** (bool): Whether playback is active
|
|
652
|
-
- **current_index** (int): Current position in playlist
|
|
653
|
-
- **playlist_length** (int): Total number of items in playlist
|
|
654
603
|
- **frequency** (float): Current broadcast frequency
|
|
655
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
|
|
656
608
|
|
|
657
609
|
Example:
|
|
658
610
|
>>> status = pw.get_status()
|
|
659
611
|
>>> print(f"Playing: {status['is_playing']}")
|
|
660
|
-
>>> print(f"
|
|
612
|
+
>>> print(f"Current file: {status['current_file']}")
|
|
661
613
|
"""
|
|
662
614
|
return {
|
|
663
615
|
'is_playing': self.is_playing,
|
|
664
|
-
'current_index': self.current_index,
|
|
665
|
-
'playlist_length': len(self.playlist),
|
|
666
616
|
'frequency': self.frequency,
|
|
667
|
-
'current_file': self.
|
|
617
|
+
'current_file': self.current_file,
|
|
618
|
+
'ps': self.ps,
|
|
619
|
+
'rt': self.rt,
|
|
620
|
+
'pi': self.pi
|
|
668
621
|
}
|
|
669
622
|
|
|
670
623
|
def cleanup(self):
|
|
@@ -686,11 +639,11 @@ class PiWave:
|
|
|
686
639
|
def __del__(self):
|
|
687
640
|
self.cleanup()
|
|
688
641
|
|
|
689
|
-
def send(self,
|
|
642
|
+
def send(self, file_path: str):
|
|
690
643
|
"""Alias for the play method.
|
|
691
644
|
|
|
692
|
-
:param
|
|
693
|
-
:type
|
|
645
|
+
:param file_path: Path to local audio file
|
|
646
|
+
:type file_path: str
|
|
694
647
|
:return: True if playback started successfully, False otherwise
|
|
695
648
|
:rtype: bool
|
|
696
649
|
|
|
@@ -698,22 +651,9 @@ class PiWave:
|
|
|
698
651
|
This is an alias for :meth:`play` for backward compatibility.
|
|
699
652
|
|
|
700
653
|
Example:
|
|
701
|
-
>>> pw.send(
|
|
702
|
-
"""
|
|
703
|
-
return self.play(files)
|
|
704
|
-
|
|
705
|
-
def restart(self):
|
|
706
|
-
"""Restart playback from the beginning of the playlist.
|
|
707
|
-
|
|
708
|
-
Resets the current position to the first track and starts playback.
|
|
709
|
-
Only works if there are files in the playlist.
|
|
710
|
-
|
|
711
|
-
Example:
|
|
712
|
-
>>> pw.restart()
|
|
654
|
+
>>> pw.send('song.mp3')
|
|
713
655
|
"""
|
|
714
|
-
|
|
715
|
-
self.current_index = 0
|
|
716
|
-
self.play()
|
|
656
|
+
return self.play(file_path)
|
|
717
657
|
|
|
718
658
|
if __name__ == "__main__":
|
|
719
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.7.dist-info/METADATA
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: piwave
|
|
3
|
-
Version: 2.0.7
|
|
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
|
-
silent=False,
|
|
126
|
-
on_track_change=lambda file, index: print(f"Now playing: {file}"),
|
|
127
|
-
on_error=lambda error: print(f"Error occurred: {error}")
|
|
128
|
-
)
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
3. **Adding files to the playlist**:
|
|
132
|
-
|
|
133
|
-
```python
|
|
134
|
-
files = ["path/to/your/audiofile.mp3", "http://example.com/stream.mp3"]
|
|
135
|
-
piwave.add_files(files)
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
4. **Starting playback**:
|
|
139
|
-
|
|
140
|
-
```python
|
|
141
|
-
piwave.play() # or files = ["path/to/your/audiofile.mp3", "http://example.com/stream.mp3"]; piwave.play(files)
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
5. **Stopping playback**:
|
|
145
|
-
|
|
146
|
-
```python
|
|
147
|
-
piwave.stop()
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
6. **Pausing and resuming playback**:
|
|
151
|
-
|
|
152
|
-
```python
|
|
153
|
-
piwave.pause()
|
|
154
|
-
piwave.resume()
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
7. **Skipping tracks**:
|
|
158
|
-
|
|
159
|
-
```python
|
|
160
|
-
piwave.next_track()
|
|
161
|
-
piwave.previous_track()
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
8. **Changing frequency**:
|
|
165
|
-
|
|
166
|
-
```python
|
|
167
|
-
piwave.set_frequency(95.0)
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
9. **Getting status**:
|
|
171
|
-
|
|
172
|
-
```python
|
|
173
|
-
status = piwave.get_status()
|
|
174
|
-
print(status)
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### Configuration
|
|
178
|
-
|
|
179
|
-
- `frequency`: The broadcast frequency in MHz (default: 90.0).
|
|
180
|
-
- `ps`: Program Service name (up to 8 characters, default: "PiWave").
|
|
181
|
-
- `rt`: Radio Text (up to 64 characters, default: "PiWave: The best python module for managing your pi radio").
|
|
182
|
-
- `pi`: Program Identifier (up to 4 characters, default: "FFFF").
|
|
183
|
-
- `loop`: Whether to loop playback of files (default: False).
|
|
184
|
-
- `debug`: Enable detailed debug logging (default: False).
|
|
185
|
-
- `silent`: Disables every log output (default: False).
|
|
186
|
-
- `on_track_change`: Callback function when the track changes (default: None).
|
|
187
|
-
- `on_error`: Callback function when an error occurs (default: None).
|
|
188
|
-
|
|
189
|
-
## Error Handling
|
|
190
|
-
|
|
191
|
-
- **Raspberry Pi Check**: The program verifies if it is running on a Raspberry Pi. It exits with an error message if not.
|
|
192
|
-
- **Root User Check**: The program requires root privileges to run. It exits with an error message if not run as root.
|
|
193
|
-
- **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.
|
|
194
|
-
|
|
195
|
-
## License
|
|
196
|
-
|
|
197
|
-
PiWave is licensed under the GNU General Public License (GPL) v3.0. See the [LICENSE](LICENSE) file for more details.
|
|
198
|
-
|
|
199
|
-
## Contributing
|
|
200
|
-
|
|
201
|
-
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.
|
|
202
|
-
|
|
203
|
-
---
|
|
204
|
-
|
|
205
|
-
Thank you for using PiWave!
|
|
206
|
-
|
|
207
|
-
Made with <3 by Douxx
|
piwave-2.0.7.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
piwave/__init__.py,sha256=tAmruZvneieh6fgkf7chKzOX9Q6fEB-5Jt9FJ7Fl5xQ,74
|
|
2
|
-
piwave/piwave.py,sha256=GkhNuw_mZnEZbUf6AC4mx4QwYpeFsSDpj1RqXUTf3Ms,24613
|
|
3
|
-
piwave-2.0.7.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
4
|
-
piwave-2.0.7.dist-info/METADATA,sha256=7I5mVMuiAcAzklTaum2d7CqL_V4Z-urMPc6Vcgv4Cno,6845
|
|
5
|
-
piwave-2.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
piwave-2.0.7.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
|
|
7
|
-
piwave-2.0.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|