示例#1
0
 def _generate(self, destination: MP3FileWriter, progress: Progress) -> None:
     """
     generate output file, combining all input files
     """
     num_files = float(len(destination._files))
     contents: List[Dict] = []
     metadata: Optional[Dict] = None
     if destination._metadata is not None:
         metadata = destination._metadata.as_dict()
     for index, mp3file in enumerate(destination._files, 1):
         progress.pct = 100.0 * index / num_files
         progress.text = f'Adding {mp3file.filename.name}'
         src: Dict[str, Union[str, int]] = {
             'filename': mp3file.filename.name
         }
         if mp3file.start is not None:
             src['start'] = mp3file.start
         if mp3file.end is not None:
             src['end'] = mp3file.end
         if mp3file.headroom is not None:
             src['headroom'] = mp3file.headroom
         contents.append(src)
     results: Dict[str, Union[List, Optional[Dict]]] = {
         'contents': contents,
         'metadata': metadata,
     }
     self.output[destination.filename.name] = self.flatten(results)
示例#2
0
 def run_command(args: List[str],
                 progress: Progress,
                 start: int = 0,
                 duration: Optional[int] = None) -> None:
     """
     Start a new process running specified command and wait for it to complete.
     :duration: If not None, the progress percentage will be updated based upon
     the amount of time the process has been running.
     Can be terminated by setting progress.abort to True
     """
     progress.pct = 0.0
     start_time = time.time()
     with subprocess.Popen(args) as proc:
         done = False
         while not done and not progress.abort:
             rcode = proc.poll()
             if rcode is not None:
                 done = True
                 proc.wait()
             else:
                 if duration is not None:
                     elapsed = min(duration,
                                   1000.0 * (time.time() - start_time))
                     progress.pct = 100.0 * elapsed / float(duration)
                     progress.pct_text = Duration(int(elapsed) +
                                                  start).format()
                 time.sleep(0.25)
         if progress.abort:
             proc.terminate()
             proc.wait()
     progress.pct = 100.0
示例#3
0
 def _generate(self, destination: MP3FileWriter,
               progress: Progress) -> None:
     """generate output file, combining all input files"""
     assert destination._metadata is not None
     output: Optional[AudioSegment] = None
     num_files = float(len(destination._files))
     for index, mp3file in enumerate(destination._files, 1):
         progress.pct = 50.0 * index / num_files
         progress.text = f'Adding {mp3file.filename.name}'
         if progress.abort:
             return
         seg = AudioSegment.from_mp3(str(mp3file.filename))
         if mp3file.start is not None and mp3file.start > 0:
             if mp3file.end is not None:
                 seg = seg[mp3file.start:mp3file.end]
             else:
                 seg = seg[mp3file.start:]
         elif mp3file.end is not None:
             seg = seg[:mp3file.end]
         if mp3file.headroom is not None:
             seg = seg.normalize(mp3file.headroom)
         if output is None:
             output = seg
         else:
             output = output.append(seg, crossfade=int(mp3file.overlap))
     tags: Dict[str, Any] = {
         "artist": destination._metadata.artist,
         "title": destination._metadata.title
     }
     if destination._metadata.album:
         tags["album"] = destination._metadata.album
     assert output is not None
     progress.text = f'Encoding MP3 file "{destination.filename.name}"'
     progress.pct = 50.0
     if progress.abort:
         return
     dest_dir = destination.filename.parent
     if not dest_dir.exists():
         dest_dir.mkdir(parents=True)
     parameters = [
         '-ar',
         str(destination._metadata.sample_rate),
         '-ac',
         str(destination._metadata.channels),
     ]
     #parameters.append(f'-acodec copy')
     output.export(str(destination.filename),
                   format="mp3",
                   bitrate=f'{destination._metadata.bitrate}k',
                   parameters=parameters,
                   tags=tags)
     progress.pct = 100.0
