Skip to content

Instantly share code, notes, and snippets.

@n-hansen
Created February 13, 2017 03:13
Show Gist options
  • Save n-hansen/ece68353531596ff8ea80575d7e2b433 to your computer and use it in GitHub Desktop.
Save n-hansen/ece68353531596ff8ea80575d7e2b433 to your computer and use it in GitHub Desktop.
Command line utility for merging playlists into a single file with interval markers.
#!/usr/bin/env python3
# intervals.py - turn an itunes playlist into a single track with interval markers
import os
import plistlib
import urllib.request
from pydub import AudioSegment
from pydub.generators import Sine
from numpy import linspace
import argparse
OUTPUT_FORMAT = "mp3"
OUTPUT_PARAMETERS = ["-q:a", "0"]
IGNORED_PLAYLISTS = ["Library",
"Music",
"Movies",
"TV Shows",
"Podcasts",
"iTunes\xa0U",
"Audiobooks",
"Purchased"]
class Library:
def __init__(self,filepath):
with open(filepath, 'rb') as f:
lib = plistlib.load(f)
self.songs = {int(k): v["Location"] for (k,v) in lib["Tracks"].items() if "Location" in v}
self.playlists = {pl["Name"]:pl for pl in lib["Playlists"] if pl["Name"] not in IGNORED_PLAYLISTS}
def get_playlist_songs(self,pl_name):
if pl_name not in self.playlists:
return []
def get_song(song_id):
path = self.songs[song_id]
extn = path.split('.')[-1]
with urllib.request.urlopen(path) as song_file:
return AudioSegment.from_file(song_file, format=extn)
return [get_song(x["Track ID"]) for x in self.playlists[pl_name]["Playlist Items"]]
class SFX:
def __init__(self,volume=0.0,low=261.6,hi=392,steps=2,length=450,spacing=90,fade=0.03):
pulse_length = (length-(steps-1)*spacing)/steps
fade_length = int(pulse_length * fade)
pulse_length = int(pulse_length)
self.sounds = [Sine(f).to_audio_segment(pulse_length,volume).fade_in(fade_length).fade_out(fade_length)
for f in linspace(low,hi,steps)]
self.spacing=spacing
def up(self):
acc = self.sounds[0]
for s in self.sounds[1:]:
if self.spacing <= 0:
acc = acc.append(s,crossfade=(-self.spacing))
else:
acc = acc + AudioSegment.silent(duration=self.spacing) + s
return acc
def down(self):
acc = self.sounds[-1]
for s in reversed(self.sounds[0:-1]):
if self.spacing <= 0:
acc = acc.append(s,crossfade=(-self.spacing))
else:
acc = acc + AudioSegment.silent(duration=self.spacing) + s
return acc
def add_sound_at(sound1,sound2,time,fade,gain=-50):
front = sound1[:time].fade(to_gain=gain,end=time,duration=fade)
middle = sound1[time:(time+len(sound2))].apply_gain(gain).overlay(sound2)
back = sound1[(time+len(sound2)):].fade(from_gain=gain,start=0,duration=fade)
return (front + middle + back)
def add_intervals(audio,warmup,a_len,b_len,cooldown,count,fade=180):
two_note = SFX(volume=audio.max_dBFS,steps=2)
three_note = SFX(volume=audio.max_dBFS,steps=3)
ten_note = SFX(volume=audio.max_dBFS,steps=10,length=800,spacing=-30)
end_warmup = three_note.up()
a2b = two_note.down()
b2a = two_note.up()
start_cooldown = three_note.down()
done = ten_note.down()
cursor = warmup
audio = add_sound_at(audio,end_warmup,cursor,fade)
for i in range(count):
cursor = cursor + a_len
audio = add_sound_at(audio,a2b,cursor,fade)
cursor = cursor + b_len
if i < (count-1):
audio = add_sound_at(audio,b2a,cursor,fade)
audio = add_sound_at(audio,start_cooldown,cursor,fade)
audio = add_sound_at(audio,done,cursor+cooldown,fade)
return audio
def main():
parser = argparse.ArgumentParser()
parser.add_argument("playlist",
help="Name of the iTunes playlist to use.")
parser.add_argument("warmup",
help="Length (in seconds) of the warm-up interval.",
type=float)
parser.add_argument("a",
help="Length (in seconds) of the A interval.",
type=float)
parser.add_argument("b",
help="Length (in seconds) of the B interval.",
type=float)
parser.add_argument("cooldown",
help="Length (in seconds) of the cool-down interval.",
type=float)
parser.add_argument("intervals",
help="Number of intervals (not including warm-up and cool-down).",
type=int)
parser.add_argument("-o","--outfile",
help="Output file.",
default="out.mp3")
parser.add_argument("-l","--library",
help="Path to iTunes Music Library.xml",
default=os.path.expanduser("~/Music/iTunes/iTunes Music Library.xml"))
args = parser.parse_args()
lib = Library(args.library)
if args.playlist not in lib.playlists:
print("Could not find the requested playlist in the iTunes library file.")
return
base_audio_track = sum(lib.get_playlist_songs(args.playlist))
final_audio_track = add_intervals(base_audio_track,
int(1000*args.warmup),
int(1000*args.a),
int(1000*args.b),
int(1000*args.cooldown),
args.intervals)
final_audio_track.export(args.outfile,
format=OUTPUT_FORMAT,
parameters=OUTPUT_PARAMETERS,
tags={"title":args.playlist})
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment