#!/usr/bin/env python """Render Ambient-Mix XML to MP3""" import untangle, pydub, argparse, math, os from typing import List __author__ = "Kevin Whitaker" __license__ = "GPL" __version__ = "1.0" human_unit_to_sec = { "1m": 60, "10m": 600, "1h": 3600 } def convert_volume_to_decibel(volume: int): return -(math.log10(1+volume/100)) def convert_balance_to_pan(balance: int): return (balance*2)/100 def load_sample_to_segment(audio_file): if not os.path.exists(audio_file): print("Needed audio file "+audio_file+" does not exist.") exit(1) return pydub.AudioSegment.from_mp3(audio_file) class Channel(): base_segment: pydub.AudioSegment length: int = 3600 local_url: str = "" name: str = "" id: int = -1 url: str = "" muted: bool = False volume: int = 100 #between 0 and 100 balance: int = 0 #between -50 and +50 random: bool = False random_counter: int = 1 random_unit: int = human_unit_to_sec["1h"] crossfade: bool = False def __init__(self, length: int, source_dir: str, id: int): self.length = length self.id = id self.local_url = source_dir+"/"+str(id)+".mp3" self.base_segment = pydub.AudioSegment.silent(duration= length*1000) def render_channel(self): if not self.muted: inital_audio = load_sample_to_segment(self.local_url) gained_audio = inital_audio.apply_gain(convert_volume_to_decibel(self.volume)) panned_audio = gained_audio.pan(convert_balance_to_pan(self.balance)) if self.random: trigger_every = int(self.random_unit/self.random_counter) canvas: pydub.AudioSegment = self.base_segment for sec in range(trigger_every,self.length,trigger_every): canvas = canvas.overlay(panned_audio,position=sec*1000) return canvas elif self.crossfade: fadein_audio = panned_audio.fade_in(duration=1000) crossfaded_audio = fadein_audio.fade_out(duration=1000) return self.base_segment.overlay(crossfaded_audio, loop=True) else: return self.base_segment.overlay(panned_audio, loop=True) else: return self.base_segment class Mix(): channels: List[Channel] = [] length: int = 3600 final_segment: pydub.AudioSegment def __init__(self, xml_file: str, length: int, source_dir: str): self.length = length self.final_segment = pydub.AudioSegment.silent(duration=length*1000) xml = untangle.parse(xml_file) for channel_num in range(1,9): channel_xml = getattr(xml.audio_template, "channel"+str(channel_num)) channel = Channel(length, source_dir, channel_xml.id_audio.cdata) channel.name = channel_xml.name_audio.cdata channel.url = channel_xml.url_audio.cdata channel.muted = channel_xml.mute.cdata == "true" channel.volume = int(channel_xml.volume.cdata) channel.balance = int(channel_xml.balance.cdata) channel.random = channel_xml.random.cdata == "true" channel.random_counter = int(channel_xml.random_counter.cdata) channel.random_unit = human_unit_to_sec[channel_xml.random_unit.cdata] channel.crossfade = channel_xml.crossfade.cdata == "true" self.channels.append(channel) def render_channels(self): for channel in self.channels: self.final_segment = self.final_segment.overlay(channel.render_channel()) def export_final(self, outfile: str): self.final_segment.export(outfile, format="mp3") parser = argparse.ArgumentParser(description="Render Ambient-Mix XML to MP3") parser.add_argument("-l", help="length of final render in seconds", default=3600, dest="length") parser.add_argument("-s", help="directory containing audio samples", default="sounds", dest="samples") parser.add_argument("infile", help="Input XML file defining mix.") parser.add_argument("outfile", help="Output MP3 file of rendered mix.") args = parser.parse_args() if not os.path.exists(args.samples): print("Samples directory does not exist.") exit(1) mixer = Mix(args.infile, int(args.length), args.samples) mixer.render_channels() mixer.export_final(args.outfile)