示例#4
0
 def render(self, filename: str, document: DG.Document,
            progress: Progress) -> None:
     """Renders the given document as a PDF file"""
     # pagesize is a tuple of (width, height)
     # see reportlab.lib.pagesizes for detains
     doc = self.render_document(filename, document)
     elements: List[Flowable] = []
     num_elts = float(len(document._elements))
     for index, elt in enumerate(document._elements):
         progress.pct = 100.0 * float(index) / num_elts
         elements.append(self.renderers[type(elt)](elt))
     doc.build(elements)
     progress.pct = 100.0
示例#5
0
    def generate_card_results(self, tracks: List[Song],
                              cards: List[BingoTicket]):
        """generate PDF showing when each ticket wins"""
        doc = DG.Document(pagesize=PageSizes.A4,
                          title=f'{self.options.game_id} - {self.options.title}',
                          topMargin="0.25in",
                          bottomMargin="0.25in",
                          rightMargin="0.25in",
                          leftMargin="0.25in")

        doc.append(self.options.palette.logo_image("6.2in"))
        doc.append(DG.Spacer(width=0, height="0.05in"))

        doc.append(DG.Paragraph(
            f'Results For Game Number: <b>{self.options.game_id}</b>',
            self.TEXT_STYLES['results-heading']))
        doc.append(DG.Paragraph(
            self.options.title,
            self.TEXT_STYLES['results-title']))

        pstyle = self.TEXT_STYLES['results-cell']
        heading: DG.TableRow = [
            DG.Paragraph('<b>Ticket Number</b>', pstyle),
            DG.Paragraph('<b>Wins after track</b>', pstyle),
            DG.Paragraph('<b>Start Time</b>', pstyle),
        ]
        data: List[DG.TableRow] = []

        cards = copy.copy(cards)
        cards.sort(key=lambda card: card.ticket_number, reverse=False)
        for card in cards:
            win_point = self.get_when_ticket_wins(tracks, card)
            song = tracks[win_point - 1]
            data.append([
                DG.Paragraph(f'{card.ticket_number}', pstyle),
                DG.Paragraph(
                    f'Track {win_point} - {song.title} ({song.artist})',
                    pstyle),
                DG.Paragraph(Duration(song.start_time).format(), pstyle)
            ])

        col_widths: List[Dimension] = [
            Dimension("0.75in"), Dimension("5.5in"), Dimension("0.85in"),
        ]
        hstyle = pstyle.replace(
            name='results-table-heading',
            background=self.options.palette.title_bg)
        tstyle = TableStyle(name='results-table',
                            borderColour=Colour('black'),
                            borderWidth=1.0,
                            gridColour=Colour('black'),
                            gridWidth=0.5,
                            verticalAlignment=VerticalAlignment.CENTER,
                            headingStyle=hstyle)
        table = DG.Table(data, heading=heading, repeat_heading=True,
                         colWidths=col_widths, style=tstyle)
        doc.append(table)
        filename = str(self.options.ticket_results_output_name())
        self.doc_gen.render(filename, doc, Progress())
示例#6
0
 def _generate(self, destination: MP3FileWriter,
               progress: Progress) -> None:
     """generate output file, combining all input files"""
     output: Optional[AudioSegment] = None
     num_files = float(len(destination._files))
     for index, mp3file in enumerate(destination._files, 1):
         progress.pct = 50.0 * index / num_files
         progress.text = f'Adding {mp3file.filename.name}'
         if progress.abort:
             return
         seg = AudioSegment.from_mp3(str(mp3file.filename))
         if mp3file.start is not None:
             if mp3file.end is not None:
                 seg = seg[mp3file.start:mp3file.end]
             else:
                 seg = seg[mp3file.start:]
         elif mp3file.end is not None:
             seg = seg[:mp3file.end]
         if mp3file.headroom is not None:
             seg = seg.normalize(mp3file.headroom)
         if output is None:
             output = seg
         else:
             output += seg
     tags = None
     if destination._metadata is not None:
         tags = {
             "artist": Song.clean(destination._metadata.artist),
             "title": Song.clean(destination._metadata.title)
         }
         if destination._metadata.album:
             tags["album"] = Song.clean(destination._metadata.album)
     assert output is not None
     progress.text = f'Encoding MP3 file "{destination.filename.name}"'
     progress.pct = 50.0
     if progress.abort:
         return
     dest_dir = destination.filename.parent
     if not dest_dir.exists():
         dest_dir.mkdir(parents=True)
     output.export(str(destination.filename),
                   format="mp3",
                   bitrate=destination.bitrate,
                   tags=tags)
     progress.pct = 100.0
