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)
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
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
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
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())
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
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
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
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))
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
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])
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())
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
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()
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()
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())