piwave 2.1.0__py3-none-any.whl → 2.1.2__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.
@@ -21,13 +21,20 @@ def get_best_backend(mode: str, frequency: float):
21
21
  return "pi_fm_rds"
22
22
  elif "fm_transmitter" in backends:
23
23
  return "fm_transmitter"
24
-
25
24
  else:
26
25
  if "fm_transmitter" in backends:
27
26
  return "fm_transmitter"
28
27
  elif "pi_fm_rds" in backends:
29
28
  return "pi_fm_rds"
30
29
 
30
+ elif mode == "live_broadcast":
31
+ if "fm_transmitter" in backends:
32
+ backend = backends["fm_transmitter"]()
33
+ min_freq, max_freq = backend.frequency_range
34
+ if min_freq <= frequency <= max_freq:
35
+ return "fm_transmitter"
36
+
37
+ return None
31
38
 
32
39
  return None
33
40
 
piwave/backends/base.py CHANGED
@@ -38,6 +38,11 @@ class Backend(ABC):
38
38
  @abstractmethod
39
39
  def supports_rds(self):
40
40
  pass
41
+
42
+ @property
43
+ @abstractmethod
44
+ def supports_live_streaming(self):
45
+ pass
41
46
 
42
47
  @property
43
48
  def cache_file(self):
@@ -205,6 +210,10 @@ class Backend(ABC):
205
210
  @abstractmethod
206
211
  def build_command(self, wav_file: str):
207
212
  pass
213
+
214
+ @abstractmethod
215
+ def build_live_command(self):
216
+ pass
208
217
 
209
218
  def validate_settings(self):
210
219
  min_freq, max_freq = self.frequency_range
@@ -19,15 +19,26 @@ class FmTransmitterBackend(Backend):
19
19
  def supports_rds(self):
20
20
  return False
21
21
 
22
+ @property
23
+ def supports_live_streaming(self):
24
+ return True
25
+
22
26
  def _get_executable_name(self):
23
27
  return "fm_transmitter"
24
28
 
25
29
  def _get_search_paths(self):
