]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blobdiff - helpers/__init__.py
Coding styles
[perso/Immae/Projets/Python/MusicSampler.git] / helpers / __init__.py
index 8570008f265efe7e86ae1f21be28c9c68213cae8..9d6663893bc32634b1163f7a525b8b04a7bdf9a5 100644 (file)
 # -*- coding: utf-8 -*-
-from pygame import *
-import pydub
+import argparse
 import sys
-import time
-import threading
-
-draw_lock = threading.RLock()
-
-class Action:
-    action_types = [
-        'command',
-        'pause',
-        'play',
-        'stop',
-        'stop_all_actions',
-        'volume',
-        'wait',
-    ]
-
-    def __init__(self, action, key, **kwargs):
-        if action in self.action_types:
-            self.action = action
-        else:
-            raise Exception("Unknown action {}".format(action))
-
-        self.key = key
-        self.arguments = kwargs
-
-    def ready(self):
-        if 'music' in self.arguments:
-            return self.arguments['music'].loaded
-        else:
-            return True
-
-    def run(self):
-        print(getattr(self, self.action + "_print")(**self.arguments))
-        return getattr(self, self.action)(**self.arguments)
-
-    def command(self, command = "", **kwargs):
-        pass
-
-    def pause(self, music = None, **kwargs):
-        if music is not None:
-            music.pause()
-        else:
-            mixer.pause()
-
-    def play(self, music = None, fade_in = 0, start_at = 0,
-            restart_if_running = False, volume = 100, **kwargs):
-        if music is not None:
-            music.play()
-        else:
-            mixer.unpause()
-
-    def stop(self, music = None, fade_out = 0, **kwargs):
-        if music is not None:
-            music.stop()
-        else:
-            mixer.stop()
-
-    def stop_all_actions(self, **kwargs):
-        self.key.mapping.stop_all_running()
-
-    def volume(self, music = None, value = 100, **kwargs):
-        pass
-
-    def wait(self, duration = 0, **kwargs):
-        time.sleep(duration)
-
-    def command_print(self, command = "", **kwargs):
-        return "running command {}".format(command)
-
-    def pause_print(self, music = None, **kwargs):
-        if music is not None:
-            return "pausing {}".format(music.filename)
-        else:
-            return "pausing all musics"
-
-    def play_print(self, music = None, fade_in = 0, start_at = 0,
-            restart_if_running = False, volume = 100, **kwargs):
-        message = "starting "
-        if music is not None:
-            message += music.filename
-        else:
-            message += "music"
-
-        if start_at != 0:
-            message += " at {}s".format(start_at)
-
-        if fade_in != 0:
-            message += " with {}s fade_in".format(fade_in)
-
-        message += " at volume {}%".format(volume)
-
-        if restart_if_running:
-            message += " (restarting if already running)"
-
-        return message
-
-    def stop_print(self, music = None, fade_out = 0, **kwargs):
-        if music is not None:
-            if fade_out == 0:
-                return "stopping music {}".format(music.filename)
-            else:
-                return "stopping music {} with {}s fadeout".format(music.filename, fade_out)
-        else:
-            if fade_out == 0:
-                return "stopping all musics"
-            else:
-                return "stopping all musics with {}s fadeout".format(fade_out)
-
-    def stop_all_actions_print(self):
-        return "stopping all actions"
-
-    def volume_print(self, music = None, value = 100, *kwargs):
-        if music is not None:
-            return "setting volume of {} to {}%".format(music.filename, value)
-        else:
-            return "setting volume to {}%".format(value)
-
-    def wait_print(self, duration, **kwargs):
-        return "waiting {}s".format(duration)
-
-class Key:
-    row_positions = {
-        'first':    0,
-        'second':  50,
-        'third':  100,
-        'fourth': 150,
-        'fifth':  200,
-        'sixth':  250,
-    }
-
-    default_outer_color = (120, 120, 120)
-    lighter_outer_color = (200, 200, 200)
-    default_inner_color = (255, 255, 255)
-    mapped_inner_color  = (  0, 255,   0)
-    mapped_unready_inner_color = (255, 165, 0)
-
-    def __init__(self, mapping, key_name, key_sym, top, left, width = 48, height = 48, disabled = False):
-        self.mapping = mapping
-        self.key_name = key_name
-        self.key_sym  = key_sym
-
-        if isinstance(top, str):
-            self.top = self.row_positions[top]
-        else:
-            self.top = top
-
-        self.left   = left
-        self.width  = width
-        self.height = height
-
-        self.bottom = self.top  + self.height
-        self.right  = self.left + self.width
-
-        self.rect     = (self.left, self.top, self.right, self.bottom)
-        self.position = (self.left, self.top)
-
-        if disabled:
-            self.outer_color = self.lighter_outer_color
-            self.linewidth = 1
-        else:
-            self.outer_color = self.default_outer_color
-            self.linewidth = 3
-
-        self.inner_color = self.default_inner_color
-        self.actions = []
-
-    def square(self, all_actions_ready):
-        if self.has_actions():
-            if all_actions_ready:
-                self.inner_color = self.mapped_inner_color
-            else:
-                self.inner_color = self.mapped_unready_inner_color
-
-        return RoundedRect((0, 0, self.width, self.height),
-            self.outer_color, self.inner_color, self.linewidth)
-
-    def collidepoint(self, position):
-        return self.surface.get_rect().collidepoint(
-                position[0] - self.position[0],
-                position[1] - self.position[1]
-                )
-
-    def draw(self, background_surface):
-        draw_lock.acquire()
-        all_actions_ready = self.all_actions_ready()
-
-        self.surface = self.square(all_actions_ready).surface()
-
-        if getattr(sys, 'frozen', False):
-            police = font.Font(sys._MEIPASS + "/Ubuntu-Regular.ttf", 14)
-        else:
-            police = font.Font("Ubuntu-Regular.ttf", 14)
-
-        text = police.render(self.key_sym, True, (0,0,0))
-        self.surface.blit(text, (5,5))
-        background_surface.blit(self.surface, self.position)
-        draw_lock.release()
-
-        return not all_actions_ready
-
-    def poll_redraw(self, background):
-        while True:
-            time.sleep(1)
-            if self.all_actions_ready():
-                self.draw(background)
-                self.mapping.blit()
-                break
-
-    def has_actions(self):
-        return len(self.actions) > 0
-
-    def all_actions_ready(self):
-        return all(action.ready() for action in self.actions)
-
-    def add_action(self, action_name, **arguments):
-        self.actions.append(Action(action_name, self, **arguments))
-
-    def do_actions(self):
-        print("running actions for {}".format(self.key_sym))
-        start_time = time.time()
-        self.mapping.start_running(self, start_time)
-        for action in self.actions:
-            if self.mapping.keep_running(self, start_time):
-                action.run()
-
-        self.mapping.finished_running(self, start_time)
-
-    def list_actions(self, surface):
-        # FIXME: Todo
-        print("bouh", self.key_sym)
-        surface.fill((255, 0, 0))
-
-
-class Mapping:
-    WIDTH  = 903
-    HEIGHT = 298
-    SIZE   = WIDTH, HEIGHT
-
-    KEYS = [
-        (K_ESCAPE, 'ESC', 'first',   0, {}),
-
-        (K_F1,     'F1',  'first', 100, {}),
-        (K_F2,     'F2',  'first', 150, {}),
-        (K_F3,     'F3',  'first', 200, {}),
-        (K_F4,     'F4',  'first', 250, {}),
-
-        (K_F5,     'F5',  'first', 325, {}),
-        (K_F6,     'F6',  'first', 375, {}),
-        (K_F7,     'F7',  'first', 425, {}),
-        (K_F8,     'F8',  'first', 475, {}),
-
-        (K_F9,     'F9',  'first', 550, {}),
-        (K_F10,    'F10', 'first', 600, {}),
-        (K_F11,    'F11', 'first', 650, {}),
-        (K_F12,    'F12', 'first', 700, {}),
-
-
-        (178,          '²', 'second',   0, {}),
-        (K_AMPERSAND,  '&', 'second',  50, {}),
-        (233,          'é', 'second', 100, {}),
-        (K_QUOTEDBL,   '"', 'second', 150, {}),
-        (K_QUOTE,      "'", 'second', 200, {}),
-        (K_LEFTPAREN,  '(', 'second', 250, {}),
-        (K_MINUS,      '-', 'second', 300, {}),
-        (232,          'è', 'second', 350, {}),
-        (K_UNDERSCORE, '_', 'second', 400, {}),
-        (231,          'ç', 'second', 450, {}),
-        (224,          'à', 'second', 500, {}),
-        (K_RIGHTPAREN, ')', 'second', 550, {}),
-        (K_EQUALS,     '=', 'second', 600, {}),
-
-        (K_BACKSPACE,  '<-', 'second', 650, { 'width': 98 }),
-
-
-        (K_TAB,        'tab', 'third',   0, { 'width' : 73 }),
-        (K_a,          'a',   'third',  75, {}),
-        (K_z,          'z',   'third', 125, {}),
-        (K_e,          'e',   'third', 175, {}),
-        (K_r,          'r',   'third', 225, {}),
-        (K_t,          't',   'third', 275, {}),
-        (K_y,          'y',   'third', 325, {}),
-        (K_u,          'u',   'third', 375, {}),
-        (K_i,          'i',   'third', 425, {}),
-        (K_o,          'o',   'third', 475, {}),
-        (K_p,          'p',   'third', 525, {}),
-        (K_CARET,      '^',   'third', 575, {}),
-        (K_DOLLAR,     '$',   'third', 625, {}),
-
-        (K_RETURN, 'Enter', 'third', 692, { 'width': 56, 'height': 98 }),
-
-        (K_CAPSLOCK,    'CAPS', 'fourth',  0, { 'width': 88, 'disabled': True }),
-
-        (K_q,        'q', 'fourth',  90, {}),
-        (K_s,        's', 'fourth', 140, {}),
-        (K_d,        'd', 'fourth', 190, {}),
-        (K_f,        'f', 'fourth', 240, {}),
-        (K_g,        'g', 'fourth', 290, {}),
-        (K_h,        'h', 'fourth', 340, {}),
-        (K_j,        'j', 'fourth', 390, {}),
-        (K_k,        'k', 'fourth', 440, {}),
-        (K_l,        'l', 'fourth', 490, {}),
-        (K_m,        'm', 'fourth', 540, {}),
-        (249,        'ù', 'fourth', 590, {}),
-        (K_ASTERISK, '*', 'fourth', 640, {}),
-
-
-        (K_LSHIFT, 'LShift', 'fifth', 0, { 'width': 63, 'disabled': True }),
-
-        (K_LESS,      '<', 'fifth',  65, {}),
-        (K_w,         'w', 'fifth', 115, {}),
-        (K_x,         'x', 'fifth', 165, {}),
-        (K_c,         'c', 'fifth', 215, {}),
-        (K_v,         'v', 'fifth', 265, {}),
-        (K_b,         'b', 'fifth', 315, {}),
-        (K_n,         'n', 'fifth', 365, {}),
-        (K_COMMA,     ',', 'fifth', 415, {}),
-        (K_SEMICOLON, ';', 'fifth', 465, {}),
-        (K_COLON,     ':', 'fifth', 515, {}),
-        (K_EXCLAIM,   '!', 'fifth', 565, {}),
-
-        (K_RSHIFT, 'RShift', 'fifth', 615, { 'width': 133, 'disabled': True }),
-
-        (K_LCTRL,    'LCtrl',   'sixth',   0, { 'width': 63, 'disabled': True }),
-        (K_LSUPER,   'LSuper',  'sixth', 115, { 'disabled': True }),
-        (K_LALT,     'LAlt',    'sixth', 165, { 'disabled': True }),
-        (K_SPACE,    'Espace',  'sixth', 215, { 'width': 248 }),
-        (K_MODE,     'AltGr',   'sixth', 465, { 'disabled': True }),
-        (314,        'Compose', 'sixth', 515, { 'disabled': True }),
-        (K_RCTRL,    'RCtrl',   'sixth', 565, { 'width': 63, 'disabled': True }),
-
-
-        (K_INSERT,   'ins',  'second', 755, {}),
-        (K_HOME,     'home', 'second', 805, {}),
-        (K_PAGEUP,   'pg_u', 'second', 855, {}),
-        (K_DELETE,   'del',  'third',  755, {}),
-        (K_END,      'end',  'third',  805, {}),
-        (K_PAGEDOWN, 'pg_d', 'third',  855, {}),
-
-
-        (K_UP,       'up',    'fifth',  805, {}),
-        (K_DOWN,     'down',  'sixth',  805, {}),
-        (K_LEFT,     'left',  'sixth',  755, {}),
-        (K_RIGHT,    'right', 'sixth',  855, {}),
-    ]
-
-    def __init__(self, screen):
-        self.screen = screen
-        self.background = Surface(self.SIZE).convert()
-        self.background.fill((250, 250, 250))
-        self.keys = {}
-        self.running = []
-        for key in self.KEYS:
-            self.keys[key[0]] = Key(self, *key[0:4], **key[4])
-
-    def draw(self):
-        for key_name in self.keys:
-            key = self.keys[key_name]
-            should_redraw_key = key.draw(self.background)
-
-            if should_redraw_key:
-                threading.Thread(target = key.poll_redraw, args = [self.background]).start()
-        self.blit()
-
-    def blit(self):
-        draw_lock.acquire()
-        self.screen.blit(self.background, (5, 5))
-        display.flip()
-        draw_lock.release()
-
-    def find_by_key_num(self, key_num):
-        if key_num in self.keys:
-            return self.keys[key_num]
-        return None
-
-    def find_by_collidepoint(self, position):
-        for key in self.keys:
-            if self.keys[key].collidepoint(position):
-                return self.keys[key]
-        return None
-
-    def find_by_unicode(self, key_sym):
-        for key in self.keys:
-            if self.keys[key].key_sym == key_sym:
-                return self.keys[key]
-        return None
-
-    def stop_all_running(self):
-        self.running = []
-
-    def start_running(self, key, start_time):
-        self.running.append((key, start_time))
-
-    def keep_running(self, key, start_time):
-        return (key, start_time) in self.running
-
-    def finished_running(self, key, start_time):
-        if (key, start_time) in self.running:
-            self.running.remove((key, start_time))
-
-
-class MusicFile:
-    def __init__(self, filename, lock):
-        self.filename = filename
-        self.channel = None
-        self.raw_data = None
-        self.sound = None
-
-        self.loaded = False
-        threading.Thread(target = self.load_sound, args = [lock]).start()
-
-    def load_sound(self, lock):
-        lock.acquire()
-        print("Loading {}".format(self.filename))
-        self.raw_data = pydub.AudioSegment.from_file(self.filename).raw_data
-        self.sound = mixer.Sound(self.raw_data)
-        print("Loaded {}".format(self.filename))
-        self.loaded = True
-        lock.release()
-
-    def play(self):
-        self.channel = self.sound.play()
-
-    def pause(self):
-        if self.channel is not None:
-            self.channel.pause()
-
-    def stop(self):
-        self.channel = None
-        self.sound.stop()
-
-
-class RoundedRect:
-    def __init__(self, rect, outer_color, inner_color, linewidth = 2, radius = 0.4):
-        self.rect        = Rect(rect)
-        self.outer_color = Color(*outer_color)
-        self.inner_color = Color(*inner_color)
-        self.linewidth   = linewidth
-        self.radius      = radius
-
-    def surface(self):
-        rectangle       = self.filledRoundedRect(self.rect, self.outer_color, self.radius)
-
-        inner_rect      = Rect((
-                self.rect.left   + 2 * self.linewidth,
-                self.rect.top    + 2 * self.linewidth,
-                self.rect.right  - 2 * self.linewidth,
-                self.rect.bottom - 2 * self.linewidth
-                ))
-
-        inner_rectangle = self.filledRoundedRect(inner_rect, self.inner_color, self.radius)
-
-        rectangle.blit(inner_rectangle, (self.linewidth, self.linewidth))
-
-        return rectangle
-
-    def filledRoundedRect(self, rect, color, radius=0.4):
-        """
-        filledRoundedRect(rect,color,radius=0.4)
-
-        rect    : rectangle
-        color   : rgb or rgba
-        radius  : 0 <= radius <= 1
-        """
-
-        alpha        = color.a
-        color.a      = 0
-        pos          = rect.topleft
-        rect.topleft = 0,0
-        rectangle    = Surface(rect.size,SRCALPHA)
-
-        circle       = Surface([min(rect.size)*3]*2,SRCALPHA)
-        draw.ellipse(circle,(0,0,0),circle.get_rect(),0)
-        circle       = transform.smoothscale(circle,[int(min(rect.size)*radius)]*2)
-
-        radius              = rectangle.blit(circle,(0,0))
-        radius.bottomright  = rect.bottomright
-        rectangle.blit(circle,radius)
-        radius.topright     = rect.topright
-        rectangle.blit(circle,radius)
-        radius.bottomleft   = rect.bottomleft
-        rectangle.blit(circle,radius)
-
-        rectangle.fill((0,0,0),rect.inflate(-radius.w,0))
-        rectangle.fill((0,0,0),rect.inflate(0,-radius.h))
-
-        rectangle.fill(color,special_flags=BLEND_RGBA_MAX)
-        rectangle.fill((255,255,255,alpha),special_flags=BLEND_RGBA_MIN)
-
-        return rectangle
-
-
-def parse_config(mapping):
-    import yaml
-    stream = open("config.yml", "r")
-    config = yaml.load(stream)
-    stream.close()
-
-    aliases = config['aliases']
-    seen_files = {}
-
-    file_lock = threading.RLock()
-
-    for mapped_key in config['keys']:
-        key = mapping.find_by_unicode(mapped_key)
-        if key is None:
-            continue
-
-        for action in config['keys'][mapped_key]:
-            action_name = list(action)[0]
-            action_args = {}
-            if action[action_name] is None:
-                action[action_name] = []
-
-            if 'include' in action[action_name]:
-                included = action[action_name]['include']
-                del(action[action_name]['include'])
-
-                if isinstance(included, str):
-                    action[action_name].update(aliases[included], **action[action_name])
-                else:
-                    for included_ in included:
-                        action[action_name].update(aliases[included_], **action[action_name])
-
-            for argument in action[action_name]:
-                if argument == 'file':
-                    filename = action[action_name]['file']
-                    if filename not in seen_files:
-                        seen_files[filename] = MusicFile(filename, file_lock)
-
-                    action_args['music'] = seen_files[filename]
-
-                else:
-                    action_args[argument] = action[action_name][argument]
-
-            key.add_action(action_name, **action_args)
+import os
+import math
+import sounddevice as sd
+import logging
+
+class Config:
+    pass
+
+def path():
+    if getattr(sys, 'frozen', False):
+        return sys._MEIPASS + "/"
+    else:
+        path = os.path.dirname(os.path.realpath(__file__))
+        return path + "/../"
+
+def parse_args():
+    argv = sys.argv[1 :]
+    sys.argv = sys.argv[: 1]
+    if "--" in argv:
+        index = argv.index("--")
+        kivy_args = argv[index+1 :]
+        argv = argv[: index]
+
+        sys.argv.extend(kivy_args)
+
+    parser = argparse.ArgumentParser(
+            description="A Music Sampler application.",
+            formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+    parser.add_argument("-c", "--config",
+            default="config.yml",
+            required=False,
+            help="Config file to load")
+    parser.add_argument("-d", "--debug",
+            nargs=0,
+            action=DebugModeAction,
+            help="Print messages in console")
+    parser.add_argument("-m", "--builtin-mixing",
+            action="store_true",
+            help="Make the mixing of sounds manually\
+                    (do it if the system cannot handle it correctly)")
+    parser.add_argument("-l", "--latency",
+            default="high",
+            required=False,
+            help="Latency: low, high or number of seconds")
+    parser.add_argument("-b", "--blocksize",
+            default=0,
+            type=int,
+            required=False,
+            help="Blocksize: If not 0, the number of frames to take\
+                    at each step for the mixer")
+    parser.add_argument("-f", "--frame-rate",
+            default=44100,
+            type=int,
+            required=False,
+            help="Frame rate to play the musics")
+    parser.add_argument("-x", "--channels",
+            default=2,
+            type=int,
+            required=False,
+            help="Number of channels to use")
+    parser.add_argument("-s", "--sample-width",
+            default=2,
+            type=int,
+            required=False,
+            help="Sample width (number of bytes for each frame)")
+    parser.add_argument("-V", "--version",
+            action="version",
+            help="Displays the current version and exits. Only use\
+                    in bundled package",
+            version=show_version())
+    parser.add_argument("--device",
+            action=SelectDeviceAction,
+            help="Select this sound device"
+            )
+    parser.add_argument("--list-devices",
+            nargs=0,
+            action=ListDevicesAction,
+            help="List available sound devices"
+            )
+    parser.add_argument('--',
+            dest="args",
+            help="Kivy arguments. All arguments after this are interpreted\
+                    by Kivy. Pass \"-- --help\" to get Kivy's usage.")
+
+    from kivy.logger import Logger
+    Logger.setLevel(logging.ERROR)
+
+    args = parser.parse_args(argv)
+
+    Config.yml_file = args.config
+
+    Config.latency = args.latency
+    Config.blocksize = args.blocksize
+    Config.frame_rate = args.frame_rate
+    Config.channels = args.channels
+    Config.sample_width = args.sample_width
+    Config.builtin_mixing = args.builtin_mixing
+
+class DebugModeAction(argparse.Action):
+    def __call__(self, parser, namespace, values, option_string=None):
+        from kivy.logger import Logger
+        Logger.setLevel(logging.DEBUG)
+
+class SelectDeviceAction(argparse.Action):
+    def __call__(self, parser, namespace, values, option_string=None):
+        sd.default.device = values
+
+class ListDevicesAction(argparse.Action):
+    nargs = 0
+    def __call__(self, parser, namespace, values, option_string=None):
+        print(sd.query_devices())
+        sys.exit()
+
+def show_version():
+    if getattr(sys, 'frozen', False):
+        with open(path() + ".pyinstaller_commit", "r") as f:
+            return f.read()
+    else:
+        return "option '-v' can only be used in bundled package"
+
+def duration_to_min_sec(duration):
+    minutes = int(duration / 60)
+    seconds = int(duration) % 60
+    if minutes < 100:
+        return "{:2}:{:0>2}".format(minutes, seconds)
+    else:
+        return "{}:{:0>2}".format(minutes, seconds)
+
+def gain(volume, old_volume=None):
+    if old_volume is None:
+        return 20 * math.log10(volume / 100)
+    else:
+        return [
+                20 * math.log10(max(volume, 0.1) / max(old_volume, 0.1)),
+                max(volume, 0)]
+
+def debug_print(message):
+    from kivy.logger import Logger
+    Logger.debug('MusicSampler: ' + message)
+
+def error_print(message):
+    from kivy.logger import Logger
+    Logger.error('MusicSampler: ' + message)