有Linux的节拍检测软件吗?[关闭]


29

Amarok 2可以使用ID3v2标签的“ bpm”字段搜索音乐收藏。这将是非常不错的重新标记整个音乐收藏,我能找到我喜欢这首歌的“心情”。

但是,我还没有找到任何可以帮助我的节拍检测软件。你曾经用过吗?CLI,最好。我也很感兴趣是否有相同的标记“ bpm”字段的FLAC。

谢谢!:)

PS我知道这里有一个不错的心情栏功能,但是它对搜索毫无用处。


3
你看过这个页面吗?mmartins.com/mmartins/bpmdetection/bpmdetection.asp 似乎正是您要寻找的东西。
DaveParillo '04

@DaveParillo,“音轨气氛”链接是指向您硬盘的链接,因此对您来说没有任何意义
Justin Smith,2010年

@Justin Smith,他的意思是BpmDj docs中的文件:)这是在线版本:bpmdj.yellowcouch.org/clustering.html
kolypto 2010年

@贾斯汀-抱歉-抽搐的触发手指,我想。
DaveParillo 2010年

Answers:


17

在站点DaveParillo建议我找到了BpmDj项目。它有一个bpmcount可执行文件,可以很好地计算bpm:它可以处理mp3和flac:

161.135 Metallica/2008 - Death Magnetic/01-That Was Just Your Life.flac
63.5645 Doom3.mp3

剩下的唯一事情就是重新标记集合。每当我成功时,我都会更新此答案。谢谢!:)


步骤1

bpmcount针对整个集合运行并将结果存储到文本文件中。问题在于,它bpmcount不时崩溃,并在处理多个文件时尝试占用多达2GB的内存,因此我们应该逐一提供文件名。像这样:

musicdir='/home/ootync/music'
find "$musicdir" -iregex ".*\.\(mp3\|ogg\|flac\|ape\)" -exec bpmcount {} \; \
    | fgrep "$musicdir" > "$musicdir/BPMs.txt"

第2步

我们需要一些其他软件包:apt-get install vorbis-tools flac python-mutagen。现在看看如何添加'bpm'标签:

mid3v2 --TBPM 100 doom3.mp3
vorbiscomment -a -t "BPM=100" mother.ogg
metaflac --set-tag="BPM=100" metallica.flac

las,我没有* .ape曲目

现在我们有了BPM,应该重新标记整个集合。这是脚本:

cat "$musicdir/BPMs.txt" | while read bpm file ; do
    bpm=`printf "%.0f" "$bpm"` ;
    case "$file" in 
        *.mp3) mid3v2 --TBPM "$bpm" "$file" > /dev/null ;; 
        *.ogg) vorbiscomment -a -t "BPM=$bpm" "$file" ;; 
        *.flac) metaflac --set-tag="BPM=$bpm" "$file" ;; 
        esac
    done

重新访问步骤2.1 这是一个将BPM标签添加到您的收藏的脚本。

每个CPU内核运行一个进程,以加快进程速度。此外,它不使用临时文件,并且能够检测文件是否已被标记。

此外,我发现FLAC有时同时具有ID3和VorbisComment。此脚本会同时更新两者。

#!/bin/bash

function display_help() {
    cat <<-HELP
            Recursive BPM-writer for multicore CPUs.
            It analyzes BPMs of every media file and writes a correct tag there.
            Usage: $(basename "$0") path [...]
            HELP
    exit 0
    }

[ $# -lt 1 ] && display_help

#=== Requirements
requires="bpmcount mid3v2 vorbiscomment metaflac"
which $requires > /dev/null || { echo "E: These binaries are required: $requires" >&2 ; exit 1; }

#=== Functions

function bpm_read(){
    local file="$1"
    local ext="${file##*.}"
    declare -l ext
    # Detect
    { case "$ext" in
        'mp3')  mid3v2 -l "$file" ;;
        'ogg')  vorbiscomment -l "$file" ;;
        'flac') metaflac --export-tags-to=- "$file" ;;
        esac ; } | fgrep 'BPM=' | cut -d'=' -f2
    }
