piwave 2.0.7__py3-none-any.whl → 2.0.9__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 +183 -199
- piwave-2.0.9.dist-info/METADATA +419 -0
- piwave-2.0.9.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.9.dist-info}/WHEEL +0 -0
- {piwave-2.0.7.dist-info → piwave-2.0.9.dist-info}/licenses/LICENSE +0 -0
- {piwave-2.0.7.dist-info → piwave-2.0.9.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,9 +118,9 @@ 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,
|
|
123
|
+
loop: bool = False,
|
|
124
124
|
on_track_change: Optional[Callable] = None,
|
|
125
125
|
on_error: Optional[Callable] = None):
|
|
126
126
|
"""Initialize PiWave FM transmitter.
|
|
@@ -133,12 +133,12 @@ class PiWave:
|
|
|
133
133
|
:type rt: str
|
|
134
134
|
:param pi: Program Identification code (4 hex digits)
|
|
135
135
|
:type pi: str
|
|
136
|
-
:param loop: Whether to loop the playlist when it ends
|
|
137
|
-
:type loop: bool
|
|
138
136
|
:param debug: Enable debug logging
|
|
139
137
|
:type debug: bool
|
|
140
138
|
:param silent: Removes every output log
|
|
141
139
|
:type silent: bool
|
|
140
|
+
:param loop: Loop the current track continuously (default: False)
|
|
141
|
+
:type loop: bool
|
|
142
142
|
:param on_track_change: Callback function called when track changes
|
|
143
143
|
:type on_track_change: Optional[Callable]
|
|
144
144
|
:param on_error: Callback function called when an error occurs
|
|
@@ -159,9 +159,7 @@ class PiWave:
|
|
|
159
159
|
self.on_track_change = on_track_change
|
|
160
160
|
self.on_error = on_error
|
|
161
161
|
|
|
162
|
-
self.
|
|
163
|
-
self.converted_files: dict[str, str] = {}
|
|
164
|
-
self.current_index = 0
|
|
162
|
+
self.current_file: Optional[str] = None
|
|
165
163
|
self.is_playing = False
|
|
166
164
|
self.is_stopped = False
|
|
167
165
|
|
|
@@ -170,7 +168,6 @@ class PiWave:
|
|
|
170
168
|
self.stop_event = threading.Event()
|
|
171
169
|
|
|
172
170
|
self.temp_dir = tempfile.mkdtemp(prefix="piwave_")
|
|
173
|
-
self.stream_process: Optional[subprocess.Popen] = None
|
|
174
171
|
|
|
175
172
|
Log.config(silent=silent)
|
|
176
173
|
|
|
@@ -185,7 +182,7 @@ class PiWave:
|
|
|
185
182
|
|
|
186
183
|
def _log_debug(self, message: str):
|
|
187
184
|
if self.debug:
|
|
188
|
-
Log.print(f"[DEBUG] {message}", '
|
|
185
|
+
Log.print(f"[DEBUG] {message}", 'bright_cyan')
|
|
189
186
|
|
|
190
187
|
|
|
191
188
|
def _validate_environment(self):
|
|
@@ -200,10 +197,10 @@ class PiWave:
|
|
|
200
197
|
|
|
201
198
|
def _is_raspberry_pi(self) -> bool:
|
|
202
199
|
try:
|
|
203
|
-
with open(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
except
|
|
200
|
+
with open('/proc/cpuinfo', 'r') as f:
|
|
201
|
+
cpuinfo = f.read()
|
|
202
|
+
return 'raspberry' in cpuinfo.lower()
|
|
203
|
+
except:
|
|
207
204
|
return False
|
|
208
205
|
|
|
209
206
|
def _is_root(self) -> bool:
|
|
@@ -261,52 +258,17 @@ class PiWave:
|
|
|
261
258
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
262
259
|
return False
|
|
263
260
|
|
|
264
|
-
def _is_url(self, path: str) -> bool:
|
|
265
|
-
parsed = urlparse(path)
|
|
266
|
-
return parsed.scheme in ('http', 'https', 'ftp')
|
|
267
|
-
|
|
268
261
|
def _is_wav_file(self, filepath: str) -> bool:
|
|
269
262
|
return filepath.lower().endswith('.wav')
|
|
270
263
|
|
|
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
264
|
|
|
296
265
|
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
|
|
266
|
+
if self._is_wav_file(filepath):
|
|
302
267
|
return filepath
|
|
303
268
|
|
|
304
269
|
Log.file_message(f"Converting {filepath} to WAV")
|
|
305
270
|
|
|
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"
|
|
271
|
+
output_file = f"{os.path.splitext(filepath)[0]}_converted.wav"
|
|
310
272
|
|
|
311
273
|
cmd = [
|
|
312
274
|
'ffmpeg', '-i', filepath, '-acodec', 'pcm_s16le',
|
|
@@ -314,7 +276,7 @@ class PiWave:
|
|
|
314
276
|
]
|
|
315
277
|
|
|
316
278
|
try:
|
|
317
|
-
|
|
279
|
+
subprocess.run(
|
|
318
280
|
cmd,
|
|
319
281
|
stdout=subprocess.PIPE,
|
|
320
282
|
stderr=subprocess.PIPE,
|
|
@@ -324,7 +286,6 @@ class PiWave:
|
|
|
324
286
|
|
|
325
287
|
self._log_debug(f"FFmpeg conversion successful for {filepath}")
|
|
326
288
|
|
|
327
|
-
self.converted_files[filepath] = output_file
|
|
328
289
|
return output_file
|
|
329
290
|
|
|
330
291
|
except subprocess.TimeoutExpired:
|
|
@@ -374,7 +335,8 @@ class PiWave:
|
|
|
374
335
|
]
|
|
375
336
|
|
|
376
337
|
try:
|
|
377
|
-
|
|
338
|
+
loop_status = "looping" if self.loop else f"Duration: {duration:.1f}s"
|
|
339
|
+
Log.broadcast_message(f"Playing {wav_file} ({loop_status}) at {self.frequency}MHz")
|
|
378
340
|
self.current_process = subprocess.Popen(
|
|
379
341
|
cmd,
|
|
380
342
|
stdout=subprocess.PIPE,
|
|
@@ -383,21 +345,32 @@ class PiWave:
|
|
|
383
345
|
)
|
|
384
346
|
|
|
385
347
|
if self.on_track_change:
|
|
386
|
-
self.on_track_change(wav_file
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
348
|
+
self.on_track_change(wav_file)
|
|
349
|
+
|
|
350
|
+
if self.loop:
|
|
351
|
+
# if looping is enabled, let pi_fm_rds handle the looping
|
|
352
|
+
# and just wait for stop event
|
|
353
|
+
while not self.stop_event.is_set():
|
|
354
|
+
if self.stop_event.wait(timeout=0.1):
|
|
355
|
+
self._stop_current_process()
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
# check if process exists cuz why not
|
|
359
|
+
if self.current_process.poll() is not None:
|
|
360
|
+
Log.error("Process ended unexpectedly while looping")
|
|
361
|
+
return False
|
|
362
|
+
else:
|
|
363
|
+
# if not looping wait for stopevent or end of file
|
|
364
|
+
start_time = time.time()
|
|
365
|
+
while True:
|
|
366
|
+
if self.stop_event.wait(timeout=0.1):
|
|
367
|
+
self._stop_current_process()
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
elapsed = time.time() - start_time
|
|
371
|
+
if elapsed >= duration:
|
|
372
|
+
self._stop_current_process()
|
|
373
|
+
break
|
|
401
374
|
|
|
402
375
|
return True
|
|
403
376
|
|
|
@@ -428,28 +401,25 @@ class PiWave:
|
|
|
428
401
|
def _playback_worker(self):
|
|
429
402
|
self._log_debug("Playback worker started")
|
|
430
403
|
|
|
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]
|
|
404
|
+
if not self.current_file:
|
|
405
|
+
Log.error("No file specified for playback")
|
|
406
|
+
self.is_playing = False
|
|
407
|
+
return
|
|
441
408
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
409
|
+
wav_file = self._convert_to_wav(self.current_file)
|
|
410
|
+
if not wav_file:
|
|
411
|
+
Log.error(f"Failed to convert {self.current_file}")
|
|
412
|
+
self.is_playing = False
|
|
413
|
+
return
|
|
446
414
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
415
|
+
if not os.path.exists(wav_file):
|
|
416
|
+
Log.error(f"File not found: {wav_file}")
|
|
417
|
+
self.is_playing = False
|
|
418
|
+
return
|
|
451
419
|
|
|
452
|
-
|
|
420
|
+
if not self._play_wav(wav_file):
|
|
421
|
+
if not self.stop_event.is_set():
|
|
422
|
+
Log.error(f"Playback failed for {wav_file}")
|
|
453
423
|
|
|
454
424
|
self.is_playing = False
|
|
455
425
|
self._log_debug("Playback worker finished")
|
|
@@ -460,70 +430,28 @@ class PiWave:
|
|
|
460
430
|
self.stop()
|
|
461
431
|
os._exit(0)
|
|
462
432
|
|
|
463
|
-
def
|
|
464
|
-
"""
|
|
465
|
-
|
|
466
|
-
:param files: List of file paths or URLs to add to the playlist
|
|
467
|
-
:type files: List[str]
|
|
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
|
|
433
|
+
def play(self, file_path: str) -> bool:
|
|
434
|
+
"""Start playing the specified audio file.
|
|
497
435
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
:param files: Optional list of files to play. If provided, replaces current playlist
|
|
502
|
-
:type files: Optional[List[str]]
|
|
436
|
+
:param file_path: Path to local audio file
|
|
437
|
+
:type file_path: str
|
|
503
438
|
:return: True if playback started successfully, False otherwise
|
|
504
439
|
:rtype: bool
|
|
505
440
|
|
|
506
441
|
.. note::
|
|
507
|
-
|
|
508
|
-
|
|
442
|
+
Files are automatically converted to WAV format if needed.
|
|
443
|
+
Only local files are supported. If loop is enabled, the file will
|
|
444
|
+
repeat continuously until stop() is called.
|
|
509
445
|
|
|
510
446
|
Example:
|
|
511
|
-
>>> pw.play(
|
|
512
|
-
>>> pw.play()
|
|
447
|
+
>>> pw.play('song.mp3')
|
|
448
|
+
>>> pw.play('audio.wav')
|
|
513
449
|
"""
|
|
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
450
|
|
|
524
451
|
if self.is_playing:
|
|
525
452
|
self.stop()
|
|
526
453
|
|
|
454
|
+
self.current_file = file_path
|
|
527
455
|
self.stop_event.clear()
|
|
528
456
|
self.is_stopped = False
|
|
529
457
|
self.is_playing = True
|
|
@@ -561,15 +489,6 @@ class PiWave:
|
|
|
561
489
|
finally:
|
|
562
490
|
self.current_process = None
|
|
563
491
|
|
|
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
492
|
if self.playback_thread and self.playback_thread.is_alive():
|
|
574
493
|
self.playback_thread.join(timeout=5)
|
|
575
494
|
|
|
@@ -579,7 +498,7 @@ class PiWave:
|
|
|
579
498
|
def pause(self):
|
|
580
499
|
"""Pause the current playback.
|
|
581
500
|
|
|
582
|
-
Stops the current track but maintains the
|
|
501
|
+
Stops the current track but maintains the file reference.
|
|
583
502
|
Use :meth:`resume` to continue playback.
|
|
584
503
|
|
|
585
504
|
Example:
|
|
@@ -590,40 +509,96 @@ class PiWave:
|
|
|
590
509
|
Log.info("Playback paused")
|
|
591
510
|
|
|
592
511
|
def resume(self):
|
|
593
|
-
"""Resume playback from the current
|
|
512
|
+
"""Resume playback from the current file.
|
|
594
513
|
|
|
595
|
-
Continues playback
|
|
514
|
+
Continues playback of the current file.
|
|
596
515
|
|
|
597
516
|
Example:
|
|
598
517
|
>>> pw.resume()
|
|
599
518
|
"""
|
|
600
|
-
if not self.is_playing and self.
|
|
601
|
-
self.play()
|
|
519
|
+
if not self.is_playing and self.current_file:
|
|
520
|
+
self.play(self.current_file)
|
|
521
|
+
|
|
522
|
+
def update(self,
|
|
523
|
+
frequency: Optional[float] = None,
|
|
524
|
+
ps: Optional[str] = None,
|
|
525
|
+
rt: Optional[str] = None,
|
|
526
|
+
pi: Optional[str] = None,
|
|
527
|
+
debug: Optional[bool] = None,
|
|
528
|
+
silent: Optional[bool] = None,
|
|
529
|
+
loop: Optional[bool] = None,
|
|
530
|
+
on_track_change: Optional[Callable] = None,
|
|
531
|
+
on_error: Optional[Callable] = None):
|
|
532
|
+
"""Update PiWave settings.
|
|
602
533
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
534
|
+
:param frequency: FM frequency to broadcast on (80.0-108.0 MHz)
|
|
535
|
+
:type frequency: Optional[float]
|
|
536
|
+
:param ps: Program Service name (max 8 characters)
|
|
537
|
+
:type ps: Optional[str]
|
|
538
|
+
:param rt: Radio Text message (max 64 characters)
|
|
539
|
+
:type rt: Optional[str]
|
|
540
|
+
:param pi: Program Identification code (4 hex digits)
|
|
541
|
+
:type pi: Optional[str]
|
|
542
|
+
:param debug: Enable debug logging
|
|
543
|
+
:type debug: Optional[bool]
|
|
544
|
+
:param silent: Remove every output log
|
|
545
|
+
:type silent: Optional[bool]
|
|
546
|
+
:param loop: Loop the current track continuously
|
|
547
|
+
:type loop: Optional[bool]
|
|
548
|
+
:param on_track_change: Callback function called when track changes
|
|
549
|
+
:type on_track_change: Optional[Callable]
|
|
550
|
+
:param on_error: Callback function called when an error occurs
|
|
551
|
+
:type on_error: Optional[Callable]
|
|
607
552
|
|
|
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.
|
|
553
|
+
.. note::
|
|
554
|
+
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
555
|
|
|
621
556
|
Example:
|
|
622
|
-
>>> pw.
|
|
557
|
+
>>> pw.update(frequency=101.5, ps="NewName")
|
|
558
|
+
>>> pw.update(rt="Updated radio text", debug=True, loop=True)
|
|
623
559
|
"""
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
560
|
+
updated_settings = []
|
|
561
|
+
|
|
562
|
+
if frequency is not None:
|
|
563
|
+
self.frequency = frequency
|
|
564
|
+
updated_settings.append(f"frequency: {frequency}MHz")
|
|
565
|
+
|
|
566
|
+
if ps is not None:
|
|
567
|
+
self.ps = str(ps)[:8]
|
|
568
|
+
updated_settings.append(f"PS: {self.ps}")
|
|
569
|
+
|
|
570
|
+
if rt is not None:
|
|
571
|
+
self.rt = str(rt)[:64]
|
|
572
|
+
updated_settings.append(f"RT: {self.rt}")
|
|
573
|
+
|
|
574
|
+
if pi is not None:
|
|
575
|
+
self.pi = str(pi).upper()[:4]
|
|
576
|
+
updated_settings.append(f"PI: {self.pi}")
|
|
577
|
+
|
|
578
|
+
if debug is not None:
|
|
579
|
+
self.debug = debug
|
|
580
|
+
updated_settings.append(f"debug: {debug}")
|
|
581
|
+
|
|
582
|
+
if silent is not None:
|
|
583
|
+
Log.config(silent=silent)
|
|
584
|
+
updated_settings.append(f"silent: {silent}")
|
|
585
|
+
|
|
586
|
+
if loop is not None:
|
|
587
|
+
self.loop = loop
|
|
588
|
+
updated_settings.append(f"loop: {loop}")
|
|
589
|
+
|
|
590
|
+
if on_track_change is not None:
|
|
591
|
+
self.on_track_change = on_track_change
|
|
592
|
+
updated_settings.append("on_track_change callback updated")
|
|
593
|
+
|
|
594
|
+
if on_error is not None:
|
|
595
|
+
self.on_error = on_error
|
|
596
|
+
updated_settings.append("on_error callback updated")
|
|
597
|
+
|
|
598
|
+
if updated_settings:
|
|
599
|
+
Log.success(f"Updated settings: {', '.join(updated_settings)}")
|
|
600
|
+
else:
|
|
601
|
+
Log.info("No settings updated")
|
|
627
602
|
|
|
628
603
|
def set_frequency(self, frequency: float):
|
|
629
604
|
"""Change the FM broadcast frequency.
|
|
@@ -632,7 +607,7 @@ class PiWave:
|
|
|
632
607
|
:type frequency: float
|
|
633
608
|
|
|
634
609
|
.. note::
|
|
635
|
-
The frequency change will take effect on the next
|
|
610
|
+
The frequency change will take effect on the next broadcast.
|
|
636
611
|
|
|
637
612
|
Example:
|
|
638
613
|
>>> pw.set_frequency(101.5)
|
|
@@ -640,6 +615,23 @@ class PiWave:
|
|
|
640
615
|
self.frequency = frequency
|
|
641
616
|
Log.broadcast_message(f"Frequency changed to {frequency}MHz. Will update on next file's broadcast.")
|
|
642
617
|
|
|
618
|
+
def set_loop(self, loop: bool):
|
|
619
|
+
"""Enable or disable looping for the current track.
|
|
620
|
+
|
|
621
|
+
:param loop: True to enable looping, False to disable
|
|
622
|
+
:type loop: bool
|
|
623
|
+
|
|
624
|
+
.. note::
|
|
625
|
+
The loop setting will take effect on the next broadcast.
|
|
626
|
+
|
|
627
|
+
Example:
|
|
628
|
+
>>> pw.set_loop(True) # Enable looping
|
|
629
|
+
>>> pw.set_loop(False) # Disable looping
|
|
630
|
+
"""
|
|
631
|
+
self.loop = loop
|
|
632
|
+
loop_status = "enabled" if loop else "disabled"
|
|
633
|
+
Log.broadcast_message(f"Looping {loop_status}. Will update on next file's broadcast.")
|
|
634
|
+
|
|
643
635
|
def get_status(self) -> dict:
|
|
644
636
|
"""Get current status information.
|
|
645
637
|
|
|
@@ -649,22 +641,27 @@ class PiWave:
|
|
|
649
641
|
The returned dictionary contains:
|
|
650
642
|
|
|
651
643
|
- **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
644
|
- **frequency** (float): Current broadcast frequency
|
|
655
645
|
- **current_file** (str|None): Path of currently playing file
|
|
646
|
+
- **ps** (str): Program Service name
|
|
647
|
+
- **rt** (str): Radio Text message
|
|
648
|
+
- **pi** (str): Program Identification code
|
|
649
|
+
- **loop** (bool): Whether looping is enabled
|
|
656
650
|
|
|
657
651
|
Example:
|
|
658
652
|
>>> status = pw.get_status()
|
|
659
653
|
>>> print(f"Playing: {status['is_playing']}")
|
|
660
|
-
>>> print(f"
|
|
654
|
+
>>> print(f"Current file: {status['current_file']}")
|
|
655
|
+
>>> print(f"Looping: {status['loop']}")
|
|
661
656
|
"""
|
|
662
657
|
return {
|
|
663
658
|
'is_playing': self.is_playing,
|
|
664
|
-
'current_index': self.current_index,
|
|
665
|
-
'playlist_length': len(self.playlist),
|
|
666
659
|
'frequency': self.frequency,
|
|
667
|
-
'current_file': self.
|
|
660
|
+
'current_file': self.current_file,
|
|
661
|
+
'ps': self.ps,
|
|
662
|
+
'rt': self.rt,
|
|
663
|
+
'pi': self.pi,
|
|
664
|
+
'loop': self.loop
|
|
668
665
|
}
|
|
669
666
|
|
|
670
667
|
def cleanup(self):
|
|
@@ -686,11 +683,11 @@ class PiWave:
|
|
|
686
683
|
def __del__(self):
|
|
687
684
|
self.cleanup()
|
|
688
685
|
|
|
689
|
-
def send(self,
|
|
686
|
+
def send(self, file_path: str):
|
|
690
687
|
"""Alias for the play method.
|
|
691
688
|
|
|
692
|
-
:param
|
|
693
|
-
:type
|
|
689
|
+
:param file_path: Path to local audio file
|
|
690
|
+
:type file_path: str
|
|
694
691
|
:return: True if playback started successfully, False otherwise
|
|
695
692
|
:rtype: bool
|
|
696
693
|
|
|
@@ -698,22 +695,9 @@ class PiWave:
|
|
|
698
695
|
This is an alias for :meth:`play` for backward compatibility.
|
|
699
696
|
|
|
700
697
|
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()
|
|
698
|
+
>>> pw.send('song.mp3')
|
|
713
699
|
"""
|
|
714
|
-
|
|
715
|
-
self.current_index = 0
|
|
716
|
-
self.play()
|
|
700
|
+
return self.play(file_path)
|
|
717
701
|
|
|
718
702
|
if __name__ == "__main__":
|
|
719
703
|
Log.header("PiWave Radio Module")
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: piwave
|
|
3
|
+
Version: 2.0.9
|
|
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=OSSQWrsm37eiyq4GBFkddZnY4qxTfqOmhm05ClIB7w4,24207
|
|
3
|
+
piwave-2.0.9.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
4
|
+
piwave-2.0.9.dist-info/METADATA,sha256=BkYwSKDSJ0eWXxSWZvB3v_YU2HAZJXc3WIgabvyU5yM,11697
|
|
5
|
+
piwave-2.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
piwave-2.0.9.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
|
|
7
|
+
piwave-2.0.9.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
|