All files / music-core/utils DurationUtils.ts

100% Statements 299/299
93.93% Branches 31/33
100% Functions 17/17
100% Lines 299/299

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 3001x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 1x 1x 1x 1x 1x 1x 1x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 90x 90x 90x 90x 144x 144x 144x 90x 90x 90x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 1x 4x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 6x 6x 2x 2x 4x 4x 6x 7x 84x 3x 3x 3x 84x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x  
/**
 * @file 时值工具函数
 * @description 提供音符时值相关的计算和转换
 */
 
import { NoteType } from '../models/types';
 
/**
 * 音符类型到四分音符倍数的映射
 */
const NOTE_TYPE_TO_QUARTERS: Record<NoteType, number> = {
  '1024th': 1/256,
  '512th': 1/128,
  '256th': 1/64,
  '128th': 1/32,
  '64th': 1/16,
  '32nd': 1/8,
  '16th': 1/4,
  'eighth': 1/2,
  'quarter': 1,
  'half': 2,
  'whole': 4,
  'breve': 8,
  'long': 16,
  'maxima': 32
};
 
/**
 * 四分音符倍数到音符类型的映射
 */
const QUARTERS_TO_NOTE_TYPE: Map<number, NoteType> = new Map([
  [1/256, '1024th'],
  [1/128, '512th'],
  [1/64, '256th'],
  [1/32, '128th'],
  [1/16, '64th'],
  [1/8, '32nd'],
  [1/4, '16th'],
  [1/2, 'eighth'],
  [1, 'quarter'],
  [2, 'half'],
  [4, 'whole'],
  [8, 'breve'],
  [16, 'long'],
  [32, 'maxima']
]);
 
/**
 * 获取音符类型的四分音符倍数
 * @param type 音符类型
 * @returns 四分音符倍数
 */
export function getNoteTypeQuarters(type: NoteType): number {
  return NOTE_TYPE_TO_QUARTERS[type];
}
 
/**
 * 从四分音符倍数获取音符类型
 * @param quarters 四分音符倍数
 * @returns 音符类型或 undefined
 */
export function getNoteTypeFromQuarters(quarters: number): NoteType | undefined {
  return QUARTERS_TO_NOTE_TYPE.get(quarters);
}
 
/**
 * 计算附点后的时值倍数
 * @param baseQuarters 基础时值(四分音符倍数)
 * @param dots 附点数量
 * @returns 附点后的时值
 */
export function applyDots(baseQuarters: number, dots: number): number {
  let total = baseQuarters;
  let dotValue = baseQuarters / 2;
  
  for (let i = 0; i < dots; i++) {
    total += dotValue;
    dotValue /= 2;
  }
  
  return total;
}
 
/**
 * 将 divisions 时值转换为四分音符倍数
 * @param duration divisions 时值
 * @param divisions 每四分音符的 divisions
 * @returns 四分音符倍数
 */
export function divisionsToQuarters(duration: number, divisions: number): number {
  return duration / divisions;
}
 
/**
 * 将四分音符倍数转换为 divisions 时值
 * @param quarters 四分音符倍数
 * @param divisions 每四分音符的 divisions
 * @returns divisions 时值
 */
export function quartersToDivisions(quarters: number, divisions: number): number {
  return quarters * divisions;
}
 
/**
 * 将 divisions 时值转换为秒
 * @param duration divisions 时值
 * @param divisions 每四分音符的 divisions
 * @param tempo 速度(每分钟四分音符数)
 * @returns 秒数
 */
export function divisionsToSeconds(duration: number, divisions: number, tempo: number): number {
  const quarters = divisionsToQuarters(duration, divisions);
  return (quarters * 60) / tempo;
}
 
/**
 * 将秒转换为 divisions 时值
 * @param seconds 秒数
 * @param divisions 每四分音符的 divisions
 * @param tempo 速度
 * @returns divisions 时值
 */
export function secondsToDivisions(seconds: number, divisions: number, tempo: number): number {
  const quarters = (seconds * tempo) / 60;
  return quartersToDivisions(quarters, divisions);
}
 
/**
 * 计算连音符的实际时值
 * @param writtenDuration 书写时值
 * @param actualNotes 实际音符数
 * @param normalNotes 等效正常音符数
 * @returns 实际时值
 */
export function calculateTupletDuration(
  writtenDuration: number,
  actualNotes: number,
  normalNotes: number
): number {
  return writtenDuration * normalNotes / actualNotes;
}
 
/**
 * 获取音符类型的中文名称
 * @param type 音符类型
 * @returns 中文名称
 */