function bpm_write(){
    local file="$1"
    local bpm="${2%%.*}"
    local ext="${file##*.}"
    declare -l ext
    echo "BPM=$bpm @$file"
    # Write
    case "$ext" in
        'mp3')  mid3v2 --TBPM "$bpm" "$file" ;;
        'ogg')  vorbiscomment -a -t "BPM=$bpm" "$file" ;;
        'flac') metaflac --set-tag="BPM=$bpm" "$file"
                mid3v2 --TBPM "$bpm" "$file" # Need to store to ID3 as well :(
                ;;
        esac
    }

#=== Process
function oneThread(){
    local file="$1"
    #=== Check whether there's an existing BPM
        local bpm=$(bpm_read "$file")
        [ "$bpm" != '' ] && return 0 # there's a nonempty BPM tag
    #=== Detect a new BPM
    # Detect a new bpm
    local bpm=$(bpmcount "$file" | grep '^[0-9]' | cut -f1)
    [ "$bpm" == '' ] && { echo "W: Invalid BPM '$bpm' detected @ $file" >&2 ; return 0 ; } # problems
    # Write it
    bpm_write "$file" "${bpm%%.*}" >/dev/null
    }

NUMCPU="$(grep ^processor /proc/cpuinfo | wc -l)"
find $@ -type f -regextype posix-awk -iregex '.*\.(mp3|ogg|flac)' \
    | while read file ; do
        [ `jobs -p | wc -l` -ge $NUMCPU ] && wait
        echo "$file"
        oneThread "$file" &
        done

请享用!:)


优秀!昨晚我还没有去尝试这个。至于命令行标记,请尝试mid3v2: linux.die.net/man/1/mid3v2,至少在Ex F也支持命令行编辑之前才可用。id3v2提示ID是TBPM
DaveParillo

1
谢谢,我将尝试几天,然后发布结果:)我想知道FLAC是否支持这样的事情:我必须检查一下。
kolypto 2010年

1
做好第二步的工作。希望我能投票两次!
DaveParillo 2010年

1
谢谢:) A,我的Amarok没有注意到我最喜欢的FLAC中的新标签:))提交的错误。
kolypto

您是如何安装的?他们提供的rpm在我的计算机上似乎不起作用,我正在努力进行编译。
pedrosaurio


6

我使用了kolypto的原始脚本,bpmcount并将其重写为bpm-tag(的实用性bpm-tools),我在安装时比较幸运。我也做了一些改进。

您可以在GitHub https://github.com/meridius/bpmwrap上找到它


这需要在Mac上进行一些修改,才能在Mac上运行,我已将其包含在下面的答案中(因为评论时间太长)
Adrian

2

我不知道能完全满足您需求的工具,但我玩过MusicIP

使用linux / java版本-完全分析音乐库需要很长时间,但确实可以工作。您可以找到与其他歌曲相似的歌曲。您可以右键单击生成的播放列表,然后选择选项以选择更多或更少的歌曲(如所选歌曲)。您还可以选择消除某些流派。挺酷的,但是哇哇哇,我停止使用它了。

免费版以m3u格式导出播放列表,最多可以播放75首歌曲。

目前尚不支持它,但我认为他们已尝试将其商业化为Predexis


1

虽然它不只是您说要寻找的工具,但Banshee媒体播放器可以检测bpm。

我使用Banshee进行所有音乐播放,组织和与便携式播放器同步。我没有隶属关系,但是我尽力尝试了该程序。它还可以根据曲目的各种属性(包括bpm)生成“智能播放列表”。

有一个扩展程序可以分析有关歌曲的各种内容,并找到与您正在播放的歌曲相似的歌曲。它叫做Mirage,我使用了一段时间,但是我不再使用了,因为我创建了许多适合各种心情的播放列表(根据Mirage不一定类似)。

我不知道Banshee是否会将检测到的bpm保存回文件的ID3v2“ bpm”标记中。如果有人知道如何从程序外部轻松检查bpm标记,我将进行检查。



0

我找到了另一种使用正确的BPM值标记MP3文件的工具。

它称为BPMDetect。开源。QT库在Gnome下运行良好。带有GUI,但可以编译为仅控制台版本(如readme.txt中所述,运行“ scons console = 1”)。

