UNPKG

musicvis-lib

Version:

Music analysis and visualization library

287 lines (278 loc) 11.6 kB
import { randomNormal, randomInt, group } from 'd3' import { choose, randFloat } from '../utils/MathUtils.js' import Note from '../types/Note.js' /** * @module instruments/Drums */ /** * Drum parts * * @type {object} */ export const DRUM_PARTS = { Agogo: 'DRUM_PARTS.Agogo', Cabasa: 'DRUM_PARTS.Cabasa', Castanets: 'DRUM_PARTS.Castanets', China: 'DRUM_PARTS.China', Claves: 'DRUM_PARTS.Claves', Conga: 'DRUM_PARTS.Conga', Cowbell: 'DRUM_PARTS.Cowbell', Crash: 'DRUM_PARTS.Crash', Cuica: 'DRUM_PARTS.Cuica', Cymbal: 'DRUM_PARTS.Cymbal', Golpe: 'DRUM_PARTS.Golpe', Grancassa: 'DRUM_PARTS.Grancassa', Guiro: 'DRUM_PARTS.Guiro', Hand_Clap: 'DRUM_PARTS.Hand Clap', Hand: 'DRUM_PARTS.Hand', Hi_Hat: 'DRUM_PARTS.Hi Hat', High_Floor_Tom: 'DRUM_PARTS.High Floor Tom', High_Tom: 'DRUM_PARTS.High Tom', Kick: 'DRUM_PARTS.Kick', Low_Floor_Tom: 'DRUM_PARTS.Low Floor Tom', Low_Tom: 'DRUM_PARTS.Low Tom', Mid_Tom: 'DRUM_PARTS.Mid Tom', Pedal_Hi_Hat: 'DRUM_PARTS.Pedal Hi Hat', Ride: 'DRUM_PARTS.Ride', Right_Maraca: 'DRUM_PARTS.Right Maraca', Shaker: 'DRUM_PARTS.Shaker', Snare: 'DRUM_PARTS.Snare', Splash: 'DRUM_PARTS.Splash', Surdo: 'DRUM_PARTS.Surdo', Timbale: 'DRUM_PARTS.Timbale', Tinkle_Bell: 'DRUM_PARTS.Tinkle Bell', Triangle: 'DRUM_PARTS.Triangle', Vibraslap: 'DRUM_PARTS.Vibraslap', Whistle_high: 'DRUM_PARTS.Whistle high', Whistle_low: 'DRUM_PARTS.Whistle low', Woodblock_high: 'DRUM_PARTS.Woodblock high', Woodblock_low: 'DRUM_PARTS.Woodblock low' } /** * Drum actions * * @type {object} */ export const DRUM_ACTIONS = { choke: 'DRUM_ACTIONS.choke', finger: 'DRUM_ACTIONS.finger', hit: 'DRUM_ACTIONS.hit', mute: 'DRUM_ACTIONS.mute', return: 'DRUM_ACTIONS.return', scrapReturn: 'DRUM_ACTIONS.scrapReturn', sideStick: 'DRUM_ACTIONS.sideStick', slap: 'DRUM_ACTIONS.slap' } /** * Drum parts and actions * * @todo still incomplete * @type {object} */ export const DRUM_PARTS_ACTIONS = { Agogo_high_hit: 'DRUM_PARTS_ACTIONS.Agogo high (hit)', Agogo_low_hit: 'DRUM_PARTS_ACTIONS.Agogo low (hit)', Cabasa_return: 'DRUM_PARTS_ACTIONS.Cabasa (return)', Castanets_hit: 'DRUM_PARTS_ACTIONS.Castanets (hit)', China_choke: 'DRUM_PARTS_ACTIONS.China (choke)', Claves_hit: 'DRUM_PARTS_ACTIONS.Claves (hit)', Conga_high_slap: 'DRUM_PARTS_ACTIONS.Conga high (slap)', Conga_low_mute: 'DRUM_PARTS_ACTIONS.Conga low (mute)', Cowbell_high_tip: 'DRUM_PARTS_ACTIONS.Cowbell high (tip)', Crash_medium_choke: 'DRUM_PARTS_ACTIONS.Crash medium (choke)', Cuica_mute: 'DRUM_PARTS_ACTIONS.Cuica (mute)', Cuica_open: 'DRUM_PARTS_ACTIONS.Cuica (open)', Cymbal_hit: 'DRUM_PARTS_ACTIONS.Cymbal (hit)', Golpe_finger: 'DRUM_PARTS_ACTIONS.Golpe (finger)', Grancassa_hit: 'DRUM_PARTS_ACTIONS.Grancassa (hit)', Guiro_hit: 'DRUM_PARTS_ACTIONS.Guiro (hit)', Guiro_scrap_return: 'DRUM_PARTS_ACTIONS.Guiro (scrap-return)', Hand_Clap_hit: 'DRUM_PARTS_ACTIONS.Hand Clap (hit)', Hand_slap: 'DRUM_PARTS_ACTIONS.Hand (slap)', Hi_Hat_closed: 'DRUM_PARTS_ACTIONS.Hi-Hat (closed)', Hi_Hat_open: 'DRUM_PARTS_ACTIONS.Hi-Hat (open)', High_Floor_Tom_hit: 'DRUM_PARTS_ACTIONS.High Floor Tom (hit)', High_Tom_hit: 'DRUM_PARTS_ACTIONS.High Tom (hit)', Kick_hit: 'DRUM_PARTS_ACTIONS.Kick (hit)', Low_Floor_Tom_hit: 'DRUM_PARTS_ACTIONS.Low Floor Tom (hit)', Low_Tom_hit: 'DRUM_PARTS_ACTIONS.Low Tom (hit)', Mid_Tom_hit: 'DRUM_PARTS_ACTIONS.Mid Tom (hit)', Pedal_Hi_Hat_hit: 'DRUM_PARTS_ACTIONS.Pedal Hi-Hat (hit)', Ride_choke: 'DRUM_PARTS_ACTIONS.Ride (choke)', Right_Maraca_return: 'DRUM_PARTS_ACTIONS.Right Maraca (return)', Shaker_return: 'DRUM_PARTS_ACTIONS.Shaker (return)', Snare_hit: 'DRUM_PARTS_ACTIONS.Snare (hit)', Snare_side_stick: 'DRUM_PARTS_ACTIONS.Snare (side stick)', Splash_choke: 'DRUM_PARTS_ACTIONS.Splash (choke)', Surdo_hit: 'DRUM_PARTS_ACTIONS.Surdo (hit)', Surdo_mute: 'DRUM_PARTS_ACTIONS.Surdo (mute)', Timbale_high_hit: 'DRUM_PARTS_ACTIONS.Timbale high (hit)', Timbale_low_hit: 'DRUM_PARTS_ACTIONS.Timbale low (hit)', Tinkle_Bell_hit: 'DRUM_PARTS_ACTIONS.Tinkle Bell (hit)', Triangle_hit: 'DRUM_PARTS_ACTIONS.Triangle (hit)', Triangle_mute: 'DRUM_PARTS_ACTIONS.Triangle (mute)', Vibraslap_hit: 'DRUM_PARTS_ACTIONS.Vibraslap (hit)', Whistle_high_hit: 'DRUM_PARTS_ACTIONS.Whistle high (hit)', Whistle_low_hit: 'DRUM_PARTS_ACTIONS.Whistle low (hit)', Woodblock_high_hit: 'DRUM_PARTS_ACTIONS.Woodblock high (hit)', Woodblock_low_hit: 'DRUM_PARTS_ACTIONS.Woodblock low (hit)' } /** * Pitches that are mapped onto themselves are included for other information. * Millenium MPS-850 https://www.thomann.de/de/millenium_mps_850_e_drum_set.htm * * Notation info (line and shape of symbol) so drum notation can generate a lookup from this * https://en.wikipedia.org/wiki/Percussion_notation#/media/File:Sibelius_drum_legend.png * Lines start with 0 at the top above the top most horizontal notation line, * using incremental integers for every possible position, i.e. for on and between lines * * Legend: * Map key: The orignal pitch from the input data * repPitch: Replacement pitch, used to simplify multiple zones into one * zone: Zone of the instrument this pitch comes from * order: visual order ranking of this intrumentin the UI (top-bottom or left-right) * line: y position in the drum notation (using integers for every possible position) * shape: Note shape in notation: triangle, <>, x, o, ostroke, xstroke * label: Short label for this intrument * name: Full name of this instrument * * @type {Map<number,object>} */ export const drumPitchReplacementMapMPS850 = new Map([ // Crash 1 [49, { repPitch: 49, zone: 1, order: 10, line: -1, shape: 'o', label: 'CC1', name: 'Crash Cymbal 1' }], [55, { repPitch: 49, zone: 2, order: 11, line: -1, shape: 'o', label: 'CC1', name: 'Crash Cymbal 1' }], // Crash 2 [52, { repPitch: 57, zone: 1, order: 20, line: 0, shape: 'o', label: 'CC2', name: 'Crash Cymbal 2' }], [57, { repPitch: 57, zone: 2, order: 21, line: 0, shape: 'o', label: 'CC2', name: 'Crash Cymbal 2' }], // Hi-hat stick [22, { repPitch: 46, zone: 1, order: 30, line: 0, shape: '<>', label: 'HHS', name: 'Hi-Hat' }], [26, { repPitch: 46, zone: 2, order: 31, line: 0, shape: '<>', label: 'HHS', name: 'Hi-Hat' }], [42, { repPitch: 46, zone: 3, order: 32, line: 0, shape: '<>', label: 'HHS', name: 'Hi-Hat' }], [46, { repPitch: 46, zone: 4, order: 33, line: 0, shape: '<>', label: 'HHS', name: 'Hi-Hat' }], // Hi-hat pedal [44, { repPitch: 44, zone: 1, order: 40, line: 9, shape: 'o', label: 'HHP', name: 'Hi-Hat Pedal' }], // Ride [51, { repPitch: 51, zone: 1, order: 50, line: 1, shape: 'x', label: 'Rd', name: 'Ride Cymbal' }], [53, { repPitch: 51, zone: 2, order: 51, line: 1, shape: 'x', label: 'Rd', name: 'Ride Cymbal' }], [59, { repPitch: 51, zone: 3, order: 52, line: 1, shape: 'x', label: 'Rd', name: 'Ride Cymbal' }], // Snare [38, { repPitch: 38, zone: 1, order: 60, line: 4, shape: 'o', label: 'SN', name: 'Snare' }], [40, { repPitch: 38, zone: 2, order: 61, line: 4, shape: 'o', label: 'SN', name: 'Snare' }], // Tom 1 [48, { repPitch: 48, zone: 1, order: 90, line: 2, shape: 'o', label: 'T1', name: 'Tom 1' }], [50, { repPitch: 48, zone: 2, order: 91, line: 2, shape: 'o', label: 'T1', name: 'Tom 1' }], // Tom 2 [45, { repPitch: 45, zone: 1, order: 100, line: 3, shape: 'o', label: 'T2', name: 'Tom 2' }], [47, { repPitch: 45, zone: 2, order: 101, line: 3, shape: 'o', label: 'T2', name: 'Tom 2' }], // Stand tom 1 [43, { repPitch: 43, zone: 1, order: 70, line: 5, shape: 'o', label: 'ST1', name: 'Stand Tom 1' }], [58, { repPitch: 43, zone: 2, order: 71, line: 5, shape: 'o', label: 'ST1', name: 'Stand Tom 1' }], // Stand tom 2 [39, { repPitch: 41, zone: 1, order: 80, line: 6, shape: 'o', label: 'ST2', name: 'Stand Tom 2' }], [41, { repPitch: 41, zone: 2, order: 81, line: 6, shape: 'o', label: 'ST2', name: 'Stand Tom 2' }], // Bass drum [35, { repPitch: 36, zone: 1, order: 110, line: 8, shape: 'o', label: 'BD', name: 'Bass Drum' }], [36, { repPitch: 36, zone: 2, order: 111, line: 8, shape: 'o', label: 'BD', name: 'Bass Drum' }] ]) /** * Generates a variation of an array of notes by adding, removing or changing notes * * @param {Note[]} data array of notes * @param {number} deviation width of the Gauss kernel * @param {number} pAdd probability of adding a note after each note * @param {number} pRemove probability of removing each note * @returns {Note[]} variated Note array */ export function generateDrumVariation (data, deviation = 1, pAdd = 0.1, pRemove = 0.1) { // Only use pitches that occur in the GT data const usedPitches = new Set() for (const note of data) { usedPitches.add(note.pitch) } const pitches = [...usedPitches] // Create variation by adding, removing, and shifting notes const randVelocity = randomInt(15, 128) const randTime = randomNormal(0, deviation) const variation = [] for (const note of data) { // Add and remove notes at random if (randFloat(0, 1) < pAdd) { // Add another note const start = note.start + randFloat(0, 1) const end = start + randFloat(0, 1) const velocity = randVelocity() const pitch = choose(pitches) variation.push(new Note( pitch, start, velocity, 0, end )) } if (randFloat(0, 1) < pRemove) { // Remove note (just add nothing to varation) } else { // Shift timings at random const start = note.start + randTime() const end = note.end + randTime() // Get new note const newNote = Note.from(note) newNote.start = Math.min(start, end) newNote.end = Math.max(start, end) variation.push(newNote) } } // Sort notes by start just in case variation.sort((a, b) => a.start - b.start) return variation } /** * Replaces pitches based on replacementMap * * @param {Note[]} notes notes * @param {Map} replacementMap a map pitch->replacementPitch * @returns {Notes[]} notes with replaced pitches * @throws {'No replacement map given!'} when replacementMap is missing */ export function simplifyDrumPitches (notes, replacementMap) { if (!replacementMap || !(replacementMap instanceof Map)) { throw new Error('No replacement map given!') } const errors = new Set() const simplified = notes.map(note => { const oldPitch = note.pitch let newPitch = oldPitch if (!replacementMap.has(oldPitch)) { errors.add(oldPitch) } else { newPitch = replacementMap.get(oldPitch).repPitch } const newNote = Note.from({ ...note, pitch: newPitch }) return newNote }) // TODO: return errors, do not log! also easier to test // if (errors.size > 0) { // console.warn(`Cannot replace all pitches, replacementMap misses entry for these pitches:`); // console.log(Array.from(errors)); // } return simplified // return { simplified, errors }; } /** * Returns a Map:pitch->yPosIndex for views to lookup which row * a pitch has to be drawn in * * @param {Map} replacementMap a pitch replacement map * @returns {Map} Map:pitch->yPosIndex */ export function getPitch2PositionMap (replacementMap) { const result = new Map() const uniqeRows = [...group([...replacementMap], d => d[1].repPitch)] uniqeRows.sort((a, b) => a[1][0][1].order - b[1][0][1].order) for (const [index, d] of uniqeRows.entries()) { result.set(d[0], index) } return result }