1
0

DJMusicMixerクラス:音楽ファイルを自動でDJミックス

Last updated at Posted at 2024-07-25

はじめに

音楽制作やDJプレイに興味がある方、そして音楽ファイルを自動的にミックスしたい方に朗報です!今回は、Pythonで実装されたDJMusicMixerクラスについて詳しく解説します。このクラスを使えば、複数の音楽ファイルを自動的にDJ風にミックスすることができます。

デモ動画

DJMusicMixerクラスの概要

DJMusicMixerクラスは、以下の主要な機能を提供します:

  1. 複数の音楽ファイルの読み込み
  2. トラックのミキシング
  3. エフェクトの適用
  4. クロスフェード
  5. 音声の長さ調整

これらの機能を使って、プロフェッショナルなDJのようなミックスを自動的に作成できます。

クラス定数

DJMusicMixerクラスには、以下の定数が定義されています:

DEFAULT_TRACK_DURATION = 25000  # ミリ秒
DEFAULT_CROSSFADE_DURATION = 1000  # ミリ秒
DEFAULT_OUTPUT_DIR = "output"
SUPPORTED_FORMATS = ('.mp3', '.wav', '.ogg', '.flac')

これらの定数は、クラスの動作をコントロールするために使用されます。例えば、DEFAULT_TRACK_DURATIONは各トラックのデフォルトの長さを25秒(25000ミリ秒)に設定しています。

コンストラクタ

クラスのコンストラクタ(__init__メソッド)は以下のように定義されています:

def __init__(self, input_dir: Optional[str] = None, output_dir: str = DEFAULT_OUTPUT_DIR):
    self.input_dir = input_dir
    self.output_dir = output_dir
    self._setup_directories()

このコンストラクタは、入力ディレクトリと出力ディレクトリを設定し、必要なディレクトリを作成します。

主要メソッド

mix_tracks メソッド

mix_tracksメソッドは、このクラスの中心的な機能を担っています。複数の音声ファイルを受け取り、それらをミックスして1つのファイルにします。

def mix_tracks(self, input_files: List[str], output_file: str, track_duration: int = DEFAULT_TRACK_DURATION, crossfade_duration: int = DEFAULT_CROSSFADE_DURATION) -> str:
    # メソッドの実装

このメソッドの動作を図示すると以下のようになります:

adjust_audio_length メソッド

adjust_audio_lengthメソッドは、音声ファイルの長さを調整します。これは、例えば背景音楽をビデオの長さに合わせるときに便利です。

def adjust_audio_length(self, audio_file: str, target_duration: float) -> str:
    # メソッドの実装

このメソッドの動作は以下のようになります:

ヘルパーメソッド

クラスには、主要な機能をサポートするいくつかのヘルパーメソッドがあります:

  • _load_audio: 音声ファイルを読み込みます
  • _crossfade: 2つの音声をクロスフェードさせます
  • _apply_effect: ランダムなエフェクトを適用します
  • _process_track: トラックを指定された長さに処理します

これらのメソッドは、mix_tracksメソッド内で使用され、複雑な処理を小さな単位に分割することで、コードの可読性と保守性を高めています。

使用例

では、実際にこのクラスを使用する例を見てみましょう:

if __name__ == "__main__":
    input_dir = "input_tracks"
    output_dir = "output_mixes"
    mixer = DJMusicMixer(input_dir=input_dir, output_dir=output_dir)
    
    # ディレクトリ内のすべての音声ファイルを処理
    track_duration = -1  # フルトラックを使用
    crossfade_duration = 4000  # 4秒のクロスフェード
    mixed_track = mixer.process_directory(track_duration=track_duration, crossfade_duration=crossfade_duration)
    
    if mixed_track:
        logger.info(f"ミックスしたトラックを保存しました: {mixed_track}")

        # ビデオの長さに合わせて調整(ここでは調整しない)
        video_duration = -1
        adjusted_audio = mixer.adjust_audio_length(mixed_track, video_duration)
        logger.info(f"調整した音声を保存しました: {adjusted_audio}")
    else:
        logger.error("音声ファイルの処理に失敗しました")