否则,最后,我也使用了BpmDJ的“ bpmcount”,因为我很难在64位Ubuntu主机上编译BPMDetect(由于fmodex依赖性)。因此,我采用了上面的(非常酷且写得很好)的shell脚本(请参见下文),它是从BpmDJ网站上提供的[x64 .rpm] [3]中提取的“ bpmcount”二进制文件(我刚刚提取了.rpm与

pm2cpio bpmdj-4.2.pl2-0.x86_64.rpm|cpio -idv

它就像一种魅力。我只需要修改上面的脚本,因为它开箱即用,对我而言不起作用(bpmcount二进制文件的stdout / stderr问题)。我的修改是关于文件重定向的:

local bpm=$(bpmcount "$file" 3>&1 1>/dev/null 2>&3 | grep '^[0-9]' | cut -f1)

0

这个问题有关stackoverflow的另一个推荐工具是aubio,它与python模块一起提供。

我没有尝试过,因为我有点忙于编译BpmDj。万一其他人在尝试时遇到类似的麻烦,我强烈建议您绝对确定:

  1. 下载了最新版本的BpmDj源
  2. 安装了适当的Boost库

随着最新的g ++编译器升级,似乎出现了一些问题,尤其是有关最新的debian和ubuntu版本。当他意识到这些问题后,作者便乐于解决新出现的不兼容问题,并提出了一个新版本,该版本现在可以像魅力一样进行编译。因此,最近因无休止的编译错误而陷入绝望的任何人:您现在就救了。

@ mmx,您的工具看起来也不错,但是它们依赖SoX,默认情况下没有mp3功能。因此,他们要求首先使用Lame / MAD支持编译SoX,但不幸的是,对于像我这样的懒人来说,这是太多的工作。


0

要在Mac上使用@meridius的解决方案,我必须做一些额外的工作,并稍微修改一下脚本:

# Let's install bpm-tools
git clone http://www.pogo.org.uk/~mark/bpm-tools.git
cd bpm-tools
make && make install
# There will be errors, but they did not affect the result

# The following three lines could be replaced by including this directory in your $PATH
ln -s <absolute path to bpm-tools>/bpm /usr/local/bin/bpm
ln -s <absolute path to bpm-tools>/bpm-tag /usr/local/bin/bpm-tag
ln -s <absolute path to bpm-tools>/bpm-graph /usr/local/bin/bpm-graph
cd ..

# Time to install a bunch of GNU tools
# Not all of these packages are strictly necessary for this script, but I decided I wanted the whole GNU toolchain in order to avoid this song-and-dance in the future
brew install coreutils findutils gnu-tar gnu-sed gawk gnutls gnu-indent gnu-getopt bash flac vorbis-tools
brew tap homebrew/dupes; brew install grep

# Now for Mutagen (contains mid3v2)
git clone https://github.com/nex3/mutagen.git
cd mutagen
./setup.py build
sudo ./setup.py install
# There will be errors, but they did not affect the result
cd ..

然后,我不得不修改脚本以指向所有内容的GNU版本,以及其他一些调整:

#!/usr/local/bin/bash

# ================================= FUNCTIONS =================================

function help() {
    less <<< 'BPMWRAP

Description:
    This BASH script is a wrapper for bpm-tag utility of bpm-tools and several
    audio tagging utilities. The purpose is to make BPM (beats per minute)
    tagging as easy as possible.
    Default behaviour is to look through working directory for *.mp3 files
    and compute and print their BPM in the following manner:
        [current (if any)] [computed] [filename]

Usage:
    bpmwrap [options] [directory or filenames]

Options:
    You can specify files to process by one of these ways:
        1) state files and/or directories containing them after options
        2) specify --import file
        3) specify --input file
    With either way you still can filter the resulting list using --type option(s).
    Remember that the script will process only mp3 files by default, unless
    specified otherwise!

    -i, --import file
        Use this option to set BPM tag for all files in given file instead of
        computing it. Expected format of every row is BPM number and absolute path
        to filename separated by semicolon like so:
            145;/home/trinity/music/Apocalyptica/07 beyond time.mp3
        Remember to use --write option too.
    -n, --input file
        Use this option to give the script list of FILES to process INSTEAD of paths
        where to look for them. Each row whould have one absolute path.
        This will bypass the searching part and is that way useful when you want
        to process large number of files several times. Like when you are not yet
        sure what BPM limits to set. Extension filtering will still work.
    -o, --output file
        Save output also to a file.
    -l, --list-save file
        Save list of files about to get processed. You can use this list later
        as a file for --input option.
    -t, --type filetype
        Extension of file type to work with. Defaults to mp3. Can be specified
        multiple times for more filetypes. Currently supported are mp3 ogg flac.
    -e, --existing-only
        Only show BPM for files that have it. Do NOT compute new one.
    -w, --write
        Write computed BPM to audio file but do NOT overwrite existing value.
    -f, --force
        Write computed BPM to audio file even if it already has one. Aplicable only
        with --write option.
    -m, --min minbpm
        Set minimal BPM to look for when computing. Defaults to bpm-tag minimum 84.
    -x, --max maxbpm
        Set maximal BPM to look for when computing. Defaults to bpm-tag maximum 146.
    -v, --verbose
        Show "progress" messages.
    -c, --csv-friendly
        Use semicolon (;) instead of space to separate output columns.
    -h, --help
        Show this help.

