Exemplo n.º 1
0
class ExerciseMixin:
    """
    All the exercise stuff.

    This is a mixin class. If you need a method from here, call it
    specifically. To avoid naming collisions, start all properties
    with __. All properties that do not start with ex_, are assumed to be
    provided the class that this is mixed in.
    """
    def __init__(self):
        self.__data_path = self.data_path.joinpath('exercises')
        self.__data_path.mkdir(exist_ok=True)
        self.__tabs = {}

        ExerciseMixin.create_widgets(self)

    def create_widgets(self):
        """Put some stuff up to look at."""
        self.__menu = tk.Menu(self.menubar)
        self.menubar.add_cascade(label='Exercises', menu=self.__menu)
        self.__menu.add_command(label='Add',
                                command=lambda: asyncio.ensure_future(
                                    ExerciseMixin.add_exercise(self)))
        self.__menu.add_command(label='Delete',
                                command=lambda: asyncio.ensure_future(
                                    ExerciseMixin.remove_exercise(self)))

        self.__frame = Frame(self.notebook)
        self.notebook.append({'name': 'Exercises', 'widget': self.__frame})
        self.__frame.rowconfigure(0, weight=1)
        self.__frame.columnconfigure(0, weight=1)

        self.__notebook = Notebook(self.__frame)
        self.__notebook.grid(column=0, row=0, sticky=tk.N + tk.S + tk.E + tk.W)
        self.__notebook.rowconfigure(0, weight=1)
        self.__notebook.columnconfigure(0, weight=1)
        for exercise in self.__data_path.glob('*.ly'):
            self.__tabs[exercise.stem] = ExerciseTab(
                self.__notebook,
                Exercise(data_path=self.__data_path,
                         include_path=self.include_path,
                         name=exercise.stem))

    def save_state(self):
        """Return exercise state."""
        data = {}
        for key in self.__tabs:
            data[key] = self.__tabs[key].save_state()
        return data

    def restore_state(self, data):
        """Restore saved settings."""
        for key in self.__tabs:
            if key in data:
                self.__tabs[key].restore_state(data[key])

    def export(self):
        """Export compiled data."""
        return self.__tabs[self.__notebook.get()[1]].export()

    async def add_exercise(self):
        """Add new exercise."""
        dialog = LoadFileDialog(self.root,
                                dir_or_file=Path('~'),
                                pattern="*.ly")
        file_name = await dialog.await_data()
        if file_name is not None:
            path = Path(file_name)
            ex_name = path.stem
            if not path.is_file():
                ErrorDialog(self.root, data="{} is not a file".format(path))
                return
            content = path.read_text()
            new_file = self.__data_path.joinpath("{}.ly".format(ex_name))
            if new_file.is_file():
                ErrorDialog(
                    self.root,
                    data="An exercise with name {} already exists.".format(
                        ex_name))
                return
            new_file.touch()
            new_file.write_text(content)
            self.__tabs[ex_name] = Exercise(data_path=self.__data_path,
                                            include_path=self.include_path,
                                            name=ex_name)
            self.__notebook.sort()

    async def remove_song(self):
        """Remove song."""
        tab_index, tab_name = self.__notebook.get()
        confirm = OkCancelDialog(
            self.root,
            data="Are you sure you want to delete {}? This cannot be undone.".
            format(tab_name))
        if not await confirm.await_data():
            return
        self.stopping = True
        await self.stop()
        del self.__notebook[tab_index]
        del self.__tabs[tab_name]
        file_name = Path(self.__data_path).joinpath("{}.ly".format(tab_name))
        file_name.unlink()

    async def clear_cache(self):
        """Remove all compiled files."""
        for key in self.__tabs:
            await self.__tabs[key].clear_cache()
