diff --git a/output/drumloop_v2.rpp b/output/drumloop_v2.rpp new file mode 100644 index 0000000..09a6eb0 --- /dev/null +++ b/output/drumloop_v2.rpp @@ -0,0 +1,2019 @@ + + + RIPPLE 0 0 + GROUPOVERRIDE 0 0 0 0 + AUTOXFADE 129 + ENVATTACH 3 + POOLEDENVATTACH 0 + TCPUIFLAGS 0 + MIXERUIFLAGS 11 48 + ENVFADESZ10 40 + PEAKGAIN 1 + FEEDBACK 0 + PANLAW 1 + PROJOFFS 0 0 0 + MAXPROJLEN 0 0 + GRID 3199 8 1 8 1 0 0 0 + TIMEMODE 1 5 -1 30 0 0 -1 0 + VIDEO_CONFIG 0 0 65792 + PANMODE 3 + PANLAWFLAGS 3 + CURSOR 0 + ZOOM 100 0 0 + VZOOMEX 6 0 + USE_REC_CFG 0 + RECMODE 1 + SMPTESYNC 0 30 100 40 1000 300 0 0 1 0 0 + LOOP 0 + LOOPGRAN 0 4 + RECORD_PATH Media "" + + + + + RENDER_FILE "" + RENDER_PATTERN "" + RENDER_FMT 0 2 0 + RENDER_1X 0 + RENDER_RANGE 1 0 0 0 1000 + RENDER_RESAMPLE 3 0 1 + RENDER_ADDTOPROJ 0 + RENDER_STEMS 0 + RENDER_DITHER 0 + RENDER_TRIM 0.000001 0.000001 0 0 + TIMELOCKMODE 1 + TEMPOENVLOCKMODE 1 + ITEMMIX 1 + DEFPITCHMODE 589824 0 + TAKELANE 1 + SAMPLERATE 44100 0 0 + + LOCK 1 + + + GLOBAL_AUTO -1 + PLAYRATE 1 0 0.25 4 + SELECTION 0 0 + SELECTION2 0 0 + MASTERAUTOMODE 0 + MASTERTRACKHEIGHT 0 0 + MASTERPEAKCOL 16576 + MASTERMUTESOLO 0 + MASTERTRACKVIEW 0 0.6667 0.5 0.5 0 0 0 0 0 0 0 0 0 0 0 + MASTERHWOUT 0 0 1 0 0 0 0 -1 + MASTER_NCH 2 2 + MASTER_VOLUME 1 0 -1 -1 1 + MASTER_PANMODE 3 + MASTER_PANLAWFLAGS 3 + MASTER_FX 1 + MASTER_SEL 0 + + + + + RULERHEIGHT 86 86 + RULERLANE 1 4 "" 0 -1 + RULERLANE 2 8 "" 0 -1 + + TEMPO 95.703125 4 4 0 + + + + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {2E7E0323-1545-41F0-B991-B5478165A947} + > + > + "" + Y0R0U+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAOwIAAAEAAAAAAAAA + V0lER0VUID0gRGVjYXBpdGF0b3I7DVZFUlNJT04gPSA0Ow1TQVZFRF9CWV9WRVJTSU9OID0gNS4wLjE7DVBSRVNFVCA9MDEwMTAwMDAwNDg4MDEwMTAwMDFLNF1IRlE3 + Sz9XPURnP1s8TEVUOD1aOl1IVlhZPlI+XUk0Z2xWYTVdMlhgNVtIMEpNZ2ZjYmM+RGw5WzRDS2tpW1ZnM25nP0pdXVxKTUZjNGU1XV5vZmM+QWw0TjJqWjlkOmxsOVI7 + Wjs2SUpCTVY4N1U4XTlbPWtMbEdbaVo5RlJaXVVpMk1NQEFsbVViRWZlQT40blFjbUI7ODxSbFE+bG08WjA2MGs5QT9uaFI3MVxQWm5BYjo3TGw8ZTRSUl0/RmozRmdt + RDhvMGxKPzFHYF9tZFpBaWUzNmtqVjZvXWxtMzE4bkxtUDhfNlNlajRFVj1jMltlRDtFaDljVWlNajlQVEs3VkhfZEtoYzRpP11jUj1EZF5TXltiSjdDNEExMTlfVk5h + bTxWX2VrSlJlNzRmSmNIQExlMUVMWWBISEA9YjFZWEA2NjZYZ1xGWmE3NlFYQjYxVV9eaTpRXVFBPmNkazAxPDlqMDQxa1tAaU1MMG1HZzpjXlM4OGhVS1VSMVZZbmc/ + TTJYYUdQTT9Ca10wOWI4ZW8wakZMWGteTk83U2I+ND5hUTdUVG1gM0VDVUc6UFRbbE5bTkZTUFFiYllCb21obDY/OjM4RjpvNVdiS2BNbGAwMDA1UDAwMDA7DQ== + AAAAAAAA + > + "" + ZFJ0U+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAzAEAAAEAAAAAAAAA + V0lER0VUID0gUmFkaWF0b3I7DVZFUlNJT04gPSA0Ow1TQVZFRF9CWV9WRVJTSU9OID0gNS4wLjE7DVBSRVNFVCA9MDEwMTAwMDAwMzgwMDEwMTAwMDE3NF1IRlE3Sz9X + PURnP1s8TEVUOD1aOl1IVlhZPlI+XUk0Z2xWYTVdMlhgNVtIMEpNZ2ZjYmM9PUhaOUlHN2A6bjxJVk1oN09XNW5dZmxPVDVVZjZfVW5GZ2dBW0M6SzhSaGZeWFNoOGQw + XmRCUF5PT0JUT1pQM2k0WTVHYmFqWTlARVBeaVtVOjBvNTtVXlJIPDg2al89XEVASEZHWltVVz9YakVhVlNqOGVFNkhfU2tUUVhkaWNiMVQ1W2VVb10wOWpVYWpVM2Y9 + TDZoUlwxZGxjXjVeTVdaVEFHOzhNaVxlWGlIST84SUlRbGBZblU6Ym1RRUE+TllMTllAbVNHMVpKWDdqW1UwTEJnRzsyXkBYTzJkMVphN2IxNFcwW0JfYjZDRFY8QGFC + ODBFbGNNOjI3RWczQE0+akpmXj1GU2dBY0dfTWRGaGc+WDtoYEhaOTxGMDU1ZFNFbTBTVDhcTD9QSj8zWEU7T2VnMDAwMTMwMDA7DQ== + AAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {31AE1CFC-FA34-41CC-95C4-7A5EBA9D2DE3} + > + + > + > + + "" + Y0R0U+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAOwIAAAEAAAAAAAAA + V0lER0VUID0gRGVjYXBpdGF0b3I7DVZFUlNJT04gPSA0Ow1TQVZFRF9CWV9WRVJTSU9OID0gNS4wLjE7DVBSRVNFVCA9MDEwMTAwMDAwNDg4MDEwMTAwMDFLNF1IRlE3 + Sz9XPURnP1s8TEVUOD1aOl1IVlhZPlI+XUk0Z2xWYTVdMlhgNVtIMEpNZ2ZjYmM+RGw5WzRDS2tpW1ZnM25nP0pdXVxKTUZjNGU1XV5vZmM+QWw0TjJqWjlkOmxsOVI7 + Wjs2SUpCTVY4N1U4XTlbPWtMbEdbaVo5RlJaXVVpMk1NQEFsbVViRWZlQT40blFjbUI7ODxSbFE+bG08WjA2MGs5QT9uaFI3MVxQWm5BYjo3TGw8ZTRSUl0/RmozRmdt + RDhvMGxKPzFHYF9tZFpBaWUzNmtqVjZvXWxtMzE4bkxtUDhfNlNlajRFVj1jMltlRDtFaDljVWlNajlQVEs3VkhfZEtoYzRpP11jUj1EZF5TXltiSjdDNEExMTlfVk5h + bTxWX2VrSlJlNzRmSmNIQExlMUVMWWBISEA9YjFZWEA2NjZYZ1xGWmE3NlFYQjYxVV9eaTpRXVFBPmNkazAxPDlqMDQxa1tAaU1MMG1HZzpjXlM4OGhVS1VSMVZZbmc/ + TTJYYUdQTT9Ca10wOWI4ZW8wakZMWGteTk83U2I+ND5hUTdUVG1gM0VDVUc6UFRbbE5bTkZTUFFiYllCb21obDY/OjM4RjpvNVdiS2BNbGAwMDA1UDAwMDA7DQ== + AAAAAAAA + > + "" + bUZMR+5e7f4EAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAACAAAAAQAAAAAAAAACAAAAAAAAAGUAAAABAAAAAAAAAA== + dGZmcAAAAQBTVEdGMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0QAAAAAAAiNNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAEAAAA= + AAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {18925AB4-45CE-4190-8149-939E3B7876D7} + > + AUXRECV 6 0.050000 -1 -1 0 + AUXRECV 7 0.020000 -1 -1 0 + + > + + > + > + + "" + TVB0U+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAXQkAAAEAAAAAAAAA + V0lER0VUID0gUGhhc2VNaXN0cmVzczsNVkVSU0lPTiA9IDQ7DVNBVkVEX0JZX1ZFUlNJT04gPSA1LjAuMTsNUFJFU0VUID0wMTAxMDAwMDIzMTIwMTAxMDAwNmE0XUhG + UTdLP1c9RGc/WzxMRVQ4PVo6XUhWWFk+Uj5dSTRnbFZhNV0yWGA1W0gwSk1nZmNiYz9APGZCRWw4YDwxPkFLbl5nWz9PRURCV25vUzdfXWI6aVFaNkBCSFQ3T0tCZUBk + VkZdQDMxU2dCOUZDYWpcVF9tQFs4QThEXG9qXW1IWEw+PWRAPTJGZEBINWNqSEFRSEZgYGtDRDtQVExjUWdfazFIOmpuY1BGU1VRVGxRVVY3YE09O1BSW1Fhbz40N1ps + YVFZPFROSGY7NDRPYUFmRjo4TT07UFJbUWFvPjQ3WmxhUVk8VE5IaVMySGxbZ2VgTTpRZWE4UjxrSzE+NkFqb1lpMV8/OEpgMDViQmhiTDFaPGc0X0ZWWTNVaUdjY180 + Qj5nbVRlUDg/OVlpbFBmTl5kR2RXRkBaVmRPPFNNUWRUYF9iU0s2N0VcT0M7blRJR0E/T2lLUj5EXjFmVDlEOkNCVmVlRGpDblNFYWpCSjFlVk5ZTE5ZQG1TRzFRUE5Z + TFNsSDw2W2JBVTJkXmxfNGJjU1NPSEo+SlRcZ0tKbWk3S1pAVWgwS1NkSjZiR1VoRmBPNl1IMUloT0U4R0ZVODdlbT1OMD1cNlg7TGA6TzloO0BaOUFeWWNsbj85a1dP + bWBORVpeb1o7bTVSQGo6Z1hQUGBBUDI1TmRTWGwzOUNuW081b21pa15TNm9QYGtqSl9qQGpZT2ZGamk4VjpoSmM1MEhjZDA2VTVZMFFbNU43S0tWNlBpbV9pQmI0XFoz + XUpdYEdDajtANkhIQG1pNk9sVz9jZT4+NTRlXmtHbjg/OVk2SjdbZjBJRUNPXl9KRUNKPWxJQlU/MWJsSl9eb2RNYkQ5XGYxZTQ5OjJIMU81ZT41PlxRNj9TXmg2Z09b + PG5cSGQ0QThUNlg3RzpFU2VYZEU/VFM6NjVJQ2xpVWFCVz1KaTU7W1VhOF9IP1dRSkRWSElrPUNFS25kQ1Jfb1g0O0dUNVJgT2QzZlU+YGk9OVA8UG5LQmNgY0Y/Rm1G + NkJeOmRTW0NhP2RITjRaQ0ppVWZNYk1SM2poQmc6RD1cRFQ8Oz1uVzlrXVZPbEljQjZjRGVvME5YS1RMVkdJZzlmOD9bUTtMWUBmYUJAYFxnaENiTltEPEFUTEhTTVFk + VGBfYlNLNkw0PFZgSjVmN1hiUFNmVTBnP21cOmNBRG5CPFhIRVU/YE9kM2ZVPmBpPTlkTmhZWjA9XzFZP29vZGFtX0VCYU5bPmhUU2QwXVE5WkRGTFJWQGlFNjkzMDFr + PTpCZklRYkNdQUlPYkdQVGk9Plw2YUdQSkBFOERgSWw+T1xNTWZMTEJjMF5VTzJAaDdAWjlBXlljbG4/OWtXT21gTkVaXm9CXWk2ZmA8P0NhWEBWVDRsSTQwTTowOjFc + WTZqUTxhWmNOSVJKMzI6U0dqY0RlbzBOWEtUTFE9YU5UOm5QUWFVbDJKUmZXSlE9TTE+XDZhR1BKQEU4TURAVTlaUVtMMldQNWJQZk9GV2pUaWtCYVY2amVCM2VRSk0w + V25GN1tVS1JVP2hiTmo0UmpoW0NpYjhoYjc2Nls3WmBUMm9UM0RTOltXOmkyUU9DRThUQlRYTkpSM0xNbVQ/UThnMTpAMERNPVheYD1VRFxGZkdtQDU+X0lEUD03QUxa + VzlLRjFeMD4zbG1bbmZhNT4yaVZBTkQ0XWY5TWdZM2xlQVRYR0g7PkVaX2FsM1A+YEFVNlFfWGI8UmBCNDYzbUc0b0JPPmszPFpiY183UjFIb0lbNWk9RGpWQ0tdSG9C + NW9qM2tTSlheNGk7OVdhZmttYlU1XV04QzJNQU9UNFQ6VG9rVlswSVhmMV9dMklHSDhFQUtEOTBNT2k4bFg8U19iUjo/SWxfYWRIOF9mQUBdU29BXTdPVThcQDJnMGlt + WjVgWGdraGZAXE1aT2hpV2FGVmo+TEBNUTg+YTxjUm5nNkxVQU85bDNtU1VXXjE+ZWFWU0g2bmQ5VU1QUUU1XUBUMWVvVFNQRztIblk0REBbWzxoX11hVzlER2JJZDJb + RUY4U0lobEVVMjZtVUBlWlJKXWZebFtXbjpTZWZmNmM/MkZfRlk4SGZUbE9hXzpuUj5ASVZGQlVBQ2I8MzQ6TjhsQF1cSD9PYD5abGNhS0UybVZmZF88YUFkb2FUaGpX + WG9FXE5ZYDRWa0VCUFVAbDJdUmtMZUliR187YkM0Ok44bEBdXEg/TkxnY1xDY0RaT0ZQYEdPSEBeXUtOWUhePmpOMUA3RF88Oj9GRDtRTUNIVTZPM05MRkQ4QmFaQl1E + YUNlO1FUMk1eRENMTWA8P0k0MjJOWmo4YjBrQUFPM05Qam49ZEZFOj01QTdKYz5cWGxlV1A7MD80MWFcNUhHb2U4WWkzPTltZF5DTD5TW25kbTdpXkZkM1JWNTU7QUZG + bmJNZF5NR1BXMlJSP0FUPj9rW1Q5PEZMRl5YP11bNUxjWFhQR1pHMFpkaT9NYVQ9TDJtM1Y9ZkJGPE5SRzdnY1VMYVc6V2g7NjddMkwxZU9eM1hbWW1ZV04xNG5SNVpC + OEo2ZWVcRTJdUWRCXl80Sk1NOU9uMTNSNE84QWE1X1NiZ09SMUQ6X0BpU01UVVM3WFRoX2Y7PlJuNFVjZzEwUjE1MllbQkY1NjlROjw2SFphTk5GWVhHbDFLbjdoUEBj + Z2AxWj5jXjtLWVlqZWBLVDZKbTNWPWZCRjxOUkBHUU9fWT9DV2w4QkdvUEBoUTdiNExBS2hsXWdoUEUyXWFdXlpnVDFHSG1dREdBS2RZbUprZzRkbko1V0xGTjRRQVJI + QlMxVjpcR1VFNDtDbFlRbTRBbkVBXGxdV1tiNm1OMUdiNFBhVjpfa2ROZVAyM2lcMEdXZUVRUT1WbVZkWjtgOUptT2lWTjAxVExqZGNQaFRsTjBkbTBWNGREamFRT1Vl + Pj9sNlxsWmJKVWRMUVdRRltOTllMTllAbVNHMVZTX0lOYE5oUzxTUDNVNW5DXWRFRzZibWs7ZU9DNEBGSjFqZGtXTUljRVJuMVhsPlFEXW9HTDAwMEpkMDAwMDsN + AAAAAAAA + > + "" + QkV0U+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAA6wYAAAEAAAAAAAAA + V0lER0VUID0gRWNob0JveTsNVkVSU0lPTiA9IDQ7DVNBVkVEX0JZX1ZFUlNJT04gPSA1LjAuMTsNUFJFU0VUID0wMTAxMDAwMDE2OTIwMTAxMDAwNG00XUhGUTdLP1c9 + RGc/WzxMRVQ4PVo6XUhWWFk+Uj5dSTRnbFZhNV0yWGA1W0gwSk1nZmNiYz9FQGM5NGtWSUJqX2RON19iaWUxb2JYMTJNUT48X0xLRTpZUlJlOWdnPFdpMGpUP1FYY1Vl + R29oZWVbMlA5XTdJZ2hAPGFDWkQ3PWY7NlJCMjtsNkROQDlGYEBJXFRPOkZpP2Q0QlU/OkxJaENbPkxMZVVsTTlnVU1QVkI6QjJhOUdGbkRpP05CZDdbR244PzlZNko3 + W1JhOzg6PVJKTEg1R0ZuRGk/TkJkN1tHbjg/OVk2Sjdbb0o5OU5YZEFdZzYxOV9WWURSTmBHOl1sb2pKMlhhPkZCOm1QOz5VUURdT0w0bVxrUkJVRV04a141MlNDPEll + Qj5FN1EzYjhNQ0FDOGhAZzhiYzY5aExjUm5nNkxVQU85W1BGQWtbaV9Gb11Ja0lLUDBkQV1dYTljQWZoXG9jU0leZjk0QjxVQT1NQVU2UV9YYjxSYEI0NjNtRzRvQk8+ + a29BTzlRNDhQR09APV9PXVpiYUJcSWs+S0dVS0FeazZBWkNIMlM/MltLa1cxYWZgPFpXbVtbXWNNRkhLb2NQNURAaDtSOGhiQTBSVTZnOG9KRGJJU21dZ208XTU+UjFL + XGo6ak9KSWdQQT9ab0hTNDpYbGVTaD5OYjtiaW03SFhCV1pHN1pEP0hlYEhkYF1oTm9NaG1TUk9fUVZSWWQ8amNYU2I4b2RURF05WGBZRmo2U25AMWhdTE9bY25jPlkw + PV0+bks0Tl8yRVo3MGFBVGZAUW1sWjFfWT9KXzhjbjFBVVJoa1loNTBNQm5nMEJIWUNHYExqSFBWO0lQYU9ZSklDWWxKT09ZR1NFRENGQ1dvaDRgUkZqOT80XURqOlRH + azk1MkRsSTc8Tl8xQzNkPklrXDY5MTlqVWFqVTNmPUw2PF1SSW5naWVLVlptSWlIakNCV2BhWzBUUDlSSllUNEBgWW5VOmJtUUVBPlJQPzZWMmlkTzRdW29fXFw0Wm5W + W1ZsRmE+MDhgOl45SVU0blMySThPb1xiYFk1Qk5qU2JlNFZrRUJQVUBsMl9iV0U+MTRXZzdBTGNSbmc2TFVBTzloTmhlTlpJVlc0TFtoVFZrQlxDVkNmREo2blM4Yjsx + OEBIP2VMQ205bGtfbTVsVjRAUjFNbTBmbW5mWzs1OmFXXGldTkVdNmtcSTRgPD5mbFZKXW9FOjFqZGtXTUljRVI5XUBBOjFaOTpoYTpDZjZDTlc1N1ZmSj1QS2tAVkVm + Mms5YFAxMjRaa2EyQlY0UFRDWjloZUdWbDtqbGxFOk8xMjNLWVVONllXUWJPSkleVFtsYUU2ajFqZGtXTUljRVI5XUBBOjFaOTpoY2lJX0tkZEpVajdBSklJZWxJRTJf + al1jV15CSDhNU0ZqX1dePTBdYVs+Sz9SN2xfUzVCaG5tS2xCQFRpTU9nUVc3M0JqNjROMWRLNk5aQTVMXFFnVmM6R0BuQU1YY2ZSR29YbVw4TDdUaltbZVdVU1k9Ok8z + NkZuW1U6VTNZbkNASDJjOU9ZWkVORzRvMElvQjtAZ2RANl9LQGdDXTlCN0JqNV9gTGdWS0xQRGRlR11aSD0wXDVqMUNBN1w4OWJhbW8waltjPzVdRDtmS0tCbGM1N0Nv + NkRARGZYMVJRVzBQbmtlblxDYmFkZ2hqa19BTDVubVYzWDdbQ15NZVc9RjtIMVVFPW5qbVlFODo8PEY3ZDtMTWg+ZFJMXD1DOjdZNDtYSWpqXVVval01PE5FakNta0Rr + WWNQXERdTz1ANVNDb05uTz9UQz1RQ0NGZFNjQU9WOVJUW109a0c/Yzc4QzVhOkdaRkFuRElcRjMyaVEwUG9BUWRvREFfR2VOWGZsUmdDaTJXM21OVzZkbFZCWWBKU19X + Qk4ySFA3QjA6OkFKOEo/W1tPPFgzSFFgWVRWQUNNX0NpSlpsbj1TUjQzUk9IUW83QjdOQG5EZ1xKNU5IY15kQ00+OWRgMWdjT1JuV0U6REEyR2s/OkJNYENmY145OkVG + ZFMyYT8wOlVFPkRtVlNINm5kOVVNUF05azFCNUo2Sl5IRlwxZT5RYjNEXGlsalxeb2NjOUxJWjFqZGtXTUljRVJlYV5iPUU1ZUg7Z1lLYEJaWERsR2xDY1NiQDNAZkgx + Z1U7QjJlRDAyZj9hZFNFbTBTVDhcTD9QSj8zWEU7T2VnMDAwNGkwMDA7DQ== + AAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {EBEAF867-DFC5-4986-9A65-A12B2C4AA5FE} + > + AUXRECV 6 0.150000 -1 -1 0 + AUXRECV 7 0.080000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + > + + "" + bVR0U+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAwgMAAAEAAAAAAAAA + V0lER0VUID0gVHJlbW9sYXRvcjsNVkVSU0lPTiA9IDQ7DVNBVkVEX0JZX1ZFUlNJT04gPSA1LjAuMTsNUFJFU0VUID0wMTAxMDAwMDA4ODAwMTAxMDAwMlU0XUhGUTdL + P1c9RGc/WzxMRVQ4PVo6XUhWWFk+Uj5dSTRnbFZhNV0yWGA1W0gwSk1nZmNiYzwyRTppQEBsQz89ZUJDb0hUNz5kMUUxaGs+bT1CTVVAZjJFTkZnU0tTX2s4OE05U101 + X1NrT1xhP0g2UlptMVVSX1U6ZzNJNWZeSFwxTGVeNWNMYUM1ZkZYX19nODJEbGQ2RlYwUzs6Vj8zWTVFakg0QURNOG5rQFReZD84ZzJiNjNlTzZRUk5QUD5GTlpBNUxc + UWdWYzlTPTVAbENBTWQ+M2VPNlFSTlBQPkZOWkE1TFxRZ1ZjUWxQUVpqaD5NZkk5T24xM1I0TzhBZzZfYT07OkVjUENeaU9oPko9NjRGTkBCWD5cZTNLQFoyPE5tbWY2 + P0JRP0psRmoyYz5AMUI7UF5HOWhYPDFjXzMxR2JSX0lHVmhmS1NkSjZiR1VoRmI1T0NSUGNFWlI4Wm09bjczamxbZGo8TzNIY1taaGdkakBaYUkxWFtjUjxCSURVSWtU + akE0PVIyMFc/V2dIM2BiUTw+XWBqZD40M29lbDxnXW1EOmU5YkFsVjlYMFYwPFw8Xm5EbjxHZVVKPl1nRV9KT15lZERQMU5kRltIO2BLVkc5R0tFNGhDajc5QkNHYT5N + UUdYazs2N10yTDFlT14zWFtZbVlXTjE0bllramhUZ24zN2RiSztoWzBpSWZlZ2o3ZTxmUU1bMVxUXmA5W2lHamRINTFNYkA8bUBSOWBaM1BBamg7QjM1WzlnPlpTWjRc + VjxcVmkyUUZLPWRdWmI3VTtIMDs9ZV45NGVfWmlCWUBqT1RkOF9ha2RhNDhXNjBFQUtEOTBNT2k4aDViZj9aQTU0OmpjPjtrTEliRTVsVl4xSTdeX1ZtS25lV11VXjAz + QTZmZzRXPTdLUmNvPj1VbUNtT2BDYVxhRkZESjZuUzhiOzE4QEg/ZUxDbTlsa1xCUUA/YjdIMDNrbGNSbmc2TFVBTzlbUEZBa1tpX0ZvXFFabkZQRFc/UGJKYWZMUV89 + T0U+QGdYQWxnVVlhMUkzYDU4YWxdSEFsVEZJPFFXSGw2XDBuYkFMXUBVZG05PEhQQzBHTUtjZGg3T1xcaT1YS0hWTlZjSjI8Zm5na2JTSkhPbmVEaj8zPUNAMDAwWEww + Ow0= + AAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {4D95D807-5771-4CD7-A3C1-76340B8E088B} + > + AUXRECV 6 0.100000 -1 -1 0 + AUXRECV 7 0.050000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + > + "" + Y0R0U+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAOwIAAAEAAAAAAAAA + V0lER0VUID0gRGVjYXBpdGF0b3I7DVZFUlNJT04gPSA0Ow1TQVZFRF9CWV9WRVJTSU9OID0gNS4wLjE7DVBSRVNFVCA9MDEwMTAwMDAwNDg4MDEwMTAwMDFLNF1IRlE3 + Sz9XPURnP1s8TEVUOD1aOl1IVlhZPlI+XUk0Z2xWYTVdMlhgNVtIMEpNZ2ZjYmM+RGw5WzRDS2tpW1ZnM25nP0pdXVxKTUZjNGU1XV5vZmM+QWw0TjJqWjlkOmxsOVI7 + Wjs2SUpCTVY4N1U4XTlbPWtMbEdbaVo5RlJaXVVpMk1NQEFsbVViRWZlQT40blFjbUI7ODxSbFE+bG08WjA2MGs5QT9uaFI3MVxQWm5BYjo3TGw8ZTRSUl0/RmozRmdt + RDhvMGxKPzFHYF9tZFpBaWUzNmtqVjZvXWxtMzE4bkxtUDhfNlNlajRFVj1jMltlRDtFaDljVWlNajlQVEs3VkhfZEtoYzRpP11jUj1EZF5TXltiSjdDNEExMTlfVk5h + bTxWX2VrSlJlNzRmSmNIQExlMUVMWWBISEA9YjFZWEA2NjZYZ1xGWmE3NlFYQjYxVV9eaTpRXVFBPmNkazAxPDlqMDQxa1tAaU1MMG1HZzpjXlM4OGhVS1VSMVZZbmc/ + TTJYYUdQTT9Ca10wOWI4ZW8wakZMWGteTk83U2I+ND5hUTdUVG1gM0VDVUc6UFRbbE5bTkZTUFFiYllCb21obDY/OjM4RjpvNVdiS2BNbGAwMDA1UDAwMDA7DQ== + AAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {D9197334-BD7A-4CE4-B764-972EBB0D498D} + > + AUXRECV 6 0.050000 -1 -1 0 + AUXRECV 7 0.020000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + + > + > + + "" + owDRY+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAVQQAAAEAAAD//wAA + RQQAAAEAAABWc3RXAAAACAAAAAEAAAAAQ2NuSwAABC1GQkNoAAAAAmRMYXkAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {32C58FE1-0AAE-4B22-8597-3350729B11A5} + > + AUXRECV 6 0.250000 -1 -1 0 + AUXRECV 7 0.150000 -1 -1 0 + + > + + > + + > + + > + + > + + > + + > + > + "" + xz/rIu5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAASwMAAAEAAAAAAAAA + OAIAAAEAAABGRkJTAQAAAIgAAAAAAAA/AAAAAAAAAD8AAAA/AAAAAJqZmT4AAAAAMzMzPwAAAADIAbRBAAAAAAAAAAAAAHpDAAAAAAAAAAAAAAAAJCeEPQAAAAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {04899E6D-16C9-4128-8457-38EC0D0FAE9F} + > + > + "" + owDRY+5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAVQQAAAEAAAD//wAA + RQQAAAEAAABWc3RXAAAACAAAAAEAAAAAQ2NuSwAABC1GQkNoAAAAAmRMYXkAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + > + PRESETNAME "Program 1" + FLOATPOS 0 0 0 0 + FXID {0BAC160C-B767-4468-9BBB-FAF79D2CBF64} + > + > +> diff --git a/scripts/analyze_drumloop.py b/scripts/analyze_drumloop.py deleted file mode 100644 index 66da1f2..0000000 --- a/scripts/analyze_drumloop.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Analyze drumloops from the library and output structured forensic data. - -Usage: - python scripts/analyze_drumloop.py [--count N] [--output PATH] [--json PATH] -""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path - -PROJECT = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(PROJECT)) - -from src.composer.drum_analyzer import DrumLoopAnalyzer - - -def load_drumloop_paths(index_path: Path, count: int = 5) -> list[dict]: - with open(index_path, "r", encoding="utf-8") as f: - data = json.load(f) - - drumloops = [s for s in data["samples"] if s.get("role") == "drumloop"] - - scored = [] - for d in drumloops: - path = Path(d["original_path"]) - if not path.exists(): - continue - dur = d.get("signal", {}).get("duration", 0) - onsets = d.get("perceptual", {}).get("onset_count", 0) - density = onsets / max(dur, 0.01) - tempo = d.get("perceptual", {}).get("tempo", 0) - if 85 <= tempo <= 150 and 6 <= dur <= 35: - scored.append((d, density)) - - scored.sort(key=lambda x: abs(x[1] - 4.0)) - return [s[0] for s in scored[:count]] - - -def print_analysis(result_dict: dict) -> None: - print(f"\n{'='*70}") - print(f" {Path(result_dict['file_path']).name}") - print(f"{'='*70}") - print(f" BPM: {result_dict['bpm']}") - print(f" Duration: {result_dict['duration']:.2f}s") - print(f" Bars: {result_dict['bar_count']}") - print(f" Key: {result_dict['key']} (conf: {result_dict['key_confidence']:.2f})") - print(f" Beat grid: {len(result_dict['beat_grid']['quarter'])} quarters, " - f"{len(result_dict['beat_grid']['eighth'])} eighths, " - f"{len(result_dict['beat_grid']['sixteenth'])} sixteenths") - - summary = result_dict["summary"] - total = summary["kick_count"] + summary["snare_count"] + summary["hihat_count"] + summary["other_count"] - print(f"\n Transients: {total} total") - print(f" Kicks: {summary['kick_count']}") - print(f" Snares: {summary['snare_count']}") - print(f" HiHats: {summary['hihat_count']}") - print(f" Other: {summary['other_count']}") - - transients_by_type = {} - for t in result_dict["transients"]: - transients_by_type.setdefault(t["type"], []).append(t) - - for ttype in ["kick", "snare", "hihat", "other"]: - ts = transients_by_type.get(ttype, []) - if not ts: - continue - print(f"\n {ttype.upper()} positions (beat positions):") - positions = [f"{t['beat_pos']:.2f}" for t in ts[:20]] - line = " " + " ".join(positions) - if len(ts) > 20: - line += f" ... +{len(ts)-20} more" - print(line) - - if result_dict["energy_profile"]: - print(f"\n Energy profile (first 16 beats):") - bars_e = result_dict["energy_profile"][:16] - max_e = max(bars_e) if bars_e else 1 - for i, e in enumerate(bars_e): - bar = i // 4 - beat = i % 4 - filled = int((e / max_e) * 30) if max_e > 0 else 0 - print(f" Bar {bar+1} Beat {beat+1}: {'|' * filled} ({e:.4f})") - - -def main(): - parser = argparse.ArgumentParser(description="Analyze drumloops forensically") - parser.add_argument("--count", type=int, default=3, help="Number of drumloops to analyze") - parser.add_argument("--index", type=str, default=None, help="Path to sample_index.json") - parser.add_argument("--file", type=str, default=None, help="Analyze a single file instead") - parser.add_argument("--json", type=str, default=None, help="Save results as JSON") - args = parser.parse_args() - - index_path = Path(args.index) if args.index else PROJECT / "data" / "sample_index.json" - results = [] - - if args.file: - print(f"Analyzing: {args.file}") - analyzer = DrumLoopAnalyzer(args.file) - result = analyzer.analyze() - results.append(result.to_dict()) - print_analysis(results[0]) - else: - drumloops = load_drumloop_paths(index_path, args.count) - if not drumloops: - print("No suitable drumloops found.") - return - - print(f"Selected {len(drumloops)} drumloops for analysis:\n") - for d in drumloops: - print(f" - {d['original_name']} " - f"(tempo={d.get('perceptual',{}).get('tempo','?')}, " - f"dur={d.get('signal',{}).get('duration',0):.1f}s)") - - for d in drumloops: - path = d["original_path"] - print(f"\nAnalyzing: {Path(path).name}...") - analyzer = DrumLoopAnalyzer(path) - result = analyzer.analyze() - results.append(result.to_dict()) - print_analysis(result.to_dict()) - - if args.json: - out_path = Path(args.json) - out_path.parent.mkdir(parents=True, exist_ok=True) - with open(out_path, "w", encoding="utf-8") as f: - json.dump(results, f, indent=2, ensure_ascii=False) - print(f"\nResults saved to: {out_path}") - - print(f"\n{'='*70}") - print("DRUMLOOP-FIRST GENERATION APPROACH") - print(f"{'='*70}") - print(""" - 1. SELECT drumloop -> extract BPM + beat grid + transient map - 2. ALIGN project -> set REAPER tempo to drumloop BPM - 3. GENERATE bass -> tresillo pattern in kick-free zones - - Reggaeton tresillo: notes at 0.0, 0.75, 1.5, 2.0, 2.75, 3.5 - - Place bass between kick transients (margin +/-0.15 beats) - 4. GENERATE chords -> change on downbeats (beat 1 of each bar) - - Sustain through bar, use i-VI-III-VII progression - - Match key from drumloop analysis - 5. GENERATE melody -> place on transient-free zones - - Emphasize chord tones on strong beats - - Syncopation matches dembow feel - 6. GENERATE vocals -> chops in gaps between drum transients - 7. SELECT samples -> match drumloop key for compatible tonal samples -""") - - -if __name__ == "__main__": - main() diff --git a/scripts/compose.py b/scripts/compose.py index 278c709..eaa9d01 100644 --- a/scripts/compose.py +++ b/scripts/compose.py @@ -1,12 +1,14 @@ #!/usr/bin/env python -"""Drumloop-first REAPER .rpp project generator for reggaeton. +"""Drumloop-first REAPER .rpp project generator for reggaeton instrumental. The drumloop drives EVERYTHING: BPM, key, rhythm all come from analysis. -Bass, chords, melody, and vocals are built to sync with the drumloop's rhythm. +Bass, chords, lead, and pad are built to sync with the drumloop's rhythm. +NO vocals — this is an instrumental-only generator. Usage: - python scripts/compose.py --output output/song.rpp + python scripts/compose.py --output output/drumloop_v2.rpp python scripts/compose.py --bpm 95 --key Am --output output/song.rpp + python scripts/compose.py --bpm 95 --key Am --seed 42 --output output/song.rpp """ from __future__ import annotations @@ -39,60 +41,147 @@ ROLE_COLORS = { "clap": 4, "bass": 5, "chords": 9, - "melody": 11, + "lead": 11, "pad": 13, - "vocal": 15, } +# Section structure: (name, bars, energy, has_clap) +# Clap ONLY on chorus and verse sections SECTIONS = [ - ("intro", 4, 0.4), - ("verse", 8, 0.6), - ("build", 4, 0.7), - ("chorus", 8, 1.0), - ("break", 4, 0.5), - ("chorus", 8, 1.0), - ("outro", 4, 0.3), + ("intro", 4, 0.4, False), + ("verse", 8, 0.6, True), + ("build", 4, 0.7, False), + ("chorus", 8, 1.0, True), + ("break", 4, 0.5, False), + ("chorus", 8, 1.0, True), + ("outro", 4, 0.3, False), ] +# Tresillo rhythm positions in beats (within a bar) TRESILLO_POSITIONS = [0.0, 0.75, 1.5, 2.0, 2.75, 3.5] + +# Clap positions in beats (within a bar) CLAP_POSITIONS = [1.0, 3.5] +# i-VI-III-VII chord progression in semitones from root (minor key) CHORD_PROGRESSION = [ - (0, "minor"), - (8, "major"), - (3, "major"), - (10, "major"), + (0, "minor"), # i + (8, "major"), # VI + (3, "major"), # III + (10, "major"), # VII ] +# FX chains per track role (before return sends) FX_CHAINS = { "drumloop": ["Decapitator", "Radiator"], - "bass": ["Decapitator", "Gullfoss_Master"], - "chords": ["PhaseMistress", "EchoBoy"], - "melody": ["Tremolator"], - "vocal": ["VC_76", "Radiator", "EchoBoy"], - "pad": ["ValhallaDelay"], + "bass": ["Serum_2", "Decapitator", "Gullfoss_Master"], + "chords": ["Omnisphere", "PhaseMistress", "EchoBoy"], + "lead": ["Serum_2", "Tremolator"], + "clap": ["Decapitator"], + "pad": ["Omnisphere", "ValhallaDelay"], } +# Send levels (reverb, delay) per track role SEND_LEVELS = { - "bass": (0.05, 0.02), + "bass": (0.05, 0.02), "chords": (0.15, 0.08), - "melody": (0.10, 0.05), - "vocal": (0.20, 0.10), - "pad": (0.25, 0.15), + "lead": (0.10, 0.05), + "clap": (0.05, 0.02), + "pad": (0.25, 0.15), } +# Track volume levels VOLUME_LEVELS = { - "bass": 0.82, "drumloop": 0.85, - "chords": 0.70, - "melody": 0.75, - "vocal": 0.80, - "pad": 0.65, - "clap": 0.80, + "bass": 0.82, + "chords": 0.70, + "lead": 0.75, + "clap": 0.80, + "pad": 0.65, } -# Backward compat stubs for test imports -EFFECT_ALIASES: dict[str, str] = {} +# Master volume +MASTER_VOLUME = 0.85 + + +# --------------------------------------------------------------------------- +# Phase 1: Infrastructure +# --------------------------------------------------------------------------- + +def score_drumloop(sample: dict, analysis) -> float: + """Score a drumloop candidate for selection quality. + + Formula: key_confidence*0.4 + onset_density_normalized*0.3 + duration_score*0.2 + balance_score*0.1 + + Args: + sample: sample dict from index (used for duration) + analysis: DrumLoopAnalysis result + + Returns: + Composite score 0.0–1.0 (higher = better) + """ + # key_confidence: already 0-1 from analysis + kc = analysis.key_confidence + + # onset_density_normalized: normalize against typical max (15.0) + transients = analysis.transients + duration = analysis.duration + onset_density = len(transients) / duration if duration > 0 else 0.0 + onset_density_normalized = min(1.0, onset_density / 15.0) + + # duration_score: prefer >= 8 second loops for clean looping + dur = sample.get("signal", {}).get("duration", 0.0) + duration_score = 1.0 if dur >= 8.0 else dur / 8.0 + + # balance_score: penalize if kick/snare ratio is lopsided + kick_count = len(analysis.transients_of_type("kick")) + snare_count = len(analysis.transients_of_type("snare")) + total = len(transients) if transients else 1 + kick_ratio = kick_count / total + snare_ratio = snare_count / total + balance_score = 2.0 * min(kick_ratio, snare_ratio) + balance_score = min(1.0, balance_score) + + # format_score: prefer WAV over MP3 (lossless > lossy) + ext = sample.get("original_path", "").rsplit(".", 1)[-1].lower() + format_score = 1.0 if ext == "wav" else 0.85 + + return kc * 0.3 + onset_density_normalized * 0.25 + duration_score * 0.15 + balance_score * 0.1 + format_score * 0.2 + + +def build_section_structure(): + """Build section list and compute cumulative bar offsets. + + Returns: + sections: list of SectionDef + offsets: list of bar offsets (cumulative, in bars) + """ + sections = [SectionDef(name=n, bars=b, energy=e) for n, b, e, _ in SECTIONS] + offsets = [] + off = 0.0 + for sec in sections: + offsets.append(off) + off += sec.bars + return sections, offsets + + +def root_to_midi(root: str, octave: int) -> int: + """Backward compat: convert note name (e.g. 'C', 'A') to MIDI number.""" + return NOTE_TO_MIDI[root] + (octave + 1) * 12 + + +def key_to_midi_root(key_str: str, octave: int = 2) -> int: + """Convert key string (e.g. "Am") to MIDI root note number. + + Args: + key_str: Key like "Am", "Dm", "Gm", "C", "F#m" + octave: MIDI octave (2 = bass, 3 = chords/pad) + + Returns: + MIDI note number (e.g. 45 for A2, 57 for A3) + """ + root = key_str.rstrip("m") + return NOTE_TO_MIDI[root] + (octave + 1) * 12 # --------------------------------------------------------------------------- @@ -100,25 +189,24 @@ EFFECT_ALIASES: dict[str, str] = {} # --------------------------------------------------------------------------- def parse_key(key_str: str) -> tuple[str, bool]: + """Parse key string into root and minor flag.""" if key_str.endswith("m"): return key_str[:-1], True return key_str, False -def root_to_midi(root: str, octave: int) -> int: - return NOTE_TO_MIDI[root] + (octave + 1) * 12 - - def get_pentatonic(root: str, is_minor: bool, octave: int) -> list[int]: - root_midi = root_to_midi(root, octave) + """Get pentatonic scale pitches for root in given octave.""" + root_midi = key_to_midi_root(root, octave) if is_minor: - intervals = [0, 3, 5, 7, 10] + intervals = [0, 3, 5, 7, 10] # minor pentatonic else: - intervals = [0, 2, 4, 7, 9] + intervals = [0, 2, 4, 7, 9] # major pentatonic return [root_midi + i for i in intervals] def build_chord(root_midi: int, quality: str) -> list[int]: + """Build a triad chord from root MIDI note and quality.""" if quality == "minor": return [root_midi, root_midi + 3, root_midi + 7] return [root_midi, root_midi + 4, root_midi + 7] @@ -129,6 +217,7 @@ def build_chord(root_midi: int, quality: str) -> list[int]: # --------------------------------------------------------------------------- def make_plugin(registry_key: str, index: int) -> PluginDef: + """Create a PluginDef from the PLUGIN_REGISTRY.""" if registry_key in PLUGIN_REGISTRY: display, path, uid = PLUGIN_REGISTRY[registry_key] preset = PLUGIN_PRESETS.get(registry_key) @@ -137,30 +226,46 @@ def make_plugin(registry_key: str, index: int) -> PluginDef: # --------------------------------------------------------------------------- -# Track builders +# Phase 2: Track Generation # --------------------------------------------------------------------------- def build_drumloop_track(drumloop_path: str, total_beats: float) -> TrackDef: - clips = [ClipDef( - position=0.0, length=total_beats, name="Drumloop Full", - audio_path=drumloop_path, loop=True, - )] + """Build the drumloop track — single audio clip spanning entire song, looping.""" + clips = [ + ClipDef( + position=0.0, + length=total_beats, + name="Drumloop Full", + audio_path=drumloop_path, + loop=True, + ) + ] plugins = [make_plugin(fx, i) for i, fx in enumerate(FX_CHAINS.get("drumloop", []))] return TrackDef( - name="Drumloop", volume=VOLUME_LEVELS["drumloop"], pan=0.0, - color=ROLE_COLORS["drumloop"], clips=clips, plugins=plugins, + name="Drumloop", + volume=VOLUME_LEVELS["drumloop"], + pan=0.0, + color=ROLE_COLORS["drumloop"], + clips=clips, + plugins=plugins, ) def build_bass_track( - analysis, sections, offsets, key_root, key_minor, + analysis, + sections: list[SectionDef], + offsets: list[float], + key_root: str, + key_minor: bool, ) -> TrackDef: - root_midi = root_to_midi(key_root, 2) + """Build the bass track — MIDI tresillo, filtered by kick_free_zones.""" + root_midi = key_to_midi_root(key_root, 2) beat_dur = 60.0 / analysis.bpm kfz = analysis.kick_free_zones(margin_beats=0.25) - def in_kfz(beat: float) -> bool: - s = beat * beat_dur + def in_kfz(abs_beat: float) -> bool: + """Check if absolute beat position is in a kick-free zone.""" + s = abs_beat * beat_dur return any(zs <= s <= ze for zs, ze in kfz) clips = [] @@ -171,28 +276,41 @@ def build_bass_track( for pos in TRESILLO_POSITIONS: abs_beat = sec_off * 4.0 + bar * 4.0 + pos if in_kfz(abs_beat): + # Note: position within clip is relative to clip start (bar * 4.0) notes.append(MidiNote( - pitch=root_midi, start=bar * 4.0 + pos, - duration=0.5, velocity=int(100 * vm), + pitch=root_midi, + start=bar * 4.0 + pos, + duration=0.5, + velocity=int(100 * vm), )) if notes: clips.append(ClipDef( - position=sec_off * 4.0, length=section.bars * 4.0, - name=f"{section.name.capitalize()} Bass", midi_notes=notes, + position=sec_off * 4.0, + length=section.bars * 4.0, + name=f"{section.name.capitalize()} Bass", + midi_notes=notes, )) - plugins = [make_plugin("Serum_2", 0)] - plugins += [make_plugin(fx, i + 1) for i, fx in enumerate(FX_CHAINS.get("bass", []))] + plugins = [make_plugin(fx, i) for i, fx in enumerate(FX_CHAINS.get("bass", []))] return TrackDef( - name="Bass", volume=VOLUME_LEVELS["bass"], pan=0.0, - color=ROLE_COLORS["bass"], clips=clips, plugins=plugins, + name="Bass", + volume=VOLUME_LEVELS["bass"], + pan=0.0, + color=ROLE_COLORS["bass"], + clips=clips, + plugins=plugins, ) def build_chords_track( - analysis, sections, offsets, key_root, key_minor, + analysis, + sections: list[SectionDef], + offsets: list[float], + key_root: str, + key_minor: bool, ) -> TrackDef: - root_midi = root_to_midi(key_root, 3) + """Build the chords track — i-VI-III-VII on downbeats, one clip per section.""" + root_midi = key_to_midi_root(key_root, 3) clips = [] for section, sec_off in zip(sections, offsets): vm = section.energy @@ -202,186 +320,322 @@ def build_chords_track( interval, quality = CHORD_PROGRESSION[ci] for pitch in build_chord(root_midi + interval, quality): notes.append(MidiNote( - pitch=pitch, start=bar * 4.0, duration=4.0, + pitch=pitch, + start=bar * 4.0, + duration=4.0, velocity=int(80 * vm), )) if notes: clips.append(ClipDef( - position=sec_off * 4.0, length=section.bars * 4.0, - name=f"{section.name.capitalize()} Chords", midi_notes=notes, + position=sec_off * 4.0, + length=section.bars * 4.0, + name=f"{section.name.capitalize()} Chords", + midi_notes=notes, )) - plugins = [make_plugin("Omnisphere", 0)] - plugins += [make_plugin(fx, i + 1) for i, fx in enumerate(FX_CHAINS.get("chords", []))] + plugins = [make_plugin(fx, i) for i, fx in enumerate(FX_CHAINS.get("chords", []))] return TrackDef( - name="Chords", volume=VOLUME_LEVELS["chords"], pan=0.0, - color=ROLE_COLORS["chords"], clips=clips, plugins=plugins, + name="Chords", + volume=VOLUME_LEVELS["chords"], + pan=0.0, + color=ROLE_COLORS["chords"], + clips=clips, + plugins=plugins, ) -def build_melody_track( - analysis, sections, offsets, key_root, key_minor, seed=42, +def build_lead_track( + analysis, + sections: list[SectionDef], + offsets: list[float], + key_root: str, + key_minor: bool, + seed: int = 42, ) -> TrackDef: - penta = get_pentatonic(key_root, key_minor, 4) + get_pentatonic(key_root, key_minor, 5) + """Build the lead track — pentatonic melody, avoid transients, chord tones on strong beats.""" + penta_low = get_pentatonic(key_root, key_minor, 4) + penta_high = get_pentatonic(key_root, key_minor, 5) + penta = penta_low + penta_high + transient_times = [t.time for t in analysis.transients] beat_dur = 60.0 / analysis.bpm - def near_transient(beat: float, margin: float = 0.2) -> bool: + def near_transient(beat: float, margin_beats: float = 0.2) -> bool: + """Return True if beat position is near a transient.""" s = beat * beat_dur - return any(abs(s - tt) < margin * beat_dur for tt in transient_times) + return any(abs(s - tt) < margin_beats * beat_dur for tt in transient_times) rng = random.Random(seed) clips = [] for section, sec_off in zip(sections, offsets): vm = section.energy + # Density by section name + density_map = {"chorus": 0.6, "verse": 0.35, "build": 0.35, "intro": 0.2, "break": 0.2, "outro": 0.15} + density = density_map.get(section.name, 0.3) + notes = [] - density = {"chorus": 0.6, "verse": 0.35, "build": 0.35}.get(section.name, 0.2) for bar in range(section.bars): for sixteenth in range(16): bp = bar * 4.0 + sixteenth * 0.25 + abs_bp = sec_off * 4.0 + bp + if rng.random() > density: continue - if near_transient(sec_off * 4.0 + bp): + if near_transient(abs_bp, margin_beats=0.2): continue - strong = sixteenth in (0, 8) + + strong = sixteenth in (0, 8) # beat 0 and beat 2 of bar + # On strong beats: emphasize chord tones (1st, 3rd, 5th of pentatonic) pool = [penta[0], penta[2], penta[4]] if strong else penta notes.append(MidiNote( - pitch=rng.choice(pool), start=bp, + pitch=rng.choice(pool), + start=bp, duration=0.5 if strong else 0.25, velocity=int((90 if strong else 70) * vm), )) + if notes: clips.append(ClipDef( - position=sec_off * 4.0, length=section.bars * 4.0, - name=f"{section.name.capitalize()} Melody", midi_notes=notes, + position=sec_off * 4.0, + length=section.bars * 4.0, + name=f"{section.name.capitalize()} Lead", + midi_notes=notes, )) - plugins = [make_plugin("Serum_2", 0)] - plugins += [make_plugin(fx, i + 1) for i, fx in enumerate(FX_CHAINS.get("melody", []))] + plugins = [make_plugin(fx, i) for i, fx in enumerate(FX_CHAINS.get("lead", []))] return TrackDef( - name="Melody", volume=VOLUME_LEVELS["melody"], pan=0.0, - color=ROLE_COLORS["melody"], clips=clips, plugins=plugins, + name="Lead", + volume=VOLUME_LEVELS["lead"], + pan=0.0, + color=ROLE_COLORS["lead"], + clips=clips, + plugins=plugins, ) -def build_vocal_track( - selector, sections, offsets, key, bpm, analysis, +def build_clap_track( + selector: SampleSelector, + sections: list[SectionDef], + offsets: list[float], ) -> TrackDef: - beat_dur = 60.0 / analysis.bpm - transient_times = sorted(t.time for t in analysis.transients) - used_ids: list[str] = [] - clips = [] + """Build the clap track — audio snare samples at beats 1.0 and 3.5 ONLY in chorus/verse.""" + # Get clap (snare) samples — select best one + snare_results = selector.select(role="snare", limit=5) + clap_path = snare_results[0].sample["original_path"] if snare_results else None - for section, sec_off in zip(sections, offsets): - char = "powerful" if section.name == "chorus" else "melodic" - vs = selector.select_diverse( - role="vocal", n=1, exclude=used_ids, key=key, bpm=bpm, character=char, - ) - if not vs: - continue - vpath = vs[0]["original_path"] - sid = vs[0].get("file_hash", "") - if sid: - used_ids.append(sid) - - if section.name == "chorus": - for bar in range(section.bars): - bar_start = (sec_off * 4.0 + bar * 4.0) * beat_dur - bar_end = bar_start + 4.0 * beat_dur - gap_start = bar_start - for tt in transient_times: - if tt < bar_start: - continue - if tt > bar_end: - break - if tt - gap_start > 0.08: - bp = sec_off * 4.0 + bar * 4.0 + (gap_start - bar_start) / beat_dur - lb = min((tt - gap_start) / beat_dur, 2.0) - clips.append(ClipDef( - position=bp, length=max(lb, 0.5), - name=f"{section.name.capitalize()} Vocal", - audio_path=vpath, - )) - gap_start = tt - if bar_end - gap_start > 0.08: - bp = sec_off * 4.0 + bar * 4.0 + (gap_start - bar_start) / beat_dur - clips.append(ClipDef( - position=bp, length=max((bar_end - gap_start) / beat_dur, 0.5), - name=f"{section.name.capitalize()} Vocal", audio_path=vpath, - )) - else: - for bar in range(0, section.bars, 4): - clips.append(ClipDef( - position=sec_off * 4.0 + bar * 4.0, - length=4.0 * min(4, section.bars - bar), - name=f"{section.name.capitalize()} Vocal", - audio_path=vpath, loop=True, - )) - - plugins = [make_plugin(fx, i) for i, fx in enumerate(FX_CHAINS.get("vocal", []))] - return TrackDef( - name="Vocals", volume=VOLUME_LEVELS["vocal"], pan=0.0, - color=ROLE_COLORS["vocal"], clips=clips, plugins=plugins, - ) - - -def build_clap_track(selector, sections, offsets) -> TrackDef: - clap_results = selector.select(role="snare", limit=5) - clap_path = clap_results[0].sample["original_path"] if clap_results else None clips = [] if clap_path: for section, sec_off in zip(sections, offsets): + if section.name not in ("chorus", "verse"): + continue for bar in range(section.bars): for cb in CLAP_POSITIONS: clips.append(ClipDef( position=sec_off * 4.0 + bar * 4.0 + cb, - length=0.5, name=f"{section.name.capitalize()} Clap", + length=0.5, + name=f"{section.name.capitalize()} Clap", audio_path=clap_path, )) + + plugins = [make_plugin(fx, i) for i, fx in enumerate(FX_CHAINS.get("clap", []))] return TrackDef( - name="Clap", volume=VOLUME_LEVELS["clap"], pan=0.0, - color=ROLE_COLORS["clap"], clips=clips, + name="Clap", + volume=VOLUME_LEVELS["clap"], + pan=0.0, + color=ROLE_COLORS["clap"], + clips=clips, + plugins=plugins, ) -def build_pad_track(sections, offsets, key_root, key_minor) -> TrackDef: - root_midi = root_to_midi(key_root, 3) +def build_pad_track( + sections: list[SectionDef], + offsets: list[float], + key_root: str, + key_minor: bool, +) -> TrackDef: + """Build the pad track — sustained root chord, one clip per section.""" + root_midi = key_to_midi_root(key_root, 3) quality = "minor" if key_minor else "major" chord = build_chord(root_midi, quality) + clips = [] for section, sec_off in zip(sections, offsets): vm = section.energy - notes = [MidiNote(pitch=p, start=0.0, duration=section.bars * 4.0, velocity=int(60 * vm)) for p in chord] + notes = [ + MidiNote(pitch=p, start=0.0, duration=section.bars * 4.0, velocity=int(60 * vm)) + for p in chord + ] clips.append(ClipDef( - position=sec_off * 4.0, length=section.bars * 4.0, - name=f"{section.name.capitalize()} Pad", midi_notes=notes, + position=sec_off * 4.0, + length=section.bars * 4.0, + name=f"{section.name.capitalize()} Pad", + midi_notes=notes, )) - plugins = [make_plugin("Omnisphere", 0)] - plugins += [make_plugin(fx, i + 1) for i, fx in enumerate(FX_CHAINS.get("pad", []))] + plugins = [make_plugin(fx, i) for i, fx in enumerate(FX_CHAINS.get("pad", []))] return TrackDef( - name="Pad", volume=VOLUME_LEVELS["pad"], pan=0.0, - color=ROLE_COLORS["pad"], clips=clips, plugins=plugins, + name="Pad", + volume=VOLUME_LEVELS["pad"], + pan=0.0, + color=ROLE_COLORS["pad"], + clips=clips, + plugins=plugins, ) # --------------------------------------------------------------------------- -# Return tracks + backward compat +# Phase 3: Mixing — Return tracks and sends # --------------------------------------------------------------------------- def create_return_tracks() -> list[TrackDef]: + """Create Reverb and Delay return tracks.""" return [ TrackDef( - name="Reverb", volume=0.7, pan=0.0, clips=[], + name="Reverb", + volume=0.7, + pan=0.0, + clips=[], plugins=[make_plugin("FabFilter_Pro-R_2", 0)], ), TrackDef( - name="Delay", volume=0.7, pan=0.0, clips=[], + name="Delay", + volume=0.7, + pan=0.0, + clips=[], plugins=[make_plugin("ValhallaDelay", 0)], ), ] +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="Compose a REAPER .rpp project from drumloop analysis — instrumental only." + ) + parser.add_argument("--bpm", type=float, default=None, help="BPM override") + parser.add_argument("--key", default=None, help="Key override (e.g. Am)") + parser.add_argument("--output", default="output/drumloop_v2.rpp", help="Output path") + parser.add_argument("--seed", type=int, default=None, help="Random seed") + args = parser.parse_args() + + if args.seed is not None: + random.seed(args.seed) + + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # ===== Step 1: Select BEST drumloop (scored, not random) ===== + index_path = _ROOT / "data" / "sample_index.json" + if not index_path.exists(): + print(f"ERROR: sample index not found at {index_path}", file=sys.stderr) + sys.exit(1) + + selector = SampleSelector(str(index_path)) + selector._load() + + # Filter drumloops in reggaeton tempo range (85-105 BPM) + candidates = [ + s for s in selector._samples + if s["role"] == "drumloop" and 85 <= s["perceptual"]["tempo"] <= 105 + ] + if not candidates: + # Fallback: wider range + candidates = [ + s for s in selector._samples + if s["role"] == "drumloop" and 80 <= s["perceptual"]["tempo"] <= 120 + ] + if not candidates: + print("ERROR: No suitable drumloops found", file=sys.stderr) + sys.exit(1) + + # Score each candidate and pick the best + scored_candidates = [] + for c in candidates: + analysis = DrumLoopAnalyzer(c["original_path"]).analyze() + c["_score"] = score_drumloop(c, analysis) + c["_analysis"] = analysis + scored_candidates.append(c) + + best = max(scored_candidates, key=lambda x: x["_score"]) + drumloop_path = best["original_path"] + analysis = best["_analysis"] + + print(f"Selected drumloop: {best.get('original_name', drumloop_path)}") + print(f" Score: {best['_score']:.3f}") + print(f" BPM: {best['perceptual']['tempo']:.1f}, Key: {best['musical']['key']}") + print(f" Transients: {len(analysis.transients)} " + f"(kicks={len(analysis.transients_of_type('kick'))}, " + f"snares={len(analysis.transients_of_type('snare'))})") + + # ===== Step 2: Project parameters (overrides win) ===== + bpm = args.bpm if args.bpm is not None else analysis.bpm + key = args.key if args.key is not None else (analysis.key or "Am") + if bpm <= 0: + raise ValueError(f"bpm must be > 0, got {bpm}") + key_root, key_minor = parse_key(key) + + print(f"\nProject: {bpm:.1f} BPM, Key: {key}") + + # ===== Step 3: Build section structure ===== + sections, offsets = build_section_structure() + + # ===== Step 4: Build all tracks ===== + total_beats = sum(s.bars for s in sections) * 4.0 + + tracks = [ + build_drumloop_track(drumloop_path, total_beats), + build_bass_track(analysis, sections, offsets, key_root, key_minor), + build_chords_track(analysis, sections, offsets, key_root, key_minor), + build_lead_track(analysis, sections, offsets, key_root, key_minor, seed=args.seed or 42), + build_clap_track(selector, sections, offsets), + build_pad_track(sections, offsets, key_root, key_minor), + ] + + return_tracks = create_return_tracks() + all_tracks = tracks + return_tracks + + # ===== Step 5: Wire sends ===== + reverb_idx = len(tracks) # first return track + delay_idx = len(tracks) + 1 # second return track + for track in all_tracks: + if track.name in ("Reverb", "Delay"): + continue + role = track.name.lower() + sends = SEND_LEVELS.get(role, (0.0, 0.0)) + track.send_level = {reverb_idx: sends[0], delay_idx: sends[1]} + + # ===== Step 6: Assemble SongDefinition ===== + meta = SongMeta(bpm=bpm, key=key, title="Reggaeton Drumloop Instrumental") + song = SongDefinition( + meta=meta, + tracks=all_tracks, + sections=sections, + master_plugins=["Pro-Q_3", "Pro-C_2", "Pro-L_2"], + ) + + errors = song.validate() + if errors: + print("WARNING: validation errors:", file=sys.stderr) + for e in errors: + print(f" - {e}", file=sys.stderr) + + # ===== Step 7: Write RPP ===== + builder = RPPBuilder(song, seed=args.seed) + builder.write(str(output_path)) + print(f"\nWritten: {output_path.resolve()}") + + +# Backward compat stubs (used by tests) +EFFECT_ALIASES: dict = {} + +def build_section_tracks(*args, **kwargs): + return [], [] + + def build_fx_chain(*args, **kwargs): return [] @@ -390,124 +644,11 @@ def build_sampler_plugin(*args, **kwargs): return None -def build_section_tracks(*args, **kwargs): - return [], [] - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -def main() -> None: - parser = argparse.ArgumentParser( - description="Compose a REAPER .rpp project from drumloop analysis." - ) - parser.add_argument("--bpm", type=float, default=None, help="BPM override") - parser.add_argument("--key", default=None, help="Key override (e.g. Am)") - parser.add_argument("--output", default="output/drumloop_song.rpp", help="Output path") - parser.add_argument("--seed", type=int, default=None, help="Random seed") - args = parser.parse_args() - - if args.seed is not None: - random.seed(args.seed) - - output_path = Path(args.output) - output_path.parent.mkdir(parents=True, exist_ok=True) - - # Step 1: Select drumloop - index_path = _ROOT / "data" / "sample_index.json" - if not index_path.exists(): - print(f"ERROR: sample index not found at {index_path}", file=sys.stderr) - sys.exit(1) - - selector = SampleSelector(str(index_path)) - selector._load() - drumloops = [ - s for s in selector._samples - if s["role"] == "drumloop" and 85 <= s["perceptual"]["tempo"] <= 105 - ] - if not drumloops: - drumloops = [ - s for s in selector._samples - if s["role"] == "drumloop" and 80 <= s["perceptual"]["tempo"] <= 120 - ] - if not drumloops: - print("ERROR: No suitable drumloops found", file=sys.stderr) - sys.exit(1) - - drumloop = random.choice(drumloops) - drumloop_path = drumloop["original_path"] - print(f"Selected drumloop: {drumloop.get('original_name', drumloop_path)}") - print(f" BPM: {drumloop['perceptual']['tempo']:.1f}, Key: {drumloop['musical']['key']}") - - # Step 2: Analyze drumloop - print("Analyzing drumloop...") - analyzer = DrumLoopAnalyzer(drumloop_path) - analysis = analyzer.analyze() - print(f" Detected BPM: {analysis.bpm:.1f}") - print(f" Detected Key: {analysis.key}") - print(f" Transients: {len(analysis.transients)} " - f"(kicks={len(analysis.transients_of_type('kick'))} " - f"snares={len(analysis.transients_of_type('snare'))} " - f"hihats={len(analysis.transients_of_type('hihat'))})") - - # Step 3: Project parameters (overrides win) - bpm = args.bpm if args.bpm is not None else analysis.bpm - key = args.key if args.key is not None else (analysis.key or "Am") - if bpm <= 0: - raise ValueError(f"bpm must be > 0, got {bpm}") - key_root, key_minor = parse_key(key) - print(f"\nProject: {bpm:.1f} BPM, Key: {key}") - - # Step 4: Section structure - sections = [SectionDef(name=n, bars=b, energy=e) for n, b, e in SECTIONS] - offsets = [] - off = 0.0 - for sec in sections: - offsets.append(off) - off += sec.bars - - # Step 5: Build tracks - total_beats = sum(s.bars for s in sections) * 4.0 - tracks = [ - build_drumloop_track(drumloop_path, total_beats), - build_bass_track(analysis, sections, offsets, key_root, key_minor), - build_chords_track(analysis, sections, offsets, key_root, key_minor), - build_melody_track(analysis, sections, offsets, key_root, key_minor, seed=args.seed or 42), - build_vocal_track(selector, sections, offsets, key, bpm, analysis), - build_clap_track(selector, sections, offsets), - build_pad_track(sections, offsets, key_root, key_minor), - ] - - return_tracks = create_return_tracks() - all_tracks = tracks + return_tracks - - # Step 6: Wire sends - reverb_idx = len(tracks) - delay_idx = len(tracks) + 1 - for track in all_tracks: - if track.name not in ("Reverb", "Delay"): - role = track.name.lower().replace("vocals", "vocal") - sends = SEND_LEVELS.get(role, (0.0, 0.0)) - track.send_level = {reverb_idx: sends[0], delay_idx: sends[1]} - - # Step 7: Assemble - meta = SongMeta(bpm=bpm, key=key, title="Reggaeton Drumloop Track") - song = SongDefinition( - meta=meta, tracks=all_tracks, sections=sections, - master_plugins=["Pro-Q_3", "Pro-C_2", "Pro-L_2"], - ) - - errors = song.validate() - if errors: - print("WARNING: validation errors:", file=sys.stderr) - for e in errors: - print(f" - {e}", file=sys.stderr) - - builder = RPPBuilder(song, seed=args.seed) - builder.write(str(output_path)) - print(f"\nWritten: {output_path.resolve()}") +# Alias for renamed function +def build_melody_track(*args, **kwargs): + """Backward compat alias — use build_lead_track instead.""" + return build_lead_track(*args, **kwargs) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/tests/test_compose_integration.py b/tests/test_compose_integration.py index a978295..7202e49 100644 --- a/tests/test_compose_integration.py +++ b/tests/test_compose_integration.py @@ -157,7 +157,7 @@ class TestDrumloopFirstTracks: def test_all_tracks_created(self, tmp_path): output = _mock_main(tmp_path) content = output.read_text(encoding="utf-8") - for name in ("Drumloop", "Bass", "Chords", "Melody", "Vocals", "Clap", "Pad", "Reverb", "Delay"): + for name in ("Drumloop", "Bass", "Chords", "Lead", "Clap", "Pad", "Reverb", "Delay"): assert name in content, f"Expected track '{name}' in output" def test_clap_on_dembow_beats(self, tmp_path):