Note:
    Program bpm-tag (on whis is this script based) is looking only for lowercase
    file extensions. If you get 0 (zero) BPM, this should be the case. So just
    rename the file.

License:
    GPL V2

Links:
    bpm-tools (http://www.pogo.org.uk/~mark/bpm-tools/)

Dependencies:
    bpm-tag mid3v2 vorbiscomment metaflac

Author:
    Martin Lukeš (martin.meridius@gmail.com)
    Based on work of kolypto (http://superuser.com/a/129157/137326)
    '
}

# Usage: result=$(inArray $needle haystack[@])
# @param string needle
# @param array haystack
# @returns int (1 = NOT / 0 = IS) in array
function inArray() {
    needle="$1"
    haystack=("${!2}")
    out=1
    for e in "${haystack[@]}" ; do
        if [[ "$e" = "$needle" ]] ; then
            out=0
            break
        fi
    done
    echo $out
}

# Usage: result=$(implode $separator array[@])
# @param char separator
# @param array array to implode
# @returns string separated array elements
function implode() {
    separator="$1"
    array=("${!2}")
    IFSORIG=$IFS
    IFS="$separator"
    echo "${array[*]}"
    IFS=$IFSORIG
}

# @param string file
# @returns int BPM value
function getBpm() {
    local file="$1"
    local ext="${file##*.}"
    declare -l ext # convert to lowercase
    { case "$ext" in
        'mp3')  mid3v2 -l "$file" ;;
        'ogg')  vorbiscomment -l "$file" ;;
        'flac') metaflac --export-tags-to=- "$file" ;;
    esac ; } | fgrep 'BPM=' -a | cut -d'=' -f2
}

# @param string file
# @param int BPM value
function setBpm() {
    local file="$1"
    local bpm="${2%%.*}"
    local ext="${file##*.}"
    declare -l ext # convert to lowercase
    case "$ext" in
        'mp3')  mid3v2 --TBPM "$bpm" "$file" ;;
        'ogg')  vorbiscomment -a -t "BPM=$bpm" "$file" ;;
        'flac') metaflac --set-tag="BPM=$bpm" "$file"
            mid3v2 --TBPM "$bpm" "$file" # Need to store to ID3 as well :(
        ;;
    esac
}

# # @param string file
# # @returns int BPM value
function computeBpm() {
    local file="$1"
    local m_opt=""
    [ ! -z "$m" ] && m_opt="-m $m"
    local x_opt=""
    [ ! -z "$x" ] && x_opt="-x $x"
    local row=$(bpm-tag -fn $m_opt $x_opt "$file" 2>&1 | fgrep "$file")
    echo $(echo "$row" \
        | gsed -r 's/.+ ([0-9]+\.[0-9]{3}) BPM/\1/' \
        | gawk '{printf("%.0f\n", $1)}')
}

