121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
import sounddevice as sd
|
|
import numpy as np
|
|
import threading
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import re
|
|
|
|
playing_threads = {}
|
|
|
|
def list_dante_devices():
|
|
"""List all unique Dante Virtual Soundcard output devices sorted by channel order."""
|
|
devices = sd.query_devices()
|
|
seen = {}
|
|
|
|
for d in devices:
|
|
if "DVS Transmit" in d['name'] and d['max_output_channels'] == 2:
|
|
match = re.search(r'(\d+)-', d['name']) # Extract first number before '-'
|
|
if match:
|
|
channel_number = int(match.group(1))
|
|
if channel_number not in seen:
|
|
seen[channel_number] = d
|
|
|
|
return [seen[key] for key in sorted(seen.keys())]
|
|
|
|
def generate_tone(frequency=440, sample_rate=48000):
|
|
"""Generate a continuous sine wave tone."""
|
|
t = np.linspace(0, 1, sample_rate, False)
|
|
wave = 0.5 * np.sin(2 * np.pi * frequency * t)
|
|
return wave.astype(np.float32)
|
|
|
|
def play_sound_on_device(device_index, channel, frequency=440, sample_rate=48000):
|
|
"""Continuously play a sine wave on a specific stereo Dante Virtual Soundcard channel using OutputStream."""
|
|
if channel not in [1, 2]:
|
|
print("Invalid channel. Must be 1 (Left) or 2 (Right).")
|
|
return
|
|
|
|
tone = generate_tone(frequency, sample_rate)
|
|
output = np.zeros((len(tone), 2), dtype=np.float32) # Stereo output
|
|
output[:, channel - 1] = tone # Assign tone to the selected channel
|
|
|
|
def callback(outdata, frames, time, status):
|
|
if status:
|
|
print(status)
|
|
outdata[:frames] = output[:frames]
|
|
|
|
stream = sd.OutputStream(device=device_index, samplerate=sample_rate, channels=2, callback=callback)
|
|
playing_threads[(device_index, channel)] = stream
|
|
stream.start()
|
|
|
|
def stop_sound(device_index, channel):
|
|
"""Stop playing sound on a given device and channel."""
|
|
stream = playing_threads.pop((device_index, channel), None)
|
|
if stream:
|
|
stream.stop()
|
|
stream.close()
|
|
|
|
def stop_all_sounds():
|
|
"""Stop all currently playing sounds."""
|
|
for key in list(playing_threads.keys()):
|
|
stop_sound(*key)
|
|
|
|
def play_all_sounds(frequency):
|
|
"""Play sound on all available Dante channels."""
|
|
devices = list_dante_devices()
|
|
for device in devices:
|
|
for channel in [1, 2]:
|
|
play_sound_on_device(device['index'], channel, frequency)
|
|
|
|
def start_gui():
|
|
devices = list_dante_devices()
|
|
root = tk.Tk()
|
|
root.title("Dante Virtual Soundcard Player")
|
|
|
|
frame = ttk.Frame(root)
|
|
frame.pack(padx=10, pady=10)
|
|
|
|
ttk.Label(frame, text="Frequency (Hz):").grid(row=0, column=1)
|
|
frequency_entry = ttk.Entry(frame)
|
|
frequency_entry.grid(row=0, column=2)
|
|
frequency_entry.insert(0, "440")
|
|
|
|
def toggle_all_play():
|
|
freq = float(frequency_entry.get())
|
|
play_all_sounds(freq)
|
|
|
|
def toggle_all_stop():
|
|
stop_all_sounds()
|
|
|
|
all_on_btn = ttk.Button(frame, text="All ON", command=toggle_all_play)
|
|
all_on_btn.grid(row=1, column=3, padx=5)
|
|
|
|
all_off_btn = ttk.Button(frame, text="All OFF", command=toggle_all_stop)
|
|
all_off_btn.grid(row=1, column=4, padx=5)
|
|
|
|
buttons = []
|
|
|
|
for idx, device in enumerate(devices):
|
|
device_name = device['name']
|
|
device_index = device['index']
|
|
ttk.Label(frame, text=device_name).grid(row=idx + 2, column=0, sticky="w")
|
|
|
|
for channel in [1, 2]:
|
|
btn = ttk.Button(frame, text=f"Ch {channel} ON", width=10)
|
|
btn.grid(row=idx + 2, column=channel, padx=5)
|
|
|
|
def toggle_play(dev_idx=device_index, ch=channel, button=btn):
|
|
if (dev_idx, ch) in playing_threads:
|
|
stop_sound(dev_idx, ch)
|
|
button.config(text=f"Ch {ch} ON")
|
|
else:
|
|
freq = float(frequency_entry.get())
|
|
play_sound_on_device(dev_idx, ch, freq)
|
|
button.config(text=f"Ch {ch} OFF")
|
|
|
|
btn.config(command=toggle_play)
|
|
buttons.append(btn)
|
|
|
|
root.mainloop()
|
|
|
|
if __name__ == "__main__":
|
|
start_gui() |