26
- return ["/opt/PiWave/fm_transmitter", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home", "/home/pi"]
30
+ return ["/opt/PiWave/fm_transmitter", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
27
31
 
28
32
  def build_command(self, wav_file: str):
29
33
  return [
30
34
  'sudo', self.required_executable,
31
35
  '-f', str(self.frequency),
32
36
  wav_file
37
+ ]
38
+
39
+ def build_live_command(self):
40
+ return [
41
+ 'sudo', self.required_executable,
42
+ '-f', str(self.frequency),
43
+ '-'
33
44
  ]
@@ -19,6 +19,10 @@ class PiFmRdsBackend(Backend):
19
19
  def supports_rds(self):
20
20
  return True
21
21
 
22
+ @property
23
+ def supports_live_streaming(self):
24
+ return False
25
+
22
26
  def _get_executable_name(self):
23
27
  return "pi_fm_rds"
24
28
 
@@ -40,3 +44,6 @@ class PiFmRdsBackend(Backend):
40
44
  cmd.extend(['-pi', self.pi])
41
45
 
42
46
  return cmd
47
+
48
+ def build_live_command(self):
49
+ return None # not supported sadly
piwave/piwave.py CHANGED
@@ -10,7 +10,7 @@ import time
10
10
 
11
11
  import tempfile
12
12
  import shutil
13
- import sys
13
+ import queue
14
14
  from typing import Optional, Callable
15
15
  from pathlib import Path
16
16
  from urllib.parse import urlparse
@@ -24,16 +24,17 @@ class PiWaveError(Exception):
24
24
 
25
25
  class PiWave:
26
26
  def __init__(self,
27
- frequency: float = 90.0,
28
- ps: str = "PiWave",
29
- rt: str = "PiWave: The best python module for managing your pi radio",
30
- pi: str = "FFFF",
31
- debug: bool = False,
32
- silent: bool = False,
33
- loop: bool = False,
34
- backend: str = "auto",
35
- on_track_change: Optional[Callable] = None,
36
- on_error: Optional[Callable] = None):
27
+ frequency: float = 90.0,
28
+ ps: str = "PiWave",
29
+ rt: str = "PiWave: The best python module for managing your pi radio",
30
+ pi: str = "FFFF",
31
+ debug: bool = False,
32
+ silent: bool = False,
33
+ loop: bool = False,
34
+ backend: str = "auto",
35
+ used_for: str = "file_broadcast",
36
+ on_track_change: Optional[Callable] = None,
37
+ on_error: Optional[Callable] = None):
37
38
  """Initialize PiWave FM transmitter.
38
39
 
39
40
  :param frequency: FM frequency to broadcast on (80.0-108.0 MHz)
@@ -50,7 +51,9 @@ class PiWave:
50
51
  :type silent: bool
51
52
  :param loop: Loop the current track continuously (default: False)
52
53
  :type loop: bool
53
- :param backend: Chose a specific backend to handle the broadcast (default: auto)
54
+ :param backend: Chose a specific backend to handle the broadcast (default: auto). Supports `pi_fm_rds`, `fm_transmitter` and `auto`
55
+ :type backend: str
56
+ :param backend: Give the main use for the current instance, will be used if backend: auto (default: file_broadcast). Supports `file_broadcast` and `live_broadcast`
54
57
  :type backend: str
55
58
  :param on_track_change: Callback function called when track changes
56
59
  :type on_track_change: Optional[Callable]
@@ -59,7 +62,7 @@ class PiWave:
59
62
  :raises PiWaveError: If not running on Raspberry Pi or without root privileges
60
63
 
61
64
  .. note::
62
- This class requires pi_fm_rds to be installed and accessible.
65
+ This class requires pi_fm_rds or fm_transmitter to be installed and accessible.
63
66
  Must be run on a Raspberry Pi with root privileges.
64
67
  """
65
68
 
@@ -79,6 +82,10 @@ class PiWave:
79
82
  self.playback_thread: Optional[threading.Thread] = None
80
83
  self.stop_event = threading.Event()
81
84
  self.temp_dir = tempfile.mkdtemp(prefix="piwave_")
85
+
86
+ self.is_live_streaming = False
87
+ self.live_thread: Optional[threading.Thread] = None
88
+ self.audio_queue: Optional[queue.Queue] = None
82
89
 
83
90
  Log.config(silent=silent)
84
91
 
@@ -87,11 +94,13 @@ class PiWave:
87
94
 
88
95
  discover_backends()
89
96
 
97
+ self.backend_use = used_for
98
+
90
99
  if backend == "auto":
91
- backend_name = get_best_backend("file_broadcast", self.frequency)
100
+ backend_name = get_best_backend(self.backend_use, self.frequency)
92
101
  if not backend_name:
93
102
  available = list(backends.keys())
94
- raise PiWaveError(f"No suitable backend found for {self.frequency}MHz. Available backends: {available}")
103
+ raise PiWaveError(f"No suitable backend found for {self.frequency}MHz and {self.backend_use} mode. Available backends: {available}")
95
104
  else:
96
105
  if backend not in backends:
97
106
  available = list(backends.keys())
@@ -113,6 +122,7 @@ class PiWave:
113
122
  pi=self.pi
114
123
  )
115
124
 
125
+
116
126
  min_freq, max_freq = self.backend.frequency_range
117
127
  rds_support = "with RDS" if self.backend.supports_rds else "no RDS"
118
128
  Log.info(f"Using {self.backend.name} backend ({min_freq}-{max_freq}MHz, {rds_support})")
@@ -255,7 +265,7 @@ class PiWave:
255
265
  except (subprocess.CalledProcessError, subprocess.TimeoutExpired, ValueError):
256
266
  return 0.0
257
267
 
258
- def _play_wav(self, wav_file: str) -> bool:
268
+ def _play_file(self, wav_file: str) -> bool:
259
269
  if self.stop_event.is_set():
260
270
  return False
261
271
 
@@ -315,6 +325,141 @@ class PiWave:
315
325
  self.on_error(e)
316
326
  self._stop_current_process()
317
327
  return False
