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 CHANGED
@@ -6,11 +6,11 @@ import subprocess
6
6
  import signal
7
7
  import threading
8
8
  import time
9
- import asyncio
9
+
10
10
  import tempfile
11
11
  import shutil
12
12
  import sys
13
- from typing import List, Optional, Callable
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.playlist: List[str] = []
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}", 'blue')
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("/sys/firmware/devicetree/base/model", "r") as f:
204
- model = f.read().strip()
205
- return "Raspberry Pi" in model
206
- except FileNotFoundError:
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 filepath in self.converted_files:
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
- if self._is_url(filepath):
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
- result = subprocess.run(
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
- Log.broadcast_message(f"Playing {wav_file} (Duration: {duration:.1f}s) at {self.frequency}MHz")
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, self.current_index)
387
-
388
- # wait for either
389
- # The duration to elapse (then kill the process), or
390
- # stop_event to be set (user requested stop)
391
- start_time = time.time()
392
- while True:
393
- if self.stop_event.wait(timeout=0.1):
394
- self._stop_current_process()
395
- return False
396
-
397
- elapsed = time.time() - start_time
398
- if elapsed >= duration:
399
- self._stop_current_process()
400
- break
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
- while not self.stop_event.is_set() and not self.is_stopped:
432
- if self.current_index >= len(self.playlist):
433
- if self.loop:
434
- self.current_index = 0
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
- if not os.path.exists(wav_file):
443
- Log.error(f"File not found: {wav_file}")
444
- self.current_index += 1
445
- continue
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
- if not self._play_wav(wav_file):
448
- if not self.stop_event.is_set():
449
- Log.error(f"Playback failed for {wav_file}")
450
- break
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
- self.current_index += 1
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 add_files(self, files: List[str]) -> bool:
464
- """Add audio files to the playlist.
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
- 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]]
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
- If no files are specified, plays the current playlist.
508
- Automatically stops any current playback before starting.
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(['song1.mp3', 'song2.wav']) # Play specific files
512
- >>> pw.play() # Play current playlist
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 playlist position.
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 position.
512
+ """Resume playback from the current file.
594
513
 
595
- Continues playback from where it was paused or stopped.
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.playlist:
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
- def next_track(self):
604
- """Skip to the next track in the playlist.
605
-
606
- If currently playing, stops the current track and advances to the next one.
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
- Example:
609
- >>> pw.next_track()
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.previous_track()
557
+ >>> pw.update(frequency=101.5, ps="NewName")
558
+ >>> pw.update(rt="Updated radio text", debug=True, loop=True)
623
559
  """
624
- if self.is_playing:
625
- self._stop_current_process()
626
- self.current_index = max(0, self.current_index - 1)
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 track or broadcast.
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"Track {status['current_index'] + 1} of {status['playlist_length']}")
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.playlist[self.current_index] if self.current_index < len(self.playlist) else None
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, files: List[str]):
686
+ def send(self, file_path: str):
690
687
  """Alias for the play method.
691
688
 
692
- :param files: List of file paths or URLs to play
693
- :type files: List[str]
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(['song1.mp3', 'song2.wav'])
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
- if self.playlist:
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
+ ![](https://madeby.douxx.tech)
@@ -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,,
@@ -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
@@ -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