export function getNoteTypeName(type: NoteType): string {
  const names: Record<NoteType, string> = {
    '1024th': '1024分音符',
    '512th': '512分音符',
    '256th': '256分音符',
    '128th': '128分音符',
    '64th': '64分音符',
    '32nd': '32分音符',
    '16th': '16分音符',
    'eighth': '八分音符',
    'quarter': '四分音符',
    'half': '二分音符',
    'whole': '全音符',
    'breve': '二全音符',
    'long': '四全音符',
    'maxima': '八全音符'
  };
  return names[type];
}
 
/**
 * 获取简谱中的时值表示(下划线数量)
 * @param type 音符类型
 * @returns 下划线数量(负数表示增时线)
 */
export function getJianpuUnderlines(type: NoteType): number {
  // 以四分音符为基准(0条下划线)
  // 八分音符 = 1条,十六分 = 2条,三十二分 = 3条
  // 二分音符 = -1(用横线表示),全音符 = -3
  const underlines: Record<NoteType, number> = {
    '1024th': 8,
    '512th': 7,
    '256th': 6,
    '128th': 5,
    '64th': 4,
    '32nd': 3,
    '16th': 2,
    'eighth': 1,
    'quarter': 0,
    'half': -1,
    'whole': -2,
    'breve': -3,
    'long': -4,
    'maxima': -5
  };
  return underlines[type];
}
 
/**
 * 计算小节的总时值(以 divisions 为单位)
 * @param beats 拍数
 * @param beatType 每拍的音符类型分母
 * @param divisions 每四分音符的 divisions
 * @returns 小节总时值
 */
export function getMeasureDuration(
  beats: number | number[],
  beatType: number | number[],
  divisions: number
): number {
  const totalBeats = Array.isArray(beats) 
    ? beats.reduce((a, b) => a + b, 0) 
    : beats;
  const type = Array.isArray(beatType) ? beatType[0] : beatType;
  
  // 每拍时值 = 4 / beatType 个四分音符
  const beatsInQuarters = totalBeats * (4 / type);
  return beatsInQuarters * divisions;
}
 
/**
 * 格式化时值为可读字符串
 * @param quarters 四分音符倍数
 * @returns 可读字符串
 */
export function formatDuration(quarters: number): string {
  const noteType = getNoteTypeFromQuarters(quarters);
  if (noteType) {
    return getNoteTypeName(noteType);
  }
  
  // 检查是否是附点时值
  for (let dots = 1; dots <= 3; dots++) {
    for (const [baseQuarters, type] of QUARTERS_TO_NOTE_TYPE) {
      if (Math.abs(applyDots(baseQuarters, dots) - quarters) < 0.0001) {
        const dotStr = dots === 1 ? '附点' : dots === 2 ? '复附点' : '三重附点';
        return `${dotStr}${getNoteTypeName(type)}`;
      }
    }
  }
  
  // 返回四分音符倍数
  return `${quarters}个四分音符`;
}
 
/**
 * 检查时值是否需要符杠(是否小于四分音符)
 * @param type 音符类型
 * @returns 是否需要符杠
 */
export function needsBeam(type: NoteType): boolean {
  return NOTE_TYPE_TO_QUARTERS[type] < 1;
}
 
/**
 * 获取符杠数量
 * @param type 音符类型
 * @returns 符杠数量(0表示不需要符杠)
 */
export function getBeamCount(type: NoteType): number {
  const beamCounts: Partial<Record<NoteType, number>> = {
    'eighth': 1,
    '16th': 2,
    '32nd': 3,
    '64th': 4,
    '128th': 5,
    '256th': 6,
    '512th': 7,
    '1024th': 8
  };
  return beamCounts[type] ?? 0;
}
 
/**
 * 判断音符类型的大小关系
 * @param type1 类型1
 * @param type2 类型2
 * @returns 负数表示 type1 更短,0表示相等,正数表示 type1 更长
 */
export function compareNoteTypes(type1: NoteType, type2: NoteType): number {
  return NOTE_TYPE_TO_QUARTERS[type1] - NOTE_TYPE_TO_QUARTERS[type2];
}
 
/**
 * 获取比指定类型短一级的音符类型
 * @param type 音符类型
 * @returns 更短的音符类型或 undefined
 */
export function getShorterNoteType(type: NoteType): NoteType | undefined {
  const quarters = NOTE_TYPE_TO_QUARTERS[type];
  return QUARTERS_TO_NOTE_TYPE.get(quarters / 2);
}
 
/**
 * 获取比指定类型长一级的音符类型
 * @param type 音符类型
 * @returns 更长的音符类型或 undefined
 */
export function getLongerNoteType(type: NoteType): NoteType | undefined {
  const quarters = NOTE_TYPE_TO_QUARTERS[type];
  return QUARTERS_TO_NOTE_TYPE.get(quarters * 2);
}