示例#7
0
 def __init__(self, args: Tuple[Any, ...], options: Options,
              finalise: Callable[[Any], None]):
     self.progress = Progress()
     self.options = options
     self.finalise = finalise
     self.bg_thread = threading.Thread(target=self.run,
                                       args=args,
                                       daemon=True)
     self.result: Optional[Any] = None
示例#8
0
 def search(self, parser: MP3Parser, progress: Progress) -> None:
     """
     Walk self._fullpath searching for all songs and
     sub-directories.
     This function will block until all of the songs and
     directories have been checked.
     """
     try:
         max_workers = len(os.sched_getaffinity(0)) + 2  # type: ignore
     except AttributeError:
         cpu_count = os.cpu_count()
         if cpu_count is None:
             max_workers = 3
         else:
             max_workers = cpu_count + 2
     with futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
         todo = set(self.search_async(pool, parser, 0))
         done: Set[futures.Future] = set()
         while todo and not progress.abort:
             completed, not_done = futures.wait(
                 todo, timeout=0.25, return_when=futures.FIRST_COMPLETED)
             todo.update(set(not_done))
             for future in completed:
                 if progress.abort:
                     break
                 try:
                     err = future.exception()
                     if err is not None:
                         progress.text = f'Error: {err}'
                     else:
                         result = future.result()
                         if isinstance(result, list):
                             todo.update(set(result))
                         elif result is not None:
                             progress.text = result.filename
                 except (futures.TimeoutError, futures.CancelledError):
                     pass
                 except KeyboardInterrupt:
                     progress.abort = True
                 todo.remove(future)
                 done.add(future)
             num_tasks = len(todo) + len(done)
             if num_tasks > 0:
                 progress.pct = 100.0 * len(done) / num_tasks
示例#9
0
 def _generate(self, destination: MP3FileWriter,
               progress: Progress) -> None:
     """generate output file, combining all input files"""
     assert destination._metadata is not None
     #destination._files = destination._files[:6]
     num_files = len(destination._files)
     if num_files == 0:
         return
     args: List[str] = [
         'ffmpeg', '-hide_banner', '-loglevel', 'panic', '-v', 'quiet'
     ]
     concat = self.append_input_files(args, destination._files)
     progress.text = f'Encoding MP3 file "{destination.filename.name}"'
     mdata = [f'title="{destination._metadata.title}"']
     if destination._metadata.artist:
         mdata.append(f'artist="{destination._metadata.artist}"')
     if destination._metadata.album:
         mdata.append(f'album="{destination._metadata.album}"')
     if num_files > 1:
         args += [
             '-filter_complex',
             self.build_filter_argument(destination, concat)
         ]
         if destination.headroom is not None:
             args[-1] += f';[outa]loudnorm=tp=-{destination.headroom}[outb]'
             args += ['-map', '[outb]']
         else:
             args += ['-map', '[outa]']
     elif destination.headroom is not None:
         args += ['-af', f'loudnorm=tp=-{destination.headroom}']
     for item in mdata:
         args += ['-metadata', item]
     args += [
         '-ab',
         f'{destination._metadata.bitrate}k',
         '-ac',
         str(destination._metadata.channels),
         '-ar',
         str(destination._metadata.sample_rate),
         '-acodec',
         'mp3',
         '-threads',
         '0',
         '-f',
         'mp3',
         '-y',
         str(destination.filename),
     ]
     dest_dir = destination.filename.parent
     if not dest_dir.exists():
         dest_dir.mkdir(parents=True)
     self.run_command_with_progress(args,
                                    progress,
                                    duration=int(destination.duration))
示例#10
0
def main(args: Sequence[str]) -> int:
    """used for testing directory searching from the command line"""
    #pylint: disable=import-outside-toplevel
    from musicbingo.options import Options
    from musicbingo.mp3 import MP3Factory

    opts = Options.parse(args)
    mp3parser = MP3Factory.create_parser()
    clips = Directory(None, 1, Path(opts.clip_directory), mp3parser,
                      Progress())
    clips.search()
    return 0
示例#11
0
    def test_complete_bingo_game_pipeline(self, mock_randbelow, mock_shuffle):
        """
        Test of complete Bingo game generation
        """
        self.maxDiff = 500
        filename = self.fixture_filename(
            "test_complete_bingo_game_pipeline.json")
        with filename.open('r') as jsrc:
            expected = json.load(jsrc)
        mrand = MockRandom()
        mock_randbelow.side_effect = mrand.randbelow
        mock_shuffle.side_effect = mrand.shuffle
        opts = Options(
            game_id='test-pipeline',
            games_dest=str(self.tmpdir),
            number_of_cards=24,
            title='Game title',
            crossfade=0,
        )
        editor = MockMP3Editor()
        docgen = MockDocumentGenerator()
        progress = Progress()
        gen = GameGenerator(opts, editor, docgen, progress)
        gen.generate(self.directory.songs[:40])
        with open('results.json', 'w') as rjs:
            json.dump({
                "docgen": docgen.output,
                "editor": editor.output
            },
                      rjs,
                      indent=2,
                      sort_keys=True)
        self.assertEqual(len(docgen.output), 3)
        ticket_file = "test-pipeline Bingo Tickets - (24 Tickets).pdf"
        self.assert_dictionary_equal(expected['docgen'][ticket_file],
                                     docgen.output[ticket_file])

        results_file = "test-pipeline Ticket Results.pdf"
        self.assert_dictionary_equal(expected['docgen'][results_file],
                                     docgen.output[results_file])

        listings_file = "test-pipeline Track Listing.pdf"
        self.assert_dictionary_equal(expected['docgen'][listings_file],
                                     docgen.output[listings_file])

        self.assertEqual(len(editor.output), 1)
        mp3_file = "test-pipeline Game Audio.mp3"
        self.assert_dictionary_equal(expected['editor'][mp3_file],
                                     editor.output[mp3_file])
示例#12
0
    def generate_tickets_pdf(self, cards: List[BingoTicket]) -> None:
        """generate a PDF file containing all the Bingo tickets"""
        doc = DG.Document(pagesize=PageSizes.A4,
                          title=f'{self.options.game_id} - {self.options.title}',
                          topMargin="0.15in",
                          rightMargin="0.15in",
                          bottomMargin="0.15in",
                          leftMargin="0.15in")
        page: int = 1
        num_cards: int = len(cards)
        cards_per_page: int = 3
        if self.options.rows == 2:
            cards_per_page = 4
        elif self.options.rows > 3:
            cards_per_page = 2
        id_style = self.TEXT_STYLES['ticket-id']
        title_style = id_style.replace('ticket-title',
                                       alignment=HorizontalAlignment.LEFT)

        for count, card in enumerate(cards, start=1):
            self.progress.text = f'Card {count}/{num_cards}'
            self.progress.pct = 100.0 * float(count) / float(num_cards)
            self.render_bingo_ticket(card, doc)
            data: List[DG.TableRow] = [[
                DG.Paragraph(self.options.title, title_style),
                DG.Paragraph(
                    f"{self.options.game_id} / T{card.ticket_number} / P{page}",
                    id_style),
            ]]
            tstyle = TableStyle(name='ticket-id',
                                borderWidth=0,
                                gridWidth=0,
                                verticalAlignment=VerticalAlignment.CENTER)
            table = DG.Table(
                data,
                colWidths=[Dimension(80), Dimension(80)],
                rowHeights=[Dimension(f'16pt')],
                style=tstyle)
            doc.append(table)
            if count % cards_per_page != 0:
                doc.append(
                    DG.HorizontalLine('hline', width="100%", thickness="1px",
                                      colour=Colour('gray'), dash=[2, 2]))
                doc.append(DG.Spacer(width=0, height="0.08in"))
            else:
                doc.append(DG.PageBreak())
                page += 1
        filename = str(self.options.bingo_tickets_output_name())
        self.doc_gen.render(filename, doc, Progress())
