Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beat detection without scheduling etc. #6

Open
RobAley opened this issue Jul 17, 2019 · 3 comments
Open

Beat detection without scheduling etc. #6

RobAley opened this issue Jul 17, 2019 · 3 comments

Comments

@RobAley
Copy link

RobAley commented Jul 17, 2019

Hi,

I am trying to get the timing of beats in an MP3 file to auto-generate "step" files for music games like stepmania. Essentially, I want to quickly analyse the mp3 file and get the list of pos/bpm for each beat, without having to "play" the whole song. The code I am trying is this :

const fs = require('fs')
const Speaker = require('speaker')
const createMusicStream = require('create-music-stream')
const {MusicBeatDetector} = require('music-beat-detector')

const musicSource = process.argv[2]

const musicBeatDetector = new MusicBeatDetector({
  sensitivity : 1
})

var dump = fs.createWriteStream('/dev/null');

createMusicStream(musicSource)
  .pipe(musicBeatDetector.getAnalyzer())
  .on('peak-detected', (pos, bpm) => {
    console.log(`peak-detected at ${pos}, detected bpm ${bpm}`)
  }
)
.pipe(dump)

This does what I want and processes the file in a couple of seconds, however once we get to "Mp3 read stream: 100%" (printed twice, for some reason) the script hangs in silence for what I think is the duration of the actual mp3. I assume this is because the file is being piped at normal speed through to /dev/null as if it were being played. Is there anyway to actually just do the analysis without the extra "playing" step?

Thanks for a great lib.

@RobAley
Copy link
Author

RobAley commented Jul 18, 2019

Actually, delving further, it looks like this is cause by MusicBeatDetector not closing the stream when lame has finished sending data (I think, I'm not too hot on node streams), piping lame directly to a writable stream finishes straight away, but not with MusicBeatDetector in the middle. I think the pipe maybe times out or ends for another reason (rather than it being related to the "time" of the song), if piping to speaker this isn't an issue as it finishes before the speaker does for a typical song.

@RobAley
Copy link
Author

RobAley commented Jul 18, 2019

Ok, more digging and I've realised this is a duplicate of issue #2, the song had silence at the end and it was not finishing processing (and I had only been trying this out with one song so i hadn't noticed that it worked for others!). Specifically, _analyzeBuffer takes an age to finish (or causes an out of memory error), which is why the stream doesn't complete until after it has. The fix suggested (changing SAMPLES_WINDOW = FREQ * 0.5) works, but gives a different set of detected beats than * 1.5.

@RobAley
Copy link
Author

RobAley commented Jul 18, 2019

Yet more digging - the issue is that with the silence at the end, filteredLeft in _analyseBuffer is 0 (zero). Passing 0 to slidingWindowMax.add seems to make it hang. So it looks like the correct fix is either to fix slidingWindowMax to process 0 correctly (I've had a look and I can't get my head around it!) or to prevent filteredLeft from being 0 (if that is an incorrect value). For the moment, to work around it I have
changed _isPeak to add an if (sample) { clause as follows :

  _isPeak (sample) {
    let isPeak = false
    if (sample) {
      this.threshold = Math.max(
        this.slidingWindowMax.add(sample) * this.sensitivity,
        this.minThreashold
      )
      
      const overThreshold = sample >= this.threshold
      const enoughTimeSinceLastPeak = this.lastPeakDistance > MIN_PEAK_DISTANCE
      
      if (overThreshold && enoughTimeSinceLastPeak) {
        this.bpm = Math.round(60 * FREQ / this.lastPeakDistance)
        this.lastPeakDistance = 0
        return true
      }
    }
    if (this.plotter) {
      this.plotter({sample, threshold: this.threshold, lastPeakDistance: this.lastPeakDistance})
    }
    
    this.pos++
    this.lastPeakDistance++
    if (this.lastPeakDistance > MAX_UINT32) this.lastPeakDistance = MAX_UINT32
    
    return false
  }

This isn't an elegant solution, but it leaves the rest of the code (this.pos++) intact as that still needs to be in place to get the correct output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant