4 Commits
main ... sounds

Author SHA1 Message Date
7301186102 icon for server 2025-12-15 12:59:28 +01:00
seppmutterman@gmail.com
f1ed8d36c4 fixed the build script 2025-12-14 18:47:17 +01:00
seppmutterman@gmail.com
69b10b0864 testing stuff ; testing branch lol 2025-12-14 18:35:51 +01:00
faeac6c96f some sound additions 2025-12-12 21:15:07 +01:00
11 changed files with 522 additions and 14 deletions

View File

@@ -10,8 +10,11 @@ pyinstaller `
--hidden-import wx._xml `
--add-data "FiraCode-Regular.ttf;." `
--add-data "FiraCode-SemiBold.ttf;." `
--add-data "src\sounds\*;sounds" `
--add-data "venv\Lib\site-packages\irc\codes.txt;irc" `
--add-data "icon.ico;." `
--add-data "src\channel.ico;." `
--add-data "src\server.ico;." `
--icon "icon.ico" `
"src/main.py"

View File

@@ -561,9 +561,13 @@ class IRCPanel(wx.Panel):
logger.error(f"Error in add_message: {e}")
def _add_message_safe(self, message, username_color=None, message_color=None,
bold=False, italic=False, underline=False):
bold=False, italic=False, underline=False):
"""Add message to display with formatting (must be called from main thread)."""
try:
# Safety check: ensure text_ctrl still exists
if not self.text_ctrl or not self:
return
self.messages.append(message)
# Check if user is at bottom

View File

@@ -211,3 +211,65 @@ class LocalServerManager:
except Exception:
logger.info(message)
if __name__ == "__main__":
import argparse
import sys
import signal
def main():
parser = argparse.ArgumentParser(
description="Run a local-only IRC server."
)
parser.add_argument(
"--host", type=str, default="0.0.0.0",
help="Bind host (default: 0.0.0.0)"
)
parser.add_argument(
"--port", type=int, default=6667,
help="Bind port (default: 6667)"
)
parser.add_argument(
"--channels", type=str, default="#lobby",
help="Comma-separated list of channels (default: #lobby)"
)
parser.add_argument(
"--verbose", action="store_true",
help="Enable verbose logging"
)
args = parser.parse_args()
# Set up logging
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
# Initialize the server manager
manager = LocalServerManager(
listen_host=args.host,
listen_port=args.port
)
manager.set_channels([ch.strip() for ch in args.channels.split(",") if ch.strip()])
# Handle Ctrl+C gracefully
def signal_handler(sig, frame):
print("\nStopping server...")
manager.stop()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
try:
manager.start()
print(f"IRC server running on {args.host}:{args.port}")
# Keep the main thread alive while server runs
while manager.is_running():
import time
time.sleep(1)
except Exception as e:
print(f"Error: {e}")
manager.stop()
sys.exit(1)
main()

99
src/Win32API.py Normal file
View File