328
+
329
+ def _playback_worker_wrapper(self):
330
+ # wrapper for non-blocking playback
331
+ try:
332
+ wav_file = self._convert_to_wav(self.current_file)
333
+ if not wav_file:
334
+ Log.error(f"Failed to convert {self.current_file}")
335
+ self.is_playing = False
336
+ return
337
+
338
+ if not os.path.exists(wav_file):
339
+ Log.error(f"File not found: {wav_file}")
340
+ self.is_playing = False
341
+ return
342
+
343
+ self._play_file(wav_file)
344
+ except Exception as e:
345
+ Log.error(f"Playback error: {e}")
346
+ if self.on_error:
347
+ self.on_error(e)
348
+ finally:
349
+ self.is_playing = False
350
+
351
+
352
+ def _play_live(self, audio_source, sample_rate: int, channels: int, chunk_size: int) -> bool:
353
+ if self.is_playing or self.is_live_streaming:
354
+ self.stop()
355
+
356
+ if not self.backend.supports_live_streaming:
357
+ raise PiWaveError(
358
+ f"Backend '{self.backend_name}' doesn't support live streaming. Try using fm_transmitter instead.")
359
+
360
+
361
+ min_freq, max_freq = self.backend.frequency_range
362
+ if not (min_freq <= self.frequency <= max_freq):
363
+ raise PiWaveError(
364
+ f"Backend '{self.backend_name}' doesn't support {self.frequency}MHz"
365
+ )
366
+
367
+ self.stop_event.clear()
368
+ self.is_live_streaming = True
369
+ self.audio_queue = queue.Queue(maxsize=20)
370
+
371
+ try:
372
+ cmd = self.backend.build_live_command()
373
+ if not cmd:
374
+ raise PiWaveError(f"Backend doesn't support live streaming") # since we checked before, we shouldnt get this but meh
375
+
376
+ self.current_process = subprocess.Popen(
377
+ cmd,
378
+ stdin=subprocess.PIPE,
379
+ stdout=subprocess.PIPE,
380
+ stderr=subprocess.PIPE,
381
+ preexec_fn=os.setsid
382
+ )
383
+ except Exception as e:
384
+ Log.error(f"Failed to start live stream: {e}")
385
+ self.is_live_streaming = False
386
+ if self.on_error:
387
+ self.on_error(e)
388
+ return False
389
+
390
+ self.live_thread = threading.Thread(
391
+ target=self._live_producer_worker,
392
+ args=(audio_source, chunk_size)
393
+ )
394
+ self.live_thread.daemon = True
395
+ self.live_thread.start()
396
+
397
+ consumer_thread = threading.Thread(target=self._live_consumer_worker)
398
+ consumer_thread.daemon = True
399
+ consumer_thread.start()
400
+
401
+ Log.broadcast_message(f"Live streaming at {self.frequency}MHz ({sample_rate}Hz, {channels}ch)")
402
+ return True
403
+
404
+ def _live_producer_worker(self, audio_source, chunk_size: int):
405
+ # producer: reads from audio source, puts in queue; consumer will play it
406
+ try:
407
+ if hasattr(audio_source, '__iter__') and not isinstance(audio_source, (str, bytes)):
408
+ for chunk in audio_source:
409
+ if self.stop_event.is_set():
410
+ break
411
+ if chunk:
412
+ self.audio_queue.put(chunk, timeout=1)
413
+
414
+ elif callable(audio_source):
415
+ while not self.stop_event.is_set():
416
+ chunk = audio_source()
417
+ if not chunk:
418
+ break
419
+ self.audio_queue.put(chunk, timeout=1)
420
+
421
+ elif hasattr(audio_source, 'read'):
422
+ while not self.stop_event.is_set():
423
+ chunk = audio_source.read(chunk_size)
424
+ if not chunk:
425
+ break
426
+ self.audio_queue.put(chunk, timeout=1)
427
+
428
+ except Exception as e:
429
+ Log.error(f"Producer error: {e}")
430
+ if self.on_error:
431
+ self.on_error(e)
432
+ finally:
433
+ self.audio_queue.put(None)
434
+
435
+ def _live_consumer_worker(self):
436
+ # consumer reads from queue and puts in process
437
+ try:
438
+ while not self.stop_event.is_set():
439
+ try:
440
+ chunk = self.audio_queue.get(timeout=0.1)
441
+ if chunk is None:
442
+ break
443
+
444
+ if self.current_process and self.current_process.stdin:
445
+ self.current_process.stdin.write(chunk)
446
+ self.current_process.stdin.flush()
447
+
448
+ except queue.Empty:
449
+ continue
450
+ except BrokenPipeError:
451
+ Log.error(f"Stream process terminated: make sure that the stream you provided compiles with {self.backend_name} stdin support.")
452
+ break
453
+ except Exception as e:
454
+ Log.error(f"Write error: {e}")
455
+ break
456
+ finally:
457
+ if self.current_process and self.current_process.stdin:
458
+ try:
459
+ self.current_process.stdin.close()
460
+ except:
461
+ pass
462
+ self.is_live_streaming = False
318
463
 