Exemplo n.º 2
0
class ExerciseTab:
    """Tab containing everything for one exersise."""
    def __init__(self, notebook: Notebook, exercise: Exercise):
        # some control variables
        self.stopping = False
        self.play_next = False
        self.repeat_once = False

        self.name = exercise.name
        self.config = config = exercise.config
        self._data_path = exercise.data_path
        self._include_path = exercise.include_path
        self.tab = Frame(notebook)
        self.tab.rowconfigure(1, weight=1)
        self.tab.columnconfigure(3, weight=1)
        notebook.append({'name': self.name, 'widget': self.tab})

        # bpm selector
        self.bpm_label = Label(self.tab, text="bpm:")
        self.bpm_label.grid(column=2, row=0, sticky=tk.N + tk.W)
        self.bpm = Scale(self.tab,
                         from_=80,
                         to=160,
                         tickinterval=10,
                         showvalue=0,
                         length=300,
                         resolution=10,
                         default=140,
                         orient=tk.HORIZONTAL)
        self.bpm.grid(column=3, row=0, sticky=tk.W + tk.N)
        if 'tempo' not in config:
            self.bpm.disable()

        # pitch selector
        self.pitch_range = Listbox(self.tab, width=3, values=PITCH_LIST)
        self.pitch_range.grid(row=1, column=1, sticky=tk.N + tk.S + tk.W)
        self.pitch_range.set({pitch: True for pitch in PITCH_LIST[7:14]})

        # controls
        self.control_frame = Frame(self.tab)
        self.control_frame.grid(column=2,
                                row=2,
                                columnspan=2,
                                sticky=tk.W + tk.N)
        self._create_controls(self.control_frame, config)

        # sheet display
        self._image_cache = {}
        self.sheet = tk.Canvas(self.tab.raw, bd=0, highlightthickness=0)
        self.sheet.bind("<Configure>",
                        lambda e: asyncio.ensure_future(self._resize_sheet(e)))
        self.sheet.grid(column=2, row=1, columnspan=2, sticky=tk.NSEW)

        self.sheet.bind("<Button-1>", lambda e: self._set_repeat_once())
        asyncio.ensure_future(self._update_sheet())

    def _create_controls(self, parent, config):
        self.velocity_label = Label(parent, text="relative velocity:")
        self.velocity_label.grid(column=0, row=0, sticky=tk.N + tk.E)
        self.velocity = Spinbox(parent,
                                width=3,
                                from_=-50,
                                to=50,
                                default=0,
                                increment=1)
        self.velocity.grid(column=1, row=0, sticky=tk.W + tk.N)

        self.sound = OptionMenu(
            parent,
            default='Mi',
            option_list=SOUND_LIST,
            command=lambda _: asyncio.ensure_future(self._update_sheet()))
        self.sound.grid(column=2, row=0, sticky=tk.W + tk.N)
        if 'sound' not in config:
            self.sound.disable()

        self.key = OptionMenu(
            parent,
            option_list=PITCH_LIST,
            default='c',
            command=lambda _: asyncio.ensure_future(self._on_pitch_change()))
        self.key.grid(column=3, row=0, sticky=tk.W + tk.N)
        if 'key' in config:
            self.key.set(config['key'])
        else:
            self.key.disable()

        self.random = Checkbutton(parent, text="random")
        self.random.grid(column=4, row=0, sticky=tk.W + tk.N)
        self.autonext = Checkbutton(parent, text="autonext")
        self.autonext.grid(column=5, row=0, sticky=tk.W + tk.N)

        self.b_repeat = Button(parent,
                               text="Repeat once",
                               command=self._set_repeat_once)
        self.b_repeat.grid(column=6, row=0, sticky=tk.W + tk.N)

        self.b_next_ = Button(
            parent,
            text="Next",
            command=lambda: asyncio.ensure_future(self._next()))
        self.b_next_.grid(column=7, row=0, sticky=tk.W + tk.N)

        self.b_play = Button(
            parent,
            text="Play",
            command=lambda: asyncio.ensure_future(self.play()))
        self.b_play.grid(column=8, row=0, sticky=tk.W + tk.N)

    def save_state(self):
        """Return exercise state."""
        data = {}
        data['pitch_range'] = self.pitch_range.get()
        data['bpm'] = self.bpm.get()
        data['velocity'] = self.velocity.get()
        data['sound'] = self.sound.get()
        data['autonext'] = self.autonext.get()
        data['random'] = self.random.get()
        return data

    def restore_state(self, data):
        """Restore saved settings."""
        if 'pitch_range' in data:
            self.pitch_range.set(data['pitch_range'])
        if 'bpm' in data:
            self.bpm.set(data['bpm'])
        if 'velocity' in data:
            self.velocity.set(data['velocity'])
        if 'sound' in data:
            self.sound.set(data['sound'])
        if 'autonext' in data:
            self.autonext.set(data['autonext'])
        if 'random' in data:
            self.random.set(data['random'])

    def _get_interface(self):
        """Return exercise interface."""
        return Exercise(self._data_path,
                        self._include_path,
                        name=self.name,
                        pitch=self.key.get(),
                        bpm=self.bpm.get(),
                        sound=self.sound.get(),
                        velocity=self.velocity.get())

    async def clear_cache(self):
        """Remove all compiled files."""
        # remove files
        for file_ in chain(self._data_path.glob("{}-*.midi".format(self.name)),
                           self._data_path.glob("{}-*.png".format(self.name)),
                           self._data_path.glob("{}-*.pdf".format(self.name))):
            file_.unlink()
        self._image_cache = {}
        await self._update_sheet()

    async def _update_sheet(self):
        """Display relevant sheet."""
        # pylint: disable=invalid-name
        # type declaration
        Size = namedtuple('Size', ['width', 'height'])
        size = Size(self.sheet.winfo_width(), self.sheet.winfo_height())
        await self._resize_sheet(size)

    async def _resize_sheet(self, event):
        """Resize sheets to screen size."""
        self.sheet.delete("left")
        left = self._get_interface()
        # make sure pages are compiled
        await get_file(left)
        left_path = await get_single_sheet(self._image_cache, left,
                                           event.width, event.height)
        self.sheet.create_image(0,
                                0,
                                image=self._image_cache[left_path]['image'],
                                anchor=tk.NW,
                                tags="left")

    async def _on_pitch_change(self):
        """New pitch was picked by user or app."""
        asyncio.ensure_future(self._update_sheet())
        if is_playing():
            self.play_next = True
            await stop()
        else:
            await self.play()

    def _set_repeat_once(self, _=None):
        """Repeat this midi once, then continue."""
        self.repeat_once = True

    async def _next_sound(self):
        if 'sound' in self.config:
            current_sound = self.sound.get()
            sound_position = SOUND_LIST.index(current_sound)
            if sound_position < len(SOUND_LIST) - 1:
                self.sound.set(SOUND_LIST[sound_position + 1])
            else:
                self.sound.set(SOUND_LIST[0])
            asyncio.ensure_future(self._update_sheet())

    async def _next(self):
        """Pick next exercise configuration."""
        if 'key' not in self.config:
            await self._next_sound()
            return
        current_pitch = new_pitch = self.key.get()
        pitch_position = PITCH_LIST.index(current_pitch)
        pitch_range = self.pitch_range.get()
        pitch_selection = [
            pitch for pitch in pitch_range if pitch_range[pitch]
        ]
        if not pitch_selection:
            return
        if self.random.get():
            while new_pitch == current_pitch:
                new_pitch = choice(pitch_selection)
        else:
            while pitch_selection:
                pitch_position += 1
                if pitch_position >= len(PITCH_LIST):
                    pitch_position = 0
                    await self._next_sound()
                if PITCH_LIST[pitch_position] in pitch_selection:
                    new_pitch = PITCH_LIST[pitch_position]
                    break
        self.key.set(new_pitch)
        await self._on_pitch_change()

    async def play(self):
        """Play midi file."""
        midi = await get_file(self._get_interface(), FileType.midi)
        if is_playing():
            self.stopping = True
        playing = await play_or_stop(midi, self._on_midi_stop)
        if playing:
            self.b_play.set_text("Stop")
        else:
            self.b_play.set_text("Play")

    async def _on_midi_stop(self):
        """Handle end of midi playback."""
        if self.stopping:
            self.play_next = False
            self.stopping = False
            self.b_play.set_text("Play")
            return
        if self.play_next or self.autonext.get() == 1:
            self.play_next = False
            if not self.repeat_once:
                await self._next()
            else:
                self.repeat_once = False
                await self.play()
            return
        self.b_play.set_text("Play")

    def export(self):
        """Export compiled data."""
        return self._get_interface()
