piwave 2.0.6__py3-none-any.whl → 2.0.8__py3-none-any.whl

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