@@ -0,0 +1,99 @@
import ctypes
from ctypes import wintypes
import time
SW_HIDE = 0
SW_SHOW = 5
GWL_EXSTYLE = -20
WS_EX_TOOLWINDOW = 0x00000080
WS_EX_APPWINDOW = 0x00040000
WS_EX_LAYERED = 0x80000
LWA_ALPHA = 0x2
class Win32API:
def __init__(self, hwnd):
self.hwnd = hwnd
self.hidden = False
self._load_api()
def _load_api(self):
user32 = ctypes.windll.user32
self.ShowWindow = user32.ShowWindow
self.GetWindowLong = user32.GetWindowLongW
self.SetWindowLong = user32.SetWindowLongW
self._RegisterHotKey = user32.RegisterHotKey
self._SetLayeredWindowAttributes = user32.SetLayeredWindowAttributes
self.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int]
self.GetWindowLong.argtypes = [wintypes.HWND, ctypes.c_int]
self.SetWindowLong.argtypes = [wintypes.HWND, ctypes.c_int, ctypes.c_long]
self._RegisterHotKey.argtypes = [
wintypes.HWND,
wintypes.INT,
wintypes.UINT,
wintypes.UINT,
]
self._SetLayeredWindowAttributes.argtypes = [
wintypes.HWND,
wintypes.COLORREF,
wintypes.BYTE,
wintypes.DWORD,
]
def remove_from_taskbar(self):
style = self.GetWindowLong(self.hwnd, GWL_EXSTYLE)
new_style = (style & ~WS_EX_APPWINDOW) | WS_EX_TOOLWINDOW
self.SetWindowLong(self.hwnd, GWL_EXSTYLE, new_style)
def hide(self):
self.ShowWindow(self.hwnd, SW_HIDE)
self.hidden = True
def show(self):
self.ShowWindow(self.hwnd, SW_SHOW)
self.hidden = False
def toggle(self, fade=True, duration=500, steps=20):
"""Toggle window visibility with optional fade effect"""
if self.hidden:
if fade:
self.fade_in(duration, steps)
else:
self.show()
else:
if fade:
self.fade_out(duration, steps)
else:
self.hide()
def register_hotkey(self, hotkey_id, key, modifiers):
vk = ord(key.upper())
return self._RegisterHotKey(None, hotkey_id, modifiers, vk)
def _set_layered(self):
style = self.GetWindowLong(self.hwnd, GWL_EXSTYLE)
self.SetWindowLong(self.hwnd, GWL_EXSTYLE, style | WS_EX_LAYERED)
def fade_in(self, duration=50, steps=50):
self.show()
self._set_layered()
for idx in range(steps + 1):
alpha = int(255 * (idx / steps))
self._SetLayeredWindowAttributes(self.hwnd, 0, alpha, LWA_ALPHA)
ctypes.windll.kernel32.Sleep(int(duration / steps))
self.hidden = False
def fade_out(self, duration=50, steps=50):
self._set_layered()
for idx in range(steps + 1):
alpha = int(255 * (1 - idx /steps))
self._SetLayeredWindowAttributes(self.hwnd, 0, alpha, LWA_ALPHA)
ctypes.windll.kernel32.Sleep(int(duration / steps))
self.hidden = True
self.hide()

97
src/Win32SoundHandler.py Normal file
View File

@@ -0,0 +1,97 @@
import ctypes
import logging
from ctypes import wintypes
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def get_resource_path(relative_path):
import os
import sys
try:
base_path = sys._MEIPASS
except Exception:
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__)))
base_path = project_root
full_path = os.path.normpath(os.path.join(base_path, relative_path))
return full_path
class Win32SoundHandler:
def __init__(self, hwnd):
self.hwnd = hwnd
self.SND_ALIAS = 0x00010000
self.SND_FILENAME = 0x00020000
self.SND_ASYNC = 0x00000001
self._setup_playsoundapi()
def _setup_playsoundapi(self):
winmm = ctypes.windll.winmm
self.PlaySoundW = winmm.PlaySoundW
self.PlaySoundW.argtypes = (
wintypes.LPCWSTR,
wintypes.HMODULE,
wintypes.DWORD
)
self.PlaySoundW.restype = wintypes.BOOL
def play_info_sound(self):
self.PlaySoundW("SystemAsterisk", None, self.SND_ALIAS | self.SND_ASYNC)
def play_error_sound(self):
self.PlaySoundW("SystemHand", None, self.SND_ALIAS | self.SND_ASYNC)
def play_warn_sound(self):
self.PlaySoundW("SystemExclamation", None, self.SND_ALIAS | self.SND_ASYNC)
def play_question_sound(self):
self.PlaySoundW("SystemQuestion", None, self.SND_ALIAS | self.SND_ASYNC)
def play_default_sound(self):
self.PlaySoundW("SystemDefault", None, self.SND_ALIAS | self.SND_ASYNC)
def play_notification_sound(self):
self.PlaySoundW("SystemNotification", None, self.SND_ALIAS | self.SND_ASYNC)
def play_mail_sound(self):
self.PlaySoundW("MailBeep", None, self.SND_ALIAS | self.SND_ASYNC)
def play_exit_sound(self):
self.PlaySoundW("SystemExit", None, self.SND_ALIAS | self.SND_ASYNC)
def play_connect_server_or_channel(self):
try:
sound_path = get_resource_path("sounds/space-pdj.wav")
import os
if os.path.exists(sound_path):
self.PlaySoundW(sound_path, None, self.SND_FILENAME | self.SND_ASYNC)
else:
logger.warning(f"Sound file not found: {sound_path}")
except Exception as e:
logger.error(f"Error playing popout click sound: {e}")
def play_popout_click(self):
try:
sound_path = get_resource_path("sounds/startup.wav")
import os
if os.path.exists(sound_path):
self.PlaySoundW(sound_path, None, self.SND_FILENAME | self.SND_ASYNC)
else:
logger.warning(f"Sound file not found: {sound_path}")
except Exception as e:
logger.error(f"Error playing popout click sound: {e}")
def play_msg_recv(self):
try:
sound_path = get_resource_path("sounds/balloon.wav")
import os
if os.path.exists(sound_path):
self.PlaySoundW(sound_path, None, self.SND_FILENAME | self.SND_ASYNC)
else:
logger.warning(f"Sound file not found: {sound_path}")
except Exception as e:
logger.error(f"Error playing message received sound: {e}")