# @param string file
# @param int file number
# @param int BPM from file list given by --import option
function oneThread() {
    local file="$1"
    local filenumber="$2"
    local bpm_hard="$3"
    local bpm_old=$(getBpm "$file")
    [ -z "$bpm_old" ] && bpm_old="NONE"
    if [ "$e" ] ; then # only show existing
        myEcho "$filenumber/$NUMFILES${SEP}$bpm_old${SEP}$file"
    else # compute new one
        if [ "$bpm_hard" ] ; then
            local bpm_new="$bpm_hard"
        else
            local bpm_new=$(computeBpm "$file")
        fi
        [ "$w" ] && { # write new one
            if [[ ! ( ("$bpm_old" != "NONE") && ( -z "$f" ) ) ]] ; then
                setBpm "$file" "$bpm_new"
            else
                [ "$v" ] && myEcho "Non-empty old BPM value, skipping ..."
            fi
        }
        myEcho "$filenumber/$NUMFILES${SEP}$bpm_old${SEP}$bpm_new${SEP}$file"
    fi
}

function myEcho() {
    [ "$o" ] && echo -e "$1" >> "$o"
    echo -e "$1"
}


# ================================== OPTIONS ==================================

eval set -- $(/usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt -n $0 -o "-i:n:o:l:t:ewfm:x:vch" \
    -l "import:,input:,output:,list-save:,type:,existing-only,write,force,min:,max:,verbose,csv-friendly,help" -- "$@")

declare i n o l t e w f m x v c h
declare -a INPUTFILES
declare -a INPUTTYPES
while [ $# -gt 0 ] ; do
    case "$1" in
        -i|--import)                shift ; i="$1" ; shift ;;
        -n|--input)                 shift ; n="$1" ; shift ;;
        -o|--output)                shift ; o="$1" ; shift ;;
        -l|--list-save)         shift ; l="$1" ; shift ;;
        -t|--type)                  shift ; INPUTTYPES=("${INPUTTYPES[@]}" "$1") ; shift ;;
        -e|--existing-only) e=1 ; shift ;;
        -w|--write)                 w=1 ; shift ;;
        -f|--force)                 f=1 ; shift ;;
        -m|--min)                       shift ; m="$1" ; shift ;;
        -x|--max)                       shift ; x="$1" ; shift ;;
        -v|--verbose)               v=1 ; shift ;;
        -c|--csv-friendly)  c=1 ; shift ;;
        -h|--help)                  h=1 ; shift ;;
        --)                                 shift ;;
        -*)                                 echo "bad option '$1'" ; exit 1 ;; #FIXME why this exit isn't fired?
        *)                                  INPUTFILES=("${INPUTFILES[@]}" "$1") ; shift ;;
    esac
done


# ================================= DEFAULTS ==================================

#NOTE Remove what requisities you don't need but don't try to use them after!
#         always  mp3/flac     ogg       flac
REQUIRES="bpm-tag mid3v2 vorbiscomment metaflac"
which $REQUIRES > /dev/null || { myEcho "These binaries are required: $REQUIRES" >&2 ; exit 1; }

[ "$h" ] && {
    help
    exit 0
}

[[ $m && $x && ( $m -ge $x ) ]] && {
    myEcho "Minimal BPM can't be bigger than NOR same as maximal BPM!"
    exit 1
}
[[ "$i" && "$n" ]] && {
    echo "You cannot specify both -i and -n options!"
    exit 1
}
[[ "$i" && ( "$m" || "$x" ) ]] && {
    echo "You cannot use -m nor -x option with -i option!"
    exit 1
}
[ "$e" ] && {
    [[ "$w" || "$f" ]] && {
        echo "With -e option you don't have any value to write!"
        exit 1
    }
    [[ "$m" || "$x" ]] && {
        echo "With -e option you don't have any value to count!"
        exit 1
    }
}

for file in "$o" "$l" ; do
    if [ -f "$file" ] ; then
        while true ; do
            read -n1 -p "Do you want to overwrite existing file ${file}? (Y/n): " key
            case "$key" in
                y|Y|"") echo "" > "$file" ; break ;;
                n|N)        exit 0 ;;
            esac
            echo ""
        done
        echo ""
    fi
done