示例#13
0
 def __init__(self,
              editor: "MP3Editor",
              filename: Path,
              bitrate: str,
              metadata: Optional[Metadata] = None,
              progress: Optional[Progress] = None):
     super(MP3FileWriter, self).__init__(filename,
                                         FileMode.WRITE_ONLY,
                                         metadata=metadata,
                                         start=0,
                                         end=0)
     self._editor = editor
     self._files: List["MP3File"] = []
     self.bitrate = bitrate
     if progress is None:
         progress = Progress()
     self.progress = progress
示例#14
0
    def play_with_pyaudio(seg: AudioSegment, progress: Progress) -> None:
        """use pyaudio library to play audio segment"""
        pya = pyaudio.PyAudio()
        stream = pya.open(format=pya.get_format_from_width(seg.sample_width),
                          channels=seg.channels,
                          rate=seg.frame_rate,
                          output=True)

        try:
            chunks = utils.make_chunks(seg, 500)
            scale: float = 1.0
            if chunks:
                scale = 100.0 / float(len(chunks))
            for index, chunk in enumerate(chunks):
                if progress.abort:
                    break
                progress.pct = index * scale
                stream.write(chunk._data)
        finally:
            stream.stop_stream()
            stream.close()
            pya.terminate()
示例#15
0
    def __init__(self, root_elt: tk.Tk, options: Options):
        self.root = root_elt
        self.options = options
        self._sort_by_title_option = True
        self.clips: Directory = Directory(None, 0, Path(''), NullMP3Parser(),
                                          Progress())
        self.poll_id = None
        self.dest_directory: str = ''
        self.threads: List[BackgroundWorker] = []
        self.previous_games_songs: Set[int] = set()  # uses hash of song
        self.base_game_id: str = datetime.date.today().strftime("%y-%m-%d")

        self.main = tk.Frame(root_elt, bg=Panel.NORMAL_BACKGROUND)
        self.menu = tk.Menu(root_elt)
        file_menu = tk.Menu(self.menu, tearoff=0)
        file_menu.add_command(label="Select clip source",
                              command=self.ask_select_source_directory)
        file_menu.add_command(label="Select new clip destination",
                              command=self.ask_select_clip_destination)
        file_menu.add_command(label="Select new game destination",
                              command=self.ask_select_game_destination)
        file_menu.add_separator()
        file_menu.add_command(label="Exit", command=root_elt.quit)
        self.menu.add_cascade(label="File", menu=file_menu)

        game_mode = OptionVar(self.main,
                              options,
                              "mode",
                              GameMode,
                              command=self.set_mode)

        mode_menu = tk.Menu(self.menu, tearoff=0)
        mode_menu.add_radiobutton(label="Bingo Game",
                                  value=GameMode.BINGO.value,
                                  variable=game_mode)
        mode_menu.add_radiobutton(label="Music Quiz",
                                  value=GameMode.QUIZ.value,
                                  variable=game_mode)
        mode_menu.add_radiobutton(label="Clip generation",
                                  value=GameMode.CLIP.value,
                                  variable=game_mode)
        self.menu.add_cascade(label="Mode", menu=mode_menu)

        root_elt.config(menu=self.menu)

        self.available_songs_panel = SongsPanel(
            self.main, self.options, self.add_selected_songs_to_game)
        self.action_panel = ActionPanel(self.main, self)
        self.selected_songs_panel = SelectedSongsPanel(self.main, self.options,
                                                       self.play_song)
        self.game_panel = GenerateGamePanel(self.main, self.options,
                                            self.generate_bingo_game)
        self.quiz_panel = GenerateQuizPanel(self.main,
                                            self.generate_music_quiz)
        self.clip_panel = GenerateClipsPanel(self.main, self.options,
                                             self.generate_clips)
        self.info_panel = InfoPanel(self.main)
        self.panels = [
            self.available_songs_panel,
            self.action_panel,
            self.selected_songs_panel,
            self.game_panel,
            self.quiz_panel,
            self.clip_panel,
            self.info_panel,
        ]

        self.main.pack(side=tk.TOP, fill=tk.BOTH, expand=1, ipadx=5, ipady=5)
        self.available_songs_panel.grid(row=0, column=0, padx=0)
        self.action_panel.grid(row=0, column=1, padx=5)
        self.selected_songs_panel.grid(row=0, column=2)

        self.generate_unique_game_id()
        self.set_mode(self.options.mode)
        self.search_clips_directory()