Exemplo n.º 3
0
class SongMixin:
    """
    All the song stuff.

    This is a mixin class. If you need a method from here, call it
    specifically. To avoid naming collisions, start all properties
    with __. All properties that do not start with so_, are assumed to be
    provided the class that this is mixed in.
    """
    def __init__(self):
        self.__data_path = self.data_path.joinpath('songs')
        self.__data_path.mkdir(exist_ok=True)
        self.__tabs = {}
        self.__scroll_time = datetime.now()
        self.__jpmidi_port = 'system:'

        SongMixin.create_widgets(self)

    def create_widgets(self):
        """Put some stuff up to look at."""
        self.__menu = tk.Menu(self.menubar)
        self.menubar.add_cascade(label='Songs', menu=self.__menu)
        self.__menu.add_command(label='Midi', state=tk.DISABLED)
        self.__menu.add_command(label='Select jpmidi port',
                                command=lambda: asyncio.ensure_future(
                                    self.select_port(pmidi=False)))
        self.__menu.add_separator()
        self.__menu.add_command(label='Options', state=tk.DISABLED)
        self.__midi_executable = tk.StringVar()
        self.__midi_executable.set('pmidi')
        self.__menu.add_radiobutton(label='pmidi',
                                    value='pmidi',
                                    variable=self.__midi_executable)
        self.__menu.add_radiobutton(label='jpmidi',
                                    value='jpmidi',
                                    variable=self.__midi_executable)
        self.__await_jack = tk.IntVar()
        self.__await_jack.set(False)
        self.__menu.add_checkbutton(label='-- await jack_transport',
                                    variable=self.__await_jack)
        self.__menu.add_separator()
        self.__menu.add_command(
            label='Add',
            command=lambda: asyncio.ensure_future(SongMixin.add_song(self)))
        self.__menu.add_command(
            label='Delete',
            command=lambda: asyncio.ensure_future(SongMixin.remove_song(self)))

        self.__frame = Frame(self.notebook)
        self.notebook.append({'name': 'Songs', 'widget': self.__frame})
        self.__frame.rowconfigure(0, weight=1)
        self.__frame.columnconfigure(0, weight=1)

        self.__notebook = Notebook(self.__frame)
        self.__notebook.grid(column=0, row=0, sticky=tk.N + tk.S + tk.E + tk.W)
        self.__notebook.rowconfigure(0, weight=1)
        self.__notebook.columnconfigure(0, weight=1)
        for song in self.__data_path.glob('*.ly'):
            self.__tabs[song.stem] = SongTab(
                self.__notebook,
                Song(data_path=self.__data_path,
                     include_path=self.include_path,
                     name=song.stem))

    def save_state(self):
        """Return exercise state."""
        # FIXME: imagine a user adding a song named await_jeck
        data = {}
        data['midi_executable'] = self.__midi_executable.get()
        data['await_jack'] = self.__await_jack.get()
        data['jpmidi_port'] = self.__jpmidi_port
        for key in self.__tabs:
            data[key] = self.__tabs[key].save_state()
        return data

    def restore_state(self, data):
        """Restore saved settings."""
        if 'midi_executable' in data:
            self.__midi_executable.set(data['midi_executable'])
        if 'await_jack' in data:
            self.__await_jack.set(data['await_jack'])
        if 'jpmidi_port' in data:
            self.__jpmidi_port = data['jpmidi_port']
        for key in self.__tabs:
            if key in data:
                self.__tabs[key].restore_state(data[key])

    def export(self):
        """Export compiled data."""
        return self.__tabs[self.__notebook.get()[1]].export()

    async def add_song(self):
        """Add new song."""
        # FIXME: race condition
        dialog = LoadFileDialog(self.root,
                                dir_or_file=Path('~'),
                                pattern="*.ly")
        file_name = await dialog.await_data()
        if file_name is not None:
            path = Path(file_name)
            so_name = path.stem
            if not path.is_file():
                ErrorDialog(self.root, data="{} is not a file".format(path))
                return
            content = path.read_text()
            new_file = self.__data_path.joinpath("{}.ly".format(so_name))
            if new_file.is_file():
                ErrorDialog(self.root,
                            data="An song with name {} already exists.".format(
                                so_name))
                return
            new_file.touch()
            new_file.write_text(content)
            self.__tabs[so_name] = Song(data_path=self.__data_path,
                                        include_path=self.include_path,
                                        name=so_name)
            self.__notebook.sort()

    async def remove_song(self):
        """Remove song."""
        tab_index, tab_name = self.__notebook.get()
        confirm = OkCancelDialog(
            self.root,
            data="Are you sure you want to delete {}? This cannot be undone.".
            format(tab_name))
        if not await confirm.await_data():
            return
        self.stopping = True
        await self.stop()
        del self.__notebook[tab_index]
        del self.__tabs[tab_name]
        file_name = Path(self.__data_path).joinpath("{}.ly".format(tab_name))
        file_name.unlink()

    async def clear_cache(self):
        """Remove all compiled files."""
        for key in self.__tabs:
            await self.__tabs[key].clear_cache()