[ ${#INPUTTYPES} -eq 0 ] && INPUTTYPES=("mp3")

# NUMCPU="$(ggrep ^processor /proc/cpuinfo | wc -l)"
NUMCPU="$(sysctl -a | ggrep machdep.cpu.core_count | gsed -r 's/(.*)([0-9]+)(.*)/\2/')"
LASTPID=0
TYPESALLOWED=("mp3" "ogg" "flac")
# declare -A BPMIMPORT # array of BPMs from --import file, keys are file names
declare -A BPMIMPORT # array of BPMs from --import file, keys are file names

for type in "${INPUTTYPES[@]}" ; do
    [[ $(inArray $type TYPESALLOWED[@]) -eq 1 ]] && {
        myEcho "Filetype $type is not one of allowed types (${TYPESALLOWED[@]})!"
        exit 1
    }
done

### here are three ways how to pass files to the script...
if [ "$i" ] ; then # just parse given file list and set BPM to listed files
    if [ -f "$i" ] ; then
        # myEcho "Setting BPM tags from given file ..."
        while read row ; do
            bpm="${row%%;*}"
            file="${row#*;}"
            ext="${file##*.}"
            ext="${ext,,}" # convert to lowercase
            if [ -f "$file" ] ; then
                if [ $(inArray $ext INPUTTYPES[@]) -eq 0 ] ; then
                    FILES=("${FILES[@]}" "$file")
                    BPMIMPORT["$file"]="$bpm"
                else
                    myEcho "Skipping file on row $rownumber (unwanted filetype $ext) ... $file"
                fi
            else
                myEcho "Skipping non-existing file $file"
            fi
        done < "$i"
    else
        myEcho "Given import file does not exists!"
        exit 1
    fi
elif [ "$n" ] ; then # get files from file list
    if [ -f "$n" ] ; then
        rownumber=1
        while read file ; do
            if [ -f "$file" ] ; then
                ext="${file##*.}"
                ext="${ext,,}" # convert to lowercase
                if [ $(inArray $ext INPUTTYPES[@]) -eq 0 ] ; then
                    FILES=("${FILES[@]}" "$file")
                else
                    myEcho "Skipping file on row $rownumber (unwanted filetype $ext) ... $file"
                fi
            else
                myEcho "Skipping file on row $rownumber (non-existing) ... $file"
            fi
            let rownumber++
        done < "$n"
        unset rownumber
    else
        myEcho "Given input file $n does not exists!"
        exit 1
    fi
else # get files from given parameters
    [ ${#INPUTFILES[@]} -eq 0 ] && INPUTFILES=`pwd`
    for file in "${INPUTFILES[@]}" ; do
        [ ! -e "$file" ] && {
            myEcho "File or directory $file does not exist!"
            exit 1
        }
    done
    impl_types=`implode "|" INPUTTYPES[@]`
    while read file ; do
        echo -ne "Creating list of files ... (${#FILES[@]}) ${file}\033[0K"\\r
        FILES=("${FILES[@]}" "$file")
    done < <(gfind "${INPUTFILES[@]}" -type f -regextype posix-awk -iregex ".*\.($impl_types)")
    echo -e "Counted ${#FILES[@]} files\033[0K"\\r
fi

[ "$l" ] && printf '%s\n' "${FILES[@]}" > "$l"

NUMFILES=${#FILES[@]}
FILENUMBER=1

[ $NUMFILES -eq 0 ] && {
    myEcho "There are no ${INPUTTYPES[@]} files in given files/paths."
    exit 1
}

declare SEP=" "
[ "$c" ] && SEP=";"


# =============================== MAIN SECTION ================================

if [ "$e" ] ; then # what heading to show
    myEcho "num${SEP}old${SEP}filename"
else
    myEcho "num${SEP}old${SEP}new${SEP}filename"
fi

for file in "${FILES[@]}" ; do
    [ `jobs -p | wc -l` -ge $NUMCPU ] && wait
    [ "$v" ] && myEcho "Parsing (${FILENUMBER}/${NUMFILES})\t$file ..."
    oneThread "$file" "$FILENUMBER" "${BPMIMPORT[$file]}" &
    LASTPID="$!"
    let FILENUMBER++
done

[ "$v" ] && myEcho "Waiting for last process ..."
wait $LASTPID
[ "$v" ] && myEcho \\n"DONE"

感谢您的辛勤工作@kolypto和@meridius。

...我为维护CLI工作流程而付出的辛苦,不花钱购买音乐工具...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.