319
464
 
320
465
  def _stop_current_process(self):
@@ -365,38 +510,45 @@ class PiWave:
365
510
  self.stop()
366
511
  os._exit(0)
367
512
 
368
- def play(self, file_path: str) -> bool:
369
- """Start playing the specified audio file.
370
-
371
- :param file_path: Path to local audio file
372
- :type file_path: str
373
- :return: True if playback started successfully, False otherwise
374
- :rtype: bool
513
+ def play(self, source, sample_rate: int = 44100, channels: int = 2, chunk_size: int = 4096, blocking: bool = False):
514
+ """Play audio from file or live source.
375
515
 
376
- .. note::
377
- Files are automatically converted to WAV format if needed.
378
- Only local files are supported. If loop is enabled, the file will
379
- repeat continuously until stop() is called.
516
+ :param source: Either a file path (str) or live audio source (generator/callable/file-like)
517
+ :param sample_rate: Sample rate for live audio (ignored for files)
518
+ :param channels: Channels for live audio (ignored for files)
519
+ :param chunk_size: Chunk size for live audio (ignored for files)
520
+ :param blocking: If the playback should be blocking or not (ignored for live, always non-blocking)
521
+ :return: True if playback/streaming started successfully
522
+ :rtype: bool
380
523
 
381
524
  Example:
382
- >>> pw.play('song.mp3')
383
- >>> pw.play('audio.wav')
525
+ >>> pw.play('song.mp3') # File playback
526
+ >>> pw.play(mic_generator()) # Live streaming
384
527
  """
528
+
529
+ # autodetect if source is live or file
530
+ if isinstance(source, str):
531
+ # file (string)
532
+ if blocking:
533
+ return self._play_file(source)
534
+ else:
535
+ if self.is_playing:
536
+ self.stop()
537
+
538
+ self.current_file = source
539
+ self.is_playing = True
540
+ self.stop_event.clear()
541
+
542
+ self.playback_thread = threading.Thread(
543
+ target=self._playback_worker_wrapper,
544
+ daemon=True
545
+ )
546
+ self.playback_thread.start()
547
+ return True
385
548
 
386
- if self.is_playing:
387
- self.stop()
388
-
389
- self.current_file = file_path
390
- self.stop_event.clear()
391
- self.is_stopped = False
392
- self.is_playing = True
393
-
394
- self.playback_thread = threading.Thread(target=self._playback_worker)
395
- self.playback_thread.daemon = True
396
- self.playback_thread.start()
397
-
398
- Log.success("Playback started")
399
- return True
549
+ else:
550
+ # live
551
+ return self._play_live(source, sample_rate, channels, chunk_size)
400
552
 
401
553
  def stop(self):
402
554
  """Stop all playback and streaming.
@@ -407,14 +559,21 @@ class PiWave:
407
559
  Example:
408
560
  >>> pw.stop()
409
561
  """
410
- if not self.is_playing:
562
+ if not self.is_playing and not self.is_live_streaming:
411
563
  return
412
564
 
413
- Log.warning("Stopping playback...")
414
-
565
+ Log.warning("Stopping...")
566
+
415
567
  self.is_stopped = True
416
568
  self.stop_event.set()
417
-
569
+
570
+ if self.audio_queue:
571
+ while not self.audio_queue.empty():
572
+ try:
573
+ self.audio_queue.get_nowait()
574
+ except queue.Empty:
575
+ break
576
+
418
577
  if self.current_process:
419
578
  try:
420
579
  os.killpg(os.getpgid(self.current_process.pid), signal.SIGTERM)
@@ -423,12 +582,15 @@ class PiWave:
423
582
  pass
424
583
  finally:
425
584
  self.current_process = None
426
-
585
+
427
586
  if self.playback_thread and self.playback_thread.is_alive():
428
587
  self.playback_thread.join(timeout=5)
429
-
588
+ if self.live_thread and self.live_thread.is_alive():
589
+ self.live_thread.join(timeout=3)
590
+
430
591
  self.is_playing = False
431
- Log.success("Playback stopped")
592
+ self.is_live_streaming = False
593
+ Log.success("Stopped")
432
594
 
433
595
  def pause(self):
434
596
  """Pause the current playback.
