1

Given the following directory structure...

my-project
    + frames
         + frame-1.jpg
         + frame-2.jpg
         ...
         + frame-200.jpg
    + locales
         + default
         |   + music.mp3
         + en
             + music.mp3

For each locale in directory locales (default and en) I need to:

  • call ffprobe to determine the DURATION in seconds of the audio file (*.mp3);
  • determine the number of jpg frames that form the final video, which is used for all the locales;
  • calculate the FRAMERATE (FRAMERATE = 1 / (DURATION / FRAMES));
  • call ffmpeg to create an mp4 video out of the jpg frames and the audio file, using the calculated FRAMERATE.

Here below is an extract of my Makefile:

.DEFAULT_GOAL := build

FRAMES_DIR := ${CURDIR}/frames
LOCALES_DIR := ${CURDIR}/locales
OUTDIR:= ${CURDIR}/build

.PHONY: build

build:
    @for dir in $(shell find $(LOCALES_DIR) -type d -depth 1 -exec base name {} \;); do
        $(eval DURATION := $(shell ffprobe -v error -show_entries format=duration -of \
            default=noprint_wrappers=1:nokey=1 $(LOCALES_DIR)/$$dir/*.mp3)); \
        $(eval FRAMES := $(shell ls $(FRAMES_DIR) | wc -l))
        $(eval FRAMERATE := $(shell awk "BEGIN {print 1 / ($(DURATION) / $(FRAMES))}"))
        ffmpeg -y -framerate $(FRAMERATE) -start_number 1 -i $(FRAMES_DIR)/frame_%d.jpeg -i $(LOCALES_DIR)/$$dir/*.mp3 \
           -c:v libx264 -pix_fmt yuv420p -c:a aac -strict experimental \
           -shortest $(OUTDIR)/$video-$$dir.mp4; \
    done;

And here's the expected result:

my-project
    + build
         + video-default.mp4
         + video-en.mp4

The problem is that the ffprobe command fails as the input path is wrong, i.e. $$dir does not contain the expected subdirectory name (default or en). Long story short, it seems $$dir is not visible inside $(shell ...).

Am I missing something? Thanks.

j3d
  • 9,492
  • 22
  • 88
  • 172
  • `for` is executed by the shell, which is invoked by make. `$(eval)` (and the `$(shell)` inside it) is executed, _once_, by make, well before the shell is executed. – Biffen Feb 16 '21 at 13:20
  • Can you tell us more about what you're trying to do? I'm pretty sure the construct you've got here isn't going to do what you want. – larsks Feb 16 '21 at 13:21
  • Ah I see... and what's the usual way of dealing with such a case? – j3d Feb 16 '21 at 13:23
  • 1
    I don’t know what you think using make functions mixed with shell commands is supposed to achieve; it looks like you’re trying to assign the output of a command to a variable and then print that variable. Why not simply `for dir in $(find ...) ; do ffprobe ... ; done`? Or even `find ... -exec ffprobe ...` (which won’t fail on paths with spaces)? – Biffen Feb 16 '21 at 13:23
  • Let me add more info to the question :-) – j3d Feb 16 '21 at 13:31
  • OK, just explained the full use case in the question. Thank you very much :-) – j3d Feb 16 '21 at 14:22
  • I think a better and simpler approach would be to just have a multiline shell command for your target. To use a multiline shell command, take a look at https://stackoverflow.com/questions/10121182/multi-line-bash-commands-in-makefile – Shane Bishop Feb 16 '21 at 16:57

1 Answers1

4

Try using a multiline shell script, as below:

build:
    find "${LOCALES_DIR}" -type d -depth 1 -exec base name {} \; | while read -r dir; \
    do \
        DURATION="$$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${LOCALES_DIR}/$$dir"/*.mp3)"; \
        FRAMES="$$(ls -1 "$$FRAMES_DIR" | wc -l)"; \
        FRAMERATE="$$(awk "BEGIN {print 1 / ($$DURATION / $$FRAMES)}")"; \
        ffmpeg -y -framerate "$$FRAMERATE" -start_number 1 -i "$$FRAMES_DIR"/frame_%d.jpeg -i "${LOCALES_DIR}/$$dir"/*.mp3 \
               -c:v libx264 -pix_fmt yuv420p -c:a aac -strict experimental \
               -shortest "${OUTDIR}"/$$video-"$$dir".mp4 < /dev/null; \
    done;

I made some changes:

  • I used a while read loop to avoid problems with paths with spaces
  • I added -1 to ls so that each item is printed on its own line, which should ensure wc -l gives an accurate count
  • I redirect /dev/null as stdin to ffmpeg so that ffmpeg can't interfere with the while read loop
Shane Bishop
  • 3,905
  • 4
  • 17
  • 47
  • Thanks for the example. Unfortunately it doesn't work... perhu there's an issue with the bash/sh shell on mac os. – j3d Feb 16 '21 at 18:26
  • What error do you get? Note that you never defined `video`. – Shane Bishop Feb 16 '21 at 18:58
  • I forgot to escape `$` in the script by using `$$`. I've updated my answer. – Shane Bishop Feb 16 '21 at 19:04
  • The body of the loop works... but `find "$$LOCALES_DIR" -type d ...` doesn't work because `LOCALES_DIR` is empty. If I change it to `find $(LOCALES_DIR) -type d ...` then `find` works but `LOCALES_DIR` cannot be referenced inside the loop. – j3d Feb 16 '21 at 20:15
  • Finally it worked... I had to modify like this: ````DURATION="$$(ffprobe -v ... "${LOCALES_DIR}"/"$$dir"/*.mp3)"; \```` Thanks for your valuable support :-) – j3d Feb 16 '21 at 20:23
  • Thanks @j3d. I updated my answer with your suggested fixes. – Shane Bishop Feb 17 '21 at 03:09