본문 바로가기
Programming/Clojure

음량을 기준으로 동영상 편집하기 [하스스톤 전장]

by NAMP 2023. 6. 28.

 

아래 소스로 편집한 영상입니다. (음량 조절 및 이미지 추가 하였습니다.)

 

동영상 편집을 위한 프로그램을 만든다.

음량을 기준으로 편집한다.

사용하는 명령어는 2가지 이다.

1초마다 음량을 기록한다.

ffmpeg -i input.mp4 -af asetnsamples=44100,astats=metadata=1:reset=1,ametadata=print:key=lavfi.astats.Overall.RMS_level:file=log.txt -f null -

특정시간대별로 영상을 편집한다. (잘라서 붙인다.)

ffmpeg -i input.mp4 -filter_complex "\ [0:v]trim=0:10,setpts=PTS-STARTPTS[v0]; \ [0:a]atrim=0:10,asetpts=PTS-STARTPTS[a0]; \ [0:v]trim=20:30,setpts=PTS-STARTPTS[v1]; \ [0:a]atrim=20:30,asetpts=PTS-STARTPTS[a1]; \ [v0][a0][v1][a1]concat=n=2:v=1:a=1[outv][outa]" \ -map "[outv]" -map "[outa]" output.mp4

clojure 작성

위의 명령어를 사용하기 위한 코드를 작성한다.

네임스페이스와 경로 설정

(ns your-ns
  (:require [clojure.java.io :as io]
            [clojure.java.shell :refer [sh]]
            [clojure.string :as str]))

(def base-path (.getPath (io/resource "cli/.temp")))

파일명 설정

(defn in-file-path [file-name]
  (-> (io/file base-path file-name)
      .getPath))

(defn out-file-path [file-name ext]
  (let [name (-> (str/split file-name #"\.")
                 first)]
    (-> (io/file base-path (str name "-" ext))
        .getPath)))

음량 정보 출력

(defn level-file-path [file-name]
  (out-file-path file-name (str "level.txt")))

(defn ffmpeg-level-cli [file-name]
  (let [in-path  (in-file-path file-name)
        out-path (level-file-path file-name)]
    ["ffmpeg"
     "-i" in-path
     "-af" (str "asetnsamples=44100,astats=metadata=1:reset=1,ametadata=print:key=lavfi.astats.Overall.RMS_level:file=" out-path)
     "-f" "null" "-"]))

(defn create-level-file [file-name]
  (let [out-path (level-file-path file-name)
        exist?   (.exists (io/file out-path))]
    (when (false? exist?)
      (future
        (prn "[START] create-level-file")
        (apply sh (ffmpeg-level-cli file-name))
        (prn "[END] create-level-file")))))

음량 정보 형태 확인

frame:0    pts:368     pts_time:0.00834467
lavfi.astats.Overall.RMS_level=-36.518740
frame:1    pts:44468   pts_time:1.00834
lavfi.astats.Overall.RMS_level=-34.606100
frame:2    pts:88568   pts_time:2.00834
lavfi.astats.Overall.RMS_level=-33.116027
...
...
...

frame, pts, pts_time, RMS_level 네 개의 정보가 두 줄로 나온다.
이를 사용하여 원하는 시간을 뽑아낸다.

먼저 파일을 읽는다.

(defn read-level-file [file-name]
  (let [partitioned (->> (level-file-path file-name)
                         slurp
                         str/trim-newline
                         str/split-lines
                         (partition 2))]
    (->> (map (fn [[line-1 line-2]]
                (let [line                     (str line-1 " " line-2)
                      [_ frame pts time level] (re-matches #"frame:(\d+).*pts:(\d+).*pts_time:([\d\.]+).*level=([\d\.-]+).*" line)]
                  {(parse-long frame) (parse-double level)}))
              partitioned)
         (apply merge)
         (into (sorted-map)))))

특정 음량을 기준으로 분리한다.

(defn valid-frame [file-name decibel {:keys [pre-frame post-frame]
                                      :or   {pre-frame  4
                                             post-frame 2}}]
  (let [level-map (read-level-file file-name)]
    (->> (map (fn [[frame level]]
                (when (>= level decibel)
                  (concat (mapv (fn [v] (- frame v)) (range 1 (inc pre-frame)))
                          [frame]
                          (mapv (fn [v] (+ frame v)) (range 1 (inc post-frame))))))
              level-map)
         (remove nil?)
         (apply concat)
         (into (sorted-set))
         (remove #(> 0 %)))))

부드럽게 이어지도록 앞 4초, 뒤 2초를 추가하였다.

편집할 시간을 구한다.

(defn valid-time [file-name decibel]
  (let [frame-set    (valid-frame file-name decibel {})
        start        (first frame-set)
        rest         (rest frame-set)
        result       (reduce (fn [{:keys [start last data]
                                   :as   acc} v]
                               (if (= (inc last) v)
                                 (assoc acc :last v)
                                 (-> acc
                                     (assoc :start v)
                                     (assoc :last v)
                                     (assoc :data (conj data {:start start
                                                              :end   last})))))
                             {:start start
                              :last  start
                              :data  []}
                             rest)
        result-start (:start result)
        result-last  (:last result)
        data         (:data result)]
    (if (= result-start result-last)
      data
      (conj data {:start result-start
                  :end   result-last}))))

편집 실행

(defn ffmpeg-filter [time-list]
  (let [with-index  (map-indexed (fn [index item] [index item]) time-list)
        part-list   (->> (mapv (fn [[index {:keys [start end]}]]
                                 [(str "[0:v]trim=" start ":" end ",setpts=PTS-STARTPTS[v" index "]")
                                  (str "[0:a]atrim=" start ":" end ",asetpts=PTS-STARTPTS[a" index "]")])
                               with-index)
                         (apply concat)
                         (str/join "; "))
        part-index  (->> (mapv (fn [[index _]] (str "[v" index "][a" index "]")) with-index)
                         (str/join ""))
        part-concat (str "concat=n=" (count time-list) ":v=1:a=1[outv][outa]")]
    (str part-list "; " part-index part-concat)))

(defn trim-out-path [file-name decibel]
  (out-file-path file-name (str "out-db" decibel ".mp4")))

(defn ffmpeg-trim-cli [file-name decibel]
  (let [time-list (valid-time file-name decibel)
        in-path   (in-file-path file-name)
        out-path  (trim-out-path file-name decibel)]
    ["ffmpeg"
     "-i" in-path
     "-filter_complex" (ffmpeg-filter time-list)
     "-map" "[outv]"
     "-map" "[outa]"
     out-path]))

(defn trim-video [file-name decibel]
  (let [total-time (time-length file-name decibel)
        out-path   (trim-out-path file-name decibel)
        exist?     (.exists (io/file out-path))]
    (when (false? exist?)
      (future
        (-> (do (prn "[START] trim-video / total-time:" total-time " / out-file:" out-path)
                (apply sh (ffmpeg-trim-cli file-name decibel))
                (prn "[END] time-video"))
            time)))))

'Programming > Clojure' 카테고리의 다른 글

4Clojure - #32 Duplicate a Sequence  (0) 2014.05.12

댓글