DivisionsHandler.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /**
  2. * Divisions处理器
  3. *
  4. * @description MusicXML divisions是时值计算的基础
  5. * divisions定义了每个四分音符包含多少个基本时值单位
  6. *
  7. * 核心公式:realValue = duration / divisions
  8. * 其中 realValue 以四分音符为单位(1.0 = 四分音符)
  9. *
  10. * @example
  11. * ```typescript
  12. * const handler = new DivisionsHandler();
  13. * handler.setDivisions(256);
  14. * const realValue = handler.toRealValue(128); // 返回 0.5(八分音符)
  15. * ```
  16. */
  17. /** 默认divisions值(常见值:1, 256, 480, 960) */
  18. const DEFAULT_DIVISIONS = 256;
  19. /** 允许的最大divisions值(防止异常大数值导致计算问题) */
  20. const MAX_DIVISIONS = 10000;
  21. /** 允许的最小divisions值 */
  22. const MIN_DIVISIONS = 1;
  23. /**
  24. * 时值转换结果
  25. */
  26. export interface RealValueResult {
  27. /** 实际时值(以四分音符为单位) */
  28. value: number;
  29. /** 是否有警告 */
  30. hasWarning: boolean;
  31. /** 警告信息 */
  32. warningMessage?: string;
  33. }
  34. /**
  35. * Divisions处理器
  36. *
  37. * 负责管理MusicXML的divisions值并提供时值转换功能
  38. */
  39. export class DivisionsHandler {
  40. /** 当前divisions值 */
  41. private currentDivisions: number = DEFAULT_DIVISIONS;
  42. /** 每个小节的divisions值缓存(小节索引 -> divisions值) */
  43. private measureDivisionsMap: Map<number, number> = new Map();
  44. /** 是否启用严格模式(严格模式下会抛出错误而不是警告) */
  45. private strictMode: boolean = false;
  46. /**
  47. * 创建Divisions处理器实例
  48. *
  49. * @param strictMode 是否启用严格模式(默认false)
  50. */
  51. constructor(strictMode: boolean = false) {
  52. this.strictMode = strictMode;
  53. }
  54. /**
  55. * 设置当前divisions值
  56. *
  57. * @param divisions divisions值
  58. * @throws 当divisions为0且严格模式开启时抛出错误
  59. */
  60. setDivisions(divisions: number | null | undefined): void {
  61. // 处理null/undefined
  62. if (divisions === null || divisions === undefined) {
  63. console.warn(`[DivisionsHandler] divisions为${divisions},使用默认值${DEFAULT_DIVISIONS}`);
  64. this.currentDivisions = DEFAULT_DIVISIONS;
  65. return;
  66. }
  67. // 处理0值
  68. if (divisions === 0) {
  69. const errorMsg = 'divisions值不能为0';
  70. if (this.strictMode) {
  71. throw new Error(`[DivisionsHandler] ${errorMsg}`);
  72. }
  73. console.error(`[DivisionsHandler] ${errorMsg},使用默认值${DEFAULT_DIVISIONS}`);
  74. this.currentDivisions = DEFAULT_DIVISIONS;
  75. return;
  76. }
  77. // 处理负数
  78. if (divisions < 0) {
  79. console.warn(`[DivisionsHandler] divisions为负数(${divisions}),已取绝对值`);
  80. divisions = Math.abs(divisions);
  81. }
  82. // 处理过大值
  83. if (divisions > MAX_DIVISIONS) {
  84. console.warn(`[DivisionsHandler] divisions过大(${divisions}),可能存在数据问题`);
  85. }
  86. this.currentDivisions = divisions;
  87. }
  88. /**
  89. * 获取当前divisions值
  90. *
  91. * @returns 当前divisions值
  92. */
  93. getDivisions(): number {
  94. return this.currentDivisions;
  95. }
  96. /**
  97. * 设置指定小节的divisions值
  98. *
  99. * @param measureIndex 小节索引(从0开始)
  100. * @param divisions divisions值
  101. */
  102. setMeasureDivisions(measureIndex: number, divisions: number): void {
  103. this.measureDivisionsMap.set(measureIndex, divisions);
  104. // 同时更新当前值
  105. this.setDivisions(divisions);
  106. }
  107. /**
  108. * 获取指定小节的divisions值
  109. *
  110. * @param measureIndex 小节索引(从0开始)
  111. * @returns 该小节的divisions值,如果未设置则返回当前值
  112. */
  113. getMeasureDivisions(measureIndex: number): number {
  114. return this.measureDivisionsMap.get(measureIndex) ?? this.currentDivisions;
  115. }
  116. /**
  117. * 将MusicXML的duration转换为实际时值
  118. *
  119. * @param duration MusicXML中的duration值
  120. * @returns 实际时值(以四分音符为单位,1.0 = 四分音符)
  121. *
  122. * @example
  123. * ```typescript
  124. * handler.setDivisions(256);
  125. * handler.toRealValue(256); // 1.0 (四分音符)
  126. * handler.toRealValue(128); // 0.5 (八分音符)
  127. * handler.toRealValue(512); // 2.0 (二分音符)
  128. * handler.toRealValue(384); // 1.5 (附点四分音符)
  129. * ```
  130. */
  131. toRealValue(duration: number): number {
  132. // 处理null/undefined
  133. if (duration === null || duration === undefined) {
  134. console.warn(`[DivisionsHandler] duration为${duration},返回0`);
  135. return 0;
  136. }
  137. // 处理负数
  138. if (duration < 0) {
  139. console.warn(`[DivisionsHandler] duration为负数(${duration}),已取绝对值`);
  140. duration = Math.abs(duration);
  141. }
  142. return duration / this.currentDivisions;
  143. }
  144. /**
  145. * 将MusicXML的duration转换为实际时值(带详细结果)
  146. *
  147. * @param duration MusicXML中的duration值
  148. * @returns 包含时值和警告信息的结果对象
  149. */
  150. toRealValueWithInfo(duration: number): RealValueResult {
  151. let hasWarning = false;
  152. let warningMessage: string | undefined;
  153. // 处理null/undefined
  154. if (duration === null || duration === undefined) {
  155. warningMessage = `duration为${duration},返回0`;
  156. return { value: 0, hasWarning: true, warningMessage };
  157. }
  158. // 处理负数
  159. if (duration < 0) {
  160. warningMessage = `duration为负数(${duration}),已取绝对值`;
  161. hasWarning = true;
  162. duration = Math.abs(duration);
  163. }
  164. const value = duration / this.currentDivisions;
  165. return { value, hasWarning, warningMessage };
  166. }
  167. /**
  168. * 将实际时值转换回MusicXML的duration
  169. *
  170. * @param realValue 实际时值(以四分音符为单位)
  171. * @returns MusicXML中的duration值
  172. */
  173. toDuration(realValue: number): number {
  174. return Math.round(realValue * this.currentDivisions);
  175. }
  176. /**
  177. * 计算音符时长(秒)
  178. *
  179. * @param duration MusicXML中的duration值
  180. * @param bpm 每分钟节拍数(四分音符为单位)
  181. * @returns 音符时长(秒)
  182. *
  183. * @example
  184. * ```typescript
  185. * handler.setDivisions(256);
  186. * // BPM=120,一个四分音符=0.5秒
  187. * handler.toSeconds(256, 120); // 0.5秒
  188. * handler.toSeconds(128, 120); // 0.25秒(八分音符)
  189. * ```
  190. */
  191. toSeconds(duration: number, bpm: number): number {
  192. if (bpm <= 0) {
  193. console.warn(`[DivisionsHandler] BPM为${bpm},使用默认值120`);
  194. bpm = 120;
  195. }
  196. const realValue = this.toRealValue(duration);
  197. // 四分音符时长(秒)= 60 / BPM
  198. const quarterNoteDuration = 60 / bpm;
  199. return realValue * quarterNoteDuration;
  200. }
  201. /**
  202. * 获取音符类型对应的realValue
  203. *
  204. * @param noteType 音符类型(如 'quarter', 'eighth', 'half' 等)
  205. * @returns 对应的realValue
  206. */
  207. getNoteTypeRealValue(noteType: string): number {
  208. const noteTypeMap: Record<string, number> = {
  209. 'maxima': 32, // 最长音符
  210. 'long': 16, // 长音符
  211. 'breve': 8, // 二全音符
  212. 'whole': 4, // 全音符
  213. 'half': 2, // 二分音符
  214. 'quarter': 1, // 四分音符
  215. 'eighth': 0.5, // 八分音符
  216. '16th': 0.25, // 十六分音符
  217. '32nd': 0.125, // 三十二分音符
  218. '64th': 0.0625, // 六十四分音符
  219. '128th': 0.03125, // 一百二十八分音符
  220. '256th': 0.015625, // 二百五十六分音符
  221. '512th': 0.0078125, // 五百一十二分音符
  222. '1024th': 0.00390625, // 一千零二十四分音符
  223. };
  224. const value = noteTypeMap[noteType.toLowerCase()];
  225. if (value === undefined) {
  226. console.warn(`[DivisionsHandler] 未知的音符类型: ${noteType},默认返回1.0(四分音符)`);
  227. return 1;
  228. }
  229. return value;
  230. }
  231. /**
  232. * 计算附点后的时值
  233. *
  234. * @param baseRealValue 基础时值
  235. * @param dotCount 附点数量(1个附点增加50%,2个附点增加75%)
  236. * @returns 附点后的时值
  237. */
  238. applyDots(baseRealValue: number, dotCount: number): number {
  239. if (dotCount <= 0) {
  240. return baseRealValue;
  241. }
  242. let result = baseRealValue;
  243. let addition = baseRealValue / 2;
  244. for (let i = 0; i < dotCount; i++) {
  245. result += addition;
  246. addition /= 2;
  247. }
  248. return result;
  249. }
  250. /**
  251. * 重置处理器状态
  252. */
  253. reset(): void {
  254. this.currentDivisions = DEFAULT_DIVISIONS;
  255. this.measureDivisionsMap.clear();
  256. }
  257. /**
  258. * 获取所有小节的divisions映射
  259. *
  260. * @returns 小节索引到divisions值的映射
  261. */
  262. getAllMeasureDivisions(): Map<number, number> {
  263. return new Map(this.measureDivisionsMap);
  264. }
  265. }
  266. /**
  267. * 创建DivisionsHandler实例的工厂函数
  268. *
  269. * @param strictMode 是否启用严格模式
  270. * @returns DivisionsHandler实例
  271. */
  272. export function createDivisionsHandler(strictMode: boolean = false): DivisionsHandler {
  273. return new DivisionsHandler(strictMode);
  274. }
  275. /**
  276. * 快捷函数:将duration转换为realValue
  277. *
  278. * @param duration MusicXML中的duration值
  279. * @param divisions divisions值
  280. * @returns 实际时值
  281. */
  282. export function toRealValue(duration: number, divisions: number): number {
  283. if (divisions === 0) {
  284. throw new Error('divisions不能为0');
  285. }
  286. return duration / divisions;
  287. }
  288. /**
  289. * 快捷函数:计算附点时值
  290. *
  291. * @param baseValue 基础时值
  292. * @param dots 附点数量
  293. * @returns 附点后的时值
  294. */
  295. export function applyDots(baseValue: number, dots: number): number {
  296. let result = baseValue;
  297. let addition = baseValue / 2;
  298. for (let i = 0; i < dots; i++) {
  299. result += addition;
  300. addition /= 2;
  301. }
  302. return result;
  303. }