Exemplo n.º 4
0
class MainWindow(ExerciseMixin, SongMixin):
    """Voicetrainer application."""
    def __init__(self, root):
        self.data_path = Path().home().joinpath('.voicetrainer')
        self.data_path.mkdir(exist_ok=True)
        self.include_path = self.data_path.joinpath('include')
        self.include_path.mkdir(exist_ok=True)
        self.root = root

        # compiler state
        self.compiler_count = 0
        set_compiler_cb(self.update_compiler)

        self.messages = []
        self.messages_read = 0
        self.msg_window = None
        set_compile_err_cb(self.new_message)
        set_play_err_cb(self.new_message)

        # midi state
        self.port = None
        self.port_match = 'FLUID'
        asyncio.ensure_future(self.find_port())

        # gui elements storage
        self.create_widgets()

        ExerciseMixin.__init__(self)
        SongMixin.__init__(self)

        self.restore_state()

    async def find_port(self):
        """Find qsynth port."""
        try:
            port_finder = PortFinder(self.port_match)
            async for port in port_finder:
                if port is not None:
                    self.port = port
                    set_pmidi_port(port)
                else:
                    await asyncio.sleep(5)
                    port_finder.match = self.port_match
            self.port_label.set_text('pmidi port: {}'.format(self.port))
        except Exception as err:
            ErrorDialog(self.root,
                        data="Could not find midi port\n{}".format(str(err)))
            raise

    async def select_port(self, pmidi=True):
        """Change port matching."""
        # FIXME: stay away from private mixin properties
        # pylint: disable=access-member-before-definition,invalid-name
        # pylint: disable=attribute-defined-outside-init
        selection_dialog = PortSelection(
            self.root,
            data=await list_ports(pmidi=pmidi),
            current_port=str(self.port_match)
            if pmidi else str(self._SongMixin__jpmidi_port))
        if pmidi:
            self.port_match = await selection_dialog.await_data()
            if self.port is not None:
                # not currently searching for port, start
                self.port = None
                self.port_label.set_text('pmidi port: None')
                await self.find_port()
        else:
            self._SongMixin__jpmidi_port = await selection_dialog.await_data()

    def create_widgets(self):
        """Put some stuff up to look at."""
        self.window = Frame(self.root)
        self.window.rowconfigure(0, weight=1)
        self.window.columnconfigure(2, weight=1)
        self.window.grid(column=0, row=0, sticky=tk.N + tk.S + tk.E + tk.W)
        self.top = self.window.winfo_toplevel()
        self.top.rowconfigure(0, weight=1)
        self.top.columnconfigure(0, weight=1)
        self.top.title("VoiceTrainer")
        self.top.protocol("WM_DELETE_WINDOW",
                          lambda: asyncio.ensure_future(self.quit()))

        # self.top menu
        self.menubar = tk.Menu(self.top)
        self.top['menu'] = self.menubar

        self.file_menu = tk.Menu(self.menubar)
        self.menubar.add_cascade(label='File', menu=self.file_menu)
        self.file_menu.add_command(
            label='Select port',
            command=lambda: asyncio.ensure_future(self.select_port()))
        self.file_menu.add_separator()
        self.file_menu.add_command(
            label='Export midi',
            command=lambda: asyncio.ensure_future(self.export(FileType.midi)))
        self.file_menu.add_command(
            label='Export png',
            command=lambda: asyncio.ensure_future(self.export(FileType.png)))
        self.file_menu.add_command(
            label='Export pdf',
            command=lambda: asyncio.ensure_future(self.export(FileType.pdf)))
        self.file_menu.add_command(
            label='Export lilypond',
            command=lambda: asyncio.ensure_future(self.export(FileType.lily)))
        self.file_menu.add_separator()
        self.file_menu.add_command(label='Save state', command=self.save_state)
        self.file_menu.add_command(
            label='Clear cache',
            command=lambda: asyncio.ensure_future(self.clear_cache()))
        self.file_menu.add_command(
            label='Quit', command=lambda: asyncio.ensure_future(self.quit()))

        self.notebook = Notebook(self.window)
        self.notebook.grid(column=0,
                           row=0,
                           columnspan=3,
                           sticky=tk.N + tk.S + tk.E + tk.W)
        self.notebook.rowconfigure(0, weight=1)
        self.notebook.columnconfigure(0, weight=1)
        self.separators = []

        self.statusbar = Frame(self.window)
        self.statusbar.grid(row=1, column=0, sticky=tk.N + tk.W)

        self.compiler_label = Label(self.statusbar, text="Compiler:")
        self.compiler_label.grid(row=0, column=0, sticky=tk.N + tk.E)
        self.progress = ttk.Progressbar(self.statusbar.raw,
                                        mode='indeterminate',
                                        orient=tk.HORIZONTAL)
        self.progress.grid(row=0, column=1, sticky=tk.N + tk.W)

        sep = ttk.Separator(self.statusbar.raw, orient=tk.VERTICAL)
        sep.grid(row=0, column=2)
        self.separators.append(sep)

        self.port_label = Label(self.statusbar, text='pmidi port: None')
        self.port_label.grid(row=0, column=3, sticky=tk.N + tk.W)

        sep = ttk.Separator(self.statusbar.raw, orient=tk.VERTICAL)
        sep.grid(row=0, column=4)
        self.separators.append(sep)

        self.msg_button = Button(self.statusbar.raw,
                                 text='messages',
                                 command=self.display_messages)
        self.msg_button.grid(row=0, column=5, sticky=tk.N + tk.W)

        self.resize = ttk.Sizegrip(self.window.raw)
        self.resize.grid(row=1, column=2, sticky=tk.S + tk.E)

    async def quit(self):
        """Confirm exit."""
        if self.compiler_count > 0:
            # ask conformation before quit
            confirm_exit = OkCancelDialog(
                self.root, ("Uncompleted background task\n"
                            "An exercise is still being compiled in the "
                            "background. Do you still want to exit? The "
                            "task will be aborted."))
            if not await confirm_exit.await_data():
                return
        if is_playing():
            await stop()
        self.close()

    def close(self):
        """Exit main application."""
        self.save_state()
        self.progress.stop()
        self.top.destroy()
        self.root.close()

    def save_state(self):
        """Save settings to json file."""
        data = {}
        data['port_match'] = self.port_match
        data['exercises'] = ExerciseMixin.save_state(self)
        data['songs'] = SongMixin.save_state(self)
        self.data_path.joinpath('state.json').write_text(json.dumps(data))

    def restore_state(self, _=None):
        """Restore saved settings."""
        state_file = self.data_path.joinpath('state.json')
        if not state_file.is_file():
            return
        data = json.loads(state_file.read_text())
        if 'port_match' in data:
            self.port_match = data['port_match']
        if 'exercises' in data:
            ExerciseMixin.restore_state(self, data['exercises'])
        if 'songs' in data:
            SongMixin.restore_state(self, data['songs'])

    def show_messages(self):
        """Show if there unread messages."""
        if len(self.messages) > self.messages_read:
            self.msg_button.set_text('!!!Messages!!!')
            if self.msg_window is not None:
                self.msg_window.update_data(self.messages)

    def new_message(self, msg):
        """There's a new message to report."""
        self.messages.append(msg)
        self.show_messages()

    def display_messages(self, _=None):
        """Display all messages."""
        if self.msg_window is None:
            self.msg_window = Messages(self.root,
                                       data=self.messages,
                                       on_close=self.on_close_msg_window)
        else:
            self.msg_window.to_front()
        self.messages_read = len(self.messages)
        self.msg_button.set_text('Messages')

    def on_close_msg_window(self, _):
        """Messages window was closed."""
        self.msg_window = None

    def update_compiler(self, delta_compilers: int):
        """Set compiler progress bar status."""
        self.compiler_count += delta_compilers
        if self.compiler_count > 0:
            self.progress.start()
        else:
            self.progress.stop()

    async def clear_cache(self):
        """Remove all compiled files."""
        # confirm
        confirm_remove = OkCancelDialog(
            self.root, "This will remove all compiled files. Are you sure?")
        if not await confirm_remove.await_data():
            return

        await ExerciseMixin.clear_cache(self)
        await SongMixin.clear_cache(self)

    async def export(self, file_type: FileType):
        """Export compiled data."""
        tab_name = self.notebook.get()[1]
        if tab_name == 'Songs':
            interface = SongMixin.export(self)
        elif tab_name == 'Exercises':
            interface = ExerciseMixin.export(self)
        else:
            return

        # get save_path
        file_name = interface.get_filename(file_type)
        save_dialog = SaveFileDialog(self.root,
                                     dir_or_file=Path('~'),
                                     default=file_name.name)
        save_path = await save_dialog.await_data()
        if save_path is None:
            return

        # compile and save
        if file_type is FileType.lily:
            # we don't compile lily
            save_path.write_text(interface.get_final_lily_code(file_type))
            InfoDialog(self.root, data="Export complete")
            return
        await get_file(interface, file_type)

        save_path.write_bytes(file_name.read_bytes())
        InfoDialog(self.root, data="Export complete")
