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 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,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.playlist: List[str] = []
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}, Loop: {loop}")
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}", 'blue')
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("/sys/firmware/devicetree/base/model", "r") as f:
204
- model = f.read().strip()
205
- return "Raspberry Pi" in model
206
- except FileNotFoundError:
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 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
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
- 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"
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
- result = subprocess.run(
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, self.current_index)
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
- 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]
388
+ if not self.current_file:
389
+ Log.error("No file specified for playback")
390
+ self.is_playing = False
391
+ return
441
392
 
442
- if not os.path.exists(wav_file):
443
- Log.error(f"File not found: {wav_file}")
444
- self.current_index += 1
445
- continue
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
- 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
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
- self.current_index += 1
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 add_files(self, files: List[str]) -> bool:
464
- """Add audio files to the playlist.
417
+ def play(self, file_path: str) -> bool:
418
+ """Start playing the specified audio file.
465
419
 
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
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
- If no files are specified, plays the current playlist.
508
- Automatically stops any current playback before starting.
426
+ Files are automatically converted to WAV format if needed.
427
+ Only local files are supported.
509
428
 
510
429
  Example:
511
- >>> pw.play(['song1.mp3', 'song2.wav']) # Play specific files
512
- >>> pw.play() # Play current playlist
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 playlist position.
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 position.
495
+ """Resume playback from the current file.
594
496
 
595
- Continues playback from where it was paused or stopped.
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.playlist:
601
- self.play()
602
-
603
- def next_track(self):
604
- """Skip to the next track in the playlist.
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
- If currently playing, stops the current track and advances to the next one.
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
- 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.
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.previous_track()
537
+ >>> pw.update(frequency=101.5, ps="NewName")
538
+ >>> pw.update(rt="Updated radio text", debug=True)
623
539
  """
624
- if self.is_playing:
625
- self._stop_current_process()
626
- self.current_index = max(0, self.current_index - 1)
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 track or broadcast.
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"Track {status['current_index'] + 1} of {status['playlist_length']}")
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.playlist[self.current_index] if self.current_index < len(self.playlist) else None
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, files: List[str]):
642
+ def send(self, file_path: str):
690
643
  """Alias for the play method.
691
644
 
692
- :param files: List of file paths or URLs to play
693
- :type files: List[str]
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(['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()
654
+ >>> pw.send('song.mp3')
713
655
  """
714
- if self.playlist:
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
+ ![](https://madeby.douxx.tech)
@@ -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,,
@@ -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