この例では、input_tracksディレクトリ内のすべての音声ファイルを処理し、4秒のクロスフェードを適用してミックスしています。その後、必要に応じて音声の長さを調整することができます。

全体コード


import os
import subprocess
from typing import List, Optional
from pydub import AudioSegment
import random
from loguru import logger
from art import *

class DJMusicMixer:
    # クラス定数
    DEFAULT_TRACK_DURATION = 25000  # ミリ秒
    DEFAULT_CROSSFADE_DURATION = 1000  # ミリ秒
    DEFAULT_OUTPUT_DIR = "output"
    SUPPORTED_FORMATS = ('.mp3', '.wav', '.ogg', '.flac')

    def __init__(self, input_dir: Optional[str] = None, output_dir: str = DEFAULT_OUTPUT_DIR):
        self.input_dir = input_dir
        self.output_dir = output_dir
        self._setup_directories()

    def _setup_directories(self):
        """ディレクトリのセットアップを行う"""
        os.makedirs(self.output_dir, exist_ok=True)
        logger.info(f"出力ディレクトリを作成しました: {self.output_dir}")
        if self.input_dir:
            logger.info(f"入力ディレクトリを設定しました: {self.input_dir}")

    def _load_audio(self, full_path: str) -> AudioSegment:
        """音声ファイルを読み込む"""
        logger.info(f"音声ファイルを読み込んでいます: {full_path}")
        return AudioSegment.from_file(full_path, format=os.path.splitext(full_path)[1][1:])

    def _crossfade(self, audio1: AudioSegment, audio2: AudioSegment, duration: int) -> AudioSegment:
        """2つの音声をクロスフェードさせる"""
        logger.info(f"{duration}ミリ秒でクロスフェードを適用しています")
        return audio1.append(audio2, crossfade=duration)

    def _apply_effect(self, audio: AudioSegment) -> AudioSegment:
        """ランダムなエフェクトを適用する"""
        effects = [
            ("フェードイン", lambda a: a.fade_in(min(1000, len(a)))),
            ("フェードアウト", lambda a: a.fade_out(min(1000, len(a)))),
            ("ハイパスフィルター", lambda a: a.high_pass_filter(1000)),
            ("ローパスフィルター", lambda a: a.low_pass_filter(1000)),
            # ("リバース", lambda a: a.reverse())
        ]
        effect_name, effect_func = random.choice(effects)
        logger.info(f"エフェクトを適用しています: {effect_name}")
        return effect_func(audio)

    def _process_track(self, audio: AudioSegment, track_duration: int) -> AudioSegment:
        """トラックを指定された長さに処理する"""
        if track_duration == -1:
            return audio

        if len(audio) < track_duration:
            loops = (track_duration // len(audio)) + 1
            audio = audio * loops

        start = random.randint(0, len(audio) - track_duration)
        return audio[start:start + track_duration]

    def mix_tracks(self, input_files: List[str], output_file: str, track_duration: int = DEFAULT_TRACK_DURATION, crossfade_duration: int = DEFAULT_CROSSFADE_DURATION) -> str:
        """複数の音声トラックをDJ風にミックスする"""
        logger.info(f"トラックのミキシングを開始します: {', '.join(input_files)}")
        
        mixed = AudioSegment.empty()
        
        for i, file in enumerate(input_files, 1):
            logger.info(f"トラック {i}/{len(input_files)} を処理しています: {file}")
            full_path = os.path.join(self.input_dir, file)
            audio = self._load_audio(full_path)
            
            segment = self._process_track(audio, track_duration)
            segment = self._apply_effect(segment)
            
            if len(mixed) > 0:
                mixed = self._crossfade(mixed, segment, crossfade_duration)
            else:
                mixed += segment
            
            logger.info(f"トラック {i} をミックスしました(長さ: {len(segment)}ミリ秒)")

        output_path = os.path.join(self.output_dir, output_file)
        logger.info(f"ミックスした音声を保存しています: {output_path}")
        mixed.export(output_path, format="mp3")
        logger.success(f"ミックスが完了しました: {output_path}")
        
        return output_path

    def adjust_audio_length(self, audio_file: str, target_duration: float) -> str:
        """音声ファイルの長さを目標の長さに調整する"""
        if target_duration == -1:
            logger.info(f"音声ファイル '{audio_file}' の長さを調整せずに使用します")
            return audio_file

        logger.info(f"音声ファイル '{audio_file}' の長さを {target_duration:.2f} 秒に調整しています...")
        
        audio = self._load_audio(audio_file)
        current_duration = len(audio) / 1000.0  # ミリ秒から秒に変換
        
        if current_duration < target_duration:
            loops = int(target_duration // current_duration) + 1
            logger.info(f"音声が短いため、{loops}回ループさせています")
            audio = audio * loops
        
        logger.info(f"目標の長さ {target_duration:.2f} 秒にトリミングしています")
        audio = audio[:int(target_duration * 1000)]
        
        output_path = os.path.join(self.output_dir, f"adjusted_{os.path.basename(audio_file)}")
        logger.info(f"調整した音声を保存しています: {output_path}")
        audio.export(output_path, format="mp3")
        logger.success(f"音声の長さ調整が完了しました: {output_path}")
        
        return output_path

    def process_directory(self, track_duration: int = DEFAULT_TRACK_DURATION, crossfade_duration: int = DEFAULT_CROSSFADE_DURATION) -> str:
        """指定されたディレクトリ内のすべての音声ファイルを処理する"""
        tprint(">>  VideoLoopProcessor", font="rnd-large")
        if not self.input_dir:
            logger.error("入力ディレクトリが指定されていません")
            return ""

        audio_files = [f for f in os.listdir(self.input_dir) if f.endswith(self.SUPPORTED_FORMATS)]
        if not audio_files:
            logger.warning("指定されたディレクトリに音声ファイルが見つかりません")
            return ""

        logger.info(f"ディレクトリ内の音声ファイル: {', '.join(audio_files)}")
        output_file = f"mixed_{os.path.basename(self.input_dir)}.mp3"
        return self.mix_tracks(audio_files, output_file, track_duration, crossfade_duration)

if __name__ == "__main__":
    input_dir = "input_tracks"
    output_dir = "output_mixes"
    mixer = DJMusicMixer(input_dir=input_dir, output_dir=output_dir)
    
    # ディレクトリ内のすべての音声ファイルを処理
    # track_duration = 25000  # 25秒、-1 for full track
    track_duration = -1  # 25秒、-1 for full track
    crossfade_duration = 4000  # 1秒
    mixed_track = mixer.process_directory(track_duration=track_duration, crossfade_duration=crossfade_duration)
    
    if mixed_track:
        logger.info(f"ミックスしたトラックを保存しました: {mixed_track}")

        # ビデオの長さに合わせて調整(例として180秒、-1 for no adjustment)
        # video_duration = 180  # -1 for no adjustment
        video_duration = -1  # -1 for no adjustment
        adjusted_audio = mixer.adjust_audio_length(mixed_track, video_duration)
        logger.info(f"調整した音声を保存しました: {adjusted_audio}")
    else:
        logger.error("音声ファイルの処理に失敗しました")

まとめ

DJMusicMixerクラスは、音楽ファイルの自動ミキシングを簡単に行うための強力なツールです。初心者でも使いやすいインターフェースを提供しながら、プロフェッショナルな結果を得ることができます。

このクラスを使用することで、以下のようなことが可能になります:

  1. 複数の音楽ファイルを自動的にミックス
  2. クロスフェード効果の適用
  3. ランダムなエフェクトの追加
  4. 音声の長さの調整

音楽制作やDJミックスに興味がある方は、ぜひこのクラスを試してみてください。自分だけのユニークなミックスを作成する楽しさを体験できるはずです!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0