Exemplo n.º 5
0
class SongTab:
    """Tab containing everything for one song."""
    def __init__(self, notebook: Notebook, song: Song):
        self.name = song.name
        config = song.config
        self._data_path = song.data_path
        self._include_path = song.include_path
        self.tab = Frame(notebook)
        self.tab.rowconfigure(0, weight=1)
        self.tab.columnconfigure(1, weight=1)
        notebook.append({'name': self.name, 'widget': self.tab})
        self.page = 1
        self._scroll_time = datetime.now()

        # controls
        self.controls = Frame(self.tab)
        self.controls.grid(column=0, row=0, sticky=tk.W + tk.N)
        self._create_controls(self.controls, config)

        # sheet display
        self._image_cache = {}
        self.sheet = tk.Canvas(self.tab.raw, bd=0, highlightthickness=0)
        self.sheet.bind("<Configure>",
                        lambda e: asyncio.ensure_future(self._resize_sheet(e)))
        self.sheet.grid(column=1, row=0, sticky=tk.N + tk.W + tk.S + tk.E)

        # sheet mouse events
        self.sheet.bind("<Button-1>", lambda e: self._change_page())
        self.sheet.bind(
            "<Button-4>",
            lambda e: self._change_page(scroll=True, increment=False))
        self.sheet.bind("<Button-5>", lambda e: self._change_page(scroll=True))

        asyncio.ensure_future(self._update_sheet())

    def _create_controls(self, parent, config):
        row_count = 0

        self.measure_label = Label(parent, text="measure")
        self.measure_label.grid(column=0,
                                row=row_count,
                                sticky=tk.S + tk.E + tk.W)
        self.bpm_label = Label(parent, text="bpm")
        self.bpm_label.grid(column=1, row=row_count, sticky=tk.S + tk.E + tk.W)
        row_count += 1

        if 'measures' in config:
            max_measure = int(config['measures'])
            no_measures = False
        else:
            max_measure = 1
            no_measures = True
        self.measure = Scale(parent,
                             from_=1,
                             to=max_measure,
                             tickinterval=10,
                             showvalue=True,
                             length=300,
                             resolution=1,
                             orient=tk.VERTICAL,
                             label=None,
                             default=1)
        self.measure.grid(column=0, row=row_count, sticky=tk.W + tk.N + tk.E)
        if no_measures:
            self.measure.disable()
        self.bpm = Scale(parent,
                         from_=40,
                         to=240,
                         tickinterval=20,
                         showvalue=True,
                         length=300,
                         resolution=1,
                         orient=tk.VERTICAL,
                         label=None,
                         default=140)
        self.bpm.grid(column=1, row=row_count, sticky=tk.W + tk.N + tk.E)
        if 'tempo' in config:
            self.bpm.set(config['tempo'])
        else:
            self.bpm.disable()
        row_count += 1

        self.key_label = Label(parent, text="key:")
        self.key_label.grid(column=0, row=row_count, sticky=tk.N + tk.E)
        self.key = OptionMenu(
            parent,
            option_list=PITCH_LIST,
            default='c',
            command=lambda _: asyncio.ensure_future(self._on_pitch_change()))
        self.key.grid(column=1, row=row_count, sticky=tk.W + tk.N)
        if 'key' in config:
            self.key.set(config['key'])
        else:
            self.key.disable()
        row_count += 1

        self.velocity_label = Label(parent, text="rel. velocity:")
        self.velocity_label.grid(column=0, row=row_count, sticky=tk.N + tk.E)
        self.velocity = Spinbox(parent,
                                width=3,
                                from_=-50,
                                to=50,
                                increment=1,
                                default=0)
        self.velocity.grid(column=1, row=row_count, sticky=tk.W + tk.N)
        row_count += 1

        self.instruments_frame = LabelFrame(parent, text="Instruments")
        self.instruments_frame.grid(column=0,
                                    row=row_count,
                                    columnspan=2,
                                    sticky=tk.NSEW)
        self._create_instruments_frame(self.instruments_frame, config)
        row_count += 1

        self.b_recompile = Button(
            parent,
            text='Recompile',
            command=lambda: asyncio.ensure_future(self.clear_cache()))
        self.b_recompile.grid(column=0,
                              row=row_count,
                              columnspan=2,
                              sticky=tk.NSEW)
        row_count += 1

        self.b_reset = Button(parent,
                              text='Reset to song default',
                              command=self._reset_song)
        self.b_reset.grid(column=0,
                          row=row_count,
                          columnspan=2,
                          sticky=tk.NSEW)
        row_count += 1

        if 'pages' in config:
            num_pages = int(config['pages'])
            no_pages = False
        else:
            num_pages = 1
            no_pages = True

        self.b_first_page = Button(parent,
                                   text='First page',
                                   command=lambda: self._change_page(page=1))
        self.b_first_page.grid(column=0,
                               row=row_count,
                               columnspan=2,
                               sticky=tk.NSEW)
        row_count += 1

        self.b_next_page = Button(parent,
                                  text='Next page',
                                  command=self._change_page)
        self.b_next_page.grid(column=0,
                              row=row_count,
                              columnspan=2,
                              sticky=tk.NSEW)
        row_count += 1

        self.b_prev_page = Button(
            parent,
            text='Previous page',
            command=lambda: self._change_page(increment=False))
        self.b_prev_page.grid(column=0,
                              row=row_count,
                              columnspan=2,
                              sticky=tk.NSEW)
        row_count += 1

        self.b_last_page = Button(
            parent,
            text='Last page',
            command=lambda last=num_pages: self._change_page(page=last))
        self.b_last_page.grid(column=0,
                              row=row_count,
                              columnspan=2,
                              sticky=tk.NSEW)
        row_count += 1

        if no_pages:
            self.b_first_page.disable()
            self.b_next_page.disable()
            self.b_prev_page.disable()
            self.b_last_page.disable()

        self.b_play = Button(
            parent,
            text='Play',
            command=lambda: asyncio.ensure_future(self.play()))
        self.b_play.grid(column=0, row=row_count, columnspan=2, sticky=tk.NSEW)
        row_count += 1

    def _create_instruments_frame(self, parent, config):
        self.instruments = {}
        self.instrument_labels = {}
        row_count = 0

        instrument_title = Label(parent=parent, text="Instrument")
        instrument_title.grid(column=0, row=row_count, sticky=tk.W)
        self.instrument_labels['instrument'] = instrument_title

        sheet_title = Label(parent=parent, text="Sheet")
        sheet_title.grid(column=1, row=row_count, sticky=tk.W)
        self.instrument_labels['sheet'] = sheet_title

        midi_title = Label(parent=parent, text="Midi")
        midi_title.grid(column=2, row=row_count, sticky=tk.W)
        self.instrument_labels['midi'] = midi_title

        velocity_title = Label(parent=parent, text="Velocity")
        velocity_title.grid(column=3, row=row_count, sticky=tk.W)
        self.instrument_labels['velocity'] = velocity_title

        row_count += 1

        for instrument in config['instruments']:
            name = Label(parent=parent, text=instrument)
            name.grid(column=0, row=row_count, sticky=tk.W)
            sheet = Checkbutton(
                parent,
                text="",
                default=True,
                command=lambda: asyncio.ensure_future(self._update_sheet()))
            sheet.grid(column=1, row=row_count, sticky=tk.N + tk.W + tk.E)
            midi = Checkbutton(parent, text="", default=True)
            midi.grid(column=2, row=row_count, sticky=tk.N + tk.W + tk.E)
            velocity = Spinbox(parent,
                               width=3,
                               from_=-50,
                               to=50,
                               increment=1,
                               default=0)
            velocity.grid(column=3, row=row_count, sticky=tk.W + tk.N)
            row_count += 1
            self.instruments[instrument] = {
                'name': name,
                'sheet': sheet,
                'midi': midi,
                'velocity': velocity
            }

    def _reset_song(self):
        """Set song pitch and bpm to song default."""
        config = self._get_interface().config
        if 'key' in config:
            self.key.set(config['key'])
        if 'tempo' in config:
            self.bpm.set(config['tempo'])
        asyncio.ensure_future(self._update_sheet())

    def save_state(self):
        """Return exercise state."""
        data = {}
        data['key'] = self.key.get()
        data['bpm'] = self.bpm.get()
        data['velocity'] = self.velocity.get()
        data['instruments'] = {}
        for instrument in self.instruments:
            data['instruments'][instrument] = {
                'sheet': self.instruments[instrument]['sheet'].get(),
                'midi': self.instruments[instrument]['midi'].get(),
                'velocity': self.instruments[instrument]['velocity'].get()
            }
        return data

    def restore_state(self, data):
        """Restore saved settings."""
        if 'key' in data:
            self.key.set(data['key'])
        if 'bpm' in data:
            self.bpm.set(data['bpm'])
        if 'velocity' in data:
            self.velocity.set(data['velocity'])
        if 'instruments' not in data:
            return
        for instrument in data['instruments']:
            if instrument not in self.instruments:
                continue
            if not isinstance(data['instruments'][instrument], dict):
                continue
            if 'sheet' in data['instruments'][instrument]:
                self.instruments[instrument]['sheet'].set(
                    data['instruments'][instrument]['sheet'])
            if 'midi' in data['instruments'][instrument]:
                self.instruments[instrument]['midi'].set(
                    data['instruments'][instrument]['midi'])
            if 'velocity' in data['instruments'][instrument]:
                self.instruments[instrument]['velocity'].set(
                    data['instruments'][instrument]['velocity'])

    def _get_interface(self):
        """Return song interface."""
        midi_instruments = {
            instrument: self.instruments[instrument]['midi'].get() \
            for instrument in self.instruments}
        sheet_instruments = {
            instrument: self.instruments[instrument]['sheet'].get() \
            for instrument in self.instruments}
        instrument_velocities = {
            instrument: self.instruments[instrument]['velocity'].get() \
            for instrument in self.instruments}
        return Song(self._data_path,
                    self._include_path,
                    name=self.name,
                    pitch=self.key.get(),
                    bpm=self.bpm.get(),
                    start_measure=self.measure.get(),
                    velocity=self.velocity.get(),
                    sheet_instruments=sheet_instruments,
                    instrument_velocities=instrument_velocities,
                    midi_instruments=midi_instruments)

    async def clear_cache(self):
        """Remove all compiled files."""
        # remove files
        for file_ in chain(self._data_path.glob("{}-*.midi".format(self.name)),
                           self._data_path.glob("{}-*.png".format(self.name)),
                           self._data_path.glob("{}-*.pdf".format(self.name))):
            file_.unlink()
        self._image_cache = {}
        await self._update_sheet()

    def _change_page(self, increment=True, page=None, scroll=False):
        """Change page."""
        if scroll:
            delta_t = datetime.now() - self._scroll_time
            self._scroll_time = datetime.now()
            if delta_t.total_seconds() < 0.5:
                return
        if page is not None:
            self.page = int(page)
        elif increment:
            self.page += 1
        else:
            if self.page > 1:
                self.page -= 1
        asyncio.ensure_future(self._update_sheet())

    async def _update_sheet(self):
        """Display relevant sheet."""
        # pylint: disable=invalid-name
        # type declaration
        Size = namedtuple('Size', ['width', 'height'])
        size = Size(self.sheet.winfo_width(), self.sheet.winfo_height())
        await self._resize_sheet(size)

    async def _resize_sheet(self, event):
        """Resize sheets to screen size."""
        self.sheet.delete("left")
        self.sheet.delete("right")
        left = self._get_interface()
        # make sure pages are compiled
        await get_file(left)
        left.page = self.page
        if not left.get_filename(FileType.png).is_file():
            # page does not exists, roll around to 1
            self.page = 1
            left.page = 1
        left_path = await get_single_sheet(self._image_cache, left,
                                           (event.width - 1) / 2, event.height)
        self.sheet.create_image(0,
                                0,
                                image=self._image_cache[left_path]['image'],
                                anchor=tk.NW,
                                tags="left")
        # right page
        right = self._get_interface()
        right.page = self.page + 1
        if right.get_filename(FileType.png).is_file():
            right_path = await get_single_sheet(self._image_cache, right,
                                                (event.width - 1) / 2,
                                                event.height)
            self.sheet.create_image(
                self._image_cache[left_path]['image'].width() + 1,
                0,
                image=self._image_cache[right_path]['image'],
                anchor=tk.NW,
                tags="right")

    async def _on_pitch_change(self):
        """New pitch was picked by user or app."""
        asyncio.ensure_future(self._update_sheet())
        await get_file(self._get_interface(), FileType.midi)

    async def play(self):
        """Play midi file."""
        midi = await get_file(self._get_interface(), FileType.midi)
        # if self.__midi_executable.get() == 'pmidi':
        playing = await play_or_stop(midi, self._on_midi_stop)
        # else:
        #     # FIXME: fetch midi_executable and await_jack somehow
        #     # have them trickle down from MainWindow with singular
        #     # play/stop button
        #     playing = await play_or_stop(
        #         midi,
        #         on_midi_end=self._on_midi_stop,
        #         pmidi=False)
        if playing:
            self.b_play.set_text("Stop")
        else:
            self.b_play.set_text("Play")

    async def _on_midi_stop(self):
        """Handle end of midi playback."""
        self.b_play.set_text("Play")

    def export(self):
        """Export compiled data."""
        return self._get_interface()