@@ -455,16 +617,17 @@ class PiWave:
455
617
  self.play(self.current_file)
456
618
 
457
619
  def update(self,
458
- frequency: Optional[float] = None,
459
- ps: Optional[str] = None,
460
- rt: Optional[str] = None,
461
- pi: Optional[str] = None,
462
- debug: Optional[bool] = None,
463
- silent: Optional[bool] = None,
464
- loop: Optional[bool] = None,
465
- backend: Optional[str] = None,
466
- on_track_change: Optional[Callable] = None,
467
- on_error: Optional[Callable] = None):
620
+ frequency: Optional[float] = None,
621
+ ps: Optional[str] = None,
622
+ rt: Optional[str] = None,
623
+ pi: Optional[str] = None,
624
+ debug: Optional[bool] = None,
625
+ silent: Optional[bool] = None,
626
+ loop: Optional[bool] = None,
627
+ backend: Optional[str] = None,
628
+ used_for: Optional[str] = None,
629
+ on_track_change: Optional[Callable] = None,
630
+ on_error: Optional[Callable] = None):
468
631
  """Update PiWave settings.
469
632
 
470
633
  :param frequency: FM frequency to broadcast on (80.0-108.0 MHz)
@@ -483,6 +646,8 @@ class PiWave:
483
646
  :type loop: Optional[bool]
484
647
  :param backend: Backend used to broadcast
485
648
  :type backend: Optional[str]
649
+ :param backend: Give the main use for the current instance, will be used if backend: auto. Supports `file_broadcast` and `live_broadcast`
650
+ :type backend: Optional[str]
486
651
  :param on_track_change: Callback function called when track changes
487
652
  :type on_track_change: Optional[Callable]
488
653
  :param on_error: Callback function called when an error occurs
@@ -499,9 +664,12 @@ class PiWave:
499
664
 
500
665
  freq_to_use = frequency if frequency is not None else self.frequency
501
666
 
667
+ if used_for is not None:
668
+ self.backend_use = used_for
669
+
502
670
  if backend is not None:
503
671
  if backend == "auto":
504
- backend_name = get_best_backend("file_broadcast", freq_to_use)
672
+ backend_name = get_best_backend(self.backend_use, freq_to_use)
505
673
  if not backend_name:
506
674
  available = list(backends.keys())
507
675
  raise PiWaveError(f"No suitable backend found for {freq_to_use}MHz. Available: {available}")
@@ -608,6 +776,7 @@ class PiWave:
608
776
  The returned dictionary contains:
609
777
 
610
778
  - **is_playing** (bool): Whether playback is active
779
+ - **is_live_streaming** (bool): Whether live playback is active
611
780
  - **frequency** (float): Current broadcast frequency
612
781
  - **current_file** (str|None): Path of currently playing file
613
782
  - **current_backend** (str): Currently used backend
@@ -627,6 +796,7 @@ class PiWave:
627
796
  """
628
797
  return {
629
798
  'is_playing': self.is_playing,
799
+ 'is_live_streaming': self.is_live_streaming,
630
800
  'frequency': self.frequency,
631
801
  'current_file': self.current_file,
632
802
  'current_backend': self.backend_name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piwave
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: A python module to broadcast radio waves with your Raspberry Pi.
5
5
  Home-page: https://github.com/douxxtech/piwave
6
6
  Author: Douxx
@@ -37,6 +37,7 @@ Dynamic: license-file
37
37
  - **Multi-Backend Architecture**: Supports multiple backends for different actions
38
38
  - **Wide Frequency Support**: 1-250 MHz coverage through different backends
39
39
  - **RDS Support**: Program Service, Radio Text, and Program Identifier broadcasting
40
+ - **Live Stream Support**: Broadcast live from a stream source.
40
41
  - **Smart Backend Selection**: Automatically chooses the best backend to suit your needs
41
42
  - **Audio Format Support**: Converts most audio formats (MP3, FLAC, M4A, etc.) to WAV
42
43
  - **Real-time Settings Updates**: Change frequency, RDS data, and settings without restart
@@ -51,12 +52,14 @@ Dynamic: license-file
51
52
  ### PiFmRds Backend
52
53
  - **Frequency Range**: 80.0 - 108.0 MHz (Standard FM band)
53
54
  - **RDS Support**: ✅ Full support (PS, RT, PI)
55
+ - **Live Support**: ❌ No live support
54
56
  - **Repository**: [ChristopheJacquet/PiFmRds](https://github.com/ChristopheJacquet/PiFmRds)
55
57
  - **Best For**: Standard FM broadcasting with RDS features
56
58
 
57
59
  ### FmTransmitter Backend
58
60
  - **Frequency Range**: 1.0 - 250.0 MHz (Extended range)
59
61
  - **RDS Support**: ❌ No RDS support
62
+ - **Live Support**: ✅ Experimental support
60
63
  - **Repository**: [markondej/fm_transmitter](https://github.com/markondej/fm_transmitter)
61
64
  - **Best For**: Non-standard frequencies and experimental broadcasting
62
65
 
@@ -141,42 +144,6 @@ curl -sL https://setup.piwave.xyz/uninstall | sudo bash
141
144
  make
142
145
  ```
143
146
 
144
- ## Backend Management
145
-
146
- ### CLI Commands
147
-
148
- ```bash
149
- # Search for available backends on system
150
- python3 -m piwave search
151
-
152
- # List cached backends
153
- python3 -m piwave list
154
-
155
- # Manually add backend executable path
156
- python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
157
-
158
- # Show package information
159
- python3 -m piwave info
160
-
161
- # Broadcast a file directly
162
- python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
163
- ```
164
-
165
- ### Programmatic Backend Discovery
166
-
167
- ```python
168
- from piwave.backends import discover_backends, list_backends, search_backends
169
-
170
- # Load cached backends
171
- discover_backends()
172
-
173
- # Search for new backends (ignores cache)
174
- search_backends()
175
-
176
- # List available backends with details
177
- backends_info = list_backends()
178
- ```
179
-
180
147
  ## Quick Start
181
148
 
182
149
  ### Basic Usage
@@ -256,6 +223,44 @@ print(f"Backend supports RDS: {status['backend_supports_rds']}")
256
223
  print(f"Frequency range: {status['backend_frequency_range']}")
257
224
  ```
258
225
 
226
+
227
+ ## Backend Management
228
+
229
+ ### CLI Commands
230
+
231
+ ```bash
232
+ # Search for available backends on system
233
+ python3 -m piwave search
234
+
235
+ # List cached backends
236
+ python3 -m piwave list
237
+
238
+ # Manually add backend executable path
239
+ python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
240
+
241
+ # Show package information
242
+ python3 -m piwave info
243
+
244
+ # Broadcast a file directly
245
+ python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
246
+ ```
247
+
248
+ ### Programmatic Backend Discovery
249
+
250
+ ```python
251
+ from piwave.backends import discover_backends, list_backends, search_backends
252
+
253
+ # Load cached backends
254
+ discover_backends()
255
+
256
+ # Search for new backends (ignores cache)
257
+ search_backends()
258
+
259
+ # List available backends with details
260
+ backends_info = list_backends()
261
+ ```
262
+
263
+
259
264
  ## Complete Examples
260
265
 
261
266
  <details>
@@ -443,16 +448,17 @@ if __name__ == "__main__":
443
448
 
444
449
  ```python
445
450
  PiWave(
446
- frequency=90.0, # Broadcast frequency (1.0-250.0 MHz)
447
- ps="PiWave", # Program Service name (max 8 chars)
448
- rt="PiWave: ...", # Radio Text (max 64 chars)
449
- pi="FFFF", # Program Identifier (4 hex digits)
450
- debug=False, # Enable debug logging
451
- silent=False, # Disable all logging
452
- loop=False, # Loop current track continuously
453
- backend="auto", # Backend selection ("auto", "pi_fm_rds", "fm_transmitter")
454
- on_track_change=None, # Callback for track changes
455
- on_error=None # Callback for errors
451
+ frequency=90.0, # Broadcast frequency (1.0-250.0 MHz)
452
+ ps="PiWave", # Program Service name (max 8 chars)
453
+ rt="PiWave: ...", # Radio Text (max 64 chars)
454
+ pi="FFFF", # Program Identifier (4 hex digits)
455
+ debug=False, # Enable debug logging
456
+ silent=False, # Disable all logging
457
+ loop=False, # Loop current track continuously
458
+ backend="auto", # Backend selection ("auto", "pi_fm_rds", "fm_transmitter")
459
+ used_for="file_broadcast", # Backend main purpose, used if backend = auto ("file_broadcast", "live_broadcast")
460
+ on_track_change=None, # Callback for track changes
461
+ on_error=None # Callback for errors
456
462
  )
457
463
  ```
458
464
 
@@ -495,6 +501,7 @@ status = pw.get_status()
495
501
  # Returns:
496
502
  {
497
503
  'is_playing': bool,
504
+ 'is_live_streaming': bool,
498
505
  'frequency': float,
499
506
  'current_file': str|None,
500
507
  'current_backend': str,
@@ -0,0 +1,13 @@
1
+ piwave/__init__.py,sha256=jz2r-qclltKTxJjlGnqhAwIlUgjvRTR31f4hjTHP5NA,230
2
+ piwave/__main__.py,sha256=ygl6F8VmOZjsnnUbAbzLoA7_1qv18ruG6TW4EocSaQE,3927
3
+ piwave/logger.py,sha256=lPG3cz3ByqC1p1UKpwlv3R9KgiAb0zzyu6c3bhTpST0,2766
4
+ piwave/piwave.py,sha256=7gowAQxWco-ZYtf5O-8x1un0lCK4BeSkz1-gjwLpQF4,32750
5
+ piwave/backends/__init__.py,sha256=DUbdyYf2V2XcDB05vmFWEkuJ292YTNiNJjzh1raJ5Cg,3756
6
+ piwave/backends/base.py,sha256=amjdR3pwx-0XN2ngpwfYNt74j5kWrW1cI0zMOTMU-q8,7668
7
+ piwave/backends/fm_transmitter.py,sha256=6CuYpkCgb40PEpemMlbTkQ6Gq8qFe3C1TSSLLnwXXX4,1338
8
+ piwave/backends/pi_fm_rds.py,sha256=l1y9JUKjw-d9lh9X7HLQNEwbNVO-whYlzaYnqEfRviI,1429
9
+ piwave-2.1.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
10
+ piwave-2.1.2.dist-info/METADATA,sha256=xItxk81Sl2CLt2YIHw9kgG7Uv6oap_kqkQeDgReYnf0,20563
11
+ piwave-2.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ piwave-2.1.2.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
13
+ piwave-2.1.2.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- piwave/__init__.py,sha256=jz2r-qclltKTxJjlGnqhAwIlUgjvRTR31f4hjTHP5NA,230
2
- piwave/__main__.py,sha256=ygl6F8VmOZjsnnUbAbzLoA7_1qv18ruG6TW4EocSaQE,3927
3
- piwave/logger.py,sha256=lPG3cz3ByqC1p1UKpwlv3R9KgiAb0zzyu6c3bhTpST0,2766
4
- piwave/piwave.py,sha256=jmd10s2FUzL18GXjhyvIS6XwJl2K34G-xhMNHsvMKYk,25437
5
- piwave/backends/__init__.py,sha256=1lEfdEl-I9I4awRi0EHTVtRYAoSNC5T2zJhF0RS_xtA,3462
6
- piwave/backends/base.py,sha256=DGlDG9ybe6RNB7dUM7xedOD8DAo2e6nrtbS_LjnicAY,7513
7
- piwave/backends/fm_transmitter.py,sha256=Zj6NUKvDJ6yC6xEV67leusaNZAYo_FIFAvw7eVp2diw,1105
8
- piwave/backends/pi_fm_rds.py,sha256=TEHjn11GFIp4ZizEaD-0P3Cx_Agu7XFbyT6OxB1PpGk,1274
9
- piwave-2.1.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
10
- piwave-2.1.0.dist-info/METADATA,sha256=HxoDpTSGbLXhQR86G9gFI-Ko1_QtP-V-4Zu3-Ahq08c,20247
11
- piwave-2.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- piwave-2.1.0.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
13
- piwave-2.1.0.dist-info/RECORD,,
File without changes