Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions BinauralLab.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python BinauralLab.py
148 changes: 116 additions & 32 deletions BinauralLab.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,24 @@ class BinauralApp:
"Gamma Consciousness":{"carrier": 300.0, "beat":45.0,"desc": "Gamma for expanded awareness."},
"Focus 49": {"carrier": 210.0, "beat":49.0,"desc": "Edge of mapped human territory."}
}



CATEGORIES = {
"Delta Waves (0.5–4 Hz)": list(DEFAULT_PRESETS.keys())[:7],
"Theta Waves (4–8 Hz)": list(DEFAULT_PRESETS.keys())[7:18],
"Alpha Waves (8–12 Hz)": list(DEFAULT_PRESETS.keys())[18:27],
"Beta Waves (13–30 Hz)": list(DEFAULT_PRESETS.keys())[27:37],
"Gamma Waves (30–50 Hz)": list(DEFAULT_PRESETS.keys())[37:41],
"Alpha Waves (8–12 Hz)": list(DEFAULT_PRESETS.keys())[18:28],
"Beta Waves (13–30 Hz)": list(DEFAULT_PRESETS.keys())[28:43],
"Gamma Waves (30–50 Hz)": list(DEFAULT_PRESETS.keys())[43:51],
}

SAMPLE_RATE = 44_100
BLOCKSIZE = 1_024

# ─────────────────────────── Init ────────────────────────────
def __init__(self, master: tk.Tk):
self.master = master
master.title("Binaural Beat Lab")
master.geometry("900x780")
master.geometry("1280x1080")

self.stream = None
self.left_phase = self.right_phase = 0.0
Expand All @@ -106,18 +107,35 @@ def __init__(self, master: tk.Tk):
self._load_presets()
self._build_ui()
self._init_plot()
self._build_preset_tree()
self._refresh_ui()
self.master.after(100, self._update_plot)

self.carrier_var.trace_add("write", lambda *_: self._refresh_ui())
self.beat_var.trace_add("write", lambda *_: self._refresh_ui())

# ─────────── Dynamic Categories ────────────
# def get_band(beat):
# if beat < 4:
# return "Delta Waves (0.5–4 Hz)"
# elif beat < 8:
# return "Theta Waves (4–8 Hz)"
# elif beat < 13:
# return "Alpha Waves (8–13 Hz)"
# elif beat < 30:
# return "Beta Waves (13–30 Hz)"
# else:
# return "Gamma Waves (30–50 Hz)"

# ─────────── Load presets & personal resonances ────────────
def _load_presets(self):
if not os.path.exists(self.PRESETS_FILE):
json.dump(self.DEFAULT_PRESETS, open(self.PRESETS_FILE, "w"), indent=4)
self.presets = json.load(open(self.PRESETS_FILE, "r"))

# To
for name, preset in self.DEFAULT_PRESETS.items():
if name not in self.presets:
self.presets[name] = preset
if os.path.exists(USER_RES_FILE):
try:
data = json.load(open(USER_RES_FILE, "r"))
Expand Down Expand Up @@ -153,14 +171,14 @@ def _build_ui(self):