示例#16
0
    def generate_track_listing(self, tracks: List[Song]) -> None:
        """generate a PDF version of the track order in the game"""
        assert len(tracks) > 0
        doc = DG.Document(
            PageSizes.A4,
            topMargin="0.25in",
            bottomMargin="0.25in",
            leftMargin="0.35in",
            rightMargin="0.35in",
            title=f'{self.options.game_id} - {self.options.title}')

        doc.append(self.options.palette.logo_image("6.2in"))
        doc.append(DG.Spacer(width=0, height="0.05in"))

        doc.append(
            DG.Paragraph(
                f'Track Listing For Game Number: <b>{self.options.game_id}</b>',
                self.TEXT_STYLES['track-heading']))

        doc.append(
            DG.Paragraph(self.options.title, self.TEXT_STYLES['track-title']))

        cell_style = self.TEXT_STYLES['track-cell']
        heading: DG.TableRow = [
            DG.Paragraph('<b>Order</b>', cell_style),
            DG.Paragraph('<b>Title</b>', cell_style),
            DG.Paragraph('<b>Artist</b>', cell_style),
            DG.Paragraph('<b>Start Time</b>', cell_style),
            DG.Paragraph('', cell_style),
        ]

        data: List[DG.TableRow] = []

        for index, song in enumerate(tracks, start=1):
            order = DG.Paragraph(f'<b>{index}</b>', cell_style)
            title = DG.Paragraph(song.title, cell_style)
            if self.should_include_artist(song):
                artist = DG.Paragraph(song.artist, cell_style)
            else:
                artist = DG.Paragraph('', cell_style)
            start = DG.Paragraph(
                Duration(song.start_time).format(), cell_style)
            end_box = DG.Paragraph('', cell_style)
            data.append([order, title, artist, start, end_box])

        col_widths = [
            Dimension("0.55in"),
            Dimension("2.9in"),
            Dimension("2.9in"),
            Dimension("0.85in"),
            Dimension("0.2in")
        ]

        hstyle = cell_style.replace(name='track-table-heading',
                                    background=self.options.palette.title_bg)
        tstyle = TableStyle(name='track-table',
                            borderColour=Colour('black'),
                            borderWidth=1.0,
                            gridColour=Colour('black'),
                            gridWidth=0.5,
                            verticalAlignment=VerticalAlignment.CENTER,
                            headingStyle=hstyle)
        table = DG.Table(data,
                         heading=heading,
                         repeat_heading=True,
                         colWidths=col_widths,
                         style=tstyle)
        doc.append(table)
        filename = str(self.options.track_listing_output_name())
        self.doc_gen.render(filename, doc, Progress())