diff --git a/__init__.py b/__init__.py index b40224c..d2bac2c 100755 --- a/__init__.py +++ b/__init__.py @@ -10,11 +10,12 @@ from os.path import isfile, join from inspect import isfunction from xdg import BaseDirectory from setproctitle import setproctitle -import pyglet +import pygame import pyperclip import random import re import requests +import textwrap import webbrowser # Global variable for speech provider try: @@ -29,6 +30,7 @@ except ImportError: print("No other speech providers found.") exit() import math +import numpy as np import time localConfig = configparser.ConfigParser() @@ -113,20 +115,46 @@ def read_config(readGlobal = False): globalConfig.read_file(configfile) except: pass - -def speak(text, interupt = True): + +def speak(text, interupt=True): if speechProvider == "speechd": - if interupt == True: spd.cancel() + if interupt: spd.cancel() spd.say(text) else: if speechProvider == "accessible_output2": s.speak(text, interrupt=True) + # Display the text on screen + screen = pygame.display.get_surface() + font = pygame.font.Font(None, 36) + # Wrap the text + maxWidth = screen.get_width() - 40 # Leave a 20-pixel margin on each side + wrappedText = textwrap.wrap(text, width=maxWidth // font.size('A')[0]) + # Render each line + textSurfaces = [font.render(line, True, (255, 255, 255)) for line in wrappedText] + screen.fill((0, 0, 0)) # Clear screen with black + # Calculate total height of text block + totalHeight = sum(surface.get_height() for surface in textSurfaces) + # Start y-position (centered vertically) + currentY = (screen.get_height() - totalHeight) // 2 + # Blit each line of text + for surface in textSurfaces: + textRect = surface.get_rect(center=(screen.get_width() // 2, currentY + surface.get_height() // 2)) + screen.blit(surface, textRect) + currentY += surface.get_height() + pygame.display.flip() +def check_for_exit(): + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: + return True + return False + pygame.event.pump() def exit_game(): if speechProvider == "speechd": spd.close() - # Close the pyglet window - pyglet.app.exit() + pygame.mixer.music.stop() + pygame.quit() + exit() def initialize_gui(gameTitle): # Check for, and possibly create, storm-games path @@ -141,18 +169,101 @@ def initialize_gui(gameTitle): global gameName gameName = gameTitle setproctitle(str.lower(str.replace(gameTitle, " ", ""))) - # init pyglet window - window = pyglet.window.Window(500, 300, gameTitle) + # start pygame + pygame.init() + # start the display (required by the event loop) + pygame.display.set_mode((800, 600)) + pygame.display.set_caption(gameTitle) + # Set 32 channels for sound by default + pygame.mixer.pre_init(44100, -16, 2, 1024) + pygame.mixer.init() + pygame.mixer.set_num_channels(32) + # Reserve the cut scene channel + pygame.mixer.set_reserved(0) # Load sounds from the sound directory and creates a list like {'bottle': 'bottle.ogg'} - soundFiles = [f for f in listdir("sounds/") if isfile(join("sounds/", f)) and (f.split('.')[1].lower() in ["ogg","wav"])] - # make a dict with pyglet media {'bottle':} + try: + soundFiles = [f for f in listdir("sounds/") if isfile(join("sounds/", f)) and (f.split('.')[1].lower() in ["ogg","wav"])] + except Exception as e: + print("No sounds found.") + speak("No sounds found.", False) + #lets make a dict with pygame.mixer.Sound() objects {'bottle':} soundData = {} for f in soundFiles: - soundData[f.split('.')[0]] = pyglet.media.load("sounds/" + f, streaming = False) + soundData[f.split('.')[0]] = pygame.mixer.Sound("sounds/" + f) soundData['game-intro'].play() - time.sleep(soundData['game-intro'].duration) + time.sleep(soundData['game-intro'].get_length()) return soundData +def generate_tone(frequency, duration=0.1, sample_rate=44100, volume = 0.2): + t = np.linspace(0, duration, int(sample_rate * duration), False) + tone = np.sin(2 * np.pi * frequency * t) + stereo_tone = np.vstack((tone, tone)).T # Create a 2D array for stereo + stereo_tone = (stereo_tone * 32767).astype(np.int16) + stereo_tone = (stereo_tone * 32767 * volume).astype(np.int16) # Apply volume + stereo_tone = np.ascontiguousarray(stereo_tone) # Ensure C-contiguous array + return pygame.sndarray.make_sound(stereo_tone) + +def x_powerbar(): + clock = pygame.time.Clock() + screen = pygame.display.get_surface() + position = -50 # Start from the leftmost position + direction = 1 # Move right initially + barHeight = 20 + while True: + frequency = 440 # A4 note + leftVolume = (50 - position) / 100 + rightVolume = (position + 50) / 100 + tone = generate_tone(frequency) + channel = tone.play() + channel.set_volume(leftVolume, rightVolume) + # Visual representation + screen.fill((0, 0, 0)) + barWidth = screen.get_width() - 40 # Leave 20px margin on each side + pygame.draw.rect(screen, (100, 100, 100), (20, screen.get_height() // 2 - barHeight // 2, barWidth, barHeight)) + markerPos = int(20 + (position + 50) / 100 * barWidth) + pygame.draw.rect(screen, (255, 0, 0), (markerPos - 5, screen.get_height() // 2 - barHeight, 10, barHeight * 2)) + pygame.display.flip() + for event in pygame.event.get(): + check_for_exit() + if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + channel.stop() + return position # This will return a value between -50 and 50 + position += direction + if position > 50: + position = 50 + direction = -1 + elif position < -50: + position = -50 + direction = 1 + clock.tick(40) # Speed of bar + +def y_powerbar(): + clock = pygame.time.Clock() + screen = pygame.display.get_surface() + power = 0 + direction = 1 # 1 for increasing, -1 for decreasing + barWidth = 20 + while True: + frequency = 220 + (power * 5) # Adjust these values to change the pitch range + tone = generate_tone(frequency) + channel = tone.play() + # Visual representation + screen.fill((0, 0, 0)) + barHeight = screen.get_height() - 40 # Leave 20px margin on top and bottom + pygame.draw.rect(screen, (100, 100, 100), (screen.get_width() // 2 - barWidth // 2, 20, barWidth, barHeight)) + markerPos = int(20 + (100 - power) / 100 * barHeight) + pygame.draw.rect(screen, (255, 0, 0), (screen.get_width() // 2 - barWidth, markerPos - 5, barWidth * 2, 10)) + pygame.display.flip() + for event in pygame.event.get(): + check_for_exit() + if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + channel.stop() + return power + power += direction + if power >= 100 or power <= 0: + direction *= -1 # Reverse direction at limits + clock.tick(40) + def cut_scene(sounds, soundName): pygame.event.clear() pygame.mixer.stop() @@ -164,45 +275,46 @@ def cut_scene(sounds, soundName): pygame.mixer.stop() pygame.event.pump() -def obj_play(sounds, soundName, playerPos, objPos): - distance = playerPos - objPos - if distance > 9 or distance < -9: - # The item is out of range, so play it at 0 - left = 0 - right = 0 - elif distance == 0: - left = 0.9 - right = 0.9 + +def calculate_volume_and_pan(player_pos, obj_pos): + distance = abs(player_pos - obj_pos) + max_distance = 12 # Maximum audible distance + if distance > max_distance: + return 0, 0, 0 # No sound if out of range + # Calculate volume (non-linear scaling for more noticeable changes) + volume = ((max_distance - distance) / max_distance) ** 1.5 + # Determine left/right based on relative position + if player_pos < obj_pos: + # Object is to the right + left = max(0, 1 - (obj_pos - player_pos) / max_distance) + right = 1 + elif player_pos > obj_pos: + # Object is to the left + left = 1 + right = max(0, 1 - (player_pos - obj_pos) / max_distance) else: - angle = math.radians(distance * 5) - left = math.sqrt(2)/2.0 * (math.cos(angle) + math.sin(angle)) - right = math.sqrt(2)/2.0 * (math.cos(angle) - math.sin(angle)) - if left < 0: left *= -1 - if right < 0: right *= -1 - # x is the channel for the sound + # Player is on the object + left = right = 1 + return volume, left, right + +def obj_play(sounds, soundName, player_pos, obj_pos): + volume, left, right = calculate_volume_and_pan(player_pos, obj_pos) + if volume == 0: + return None # Don't play if out of range + # Play the sound on a new channel x = sounds[soundName].play(-1) - # Apply the position information to the channel - x.set_volume(left, right) - # return the channel so that it can be used in the update and stop functions. + # Apply the volume and pan + x.set_volume(volume * left, volume * right) return x - -def obj_update(x, playerPos, objPos): - distance = playerPos - objPos - if distance > 9 or distance < -9: - left = 0 - right = 0 - elif distance == 0: - left = 0.9 - right = 0.9 - else: - angle = math.radians(distance * 5) - left = math.sqrt(2)/2.0 * (math.cos(angle) + math.sin(angle)) - right = math.sqrt(2)/2.0 * (math.cos(angle) - math.sin(angle)) - if left < 0: left *= -1 - if right < 0: right *= -1 - # Apply the position information to the channel - x.set_volume(left, right) - # return the channel +def obj_update(x, player_pos, obj_pos): + if x is None: + return None + volume, left, right = calculate_volume_and_pan(player_pos, obj_pos) + if volume == 0: + x.stop() + return None + # Apply the volume and pan + x.set_volume(volume * left, volume * right) return x def obj_stop(x): @@ -213,6 +325,22 @@ def obj_stop(x): except: return x +def play_ambiance(sounds, soundNames, probability, randomLocation = False): + # Check if any of the sounds in the list is already playing + for soundName in soundNames: + if pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy(): + return + if random.randint(1, 100) > probability: + return + # Choose a random sound from the list + ambianceSound = random.choice(soundNames) + channel = sounds[ambianceSound].play() + if randomLocation and channel: + left_volume = random.random() + right_volume = random.random() + channel.set_volume(left_volume, right_volume) + return channel # Return the channel object for potential further manipulation + def play_random(sounds, soundName, pause = False, interrupt = False): key = [] for i in sounds.keys(): @@ -282,7 +410,7 @@ def learn_sounds(sounds): loop = True pygame.mixer.music.pause() i = 0 - soundFiles = [f for f in listdir("sounds/") if isfile(join("sounds/", f)) and (f.split('.')[1].lower() in ["ogg","wav"]) and (f.split('.')[0].lower() not in ["game-intro", "music_menu"])] + soundFiles = [f for f in listdir("sounds/") if isfile(join("sounds/", f)) and (f.split('.')[1].lower() in ["ogg","wav"]) and (f.split('.')[0].lower() not in ["game-intro", "music_menu"]) and (not f.lower().startswith("_"))] # j keeps track of last spoken index so it isn't voiced on key up. j = -1 while loop == True: @@ -317,9 +445,12 @@ def game_menu(sounds, *options): if pygame.mixer.music.get_busy(): pygame.mixer.music.unpause() else: - pygame.mixer.music.load("sounds/music_menu.ogg") - pygame.mixer.music.set_volume(0.75) - pygame.mixer.music.play(-1) + try: + pygame.mixer.music.load("sounds/music_menu.ogg") + pygame.mixer.music.set_volume(0.75) + pygame.mixer.music.play(-1) + except: + pass i = 0 # j keeps track of last spoken index so it isn't voiced on key up. j = -1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f3dd51e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +pygame>=2.0.0 +pyperclip>=1.8.0 +requests>=2.25.0 +pyxdg>=0.27 +setproctitle>=1.2.0 +numpy>=1.19.0 +accessible-output2>=0.14 diff --git a/requirements_linux.txt b/requirements_linux.txt new file mode 100644 index 0000000..18a4555 --- /dev/null +++ b/requirements_linux.txt @@ -0,0 +1,2 @@ +-r requirements.txt +python-speechd>=0.11.1