# Carrier
ttk.Label(main, text="Carrier (Hz)").grid(row=2, column=0, sticky="w", **pad)
ttk.Scale(main, from_=0.001, to=200, variable=self.carrier_var,
tk.Scale(main, from_=0.001, to=500,resolution=0.001, variable=self.carrier_var,
orient="horizontal").grid(row=2, column=1, sticky="ew", **pad)
ttk.Entry(main, textvariable=self.carrier_var, width=8
).grid(row=2, column=2, sticky="e", **pad)

# Beat
ttk.Label(main, text="Beat Δf (Hz)").grid(row=3, column=0, sticky="w", **pad)
ttk.Scale(main, from_=0.001, to=50, variable=self.beat_var,
tk.Scale(main, from_=0.001, to=50, resolution=0.001, variable=self.beat_var,
orient="horizontal").grid(row=3, column=1, sticky="ew", **pad)
ttk.Entry(main, textvariable=self.beat_var, width=8
).grid(row=3, column=2, sticky="e", **pad)
Expand Down Expand Up @@ -198,16 +216,27 @@ def _build_ui(self):
ttk.Button(af, text="⬇ Export", command=self.export_audio
).pack(side="left", **pad)

# Search presets (bonus feature)
ttk.Label(main, text="Search Presets").grid(row=8, column=0, sticky="w", **pad)

self.search_var = tk.StringVar()
search_entry = ttk.Entry(main, textvariable=self.search_var)
search_entry.grid(row=8, column=1, columnspan=3, sticky="ew", **pad)

self.search_var.trace_add("write", lambda *_: self._search_presets())
search_entry.focus()
self.master.bind("<Control-f>", lambda e: search_entry.focus())
search_entry.bind("<Escape>", lambda e: self.search_var.set(""))

# Preset tree
ttk.Label(main, text="Load Preset").grid(row=8, column=0, sticky="nw", **pad)
tree_frame = ttk.Frame(main); tree_frame.grid(row=8, column=1, columnspan=3,
ttk.Label(main, text="Load Preset").grid(row=9, column=0, sticky="nw", **pad)
tree_frame = ttk.Frame(main); tree_frame.grid(row=9, column=1, columnspan=3,
sticky="nsew", **pad)
tree_frame.rowconfigure(0, weight=1); tree_frame.columnconfigure(0, weight=1)

self.tree = ttk.Treeview(tree_frame, columns=("Desc",), show="tree headings",
selectmode="browse")
self.tree.heading("#0", text="Preset"); self.tree.heading("Desc", text="Description")
self.tree.column("#0", width=200); self.tree.column("Desc", width=400)
self.tree = ttk.Treeview(tree_frame, columns=("Beat","Carrier","Desc"), show="tree headings",selectmode="browse")
self.tree.heading("#0", text="Preset");self.tree.heading("Beat", text="Beat (Hz)");self.tree.heading("Carrier", text="Carrier (Hz)"); self.tree.heading("Desc", text="Description") # Added Beat and Carrier columns to treeview
self.tree.column("#0", width=200); self.tree.column("Beat", width=100); self.tree.column("Carrier", width=100); self.tree.column("Desc", width=400)
self.tree.tag_configure("highlight", background="lightyellow") # highlight style

vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
Expand All @@ -217,12 +246,49 @@ def _build_ui(self):

# Plot frame
self.viz_frame = ttk.Frame(main)
self.viz_frame.grid(row=9, column=0, columnspan=4, sticky="nsew", **pad)
self.viz_frame.grid(row=10, column=0, columnspan=4, sticky="nsew", **pad)

ttk.Label(main, text="Binaural beats work best with stereo headphones.",
font=("Arial", 9, "italic"), foreground="gray"
).grid(row=10, column=0, columnspan=4, pady=(2, 0))
).grid(row=11, column=0, columnspan=4, pady=(2, 0))

# ────────────────────────── Preset search ──────────────────────────
def _search_presets(self, *_):
query = self.search_var.get().strip().lower()

# If search box is empty, restore the original tree view
if not query:
self._build_preset_tree()
return

# Clear the tree and prepare to rebuild it with only matching presets
self.tree.delete(*self.tree.get_children())
self.tree_items = {}

# Iterate through all categories and their presets
for category, names in self.CATEGORIES.items():
# Find presets in this category that match the query
matches = [
name for name in names
if name in self.presets and (
query in name.lower()
or query in self.presets[name].get("desc", "").lower()
or query in str(self.presets[name].get("beat", ""))
)
]
# Only add the category if it has matching presets
if not matches:
continue
parent = self.tree.insert("", "end", text=category, open=True)
for name in matches:
p = self.presets[name]
item = self.tree.insert(
parent, "end", text=name,
values=(p.get("beat", ""), p.get("carrier", ""), p.get("desc", ""))
)
self.tree_items[name] = item


# ───────────────────────── Plotting ──────────────────────────
def _init_plot(self):
self.fig = Figure(figsize=(6, 2.5), dpi=100)
Expand All @@ -246,8 +312,8 @@ def _update_plot(self):
window = min(0.5, max(0.05, 2.0 / b))
t = np.linspace(0, window, int(self.SAMPLE_RATE * window), endpoint=False)
c = self.carrier_var.get()
y1 = np.sin(2 * np.pi * c * t)
y2 = np.sin(2 * np.pi * (c + b) * t)
y1 = np.sin(2 * np.pi * (c - b/2) * t)
y2 = np.sin(2 * np.pi * (c + b/2) * t)
env = np.abs(y1 - y2) / 2

self.line1.set_data(t, y1); self.line2.set_data(t, y2)
Expand Down Expand Up @@ -275,26 +341,40 @@ def _refresh_ui(self):
)

# ===== REST OF UI REFRESH UNCHANGED =====
for name, item in self.tree_items.items():
if name == self.selected_preset:
self.tree.item(item, tags=("highlight",))
else:
self.tree.item(item, tags=())

def _build_preset_tree(self):

self.tree.delete(*self.tree.get_children())
self.tree_items = {} # store item IDs for each preset

for cat, names in self.CATEGORIES.items():
parent = self.tree.insert("", "end", text=cat, open=True)

for name in names:
if name in self.presets:
desc = self.presets[name].get("desc", "")
tags = ("highlight",) if name == self.selected_preset else ()
self.tree.insert(parent, "end", text=name,
values=(desc,), tags=tags)
beat = self.presets[name].get("beat", "")
carrier = self.presets[name].get("carrier", "")
item = self.tree.insert(parent, "end", text=name, values=(beat, carrier, desc))
self.tree_items[name] = item

# Add any custom presets
# Add custom presets
custom = [n for n in self.presets if n not in self.DEFAULT_PRESETS]

if custom:
parent = self.tree.insert("", "end", text="Custom Sounds", open=True)

for name in custom:
desc = self.presets[name].get("desc", "")
tags = ("highlight",) if name == self.selected_preset else ()
self.tree.insert(parent, "end", text=name,
values=(desc,), tags=tags)

beat = self.presets[name].get("beat", "")
carrier = self.presets[name].get("carrier", "")
item = self.tree.insert(parent, "end", text=name, values=(beat, carrier, desc))
self.tree_items[name] = item

# ──────────────── Audio callback & stream ────────────────
def start_audio(self):
Expand All @@ -313,15 +393,17 @@ def stop_audio(self):
def _audio_callback(self, outdata, frames, *_):
c, b = self.carrier_var.get(), self.beat_var.get()
v = self.volume_var.get()
d1 = 2*np.pi*c / self.SAMPLE_RATE
d2 = 2*np.pi*(c+b) / self.SAMPLE_RATE
left_freq = max(0.001, c - b/2)
right_freq = max(0.001, c + b/2)
d1 = 2*np.pi*left_freq / self.SAMPLE_RATE
d2 = 2*np.pi*right_freq / self.SAMPLE_RATE
idx = np.arange(frames)
p1 = self.left_phase + d1*idx
p2 = self.right_phase + d2*idx
outdata[:] = (np.stack([np.sin(p1), np.sin(p2)], axis=-1) * v).astype(np.float32)
self.left_phase = (p1[-1] + d1) % (2*np.pi)
self.right_phase = (p2[-1] + d2) % (2*np.pi)

# ───────────────────────── Preset CRUD ─────────────────────────
def save_preset(self):
name = self.preset_entry.get().strip()
Expand All @@ -335,6 +417,7 @@ def save_preset(self):
}
self.selected_preset = name
json.dump(self.presets, open(self.PRESETS_FILE, "w"), indent=4)
self._build_preset_tree()
self._refresh_ui()
messagebox.showinfo("Saved", f"Preset '{name}' saved.")

Expand All @@ -346,6 +429,7 @@ def delete_preset(self):
self.presets.pop(name, None); self.selected_preset = None
self.preset_entry.delete(0, tk.END)
json.dump(self.presets, open(self.PRESETS_FILE, "w"), indent=4)
self._build_preset_tree()
self._refresh_ui()
messagebox.showinfo("Deleted", f"Preset '{name}' deleted.")

Expand All @@ -366,8 +450,8 @@ def export_audio(self):
if not dur or dur <= 0: return
fs = self.SAMPLE_RATE
t = np.linspace(0, dur, int(fs * dur), endpoint=False)
c, b = self.carrier_var.get(), self.beat_var.get()
left = np.sin(2*np.pi*c*t)
c, b = self.carrier_var.get(), (self.beat_var.get()/2)
left = np.sin(2*np.pi*(c-b)*t)
right = np.sin(2*np.pi*(c+b)*t)
data = np.int16(np.stack([left, right], axis=-1) /
np.max(np.abs(np.stack([left, right], axis=-1))) * 32767)
Expand Down
Loading