BIN
src/channel.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -1,4 +1,6 @@
import wx
import wx.aui
import wx.lib.agw.aui as aui
import irc.client
import threading
import re
@@ -16,6 +18,7 @@ from PrivacyNoticeDialog import PrivacyNoticeDialog
from IRCPanel import IRCPanel
from AboutDialog import AboutDialog
from NotesDialog import NotesDialog
if os.name == "nt": from Win32API import Win32API ; from Win32SoundHandler import Win32SoundHandler
from ScanWizard import ScanWizardDialog
from LocalServer import LocalServerManager
@@ -251,7 +254,7 @@ class IRCFrame(wx.Frame):
# Show privacy notice first
self.show_privacy_notice()
self.reactor = None
self.connection = None
self.reactor_thread = None
@@ -271,7 +274,7 @@ class IRCFrame(wx.Frame):
self.auto_join_channels = []
self.away = False
self.timestamps = True
self.notes_data = defaultdict(dict)
self.server_menu_items = {}
self.local_bind_host = "127.0.0.1"
@@ -300,6 +303,18 @@ class IRCFrame(wx.Frame):
self.motd_lines = []
self.collecting_motd = False
self.hwnd = self.GetHandle()
self.ctrl = Win32API(self.hwnd)
# Initialize sound handler for Windows
if os.name == "nt":
self.sound_handler = Win32SoundHandler(self.hwnd)
else:
self.sound_handler = None
# Sound toggle state (enabled by default)
self.sounds_enabled = True
self.setup_irc_handlers()
self.create_menubar()
self.setup_ui()
@@ -329,6 +344,17 @@ class IRCFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.on_find_next, id=1001)
self.Bind(wx.EVT_MENU, self.on_find_previous, id=1002)
self.Bind(wx.EVT_MENU, self.on_quick_escape, id=1003)
HOTKEY_ID = 1
MOD_CONTROL = 0x0002
MOD_ALT = 0x0001
# Register directly on the wx.Frame
self.RegisterHotKey(HOTKEY_ID, MOD_CONTROL | MOD_ALT, ord('H'))
self.Bind(wx.EVT_HOTKEY, self.on_hotkey, id=HOTKEY_ID)
def on_hotkey(self, event):
self.ctrl.toggle()
def build_theme(self):
"""Build a small theme descriptor that respects the host platform."""
@@ -397,7 +423,6 @@ class IRCFrame(wx.Frame):
def show_privacy_notice(self):
"""Show privacy notice dialog at startup"""
dlg = PrivacyNoticeDialog(self)
dlg.ShowModal()
dlg.Destroy()
def get_user_color(self, username):
@@ -528,15 +553,31 @@ class IRCFrame(wx.Frame):
left_panel.SetSizer(left_sizer)
# Center - Notebook
self.notebook = wx.Notebook(panel)
self.notebook = wx.aui.AuiNotebook(panel, style=
wx.aui.AUI_NB_DEFAULT_STYLE |
wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB |
wx.aui.AUI_NB_MIDDLE_CLICK_CLOSE
)
self.notebook.SetBackgroundColour(self.theme["content_bg"])
# Setup tab icons
self.setup_tab_icons()
# Bind close event
self.notebook.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.on_notebook_page_close)
# Server panel
server_panel = IRCPanel(self.notebook, self)
server_panel.set_target("SERVER")
idx = self.notebook.GetPageCount()
self.notebook.AddPage(server_panel, "Server")
# THIS is the missing line
if self.icon_server != -1:
self.notebook.SetPageImage(idx, self.icon_server)
self.channels["SERVER"] = server_panel
# Right sidebar - Users - light gray for contrast
right_panel = wx.Panel(panel)
@@ -595,8 +636,12 @@ class IRCFrame(wx.Frame):
file_menu = wx.Menu()
file_menu.Append(300, "&About", "About wxIRC Client")
file_menu.AppendSeparator()
self.sound_toggle_item = file_menu.AppendCheckItem(301, "Toggle &Sounds")
self.sound_toggle_item.Check(True) # Sounds enabled by default
file_menu.AppendSeparator()
file_menu.Append(wx.ID_EXIT, "E&xit\tCtrl+Q")
self.Bind(wx.EVT_MENU, self.on_about, id=300)
self.Bind(wx.EVT_MENU, self.on_toggle_sounds, id=301)
self.Bind(wx.EVT_MENU, self.on_close, id=wx.ID_EXIT)
# Edit menu with search
@@ -669,6 +714,11 @@ class IRCFrame(wx.Frame):
except Exception as e:
logger.error(f"Error creating menu: {e}")
def on_toggle_sounds(self, event):
"""Toggle sound effects on/off"""
self.sounds_enabled = self.sound_toggle_item.IsChecked()
logger.info(f"Sounds {'enabled' if self.sounds_enabled else 'disabled'}")
def on_about(self, event):
"""Show About dialog"""
try:
@@ -677,6 +727,11 @@ class IRCFrame(wx.Frame):
dlg.Destroy()
except Exception as e:
logger.error(f"Error showing about dialog: {e}")
if self.sound_handler and self.sounds_enabled:
try:
self.sound_handler.play_error_sound()
except:
pass
def on_notes(self, event):
"""Open notes editor dialog"""
@@ -697,6 +752,11 @@ class IRCFrame(wx.Frame):
except Exception as e:
logger.error(f"Error opening notes dialog: {e}")
self.log_server(f"Error opening notes: {e}", wx.Colour(255, 0, 0))
if self.sound_handler and self.sounds_enabled:
try:
self.sound_handler.play_error_sound()
except:
pass
def on_notes_closed(self, event=None):
"""Handle notes frame closing"""
@@ -862,6 +922,13 @@ class IRCFrame(wx.Frame):
self.connect_btn.Enable(True)
self.SetStatusText("Connection failed")
logger.error(f"Connection failed: {error_msg}")
# Play error sound
if self.sound_handler and self.sounds_enabled:
try:
self.sound_handler.play_error_sound()
except Exception as e:
logger.error(f"Error playing connection failure sound: {e}")
def disconnect(self):
"""Safely disconnect from IRC server"""
@@ -892,6 +959,13 @@ class IRCFrame(wx.Frame):
def on_disconnect_cleanup(self):
"""Clean up after disconnect"""
# Play disconnect notification sound
if self.sound_handler and self.sounds_enabled:
try:
self.sound_handler.play_warn_sound()
except Exception as e:
logger.error(f"Error playing disconnect sound: {e}")
with self.connection_lock:
self.connection = None
self.is_connecting = False
@@ -1146,11 +1220,18 @@ Available commands:
try:
if channel in self.channels and channel != "SERVER":
def _close_channel():
# Find and delete the page
for i in range(self.notebook.GetPageCount()):
if self.notebook.GetPageText(i) == channel:
self.notebook.DeletePage(i)
break
del self.channels[channel]
# Clean up channel data
if channel in self.channels:
del self.channels[channel]
if channel in self.channel_users:
del self.channel_users[channel]
idx = self.channel_list.FindString(channel)
if idx != wx.NOT_FOUND:
@@ -1159,7 +1240,7 @@ Available commands:
self.safe_ui_update(_close_channel)
except Exception as e:
logger.error(f"Error closing channel: {e}")
def log_server(self, message, color=None, bold=False, italic=False, underline=False):
try:
if "SERVER" in self.channels:
@@ -1180,8 +1261,11 @@ Available commands:
def log_channel_message(self, channel, username, message, is_action=False, is_system=False):
"""Log a message to a channel with username coloring"""
try:
# Don't create new channels if they don't exist and we're trying to log to them
if channel not in self.channels:
self.safe_ui_update(self.add_channel, channel)
# Only create channel if it's being opened by the user, not just receiving messages
return
if channel in self.channels:
timestamp = self.get_timestamp()
@@ -1204,12 +1288,140 @@ Available commands:
except Exception as e:
logger.error(f"Error logging channel message: {e}")
def setup_tab_icons(self):
try:
self.tab_image_list = wx.ImageList(32, 32)
# Icon file names to search for
icon_files = {
'server': 'server.ico',
'channel': 'channel.ico',
'query': 'channel.ico' # Reuse channel.ico for queries if no query.ico exists
}
# Search paths for icons
base_paths = [
os.path.dirname(__file__),
get_resource_path(""),
os.path.join(os.getcwd(), "src"),
]
# Try to load each icon
loaded_icons = {}
for icon_type, filename in icon_files.items():
icon_path = None
for base_path in base_paths:
test_path = os.path.join(base_path, filename)
if os.path.exists(test_path):
icon_path = test_path
break
if icon_path:
try:
img = wx.Image(icon_path, wx.BITMAP_TYPE_ICO)
img = img.Scale(32, 32, wx.IMAGE_QUALITY_HIGH)
bmp = wx.Bitmap(img)
loaded_icons[icon_type] = self.tab_image_list.Add(bmp)
logger.info(f"Loaded {icon_type} icon from {icon_path}")
except Exception as e:
logger.warning(f"Failed to load {icon_type} icon: {e}")
loaded_icons[icon_type] = -1
else:
logger.info(f"{filename} not found for {icon_type}")
loaded_icons[icon_type] = -1
# Assign icon indices
self.icon_server = loaded_icons.get('server', -1)
self.icon_channel = loaded_icons.get('channel', -1)
self.icon_query = loaded_icons.get('query', -1)
# Use fallback icons if any failed to load
if self.icon_server == -1 or self.icon_channel == -1 or self.icon_query == -1:
logger.info("Using fallback icons for missing icon files")
self._setup_fallback_icons()
self.notebook.SetImageList(self.tab_image_list)
except Exception as e:
logger.error(f"Error setting up tab icons: {e}")
self.icon_server = -1
self.icon_channel = -1
self.icon_query = -1
def _setup_fallback_icons(self):
"""Setup fallback icons using wx.ArtProvider."""
try:
self.icon_server = self.tab_image_list.Add(
wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_OTHER, (48, 48))
)
self.icon_channel = self.tab_image_list.Add(
wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, (48, 48))
)
self.icon_query = self.tab_image_list.Add(
wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_OTHER, (48, 48))
)
except Exception as e:
logger.error(f"Error setting up fallback icons: {e}")
self.icon_server = -1
self.icon_channel = -1
self.icon_query = -1
def on_notebook_page_close(self, event):
"""Handle tab close button clicks"""
try:
page_idx = event.GetSelection()
channel = self.notebook.GetPageText(page_idx)
if channel == "Server":
event.Veto()
wx.MessageBox("Can't close Server!", "Error", wx.OK | wx.ICON_ERROR)
return
if channel in self.channels:
del self.channels[channel]
if channel in self.channel_users:
del self.channel_users[channel]
# Remove from channel list
idx = self.channel_list.FindString(channel)
if idx != wx.NOT_FOUND:
self.channel_list.Delete(idx)
if channel.startswith('#') and self.is_connected():
try:
self.connection.part(channel)
except:
pass
# Allow the close to proceed
event.Skip()
except Exception as e:
logger.error(f"Error closing tab: {e}")
event.Skip()
def add_channel(self, channel):
"""Add a new channel/query tab with appropriate icon"""
try:
if channel not in self.channels:
panel = IRCPanel(self.notebook, self)
panel.set_target(channel)
self.notebook.AddPage(panel, channel)
# Determine icon based on channel type
if channel == "SERVER":
icon_idx = getattr(self, 'icon_server', -1)
elif channel.startswith('#'):
icon_idx = getattr(self, 'icon_channel', -1)
else:
icon_idx = getattr(self, 'icon_query', -1)
# Add page with icon (if icon_idx is -1, no icon will be shown)
if icon_idx >= 0:
self.notebook.AddPage(panel, channel, select=True, imageId=icon_idx)
else:
self.notebook.AddPage(panel, channel, select=True)
self.channels[channel] = panel
if channel.startswith('#'):
@@ -1226,6 +1438,11 @@ Available commands:
except Exception as e:
logger.error(f"Error opening scan wizard: {e}")
self.log_server(f"Scan wizard failed to open: {e}", wx.Colour(255, 0, 0))
if self.sound_handler and self.sounds_enabled:
try:
self.sound_handler.play_error_sound()
except:
pass
def quick_connect(self, server, port):
"""Populate connection fields and initiate a connection if idle."""
@@ -1434,6 +1651,13 @@ COMMANDS (type /help in chat for full list):
self.log_server("Connected to server!", wx.Colour(0, 128, 0), bold=True) # Dark green
self.log_server(f"Welcome message: {' '.join(event.arguments)}", wx.Colour(0, 100, 0))
# Play welcome/connection sound
if self.sound_handler and self.sounds_enabled:
try:
self.sound_handler.play_connect_server_or_channel()
except Exception as e:
logger.error(f"Error playing welcome sound: {e}")
# Auto-join channels
for channel in self.auto_join_channels:
if not channel.startswith('#'):
@@ -1452,6 +1676,13 @@ COMMANDS (type /help in chat for full list):
if nick == self.nick:
self.safe_ui_update(self.add_channel, channel)
self.log_server(f"Joined channel {channel}", wx.Colour(0, 128, 0)) # Dark green
# Play sound when we join a channel
if self.sound_handler and self.sounds_enabled:
try:
self.sound_handler.play_connect_server_or_channel()
except Exception as e:
logger.error(f"Error playing channel join sound: {e}")
self.log_channel_message(channel, nick, f"{nick} joined", is_system=True)
@@ -1509,6 +1740,13 @@ COMMANDS (type /help in chat for full list):
self.log_channel_message(channel, nick, message, is_action=True)
else:
self.log_channel_message(channel, nick, message)
# Play sound notification only for real user messages (not from self)
if self.sound_handler and self.sounds_enabled and nick.lower() != self.nick.lower():
try:
self.sound_handler.play_msg_recv()
except Exception as e:
logger.error(f"Error playing message sound: {e}")
# Highlight own nick in messages
if self.nick.lower() in message.lower():
@@ -1527,6 +1765,13 @@ COMMANDS (type /help in chat for full list):
self.log_channel_message(nick, nick, message, is_action=True)
else:
self.log_channel_message(nick, nick, message)
# Play mail sound for private messages
if self.sound_handler and self.sounds_enabled and nick.lower() != self.nick.lower():
try:
self.sound_handler.play_mail_sound()
except Exception as e:
logger.error(f"Error playing private message sound: {e}")
except Exception as e:
logger.error(f"Error in privmsg handler: {e}")
@@ -1728,14 +1973,12 @@ if os.name == 'nt':
ctypes.windll.user32.SetProcessDPIAware()
except:
pass
else:
pass
if __name__ == "__main__":
try:
if os.name == 'nt':
enable_high_dpi()
app = wx.App()
frame = IRCFrame()
frame.SetIcon(wx.Icon(get_resource_path("icon.ico"), wx.BITMAP_TYPE_ICO))

BIN
src/server.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
src/sounds/balloon.wav Normal file

Binary file not shown.

BIN
src/sounds/space-pdj.wav Normal file

Binary file not shown.

BIN
src/sounds/startup.wav Normal file

Binary file not shown.