You can subscribe to this list here.
2006 |
Jan
|
Feb
|
Mar
(1) |
Apr
(1) |
May
|
Jun
(1) |
Jul
|
Aug
(10) |
Sep
|
Oct
|
Nov
|
Dec
(3) |
---|---|---|---|---|---|---|---|---|---|---|---|---|
2007 |
Jan
(1) |
Feb
(2) |
Mar
(3) |
Apr
(2) |
May
(10) |
Jun
(2) |
Jul
(1) |
Aug
|
Sep
|
Oct
|
Nov
(3) |
Dec
|
2008 |
Jan
(6) |
Feb
(4) |
Mar
(5) |
Apr
(2) |
May
(1) |
Jun
(1) |
Jul
(4) |
Aug
(6) |
Sep
(2) |
Oct
(9) |
Nov
(1) |
Dec
(4) |
2009 |
Jan
(9) |
Feb
(2) |
Mar
(2) |
Apr
(2) |
May
(6) |
Jun
(18) |
Jul
(33) |
Aug
(39) |
Sep
(33) |
Oct
(24) |
Nov
(23) |
Dec
(22) |
2010 |
Jan
(29) |
Feb
(32) |
Mar
(51) |
Apr
(17) |
May
(31) |
Jun
(21) |
Jul
(32) |
Aug
(28) |
Sep
(35) |
Oct
(27) |
Nov
(11) |
Dec
(13) |
2011 |
Jan
(14) |
Feb
(13) |
Mar
(27) |
Apr
(27) |
May
(28) |
Jun
(20) |
Jul
(43) |
Aug
(52) |
Sep
(66) |
Oct
(61) |
Nov
(11) |
Dec
(8) |
2012 |
Jan
(20) |
Feb
(30) |
Mar
(38) |
Apr
(21) |
May
(33) |
Jun
(21) |
Jul
(25) |
Aug
(9) |
Sep
(24) |
Oct
(42) |
Nov
(27) |
Dec
(41) |
2013 |
Jan
(20) |
Feb
(35) |
Mar
(156) |
Apr
(298) |
May
(258) |
Jun
(201) |
Jul
(105) |
Aug
(60) |
Sep
(193) |
Oct
(245) |
Nov
(280) |
Dec
(194) |
2014 |
Jan
(63) |
Feb
(202) |
Mar
(200) |
Apr
(23) |
May
(53) |
Jun
(105) |
Jul
(18) |
Aug
(26) |
Sep
(110) |
Oct
(187) |
Nov
(97) |
Dec
(74) |
2015 |
Jan
(45) |
Feb
(55) |
Mar
(116) |
Apr
(116) |
May
(193) |
Jun
(164) |
Jul
(50) |
Aug
(111) |
Sep
(98) |
Oct
(71) |
Nov
(103) |
Dec
(63) |
2016 |
Jan
(33) |
Feb
(101) |
Mar
(182) |
Apr
(139) |
May
(140) |
Jun
(103) |
Jul
(165) |
Aug
(286) |
Sep
(208) |
Oct
(127) |
Nov
(97) |
Dec
(54) |
2017 |
Jan
(64) |
Feb
(335) |
Mar
(202) |
Apr
(212) |
May
(139) |
Jun
(127) |
Jul
(294) |
Aug
(154) |
Sep
(170) |
Oct
(152) |
Nov
(156) |
Dec
(62) |
2018 |
Jan
(168) |
Feb
(237) |
Mar
(196) |
Apr
(174) |
May
(174) |
Jun
(161) |
Jul
(127) |
Aug
(88) |
Sep
(149) |
Oct
(66) |
Nov
(52) |
Dec
(135) |
2019 |
Jan
(146) |
Feb
(126) |
Mar
(104) |
Apr
(58) |
May
(60) |
Jun
(28) |
Jul
(197) |
Aug
(129) |
Sep
(141) |
Oct
(148) |
Nov
(63) |
Dec
(100) |
2020 |
Jan
(74) |
Feb
(37) |
Mar
(59) |
Apr
(154) |
May
(194) |
Jun
(133) |
Jul
(313) |
Aug
(197) |
Sep
(49) |
Oct
(162) |
Nov
(143) |
Dec
(57) |
2021 |
Jan
(120) |
Feb
(107) |
Mar
(314) |
Apr
(157) |
May
(524) |
Jun
(169) |
Jul
(72) |
Aug
(133) |
Sep
(135) |
Oct
(146) |
Nov
(198) |
Dec
(325) |
2022 |
Jan
(409) |
Feb
(249) |
Mar
(138) |
Apr
(95) |
May
(102) |
Jun
(221) |
Jul
(66) |
Aug
(120) |
Sep
(192) |
Oct
(131) |
Nov
(53) |
Dec
(171) |
2023 |
Jan
(357) |
Feb
(82) |
Mar
(168) |
Apr
(218) |
May
(196) |
Jun
(86) |
Jul
(115) |
Aug
(49) |
Sep
(190) |
Oct
(102) |
Nov
(45) |
Dec
(76) |
2024 |
Jan
(86) |
Feb
(50) |
Mar
(324) |
Apr
(209) |
May
(197) |
Jun
(232) |
Jul
(194) |
Aug
(247) |
Sep
(219) |
Oct
(266) |
Nov
(328) |
Dec
(304) |
2025 |
Jan
(191) |
Feb
(115) |
Mar
(137) |
Apr
(32) |
May
(126) |
Jun
(403) |
Jul
(65) |
Aug
|
Sep
|
Oct
|
Nov
|
Dec
|
From: <sv...@op...> - 2025-05-26 13:31:55
|
Author: manx Date: Mon May 26 15:31:18 2025 New Revision: 23213 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23213 Log: [Doc] Fix release_branches.md. Modified: trunk/OpenMPT/doc/release_branches.md Modified: trunk/OpenMPT/doc/release_branches.md ============================================================================== --- trunk/OpenMPT/doc/release_branches.md Mon May 26 14:58:46 2025 (r23212) +++ trunk/OpenMPT/doc/release_branches.md Mon May 26 15:31:18 2025 (r23213) @@ -19,7 +19,7 @@ * "publish_v2_noindex.sh" -> "publish_v2_noindex_rel.sh" * " rel" -> " dist" * "bin/rel-" -> "bin/dist-" - * "auto/rel-" -> "auto/dist-" + * "auto/rel" -> "auto/dist" * ".sh dist" -> ".sh rel" * "nonrel" -> "nondist" * "7z" -> "zip" |
From: <sv...@op...> - 2025-05-26 13:00:27
|
Author: manx Date: Mon May 26 14:58:46 2025 New Revision: 23212 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23212 Log: Merged revision(s) 23211 from trunk/OpenMPT: [Doc] Fix release_branches.md. ........ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/doc/release_branches.md Modified: branches/OpenMPT-1.32/doc/release_branches.md ============================================================================== --- branches/OpenMPT-1.32/doc/release_branches.md Mon May 26 14:58:22 2025 (r23211) +++ branches/OpenMPT-1.32/doc/release_branches.md Mon May 26 14:58:46 2025 (r23212) @@ -19,6 +19,7 @@ * "publish_v2_noindex.sh" -> "publish_v2_noindex_rel.sh" * " rel" -> " dist" * "bin/rel-" -> "bin/dist-" + * "auto/rel-" -> "auto/dist-" * ".sh dist" -> ".sh rel" * "nonrel" -> "nondist" * "7z" -> "zip" |
From: <sv...@op...> - 2025-05-26 12:59:04
|
Author: manx Date: Mon May 26 14:58:22 2025 New Revision: 23211 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23211 Log: [Doc] Fix release_branches.md. Modified: trunk/OpenMPT/doc/release_branches.md Modified: trunk/OpenMPT/doc/release_branches.md ============================================================================== --- trunk/OpenMPT/doc/release_branches.md Mon May 26 14:54:46 2025 (r23210) +++ trunk/OpenMPT/doc/release_branches.md Mon May 26 14:58:22 2025 (r23211) @@ -19,6 +19,7 @@ * "publish_v2_noindex.sh" -> "publish_v2_noindex_rel.sh" * " rel" -> " dist" * "bin/rel-" -> "bin/dist-" + * "auto/rel-" -> "auto/dist-" * ".sh dist" -> ".sh rel" * "nonrel" -> "nondist" * "7z" -> "zip" |
From: <sv...@op...> - 2025-05-26 12:54:58
|
Author: manx Date: Mon May 26 14:54:46 2025 New Revision: 23210 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23210 Log: Merged revision(s) 23209 from trunk/OpenMPT: [Doc] Fix release_branches.md. ........ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/doc/release_branches.md Modified: branches/OpenMPT-1.32/doc/release_branches.md ============================================================================== --- branches/OpenMPT-1.32/doc/release_branches.md Mon May 26 14:28:10 2025 (r23209) +++ branches/OpenMPT-1.32/doc/release_branches.md Mon May 26 14:54:46 2025 (r23210) @@ -18,6 +18,7 @@ * "publish_v2.sh" -> "publish_v2_rel.sh" * "publish_v2_noindex.sh" -> "publish_v2_noindex_rel.sh" * " rel" -> " dist" + * "bin/rel-" -> "bin/dist-" * ".sh dist" -> ".sh rel" * "nonrel" -> "nondist" * "7z" -> "zip" |
From: <sv...@op...> - 2025-05-26 12:28:22
|
Author: manx Date: Mon May 26 14:28:10 2025 New Revision: 23209 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23209 Log: [Doc] Fix release_branches.md. Modified: trunk/OpenMPT/doc/release_branches.md Modified: trunk/OpenMPT/doc/release_branches.md ============================================================================== --- trunk/OpenMPT/doc/release_branches.md Mon May 26 13:32:13 2025 (r23208) +++ trunk/OpenMPT/doc/release_branches.md Mon May 26 14:28:10 2025 (r23209) @@ -18,6 +18,7 @@ * "publish_v2.sh" -> "publish_v2_rel.sh" * "publish_v2_noindex.sh" -> "publish_v2_noindex_rel.sh" * " rel" -> " dist" + * "bin/rel-" -> "bin/dist-" * ".sh dist" -> ".sh rel" * "nonrel" -> "nondist" * "7z" -> "zip" |
From: <sv...@op...> - 2025-05-26 11:32:55
|
Author: manx Date: Mon May 26 13:32:13 2025 New Revision: 23208 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23208 Log: [Mod] libopenmpt: Bump rc version. Modified: branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.h branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.mk Modified: branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.h ============================================================================== --- branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.h Mon May 26 13:31:20 2025 (r23207) +++ branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.h Mon May 26 13:32:13 2025 (r23208) @@ -23,7 +23,7 @@ /*! \brief libopenmpt patch version number */ #define OPENMPT_API_VERSION_PATCH 0 /*! \brief libopenmpt pre-release tag */ -#define OPENMPT_API_VERSION_PREREL "-rc.0" +#define OPENMPT_API_VERSION_PREREL "-rc.1" /*! \brief libopenmpt pre-release flag */ #define OPENMPT_API_VERSION_IS_PREREL 1 Modified: branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.mk ============================================================================== --- branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.mk Mon May 26 13:31:20 2025 (r23207) +++ branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.mk Mon May 26 13:32:13 2025 (r23208) @@ -1,7 +1,7 @@ LIBOPENMPT_VERSION_MAJOR=0 LIBOPENMPT_VERSION_MINOR=8 LIBOPENMPT_VERSION_PATCH=0 -LIBOPENMPT_VERSION_PREREL=-rc.0 +LIBOPENMPT_VERSION_PREREL=-rc.1 LIBOPENMPT_LTVER_CURRENT=5 LIBOPENMPT_LTVER_REVISION=0 |
From: <sv...@op...> - 2025-05-26 11:32:00
|
Author: manx Date: Mon May 26 13:30:18 2025 New Revision: 23206 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23206 Log: [Mod] libopenmpt: Bump rc version. Modified: branches/OpenMPT-1.32/doc/libopenmpt/changelog.md branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.h branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.mk Modified: branches/OpenMPT-1.32/doc/libopenmpt/changelog.md ============================================================================== --- branches/OpenMPT-1.32/doc/libopenmpt/changelog.md Mon May 26 13:29:11 2025 (r23205) +++ branches/OpenMPT-1.32/doc/libopenmpt/changelog.md Mon May 26 13:30:18 2025 (r23206) @@ -5,7 +5,7 @@ For fully detailed change log, please see the source repository directly. This is just a high-level summary. -### libopenmpt 0.8.0-pre +### libopenmpt 0.8.0-rc * [**New**] Can now read PumaTracker (`PUMA`) modules. * [**New**] Can now read Face The Music (`FTM`) modules. Modified: branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.h ============================================================================== --- branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.h Mon May 26 13:29:11 2025 (r23205) +++ branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.h Mon May 26 13:30:18 2025 (r23206) @@ -23,7 +23,7 @@ /*! \brief libopenmpt patch version number */ #define OPENMPT_API_VERSION_PATCH 0 /*! \brief libopenmpt pre-release tag */ -#define OPENMPT_API_VERSION_PREREL "-pre.15" +#define OPENMPT_API_VERSION_PREREL "-rc.0" /*! \brief libopenmpt pre-release flag */ #define OPENMPT_API_VERSION_IS_PREREL 1 Modified: branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.mk ============================================================================== --- branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.mk Mon May 26 13:29:11 2025 (r23205) +++ branches/OpenMPT-1.32/libopenmpt/libopenmpt_version.mk Mon May 26 13:30:18 2025 (r23206) @@ -1,7 +1,7 @@ LIBOPENMPT_VERSION_MAJOR=0 LIBOPENMPT_VERSION_MINOR=8 LIBOPENMPT_VERSION_PATCH=0 -LIBOPENMPT_VERSION_PREREL=-pre.15 +LIBOPENMPT_VERSION_PREREL=-rc.0 LIBOPENMPT_LTVER_CURRENT=5 LIBOPENMPT_LTVER_REVISION=0 |
From: <sv...@op...> - 2025-05-26 11:32:00
|
Author: manx Date: Mon May 26 13:31:20 2025 New Revision: 23207 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23207 Log: tag libopenmpt-0.8.0-rc.0 Added: tags/libopenmpt-0.8.0-rc.0/ - copied from r23206, branches/OpenMPT-1.32/ |
From: <sv...@op...> - 2025-05-26 11:30:00
|
Author: manx Date: Mon May 26 13:28:48 2025 New Revision: 23204 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23204 Log: [Doc] Update release_branches.md. Modified: trunk/OpenMPT/doc/release_branches.md Modified: trunk/OpenMPT/doc/release_branches.md ============================================================================== --- trunk/OpenMPT/doc/release_branches.md Mon May 26 13:28:29 2025 (r23203) +++ trunk/OpenMPT/doc/release_branches.md Mon May 26 13:28:48 2025 (r23204) @@ -40,13 +40,16 @@ 1. set OpenMPT version in `common/versionNumber.h` to `1.$VER.00.$MINORMINOR+1` 2. run `build/update_libopenmpt_version.sh bumpprerel` -10. update CI branch configuration in new branch -11. update announcement/changelog URLs for test builds in branch: +10. update old stable branch + 1. uncomment updating OpenMPT version in + `build/svn/do_libopenmpt_release.sh` +11. update CI branch configuration in new branch +12. update announcement/changelog URLs for test builds in branch: `installer/generate_update_json.py` and `generate_update_json_retro.py`: * "trunk/OpenMPT" -> "banches/OpenMPT-1.32" -12. update https://builds.openmpt.org/builds/ -13. update buildbot scripts that copy OpenMPT update information into place -14. update branch release date on libopenmpt trunk changelog +13. update https://builds.openmpt.org/builds/ +14. update buildbot scripts that copy OpenMPT update information into place +15. update branch release date on libopenmpt trunk changelog unclear: * adjust buildbot update management script |
From: <sv...@op...> - 2025-05-26 11:29:23
|
Author: manx Date: Mon May 26 13:29:11 2025 New Revision: 23205 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23205 Log: Merged revision(s) 23204 from trunk/OpenMPT: [Doc] Update release_branches.md. ........ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/doc/release_branches.md Modified: branches/OpenMPT-1.32/doc/release_branches.md ============================================================================== --- branches/OpenMPT-1.32/doc/release_branches.md Mon May 26 13:28:48 2025 (r23204) +++ branches/OpenMPT-1.32/doc/release_branches.md Mon May 26 13:29:11 2025 (r23205) @@ -40,13 +40,16 @@ 1. set OpenMPT version in `common/versionNumber.h` to `1.$VER.00.$MINORMINOR+1` 2. run `build/update_libopenmpt_version.sh bumpprerel` -10. update CI branch configuration in new branch -11. update announcement/changelog URLs for test builds in branch: +10. update old stable branch + 1. uncomment updating OpenMPT version in + `build/svn/do_libopenmpt_release.sh` +11. update CI branch configuration in new branch +12. update announcement/changelog URLs for test builds in branch: `installer/generate_update_json.py` and `generate_update_json_retro.py`: * "trunk/OpenMPT" -> "banches/OpenMPT-1.32" -12. update https://builds.openmpt.org/builds/ -13. update buildbot scripts that copy OpenMPT update information into place -14. update branch release date on libopenmpt trunk changelog +13. update https://builds.openmpt.org/builds/ +14. update buildbot scripts that copy OpenMPT update information into place +15. update branch release date on libopenmpt trunk changelog unclear: * adjust buildbot update management script |
From: <sv...@op...> - 2025-05-26 11:28:36
|
Author: manx Date: Mon May 26 13:28:29 2025 New Revision: 23203 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23203 Log: [Mod] build: Update OpenMPT version in do_libopenmpt_release.sh. Modified: branches/OpenMPT-1.31/build/svn/do_libopenmpt_release.sh Modified: branches/OpenMPT-1.31/build/svn/do_libopenmpt_release.sh ============================================================================== --- branches/OpenMPT-1.31/build/svn/do_libopenmpt_release.sh Mon May 26 11:12:01 2025 (r23202) +++ branches/OpenMPT-1.31/build/svn/do_libopenmpt_release.sh Mon May 26 13:28:29 2025 (r23203) @@ -4,12 +4,12 @@ svn up -#build/update_openmpt_version.sh bumpbuild -#VER_MAJOR=$(cat common/versionNumber.h | grep "VER_MAJORMAJOR " | awk '{print $3;}') -#VER_MINOR=$(cat common/versionNumber.h | grep "VER_MAJOR " | awk '{print $3;}') -#VER_PATCH=$(cat common/versionNumber.h | grep "VER_MINOR " | awk '{print $3;}') -#VER_BUILD=$(cat common/versionNumber.h | grep "VER_MINORMINOR " | awk '{print $3;}') -#svn ci -m "[Mod] OpenMPT: Version is now ${VER_MAJOR}.${VER_MINOR}.${VER_PATCH}.${VER_BUILD}" +build/update_openmpt_version.sh bumpbuild +VER_MAJOR=$(cat common/versionNumber.h | grep "VER_MAJORMAJOR " | awk '{print $3;}') +VER_MINOR=$(cat common/versionNumber.h | grep "VER_MAJOR " | awk '{print $3;}') +VER_PATCH=$(cat common/versionNumber.h | grep "VER_MINOR " | awk '{print $3;}') +VER_BUILD=$(cat common/versionNumber.h | grep "VER_MINORMINOR " | awk '{print $3;}') +svn ci -m "[Mod] OpenMPT: Version is now ${VER_MAJOR}.${VER_MINOR}.${VER_PATCH}.${VER_BUILD}" build/update_libopenmpt_version.sh release svn ci -m "[Mod] libopenmpt: Prepare for release." @@ -21,12 +21,12 @@ build/update_libopenmpt_version.sh bumpltrev svn ci -m "[Mod] libopenmpt: Bump patch version." -#build/update_openmpt_version.sh bumpbuild -#VER_MAJOR=$(cat common/versionNumber.h | grep "VER_MAJORMAJOR " | awk '{print $3;}') -#VER_MINOR=$(cat common/versionNumber.h | grep "VER_MAJOR " | awk '{print $3;}') -#VER_PATCH=$(cat common/versionNumber.h | grep "VER_MINOR " | awk '{print $3;}') -#VER_BUILD=$(cat common/versionNumber.h | grep "VER_MINORMINOR " | awk '{print $3;}') -#svn ci -m "[Mod] OpenMPT: Version is now ${VER_MAJOR}.${VER_MINOR}.${VER_PATCH}.${VER_BUILD}" +build/update_openmpt_version.sh bumpbuild +VER_MAJOR=$(cat common/versionNumber.h | grep "VER_MAJORMAJOR " | awk '{print $3;}') +VER_MINOR=$(cat common/versionNumber.h | grep "VER_MAJOR " | awk '{print $3;}') +VER_PATCH=$(cat common/versionNumber.h | grep "VER_MINOR " | awk '{print $3;}') +VER_BUILD=$(cat common/versionNumber.h | grep "VER_MINORMINOR " | awk '{print $3;}') +svn ci -m "[Mod] OpenMPT: Version is now ${VER_MAJOR}.${VER_MINOR}.${VER_PATCH}.${VER_BUILD}" echo "ALL DONE." echo "run './release-0.7.sh $NEWVER +release' in a website checkout after buildbot has finished." |
From: <sv...@op...> - 2025-05-26 09:13:16
|
Author: manx Date: Mon May 26 11:11:39 2025 New Revision: 23201 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23201 Log: [Fix] build: Fix version gathering in libopenmpt release script. Modified: trunk/OpenMPT/build/svn/do_libopenmpt_release.sh trunk/OpenMPT/build/svn/do_libopenmpt_release_rc.sh Modified: trunk/OpenMPT/build/svn/do_libopenmpt_release.sh ============================================================================== --- trunk/OpenMPT/build/svn/do_libopenmpt_release.sh Sun May 25 18:54:15 2025 (r23200) +++ trunk/OpenMPT/build/svn/do_libopenmpt_release.sh Mon May 26 11:11:39 2025 (r23201) @@ -15,7 +15,7 @@ svn ci -m "[Mod] libopenmpt: Prepare for release." svn up make QUIET=1 distversion-pure -NEWVER=$(cat distversion-pure) +NEWVER=$(cat bin/distversion-pure) NEWREV=$(svn info --xml . | xpath -e 'string(/info/entry/commit/@revision)' -q) svn cp -m "tag libopenmpt-${NEWVER}" -r ${NEWREV} https://source.openmpt.org/svn/openmpt/branches/OpenMPT-1.33 https://source.openmpt.org/svn/openmpt/tags/libopenmpt-${NEWVER} build/update_libopenmpt_version.sh bumppatch Modified: trunk/OpenMPT/build/svn/do_libopenmpt_release_rc.sh ============================================================================== --- trunk/OpenMPT/build/svn/do_libopenmpt_release_rc.sh Sun May 25 18:54:15 2025 (r23200) +++ trunk/OpenMPT/build/svn/do_libopenmpt_release_rc.sh Mon May 26 11:11:39 2025 (r23201) @@ -5,7 +5,7 @@ svn up make QUIET=1 distversion-pure -NEWVER=$(cat distversion-pure) +NEWVER=$(cat bin/distversion-pure) NEWREV=$(svn info --xml . | xpath -e 'string(/info/entry/commit/@revision)' -q) svn cp -m "tag libopenmpt-${NEWVER}" -r ${NEWREV} https://source.openmpt.org/svn/openmpt/branches/OpenMPT-1.33 https://source.openmpt.org/svn/openmpt/tags/libopenmpt-${NEWVER} TAGREV=$(svn info --xml https://source.openmpt.org/svn/openmpt/tags/libopenmpt-${NEWVER} | xpath -e 'string(/info/entry/commit/@revision)' -q) |
From: <sv...@op...> - 2025-05-26 09:12:13
|
Author: manx Date: Mon May 26 11:12:01 2025 New Revision: 23202 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23202 Log: Merged revision(s) 23201 from trunk/OpenMPT: [Fix] build: Fix version gathering in libopenmpt release script. ........ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/build/svn/do_libopenmpt_release.sh branches/OpenMPT-1.32/build/svn/do_libopenmpt_release_rc.sh Modified: branches/OpenMPT-1.32/build/svn/do_libopenmpt_release.sh ============================================================================== --- branches/OpenMPT-1.32/build/svn/do_libopenmpt_release.sh Mon May 26 11:11:39 2025 (r23201) +++ branches/OpenMPT-1.32/build/svn/do_libopenmpt_release.sh Mon May 26 11:12:01 2025 (r23202) @@ -15,7 +15,7 @@ svn ci -m "[Mod] libopenmpt: Prepare for release." svn up make QUIET=1 distversion-pure -NEWVER=$(cat distversion-pure) +NEWVER=$(cat bin/distversion-pure) NEWREV=$(svn info --xml . | xpath -e 'string(/info/entry/commit/@revision)' -q) svn cp -m "tag libopenmpt-${NEWVER}" -r ${NEWREV} https://source.openmpt.org/svn/openmpt/branches/OpenMPT-1.32 https://source.openmpt.org/svn/openmpt/tags/libopenmpt-${NEWVER} build/update_libopenmpt_version.sh bumppatch Modified: branches/OpenMPT-1.32/build/svn/do_libopenmpt_release_rc.sh ============================================================================== --- branches/OpenMPT-1.32/build/svn/do_libopenmpt_release_rc.sh Mon May 26 11:11:39 2025 (r23201) +++ branches/OpenMPT-1.32/build/svn/do_libopenmpt_release_rc.sh Mon May 26 11:12:01 2025 (r23202) @@ -5,7 +5,7 @@ svn up make QUIET=1 distversion-pure -NEWVER=$(cat distversion-pure) +NEWVER=$(cat bin/distversion-pure) NEWREV=$(svn info --xml . | xpath -e 'string(/info/entry/commit/@revision)' -q) svn cp -m "tag libopenmpt-${NEWVER}" -r ${NEWREV} https://source.openmpt.org/svn/openmpt/branches/OpenMPT-1.32 https://source.openmpt.org/svn/openmpt/tags/libopenmpt-${NEWVER} TAGREV=$(svn info --xml https://source.openmpt.org/svn/openmpt/tags/libopenmpt-${NEWVER} | xpath -e 'string(/info/entry/commit/@revision)' -q) |
From: <sv...@op...> - 2025-05-25 16:55:01
|
Author: manx Date: Sun May 25 18:53:55 2025 New Revision: 23199 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23199 Log: Merged revision(s) 23198 from trunk/OpenMPT: [Fix] build: CI: GitHub: OpenBSD: Fix autotools build on 7.7. ........ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml Modified: branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml ============================================================================== --- branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml Sun May 25 18:53:31 2025 (r23198) +++ branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml Sun May 25 18:53:55 2025 (r23199) @@ -62,7 +62,7 @@ sync_files: false shutdown_vm: false run: | - export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh + export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.7') && 'export AUTOMAKE_VERSION=1.17' || (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh - name: Shutdown VM uses: cross-platform-actions/action@v0.28.0 with: |
From: <sv...@op...> - 2025-05-25 16:54:27
|
Author: manx Date: Sun May 25 18:54:15 2025 New Revision: 23200 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23200 Log: Merged revision(s) 23198 from trunk/OpenMPT: [Fix] build: CI: GitHub: OpenBSD: Fix autotools build on 7.7. ........ Modified: branches/OpenMPT-1.31/ (props changed) branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml Modified: branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml ============================================================================== --- branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml Sun May 25 18:53:55 2025 (r23199) +++ branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml Sun May 25 18:54:15 2025 (r23200) @@ -62,7 +62,7 @@ sync_files: false shutdown_vm: false run: | - export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh + export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.7') && 'export AUTOMAKE_VERSION=1.17' || (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh - name: Shutdown VM uses: cross-platform-actions/action@v0.28.0 with: |
From: <sv...@op...> - 2025-05-25 16:54:13
|
Author: manx Date: Sun May 25 18:53:31 2025 New Revision: 23198 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23198 Log: [Fix] build: CI: GitHub: OpenBSD: Fix autotools build on 7.7. Modified: trunk/OpenMPT/.github/workflows/OpenBSD-Autotools.yml Modified: trunk/OpenMPT/.github/workflows/OpenBSD-Autotools.yml ============================================================================== --- trunk/OpenMPT/.github/workflows/OpenBSD-Autotools.yml Sun May 25 16:43:55 2025 (r23197) +++ trunk/OpenMPT/.github/workflows/OpenBSD-Autotools.yml Sun May 25 18:53:31 2025 (r23198) @@ -64,7 +64,7 @@ sync_files: false shutdown_vm: false run: | - export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh + export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.7') && 'export AUTOMAKE_VERSION=1.17' || (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh - name: Shutdown VM uses: cross-platform-actions/action@v0.28.0 with: |
From: <sv...@op...> - 2025-05-25 14:44:02
|
Author: sagamusix Date: Sun May 25 16:43:55 2025 New Revision: 23197 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23197 Log: Merged revision(s) 23196 from trunk/OpenMPT: [Fix] MED: Jumps to a command in another sequence cannot be resolved using the current sequence's byte mapping; use the existing jump markers as a target instead (https://www.un4seen.com/forum/?msg=144699). ........ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/soundlib/InstrumentSynth.cpp branches/OpenMPT-1.32/soundlib/InstrumentSynth.h branches/OpenMPT-1.32/soundlib/Load_fc.cpp Modified: branches/OpenMPT-1.32/soundlib/InstrumentSynth.cpp ============================================================================== --- branches/OpenMPT-1.32/soundlib/InstrumentSynth.cpp Sun May 25 16:43:24 2025 (r23196) +++ branches/OpenMPT-1.32/soundlib/InstrumentSynth.cpp Sun May 25 16:43:55 2025 (r23197) @@ -687,10 +687,9 @@ m_medArpPos = 0; return true; case Event::Type::MED_JumpScript: - if(event.u8 < chn.synthState.states.size()) + if(event.u8 < chn.synthState.states.size() && chn.pModInstrument && event.u8 < chn.pModInstrument->synth.m_scripts.size()) { - chn.synthState.states[event.u8].m_nextRow = event.u16; - chn.synthState.states[event.u8].m_ticksRemain = 0; + chn.synthState.states[event.u8].JumpToPosition(chn.pModInstrument->synth.m_scripts[event.u8], event.u16); chn.synthState.states[event.u8].m_stepsRemain = 0; } return false; Modified: branches/OpenMPT-1.32/soundlib/InstrumentSynth.h ============================================================================== --- branches/OpenMPT-1.32/soundlib/InstrumentSynth.h Sun May 25 16:43:24 2025 (r23196) +++ branches/OpenMPT-1.32/soundlib/InstrumentSynth.h Sun May 25 16:43:55 2025 (r23197) @@ -63,7 +63,7 @@ Mupp_SetWaveform, // Parameter: Source instrument (uint8), waveform (uint8), volume (uint8) MED_DefineArpeggio, // Parameter: Arpeggio note (uint8), arp length or 0 if it's not the first note (uint16) - MED_JumpScript, // Parameter: Script index (uint8), jump target (uint16) + MED_JumpScript, // Parameter: Script index (uint8), jump target (uint16 - JumpMarker ID, not event index!) MED_SetEnvelope, // Parameter: Envelope index (uint8), loop on/off (uint8), is volume envelope (uint8) MED_SetVolume, // Parameter: Volume (uint8) MED_SetWaveform, // Parameter: Waveform (uint8) @@ -106,7 +106,7 @@ { Type::Jump, Type::JumpIfTrue, Type::EvaluateLoopCounter, Type::GTK_KeyOff, - Type::MED_JumpScript, Type::MED_HoldDecay, + Type::MED_HoldDecay, Type::FTM_SetInterrupt, }; Modified: branches/OpenMPT-1.32/soundlib/Load_fc.cpp ============================================================================== --- branches/OpenMPT-1.32/soundlib/Load_fc.cpp Sun May 25 16:43:24 2025 (r23196) +++ branches/OpenMPT-1.32/soundlib/Load_fc.cpp Sun May 25 16:43:55 2025 (r23197) @@ -119,6 +119,7 @@ FileReader file{script}; const bool isVolume = startSequence > 255; const uint8 volScriptSpeed = (script[0] > 0) ? script[0] : uint8_max; + events.push_back(InstrumentSynth::Event::JumpMarker(0)); if(isVolume) events.push_back(InstrumentSynth::Event::SetStepSpeed(volScriptSpeed, true)); |
From: <sv...@op...> - 2025-05-25 14:43:36
|
Author: sagamusix Date: Sun May 25 16:43:24 2025 New Revision: 23196 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23196 Log: [Fix] MED: Jumps to a command in another sequence cannot be resolved using the current sequence's byte mapping; use the existing jump markers as a target instead (https://www.un4seen.com/forum/?msg=144699). Modified: trunk/OpenMPT/soundlib/InstrumentSynth.cpp trunk/OpenMPT/soundlib/InstrumentSynth.h trunk/OpenMPT/soundlib/Load_fc.cpp Modified: trunk/OpenMPT/soundlib/InstrumentSynth.cpp ============================================================================== --- trunk/OpenMPT/soundlib/InstrumentSynth.cpp Sun May 25 16:35:08 2025 (r23195) +++ trunk/OpenMPT/soundlib/InstrumentSynth.cpp Sun May 25 16:43:24 2025 (r23196) @@ -687,10 +687,9 @@ m_medArpPos = 0; return true; case Event::Type::MED_JumpScript: - if(event.u8 < chn.synthState.states.size()) + if(event.u8 < chn.synthState.states.size() && chn.pModInstrument && event.u8 < chn.pModInstrument->synth.m_scripts.size()) { - chn.synthState.states[event.u8].m_nextRow = event.u16; - chn.synthState.states[event.u8].m_ticksRemain = 0; + chn.synthState.states[event.u8].JumpToPosition(chn.pModInstrument->synth.m_scripts[event.u8], event.u16); chn.synthState.states[event.u8].m_stepsRemain = 0; } return false; Modified: trunk/OpenMPT/soundlib/InstrumentSynth.h ============================================================================== --- trunk/OpenMPT/soundlib/InstrumentSynth.h Sun May 25 16:35:08 2025 (r23195) +++ trunk/OpenMPT/soundlib/InstrumentSynth.h Sun May 25 16:43:24 2025 (r23196) @@ -63,7 +63,7 @@ Mupp_SetWaveform, // Parameter: Source instrument (uint8), waveform (uint8), volume (uint8) MED_DefineArpeggio, // Parameter: Arpeggio note (uint8), arp length or 0 if it's not the first note (uint16) - MED_JumpScript, // Parameter: Script index (uint8), jump target (uint16) + MED_JumpScript, // Parameter: Script index (uint8), jump target (uint16 - JumpMarker ID, not event index!) MED_SetEnvelope, // Parameter: Envelope index (uint8), loop on/off (uint8), is volume envelope (uint8) MED_SetVolume, // Parameter: Volume (uint8) MED_SetWaveform, // Parameter: Waveform (uint8) @@ -106,7 +106,7 @@ { Type::Jump, Type::JumpIfTrue, Type::EvaluateLoopCounter, Type::GTK_KeyOff, - Type::MED_JumpScript, Type::MED_HoldDecay, + Type::MED_HoldDecay, Type::FTM_SetInterrupt, }; Modified: trunk/OpenMPT/soundlib/Load_fc.cpp ============================================================================== --- trunk/OpenMPT/soundlib/Load_fc.cpp Sun May 25 16:35:08 2025 (r23195) +++ trunk/OpenMPT/soundlib/Load_fc.cpp Sun May 25 16:43:24 2025 (r23196) @@ -119,6 +119,7 @@ FileReader file{script}; const bool isVolume = startSequence > 255; const uint8 volScriptSpeed = (script[0] > 0) ? script[0] : uint8_max; + events.push_back(InstrumentSynth::Event::JumpMarker(0)); if(isVolume) events.push_back(InstrumentSynth::Event::SetStepSpeed(volScriptSpeed, true)); |
From: <sv...@op...> - 2025-05-25 14:36:23
|
Author: manx Date: Sun May 25 16:35:08 2025 New Revision: 23195 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23195 Log: Merged revision(s) 23193 from trunk/OpenMPT: [Fix] build: CI: GitHub: OpenBSD: Fix autotools build on 7.7. ........ Modified: branches/OpenMPT-1.31/ (props changed) branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml Modified: branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml ============================================================================== --- branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml Sun May 25 16:34:47 2025 (r23194) +++ branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml Sun May 25 16:35:08 2025 (r23195) @@ -62,7 +62,7 @@ sync_files: false shutdown_vm: false run: | - export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh + export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh - name: Shutdown VM uses: cross-platform-actions/action@v0.28.0 with: |
From: <sv...@op...> - 2025-05-25 14:35:53
|
Author: manx Date: Sun May 25 16:34:47 2025 New Revision: 23194 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23194 Log: Merged revision(s) 23193 from trunk/OpenMPT: [Fix] build: CI: GitHub: OpenBSD: Fix autotools build on 7.7. ........ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml Modified: branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml ============================================================================== --- branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml Sun May 25 16:34:19 2025 (r23193) +++ branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml Sun May 25 16:34:47 2025 (r23194) @@ -62,7 +62,7 @@ sync_files: false shutdown_vm: false run: | - export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh + export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh - name: Shutdown VM uses: cross-platform-actions/action@v0.28.0 with: |
From: <sv...@op...> - 2025-05-25 14:34:26
|
Author: manx Date: Sun May 25 16:34:19 2025 New Revision: 23193 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23193 Log: [Fix] build: CI: GitHub: OpenBSD: Fix autotools build on 7.7. Modified: trunk/OpenMPT/.github/workflows/OpenBSD-Autotools.yml Modified: trunk/OpenMPT/.github/workflows/OpenBSD-Autotools.yml ============================================================================== --- trunk/OpenMPT/.github/workflows/OpenBSD-Autotools.yml Sun May 25 14:04:43 2025 (r23192) +++ trunk/OpenMPT/.github/workflows/OpenBSD-Autotools.yml Sun May 25 16:34:19 2025 (r23193) @@ -64,7 +64,7 @@ sync_files: false shutdown_vm: false run: | - export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh + export MAKEFLAGS="-j$(sysctl -n hw.ncpu)"; ${{ (matrix.version == '7.7') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.6') && 'export AUTOCONF_VERSION=2.72' || (matrix.version == '7.5') && 'export AUTOCONF_VERSION=2.71' || (matrix.version == '7.4') && 'export AUTOCONF_VERSION=2.71' || '' }} ; ${{ (matrix.version == '7.6') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.5') && 'export AUTOMAKE_VERSION=1.16' || (matrix.version == '7.4') && 'export AUTOMAKE_VERSION=1.16' || '' }} ;./build/autotools/autoconfiscate.sh - name: Shutdown VM uses: cross-platform-actions/action@v0.28.0 with: |
Author: sagamusix Date: Sun May 25 14:04:43 2025 New Revision: 23192 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23192 Log: Merged revision(s) 23191 from trunk/OpenMPT: [Var] Update Signalsmith Stretch to commit c78aacf6389fb862978d4ead01cfc71a3d5f8ca8 (2025-05-01) ........ Added: branches/OpenMPT-1.32/include/SignalsmithStretch/signalsmith-linear/ - copied from r23191, trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/ Deleted: branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/dsp/ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/include/SignalsmithStretch/OpenMPT.txt branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/README.md branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h branches/OpenMPT-1.32/tracklib/TimeStretchPitchShift.cpp Modified: branches/OpenMPT-1.32/include/SignalsmithStretch/OpenMPT.txt ============================================================================== --- branches/OpenMPT-1.32/include/SignalsmithStretch/OpenMPT.txt Sun May 25 14:04:10 2025 (r23191) +++ branches/OpenMPT-1.32/include/SignalsmithStretch/OpenMPT.txt Sun May 25 14:04:43 2025 (r23192) @@ -1,3 +1,7 @@ -Signalsmith Stretch: C++ pitch/time library, commit ffa45981be0b75079b43d1a7a142234f06869254 (2025-02-12) +Signalsmith Stretch: C++ pitch shift / time stretch library, commit c78aacf6389fb862978d4ead01cfc71a3d5f8ca8 (2025-05-01) https://github.com/Signalsmith-Audio/signalsmith-stretch -Unneeded files (cmd and web folder) have been removed. \ No newline at end of file +Unneeded files have been removed. + +Signalsmith Linear: C++ audio utilities, commit 94d677be71a50789eb7a33b8927947046c0c2fa2 (2025-04-09) +https://github.com/Signalsmith-Audio/linear +Unneeded files have been removed. \ No newline at end of file Modified: branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/README.md ============================================================================== --- branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/README.md Sun May 25 14:04:10 2025 (r23191) +++ branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/README.md Sun May 25 14:04:43 2025 (r23192) @@ -90,6 +90,12 @@ You should be supplying input samples slightly ahead of the processing time (which is where changes to pitch-shift or stretch rate will be centred), and you'll receive output samples slightly behind that processing time. +### Split computation + +All of the `.preset???()` and `.configure()` methods have an optional `splitComputation` flag. When enabled, this introduces one extra interval of output latency, and uses this to spread the computation out more evenly. + +Without this (as is common for spectral processing) the library will occasionally do a bunch of computation all at once, to compute the next spectral block of audio. This is often fine, when audio's being processed across multiple threads with a decent amount of buffering (like mixing in a DAW), but if you're in a stricter situation then this flag might help. + #### Automation To follow pitch/time automation accurately, you should give it automation values from the current processing time (`.outputLatency()` samples ahead of the output), and feed it input from `.inputLatency()` samples ahead of the current processing time. @@ -114,21 +120,36 @@ stretch.flush(outputBuffers, outputSamples); ``` -Using `.seek()`/`.flush()` like this, you can perform an exact time-stretch on a fixed-length sound, and your result will have `.outputLatency()` of pre-roll. +Using `.seek()`/`.flush()` like this, you can perform an exact time-stretch on a fixed-length sound, and your result will have `.outputLatency()` of pre-roll. -## Compiling +### Formant compensation -⚠️ This has mostly been tested with Clang. If you're using another compiler and have any problems, please get in touch. +```cpp +stretch.setFormantFactor(1.2); // up ~3 semitones -🚨 It's generally be OK to enable `-ffast-math`, however there's a bug in Apple Clang 16.0.0 which can generate incorrect SIMD code. If you _have_ to use this version, we advise you don't use `-ffast-math`. +stretch.setTransposeSemitones(3); +``` -It's much slower (about 10x) if optimisation is disabled altogether, so you might want to enable optimisation where it's used, even in Debug builds. +Both of those methods both have an optional `compensatePitch` flag. Enabling this adjust for the pitch-shift (or non-linear map) when correcting/shifting formants. -### DSP Library +The formant correction is not a sharp as monophonic algorithms (such such as PSOLA). It also needs you to give it a rough estimate of fundamental frequency (relative to Nyquist): -This uses the Signalsmith DSP library for FFTs and other bits and bobs. +```cpp +// if 200Hz is the middle-register of the instrument +stretch.setFormantBase(200/sampleRate); +``` -For convenience, a copy of the library is included (with its own `LICENSE.txt`) in `dsp/`, but if you're already using this elsewhere then you should remove this copy to avoid versioning issues. +## Compiling + +⚠️ This has mostly been tested with AppleClang (Mac), and MSVC (Windows). We aim to support other compilers though, so please get in touch if you have any problems. + +Enabling `-ffast-math` (or equivalent) is fine, unless you're using Apple Clang 16.0.0 (in which case it will deliberately break, because that version generates incorrect SIMD code). + +The algorithm has a lot of number-crunching, so Debug builds are much slower (up to 10x). If possible, you might want to enable optimisation where Stretch is used, even in Debug builds. + +### Dependencies and `#define`s + +This uses the [Signalsmith Linear](https://github.com/Signalsmith-Audio/linear) library for FFTs and other speedups. There are [flags]([Linear repo](https://github.com/Signalsmith-Audio/linear?tab=readme-ov-file#building)) to enable Accelerate (`SIGNALSMITH_USE_ACCELERATE`) or IPP (`SIGNALSMITH_USE_IPP`). ## License @@ -136,14 +157,18 @@ ## Other environments / languages -There's a Web Audio wrapper in `web/` (using WASM/AudioWorklet). This will remain in-sync with the C++ library. +There's a Web Audio release in [`web/`](web/) (WASM/AudioWorklet), also available on [NPM](npmjs.com/package/signalsmith-stretch). This has its own (higher-level) API, but the stretching algorithm will remain in-sync with the C++ library. + +There's a [Python binding](https://pypi.org/project/python-stretch/) written/published by [Gregorio Andrea Giudici](https://github.com/gregogiudici/python-stretch). This is used as the default pitch/time method in [Audiomentations](https://iver56.github.io/audiomentations/). -There's a [Python binding](https://pypi.org/project/python-stretch/) written/published by [Gregorio Andrea Giudici](https://github.com/gregogiudici/python-stretch), and a [Rust wrapper](https://crates.io/crates/signalsmith-stretch) by [Colin Marc](https://github.com/colinmarc/signalsmith-stretch-rs). +There's a [Rust wrapper](https://crates.io/crates/signalsmith-stretch) by [Colin Marc](https://github.com/colinmarc/signalsmith-stretch-rs). ## Thanks We'd like to particularly thank the following people who sponsored specific features or improvements: +* **Future Audio Workshop**: chunked-computation mode +* **Jochem de Jong**: formant shifting/compensation * **Metronaut**: web audio (JS/WASM) release * **Daniel L Bowling** and the Stanford School of Medicine: web audio improvements Modified: branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h ============================================================================== --- branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h Sun May 25 14:04:10 2025 (r23191) +++ branches/OpenMPT-1.32/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h Sun May 25 14:04:43 2025 (r23192) @@ -1,63 +1,89 @@ #ifndef SIGNALSMITH_STRETCH_H #define SIGNALSMITH_STRETCH_H -#include "dsp/spectral.h" -#include "dsp/delay.h" -#include "dsp/perf.h" -SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 0); // Check version is compatible +#include "signalsmith-linear/stft.h" // https://github.com/Signalsmith-Audio/linear + #include <vector> +#include <array> #include <algorithm> #include <functional> #include <random> +#include <limits> +#include <type_traits> namespace signalsmith { namespace stretch { -template<typename Sample=float, class RandomEngine=std::default_random_engine> +namespace _impl { + template<bool conjugateSecond=false, typename V> + static std::complex<V> mul(const std::complex<V> &a, const std::complex<V> &b) { + return conjugateSecond ? std::complex<V>{ + b.real()*a.real() + b.imag()*a.imag(), + b.real()*a.imag() - b.imag()*a.real() + } : std::complex<V>{ + a.real()*b.real() - a.imag()*b.imag(), + a.real()*b.imag() + a.imag()*b.real() + }; + } + template<typename V> + static V norm(const std::complex<V> &a) { + V r = a.real(), i = a.imag(); + return r*r + i*i; + } +} + +template<typename Sample=float, class RandomEngine=void> struct SignalsmithStretch { - static constexpr size_t version[3] = {1, 1, 1}; + static constexpr size_t version[3] = {1, 3, 1}; SignalsmithStretch() : randomEngine(std::random_device{}()) {} SignalsmithStretch(long seed) : randomEngine(seed) {} - + int blockSamples() const { - return stft.windowSize(); + return int(stft.blockSamples()); } int intervalSamples() const { - return stft.interval(); + return int(stft.defaultInterval()); } int inputLatency() const { - return stft.windowSize()/2; + return int(stft.analysisLatency()); } int outputLatency() const { - return stft.windowSize() - inputLatency(); + return int(stft.synthesisLatency() + _splitComputation*stft.defaultInterval()); } void reset() { - stft.reset(); - inputBuffer.reset(); + stft.reset(0.1); + stashedInput = stft.input; + stashedOutput = stft.output; + prevInputOffset = -1; channelBands.assign(channelBands.size(), Band()); silenceCounter = 0; didSeek = false; - flushed = true; + blockProcess = {}; + freqEstimateWeighted = freqEstimateWeight = 0; } // Configures using a default preset - void presetDefault(int nChannels, Sample sampleRate) { - configure(nChannels, sampleRate*0.12, sampleRate*0.03); + void presetDefault(int nChannels, Sample sampleRate, bool splitComputation=false) { + configure(nChannels, sampleRate*0.12, sampleRate*0.03, splitComputation); } - void presetCheaper(int nChannels, Sample sampleRate) { - configure(nChannels, sampleRate*0.1, sampleRate*0.04); + void presetCheaper(int nChannels, Sample sampleRate, bool splitComputation=true) { + configure(nChannels, sampleRate*0.1, sampleRate*0.04, splitComputation); } // Manual setup - void configure(int nChannels, int blockSamples, int intervalSamples) { + void configure(int nChannels, int blockSamples, int intervalSamples, bool splitComputation=false) { + _splitComputation = splitComputation; channels = nChannels; - stft.setWindow(stft.kaiser, true); - stft.resize(channels, blockSamples, intervalSamples); - bands = stft.bands(); - inputBuffer.resize(channels, blockSamples + intervalSamples + 1); - timeBuffer.assign(stft.fftSize(), 0); + stft.configure(channels, channels, blockSamples, intervalSamples + 1); + stft.setInterval(intervalSamples, stft.kaiser); + stft.reset(0.1); + stashedInput = stft.input; + stashedOutput = stft.output; + tmpBuffer.resize(blockSamples + intervalSamples); + + bands = int(stft.bands()); channelBands.assign(bands*channels, Band()); peaks.reserve(bands/2); @@ -65,6 +91,9 @@ smoothedEnergy.resize(bands); outputMap.resize(bands); channelPredictions.resize(channels*bands); + + blockProcess = {}; + formantMetric.resize(bands + 2); } /// Frequency multiplier, and optional tonality limit (as multiple of sample-rate) @@ -79,39 +108,76 @@ } void setTransposeSemitones(Sample semitones, Sample tonalityLimit=0) { setTransposeFactor(std::pow(2, semitones/12), tonalityLimit); - customFreqMap = nullptr; } // Sets a custom frequency map - should be monotonically increasing void setFreqMap(std::function<Sample(Sample)> inputToOutput) { customFreqMap = inputToOutput; } + void setFormantFactor(Sample multiplier, bool compensatePitch=false) { + formantMultiplier = multiplier; + invFormantMultiplier = 1/multiplier; + formantCompensation = compensatePitch; + } + void setFormantSemitones(Sample semitones, bool compensatePitch=false) { + setFormantFactor(std::pow(2, semitones/12), compensatePitch); + } + // Rough guesstimate of the fundamental frequency, used for formant analysis. 0 means attempting to detect the pitch + void setFormantBase(Sample baseFreq=0) { + formantBaseFreq = baseFreq; + } + // Provide previous input ("pre-roll"), without affecting the speed calculation. You should ideally feed it one block-length + one interval template<class Inputs> void seek(Inputs &&inputs, int inputSamples, double playbackRate) { - inputBuffer.reset(); + tmpBuffer.resize(0); + tmpBuffer.resize(stft.blockSamples() + stft.defaultInterval()); + + int startIndex = std::max<int>(0, inputSamples - int(tmpBuffer.size())); // start position in input + int padStart = int(tmpBuffer.size() + startIndex) - inputSamples; // start position in tmpBuffer + Sample totalEnergy = 0; for (int c = 0; c < channels; ++c) { auto &&inputChannel = inputs[c]; - auto &&bufferChannel = inputBuffer[c]; - int startIndex = std::max<int>(0, inputSamples - stft.windowSize() - stft.interval()); for (int i = startIndex; i < inputSamples; ++i) { Sample s = inputChannel[i]; totalEnergy += s*s; - bufferChannel[i] = s; + tmpBuffer[i - startIndex + padStart] = s; } + + stft.writeInput(c, tmpBuffer.size(), tmpBuffer.data()); } + stft.moveInput(tmpBuffer.size()); if (totalEnergy >= noiseFloor) { silenceCounter = 0; silenceFirst = true; } - inputBuffer += inputSamples; didSeek = true; - seekTimeFactor = (playbackRate*stft.interval() > 1) ? 1/playbackRate : stft.interval(); + seekTimeFactor = (playbackRate*stft.defaultInterval() > 1) ? 1/playbackRate : stft.defaultInterval(); } - + template<class Inputs, class Outputs> void process(Inputs &&inputs, int inputSamples, Outputs &&outputs, int outputSamples) { +#ifdef SIGNALSMITH_STRETCH_PROFILE_PROCESS_START + SIGNALSMITH_STRETCH_PROFILE_PROCESS_START(inputSamples, outputSamples); +#endif + int prevCopiedInput = 0; + auto copyInput = [&](int toIndex){ + + int length = std::min<int>(int(stft.blockSamples() + stft.defaultInterval()), toIndex - prevCopiedInput); + tmpBuffer.resize(length); + int offset = toIndex - length; + for (int c = 0; c < channels; ++c) { + auto &&inputBuffer = inputs[c]; + for (int i = 0; i < length; ++i) { + tmpBuffer[i] = inputBuffer[i + offset]; + } + stft.writeInput(c, length, tmpBuffer.data()); + } + stft.moveInput(length); + prevCopiedInput = toIndex; + }; + Sample totalEnergy = 0; for (int c = 0; c < channels; ++c) { auto &&inputChannel = inputs[c]; @@ -120,10 +186,13 @@ totalEnergy += s*s; } } + if (totalEnergy < noiseFloor) { - if (silenceCounter >= 2*stft.windowSize()) { - if (silenceFirst) { + if (silenceCounter >= 2*stft.blockSamples()) { + if (silenceFirst) { // first block of silence processing silenceFirst = false; + //stft.reset(); + blockProcess = {}; for (auto &b : channelBands) { b.input = b.prevInput = b.output = 0; b.inputEnergy = 0; @@ -148,15 +217,7 @@ } // Store input in history buffer - for (int c = 0; c < channels; ++c) { - auto &&inputChannel = inputs[c]; - auto &&bufferChannel = inputBuffer[c]; - int startIndex = std::max<int>(0, inputSamples - stft.windowSize() - stft.interval()); - for (int i = startIndex; i < inputSamples; ++i) { - bufferChannel[i] = inputChannel[i]; - } - } - inputBuffer += inputSamples; + copyInput(inputSamples); return; } else { silenceCounter += inputSamples; @@ -165,119 +226,174 @@ silenceCounter = 0; silenceFirst = true; } - + for (int outputIndex = 0; outputIndex < outputSamples; ++outputIndex) { - stft.ensureValid(outputIndex, [&](int outputOffset) { + bool newBlock = blockProcess.samplesSinceLast >= stft.defaultInterval(); + if (newBlock) { + blockProcess.step = 0; + blockProcess.steps = 0; // how many processing steps this block will have + blockProcess.samplesSinceLast = 0; + // Time to process a spectrum! Where should it come from in the input? - int inputOffset = std::round(outputOffset*Sample(inputSamples)/outputSamples) - stft.windowSize(); + int inputOffset = std::round(outputIndex*Sample(inputSamples)/outputSamples); int inputInterval = inputOffset - prevInputOffset; prevInputOffset = inputOffset; + + copyInput(inputOffset); + stashedInput = stft.input; // save the input state, since that's what we'll analyse later + if (_splitComputation) { + stashedOutput = stft.output; // save the current output, and read from it + stft.moveOutput(stft.defaultInterval()); // the actual input jumps forward in time by one interval, ready for the synthesis + } + + blockProcess.newSpectrum = didSeek || (inputInterval > 0); + blockProcess.mappedFrequencies = customFreqMap || freqMultiplier != 1; + if (blockProcess.newSpectrum) { + // make sure the previous input is the correct distance in the past (give or take 1 sample) + blockProcess.reanalysePrev = didSeek || std::abs(inputInterval - int(stft.defaultInterval())) > 1; + if (blockProcess.reanalysePrev) blockProcess.steps += stft.analyseSteps() + 1; - bool newSpectrum = didSeek || (inputInterval > 0); - if (newSpectrum) { - for (int c = 0; c < channels; ++c) { - // Copy from the history buffer, if needed - auto &&bufferChannel = inputBuffer[c]; - for (int i = 0; i < -inputOffset; ++i) { - timeBuffer[i] = bufferChannel[i + inputOffset]; + // analyse a new input + blockProcess.steps += stft.analyseSteps() + 1; + } + + blockProcess.processFormants = formantMultiplier != 1 || (formantCompensation && blockProcess.mappedFrequencies); + + blockProcess.timeFactor = didSeek ? seekTimeFactor : stft.defaultInterval()/std::max<Sample>(1, inputInterval); + didSeek = false; + + updateProcessSpectrumSteps(); + blockProcess.steps += processSpectrumSteps; + + blockProcess.steps += stft.synthesiseSteps() + 1; + } + + size_t processToStep = newBlock ? blockProcess.steps : 0; + if (_splitComputation) { + Sample processRatio = Sample(blockProcess.samplesSinceLast + 1)/stft.defaultInterval(); + processToStep = std::min<size_t>(blockProcess.steps, (blockProcess.steps + 0.999f)*processRatio); + } + + while (blockProcess.step < processToStep) { + size_t step = blockProcess.step++; +#ifdef SIGNALSMITH_STRETCH_PROFILE_PROCESS_STEP + SIGNALSMITH_STRETCH_PROFILE_PROCESS_STEP(step, blockProcess.steps); +#endif + if (blockProcess.newSpectrum) { + if (blockProcess.reanalysePrev) { + // analyse past input + if (step < stft.analyseSteps()) { + stashedInput.swap(stft.input); + stft.analyseStep(step, stft.defaultInterval()); + stashedInput.swap(stft.input); + continue; } - // Copy the rest from the input - auto &&inputChannel = inputs[c]; - for (int i = std::max<int>(0, -inputOffset); i < stft.windowSize(); ++i) { - timeBuffer[i] = inputChannel[i + inputOffset]; + step -= stft.analyseSteps(); + if (step < 1) { + // Copy previous analysis to our band objects + for (int c = 0; c < channels; ++c) { + auto channelBands = bandsForChannel(c); + auto *spectrumBands = stft.spectrum(c); + for (int b = 0; b < bands; ++b) { + channelBands[b].prevInput = spectrumBands[b]; + } + } + continue; } - stft.analyse(c, timeBuffer); + step -= 1; } - flushed = false; // TODO: first block after a flush should be gain-compensated - for (int c = 0; c < channels; ++c) { - auto channelBands = bandsForChannel(c); - auto &&spectrumBands = stft.spectrum[c]; - for (int b = 0; b < bands; ++b) { - channelBands[b].input = spectrumBands[b]; - } + // Analyse latest (stashed) input + if (step < stft.analyseSteps()) { + stashedInput.swap(stft.input); + stft.analyseStep(step); + stashedInput.swap(stft.input); + continue; } - - if (didSeek || inputInterval != stft.interval()) { // make sure the previous input is the correct distance in the past - int prevIntervalOffset = inputOffset - stft.interval(); - for (int c = 0; c < channels; ++c) { - // Copy from the history buffer, if needed - auto &&bufferChannel = inputBuffer[c]; - for (int i = 0; i < std::min(-prevIntervalOffset, stft.windowSize()); ++i) { - timeBuffer[i] = bufferChannel[i + prevIntervalOffset]; - } - // Copy the rest from the input - auto &&inputChannel = inputs[c]; - for (int i = std::max<int>(0, -prevIntervalOffset); i < stft.windowSize(); ++i) { - timeBuffer[i] = inputChannel[i + prevIntervalOffset]; - } - stft.analyse(c, timeBuffer); - } + step -= stft.analyseSteps(); + if (step < 1) { + // Copy analysed spectrum into our band objects for (int c = 0; c < channels; ++c) { auto channelBands = bandsForChannel(c); - auto &&spectrumBands = stft.spectrum[c]; + auto *spectrumBands = stft.spectrum(c); for (int b = 0; b < bands; ++b) { - channelBands[b].prevInput = spectrumBands[b]; + channelBands[b].input = spectrumBands[b]; } } + continue; } + step -= 1; } - - Sample timeFactor = didSeek ? seekTimeFactor : stft.interval()/std::max<Sample>(1, inputInterval); - processSpectrum(newSpectrum, timeFactor); - didSeek = false; - for (int c = 0; c < channels; ++c) { - auto channelBands = bandsForChannel(c); - auto &&spectrumBands = stft.spectrum[c]; - for (int b = 0; b < bands; ++b) { - spectrumBands[b] = channelBands[b].output; + if (step < processSpectrumSteps) { + processSpectrum(step); + continue; + } + step -= processSpectrumSteps; + + if (step < 1) { + // Copy band objects into spectrum + for (int c = 0; c < channels; ++c) { + auto channelBands = bandsForChannel(c); + auto *spectrumBands = stft.spectrum(c); + for (int b = 0; b < bands; ++b) { + spectrumBands[b] = channelBands[b].output; + } } + continue; } - }); + step -= 1; + + if (step < stft.synthesiseSteps()) { + stft.synthesiseStep(step); + continue; + } + } +#ifdef SIGNALSMITH_STRETCH_PROFILE_PROCESS_ENDSTEP + SIGNALSMITH_STRETCH_PROFILE_PROCESS_ENDSTEP(); +#endif + ++blockProcess.samplesSinceLast; + if (_splitComputation) stashedOutput.swap(stft.output); for (int c = 0; c < channels; ++c) { auto &&outputChannel = outputs[c]; - auto &&stftChannel = stft[c]; - outputChannel[outputIndex] = stftChannel[outputIndex]; + Sample v = 0; + stft.readOutput(c, 1, &v); + outputChannel[outputIndex] = v; } + stft.moveOutput(1); + if (_splitComputation) stashedOutput.swap(stft.output); } - - // Store input in history buffer - for (int c = 0; c < channels; ++c) { - auto &&inputChannel = inputs[c]; - auto &&bufferChannel = inputBuffer[c]; - int startIndex = std::max<int>(0, inputSamples - stft.windowSize()); - for (int i = startIndex; i < inputSamples; ++i) { - bufferChannel[i] = inputChannel[i]; - } - } - inputBuffer += inputSamples; - stft += outputSamples; + + copyInput(inputSamples); prevInputOffset -= inputSamples; +#ifdef SIGNALSMITH_STRETCH_PROFILE_PROCESS_END + SIGNALSMITH_STRETCH_PROFILE_PROCESS_END(); +#endif } // Read the remaining output, providing no further input. `outputSamples` should ideally be at least `.outputLatency()` template<class Outputs> void flush(Outputs &&outputs, int outputSamples) { - int plainOutput = std::min<int>(outputSamples, stft.windowSize()); - int foldedBackOutput = std::min<int>(outputSamples, stft.windowSize() - plainOutput); + int plainOutput = std::min<int>(outputSamples, int(stft.blockSamples())); + int foldedBackOutput = std::min<int>(outputSamples, int(stft.blockSamples()) - plainOutput); + stft.finishOutput(1); for (int c = 0; c < channels; ++c) { + tmpBuffer.resize(plainOutput); + stft.readOutput(c, plainOutput, tmpBuffer.data()); auto &&outputChannel = outputs[c]; - auto &&stftChannel = stft[c]; for (int i = 0; i < plainOutput; ++i) { // TODO: plain output should be gain- - outputChannel[i] = stftChannel[i]; + outputChannel[i] = tmpBuffer[i]; } + tmpBuffer.resize(foldedBackOutput); + stft.readOutput(c, plainOutput, foldedBackOutput, tmpBuffer.data()); for (int i = 0; i < foldedBackOutput; ++i) { - outputChannel[outputSamples - 1 - i] -= stftChannel[plainOutput + i]; - } - for (int i = 0; i < plainOutput + foldedBackOutput; ++i) { - stftChannel[i] = 0; + outputChannel[outputSamples - 1 - i] -= tmpBuffer[i]; } } - // Skip the output we just used/cleared - stft += plainOutput + foldedBackOutput; + stft.reset(0.1); + // Reset the phase-vocoder stuff, so the next block gets a fresh start for (int c = 0; c < channels; ++c) { auto channelBands = bandsForChannel(c); @@ -285,31 +401,50 @@ channelBands[b].prevInput = channelBands[b].output = 0; } } - flushed = true; } private: + bool _splitComputation = false; + struct { + size_t samplesSinceLast = std::numeric_limits<size_t>::max(); + size_t steps = 0; + size_t step = 0; + + bool newSpectrum = false; + bool reanalysePrev = false; + bool mappedFrequencies = false; + bool processFormants = false; + Sample timeFactor; + } blockProcess; + using Complex = std::complex<Sample>; static constexpr Sample noiseFloor{1e-15}; static constexpr Sample maxCleanStretch{2}; // time-stretch ratio before we start randomising phases - int silenceCounter = 0; + size_t silenceCounter = 0; bool silenceFirst = true; Sample freqMultiplier = 1, freqTonalityLimit = 0.5; std::function<Sample(Sample)> customFreqMap = nullptr; + + bool formantCompensation = false; // compensate for pitch/freq change + Sample formantMultiplier = 1, invFormantMultiplier = 1; + + using STFT = signalsmith::linear::DynamicSTFT<Sample, false, true>; + STFT stft; + typename STFT::Input stashedInput; + typename STFT::Output stashedOutput; + + std::vector<Sample> tmpBuffer; - signalsmith::spectral::STFT<Sample> stft{0, 1, 1}; - signalsmith::delay::MultiBuffer<Sample> inputBuffer; int channels = 0, bands = 0; int prevInputOffset = -1; - std::vector<Sample> timeBuffer; - bool didSeek = false, flushed = true; + bool didSeek = false; Sample seekTimeFactor = 1; Sample bandToFreq(Sample b) const { - return (b + Sample(0.5))/stft.fftSize(); + return stft.binToFreq(b); } Sample freqToBand(Sample f) const { - return f*stft.fftSize() - Sample(0.5); + return stft.freqToBin(f); } struct Band { @@ -371,10 +506,10 @@ Complex input; Complex makeOutput(Complex phase) { - Sample phaseNorm = std::norm(phase); + Sample phaseNorm = _impl::norm(phase); if (phaseNorm <= noiseFloor) { phase = input; // prediction is too weak, fall back to the input - phaseNorm = std::norm(input) + noiseFloor; + phaseNorm = _impl::norm(input) + noiseFloor; } return phase*std::sqrt(energy/phaseNorm); } @@ -384,49 +519,95 @@ return channelPredictions.data() + c*bands; } - RandomEngine randomEngine; - - void processSpectrum(bool newSpectrum, Sample timeFactor) { + // If RandomEngine=void, use std::default_random_engine; + using RandomEngineImpl = typename std::conditional< + std::is_void<RandomEngine>::value, + std::default_random_engine, + RandomEngine + >::type; + RandomEngineImpl randomEngine; + + size_t processSpectrumSteps = 0; + static constexpr size_t splitMainPrediction = 8; // it's just heavy, since we're blending up to 4 different phase predictions + void updateProcessSpectrumSteps() { + processSpectrumSteps = 0; + if (blockProcess.newSpectrum) processSpectrumSteps += channels; + if (blockProcess.mappedFrequencies) { + processSpectrumSteps += smoothEnergySteps; + processSpectrumSteps += 1; // findPeaks + } + processSpectrumSteps += 1; // updating the output map + processSpectrumSteps += channels; // preliminary phase-vocoder prediction + processSpectrumSteps += splitMainPrediction; + if (blockProcess.newSpectrum) processSpectrumSteps += 1; // .input -> .prevInput + if (blockProcess.processFormants) processSpectrumSteps += 3; + } + void processSpectrum(size_t step) { + Sample timeFactor = blockProcess.timeFactor; + + Sample smoothingBins = Sample(stft.fftSamples())/stft.defaultInterval(); + int longVerticalStep = std::round(smoothingBins); timeFactor = std::max<Sample>(timeFactor, 1/maxCleanStretch); bool randomTimeFactor = (timeFactor > maxCleanStretch); std::uniform_real_distribution<Sample> timeFactorDist(maxCleanStretch*2*randomTimeFactor - timeFactor, timeFactor); - - if (newSpectrum) { - for (int c = 0; c < channels; ++c) { - auto bins = bandsForChannel(c); - Complex rot = std::polar(Sample(1), bandToFreq(0)*stft.interval()*Sample(2*M_PI)); + if (blockProcess.newSpectrum) { + if (step < size_t(channels)) { + int channel = int(step); + auto bins = bandsForChannel(channel); + + Complex rot = std::polar(Sample(1), bandToFreq(0)*stft.defaultInterval()*Sample(2*M_PI)); Sample freqStep = bandToFreq(1) - bandToFreq(0); - Complex rotStep = std::polar(Sample(1), freqStep*stft.interval()*Sample(2*M_PI)); - + Complex rotStep = std::polar(Sample(1), freqStep*stft.defaultInterval()*Sample(2*M_PI)); + for (int b = 0; b < bands; ++b) { auto &bin = bins[b]; - bin.output = signalsmith::perf::mul(bin.output, rot); - bin.prevInput = signalsmith::perf::mul(bin.prevInput, rot); - rot = signalsmith::perf::mul(rot, rotStep); + bin.output = _impl::mul(bin.output, rot); + bin.prevInput = _impl::mul(bin.prevInput, rot); + rot = _impl::mul(rot, rotStep); } + return; } + step -= channels; } + if (blockProcess.mappedFrequencies) { + if (step < smoothEnergySteps) { + smoothEnergy(step, smoothingBins); + return; + } + step -= smoothEnergySteps; + if (step-- == 0) { + findPeaks(); + return; + } + } + if (step-- == 0) { + if (blockProcess.mappedFrequencies) { + updateOutputMap(); + } else { // we're not pitch-shifting, so no need to find peaks etc. + for (int c = 0; c < channels; ++c) { + Band *bins = bandsForChannel(c); + for (int b = 0; b < bands; ++b) { + bins[b].inputEnergy = _impl::norm(bins[b].input); + } + } - Sample smoothingBins = Sample(stft.fftSize())/stft.interval(); - int longVerticalStep = std::round(smoothingBins); - if (customFreqMap || freqMultiplier != 1) { - findPeaks(smoothingBins); - updateOutputMap(); - } else { // we're not pitch-shifting, so no need to find peaks etc. - for (int c = 0; c < channels; ++c) { - Band *bins = bandsForChannel(c); for (int b = 0; b < bands; ++b) { - bins[b].inputEnergy = std::norm(bins[b].input); + outputMap[b] = {Sample(b), 1}; } } - for (int b = 0; b < bands; ++b) { - outputMap[b] = {Sample(b), 1}; + return; + } + if (blockProcess.processFormants) { + if (step < 3) { + updateFormants(step); + return; } + step -= 3; } - // Preliminary output prediction from phase-vocoder - for (int c = 0; c < channels; ++c) { + if (step < size_t(channels)) { + int c = int(step); Band *bins = bandsForChannel(c); auto *predictions = predictionsForChannel(c); for (int b = 0; b < bands; ++b) { @@ -442,137 +623,152 @@ auto &outputBin = bins[b]; Complex prevInput = getFractional<&Band::prevInput>(c, lowIndex, fracIndex); - Complex freqTwist = signalsmith::perf::mul<true>(prediction.input, prevInput); - Complex phase = signalsmith::perf::mul(outputBin.output, freqTwist); + Complex freqTwist = _impl::mul<true>(prediction.input, prevInput); + Complex phase = _impl::mul(outputBin.output, freqTwist); outputBin.output = phase/(std::max(prevEnergy, prediction.energy) + noiseFloor); } + return; } + step -= channels; - // Re-predict using phase differences between frequencies - for (int b = 0; b < bands; ++b) { - // Find maximum-energy channel and calculate that - int maxChannel = 0; - Sample maxEnergy = predictionsForChannel(0)[b].energy; - for (int c = 1; c < channels; ++c) { - Sample e = predictionsForChannel(c)[b].energy; - if (e > maxEnergy) { - maxChannel = c; - maxEnergy = e; + if (step < splitMainPrediction) { + // Re-predict using phase differences between frequencies + size_t chunk = step; + int startB = int(bands*chunk/splitMainPrediction); + int endB = int(bands*(chunk + 1)/splitMainPrediction); + for (int b = startB; b < endB; ++b) { + // Find maximum-energy channel and calculate that + int maxChannel = 0; + Sample maxEnergy = predictionsForChannel(0)[b].energy; + for (int c = 1; c < channels; ++c) { + Sample e = predictionsForChannel(c)[b].energy; + if (e > maxEnergy) { + maxChannel = c; + maxEnergy = e; + } } - } - auto *predictions = predictionsForChannel(maxChannel); - auto &prediction = predictions[b]; - auto *bins = bandsForChannel(maxChannel); - auto &outputBin = bins[b]; - - Complex phase = 0; - auto mapPoint = outputMap[b]; - - // Upwards vertical steps - if (b > 0) { - Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; - Complex downInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - binTimeFactor); - Complex shortVerticalTwist = signalsmith::perf::mul<true>(prediction.input, downInput); + auto *predictions = predictionsForChannel(maxChannel); + auto &prediction = predictions[b]; + auto *bins = bandsForChannel(maxChannel); + auto &outputBin = bins[b]; - auto &downBin = bins[b - 1]; - phase += signalsmith::perf::mul(downBin.output, shortVerticalTwist); - - if (b >= longVerticalStep) { - Complex longDownInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - longVerticalStep*binTimeFactor); - Complex longVerticalTwist = signalsmith::perf::mul<true>(prediction.input, longDownInput); + Complex phase = 0; + auto mapPoint = outputMap[b]; - auto &longDownBin = bins[b - longVerticalStep]; - phase += signalsmith::perf::mul(longDownBin.output, longVerticalTwist); - } - } - // Downwards vertical steps - if (b < bands - 1) { - auto &upPrediction = predictions[b + 1]; - auto &upMapPoint = outputMap[b + 1]; + // Upwards vertical steps + if (b > 0) { + Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; + Complex downInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - binTimeFactor); + Complex shortVerticalTwist = _impl::mul<true>(prediction.input, downInput); - Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; - Complex downInput = getFractional<&Band::input>(maxChannel, upMapPoint.inputBin - binTimeFactor); - Complex shortVerticalTwist = signalsmith::perf::mul<true>(upPrediction.input, downInput); + auto &downBin = bins[b - 1]; + phase += _impl::mul(downBin.output, shortVerticalTwist); + + if (b >= longVerticalStep) { + Complex longDownInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - longVerticalStep*binTimeFactor); + Complex longVerticalTwist = _impl::mul<true>(prediction.input, longDownInput); - auto &upBin = bins[b + 1]; - phase += signalsmith::perf::mul<true>(upBin.output, shortVerticalTwist); - - if (b < bands - longVerticalStep) { - auto &longUpPrediction = predictions[b + longVerticalStep]; - auto &longUpMapPoint = outputMap[b + longVerticalStep]; + auto &longDownBin = bins[b - longVerticalStep]; + phase += _impl::mul(longDownBin.output, longVerticalTwist); + } + } + // Downwards vertical steps + if (b < bands - 1) { + auto &upPrediction = predictions[b + 1]; + auto &upMapPoint = outputMap[b + 1]; + + Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; + Complex downInput = getFractional<&Band::input>(maxChannel, upMapPoint.inputBin - binTimeFactor); + Complex shortVerticalTwist = _impl::mul<true>(upPrediction.input, downInput); + + auto &upBin = bins[b + 1]; + phase += _impl::mul<true>(upBin.output, shortVerticalTwist); + + if (b < bands - longVerticalStep) { + auto &longUpPrediction = predictions[b + longVerticalStep]; + auto &longUpMapPoint = outputMap[b + longVerticalStep]; - Complex longDownInput = getFractional<&Band::input>(maxChannel, longUpMapPoint.inputBin - longVerticalStep*binTimeFactor); - Complex longVerticalTwist = signalsmith::perf::mul<true>(longUpPrediction.input, longDownInput); + Complex longDownInput = getFractional<&Band::input>(maxChannel, longUpMapPoint.inputBin - longVerticalStep*binTimeFactor); + Complex longVerticalTwist = _impl::mul<true>(longUpPrediction.input, longDownInput); - auto &longUpBin = bins[b + longVerticalStep]; - phase += signalsmith::perf::mul<true>(longUpBin.output, longVerticalTwist); + auto &longUpBin = bins[b + longVerticalStep]; + phase += _impl::mul<true>(longUpBin.output, longVerticalTwist); + } } - } - outputBin.output = prediction.makeOutput(phase); - - // All other bins are locked in phase - for (int c = 0; c < channels; ++c) { - if (c != maxChannel) { - auto &channelBin = bandsForChannel(c)[b]; - auto &channelPrediction = predictionsForChannel(c)[b]; - - Complex channelTwist = signalsmith::perf::mul<true>(channelPrediction.input, prediction.input); - Complex channelPhase = signalsmith::perf::mul(outputBin.output, channelTwist); - channelBin.output = channelPrediction.makeOutput(channelPhase); + outputBin.output = prediction.makeOutput(phase); + + // All other bins are locked in phase + for (int c = 0; c < channels; ++c) { + if (c != maxChannel) { + auto &channelBin = bandsForChannel(c)[b]; + auto &channelPrediction = predictionsForChannel(c)[b]; + + Complex channelTwist = _impl::mul<true>(channelPrediction.input, prediction.input); + Complex channelPhase = _impl::mul(outputBin.output, channelTwist); + channelBin.output = channelPrediction.makeOutput(channelPhase); + } } } + return; } + step -= splitMainPrediction; - if (newSpectrum) { - for (auto &bin : channelBands) { - bin.prevInput = bin.input; + if (blockProcess.newSpectrum) { + if (step-- == 0) { + for (auto &bin : channelBands) { + bin.prevInput = bin.input; + } } } } // Produces smoothed energy across all channels - void smoothEnergy(Sample smoothingBins) { + static constexpr size_t smoothEnergySteps = 3; + Sample smoothEnergyState = 0; + void smoothEnergy(size_t step, Sample smoothingBins) { Sample smoothingSlew = 1/(1 + smoothingBins*Sample(0.5)); - for (auto &e : energy) e = 0; - for (int c = 0; c < channels; ++c) { - Band *bins = bandsForChannel(c); + if (step-- == 0) { + for (auto &e : energy) e = 0; + for (int c = 0; c < channels; ++c) { + Band *bins = bandsForChannel(c); + for (int b = 0; b < bands; ++b) { + Sample e = _impl::norm(bins[b].input); + bins[b].inputEnergy = e; // Used for interpolating prediction energy + energy[b] += e; + } + } for (int b = 0; b < bands; ++b) { - Sample e = std::norm(bins[b].input); - bins[b].inputEnergy = e; // Used for interpolating prediction energy - energy[b] += e; + smoothedEnergy[b] = energy[b]; } + smoothEnergyState = 0; + return; } - for (int b = 0; b < bands; ++b) { - smoothedEnergy[b] = energy[b]; + + // The two other steps are repeated smoothing passes, down and up + Sample e = smoothEnergyState; + for (int b = bands - 1; b >= 0; --b) { + e += (smoothedEnergy[b] - e)*smoothingSlew; + smoothedEnergy[b] = e; } - Sample e = 0; - for (int repeat = 0; repeat < 2; ++repeat) { - for (int b = bands - 1; b >= 0; --b) { - e += (smoothedEnergy[b] - e)*smoothingSlew; - smoothedEnergy[b] = e; - } - for (int b = 0; b < bands; ++b) { - e += (smoothedEnergy[b] - e)*smoothingSlew; - smoothedEnergy[b] = e; - } + for (int b = 0; b < bands; ++b) { + e += (smoothedEnergy[b] - e)*smoothingSlew; + smoothedEnergy[b] = e; } + smoothEnergyState = e; } Sample mapFreq(Sample freq) const { if (customFreqMap) return customFreqMap(freq); if (freq > freqTonalityLimit) { - Sample diff = freq - freqTonalityLimit; - return freqTonalityLimit*freqMultiplier + diff; + return freq + (freqMultiplier - 1)*freqTonalityLimit; } return freq*freqMultiplier; } // Identifies spectral peaks using energy across all channels - void findPeaks(Sample smoothingBins) { - smoothEnergy(smoothingBins); - + void findPeaks() { peaks.resize(0); int start = 0; @@ -631,6 +827,118 @@ outputMap[b] = {b + topOffset, 1}; } } + + // If we mapped formants the same way as mapFreq(), this would be the inverse + Sample invMapFormant(Sample freq) const { + if (freq*invFormantMultiplier > freqTonalityLimit) { + return freq + (1 - formantMultiplier)*freqTonalityLimit; + } + return freq*invFormantMultiplier; + } + + Sample freqEstimateWeighted = 0; + Sample freqEstimateWeight = 0; + Sample estimateFrequency() { + // 3 highest peaks in the input + std::array<int, 3> peakIndices{0, 0, 0}; + for (int b = 1; b < bands - 1; ++b) { + Sample e = formantMetric[b]; + // local maxima only + if (e < formantMetric[b - 1] || e <= formantMetric[b + 1]) continue; + + if (e > formantMetric[peakIndices[0]]) { + if (e > formantMetric[peakIndices[1]]) { + if (e > formantMetric[peakIndices[2]]) { + peakIndices = {peakIndices[1], peakIndices[2], b}; + } else { + peakIndices = {peakIndices[1], b, peakIndices[2]}; + } + } else { + peakIndices[0] = b; + } + } + } + + // VERY rough pitch estimation + int peakEstimate = peakIndices[2]; + if (formantMetric[peakIndices[1]] > formantMetric[peakIndices[2]]*0.1) { + int diff = std::abs(peakEstimate - peakIndices[1]); + if (diff > peakEstimate/8 && diff < peakEstimate*7/8) peakEstimate = peakEstimate%diff; + if (formantMetric[peakIndices[0]] > formantMetric[peakIndices[2]]*0.01) { + int diff = std::abs(peakEstimate - peakIndices[0]); + if (diff > peakEstimate/8 && diff < peakEstimate*7/8) peakEstimate = peakEstimate%diff; + } + } + Sample weight = formantMetric[peakIndices[2]]; + // Smooth it out a bit + freqEstimateWeighted += (peakEstimate*weight - freqEstimateWeighted)*0.25; + freqEstimateWeight += (weight - freqEstimateWeight)*0.25; + + return freqEstimateWeighted/(freqEstimateWeight + Sample(1e-30)); + } + + Sample freqEstimate; + + std::vector<Sample> formantMetric; + Sample formantBaseFreq = 0; + void updateFormants(size_t step) { + if (step-- == 0) { + for (auto &e : formantMetric) e = 0; + for (int c = 0; c < channels; ++c) { + Band *bins = bandsForChannel(c); + for (int b = 0; b < bands; ++b) { + formantMetric[b] += bins[b].inputEnergy; + } + } + + freqEstimate = freqToBand(formantBaseFreq); + if (formantBaseFreq <= 0) freqEstimate = estimateFrequency(); + + for (int b = 0; b < bands; ++b) { + formantMetric[b] = std::sqrt(formantMetric[b]); + } + } else if (step-- == 0) { + Sample slew = 1/(freqEstimate*0.5 + 1); + Sample e = 0; + for (size_t repeat = 0; repeat < 2; ++repeat) { + for (int b = bands - 1; b >= 0; --b) { + e += (formantMetric[b] - e)*slew; + formantMetric[b] = e; + } + for (int b = 0; b < bands; ++b) { + e += (formantMetric[b] - e)*slew; + formantMetric[b] = e; + } + } + } else { + auto getFormant = [&](Sample band) -> Sample { + if (band < 0) return 0; + band = std::min<Sample>(band, bands); + int floorBand = std::floor(band); + Sample fracBand = band - floorBand; + Sample low = formantMetric[floorBand], high = formantMetric[floorBand + 1]; + return low + (high - low)*fracBand; + }; + + for (int b = 0; b < bands; ++b) { + Sample inputF = bandToFreq(b); + Sample outputF = formantCompensation ? mapFreq(inputF) : inputF; + outputF = invMapFormant(outputF); + + Sample inputE = formantMetric[b]; + Sample targetE = getFormant(freqToBand(outputF)); + + Sample formantRatio = targetE/(inputE + Sample(1e-30)); + Sample energyRatio = formantRatio*formantRatio; + + for (int c = 0; c < channels; ++c) { + Band *bins = bandsForChannel(c); + // This is what's used to decide the output energy, so this affects the output + bins[b].inputEnergy *= energyRatio; + } + } + } + } }; }} // namespace Modified: branches/OpenMPT-1.32/tracklib/TimeStretchPitchShift.cpp ============================================================================== --- branches/OpenMPT-1.32/tracklib/TimeStretchPitchShift.cpp Sun May 25 14:04:10 2025 (r23191) +++ branches/OpenMPT-1.32/tracklib/TimeStretchPitchShift.cpp Sun May 25 14:04:43 2025 (r23192) @@ -20,9 +20,9 @@ #if MPT_COMPILER_MSVC #pragma warning(push) #pragma warning(disable:4305) -#pragma warning(disable:4267) #pragma warning(disable:4244) #pragma warning(disable:4458) +#pragma warning(disable:4456) #endif // MPT_COMPILER_MSVC #include <SignalsmithStretch/signalsmith-stretch.h> #if MPT_COMPILER_MSVC |
Author: sagamusix Date: Sun May 25 14:04:10 2025 New Revision: 23191 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23191 Log: [Var] Update Signalsmith Stretch to commit c78aacf6389fb862978d4ead01cfc71a3d5f8ca8 (2025-05-01) Added: trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/ trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/LICENSE.txt (contents, props changed) trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/README.md (contents, props changed) trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/fft.h (contents, props changed) trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/stft.h (contents, props changed) Deleted: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/ Modified: trunk/OpenMPT/include/SignalsmithStretch/OpenMPT.txt trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/README.md trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h trunk/OpenMPT/tracklib/TimeStretchPitchShift.cpp Modified: trunk/OpenMPT/include/SignalsmithStretch/OpenMPT.txt ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/OpenMPT.txt Sun May 25 13:24:35 2025 (r23190) +++ trunk/OpenMPT/include/SignalsmithStretch/OpenMPT.txt Sun May 25 14:04:10 2025 (r23191) @@ -1,3 +1,7 @@ -Signalsmith Stretch: C++ pitch/time library, commit ffa45981be0b75079b43d1a7a142234f06869254 (2025-02-12) +Signalsmith Stretch: C++ pitch shift / time stretch library, commit c78aacf6389fb862978d4ead01cfc71a3d5f8ca8 (2025-05-01) https://github.com/Signalsmith-Audio/signalsmith-stretch -Unneeded files (cmd and web folder) have been removed. \ No newline at end of file +Unneeded files have been removed. + +Signalsmith Linear: C++ audio utilities, commit 94d677be71a50789eb7a33b8927947046c0c2fa2 (2025-04-09) +https://github.com/Signalsmith-Audio/linear +Unneeded files have been removed. \ No newline at end of file Modified: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/README.md ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/README.md Sun May 25 13:24:35 2025 (r23190) +++ trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/README.md Sun May 25 14:04:10 2025 (r23191) @@ -90,6 +90,12 @@ You should be supplying input samples slightly ahead of the processing time (which is where changes to pitch-shift or stretch rate will be centred), and you'll receive output samples slightly behind that processing time. +### Split computation + +All of the `.preset???()` and `.configure()` methods have an optional `splitComputation` flag. When enabled, this introduces one extra interval of output latency, and uses this to spread the computation out more evenly. + +Without this (as is common for spectral processing) the library will occasionally do a bunch of computation all at once, to compute the next spectral block of audio. This is often fine, when audio's being processed across multiple threads with a decent amount of buffering (like mixing in a DAW), but if you're in a stricter situation then this flag might help. + #### Automation To follow pitch/time automation accurately, you should give it automation values from the current processing time (`.outputLatency()` samples ahead of the output), and feed it input from `.inputLatency()` samples ahead of the current processing time. @@ -114,21 +120,36 @@ stretch.flush(outputBuffers, outputSamples); ``` -Using `.seek()`/`.flush()` like this, you can perform an exact time-stretch on a fixed-length sound, and your result will have `.outputLatency()` of pre-roll. +Using `.seek()`/`.flush()` like this, you can perform an exact time-stretch on a fixed-length sound, and your result will have `.outputLatency()` of pre-roll. -## Compiling +### Formant compensation -⚠️ This has mostly been tested with Clang. If you're using another compiler and have any problems, please get in touch. +```cpp +stretch.setFormantFactor(1.2); // up ~3 semitones -🚨 It's generally be OK to enable `-ffast-math`, however there's a bug in Apple Clang 16.0.0 which can generate incorrect SIMD code. If you _have_ to use this version, we advise you don't use `-ffast-math`. +stretch.setTransposeSemitones(3); +``` -It's much slower (about 10x) if optimisation is disabled altogether, so you might want to enable optimisation where it's used, even in Debug builds. +Both of those methods both have an optional `compensatePitch` flag. Enabling this adjust for the pitch-shift (or non-linear map) when correcting/shifting formants. -### DSP Library +The formant correction is not a sharp as monophonic algorithms (such such as PSOLA). It also needs you to give it a rough estimate of fundamental frequency (relative to Nyquist): -This uses the Signalsmith DSP library for FFTs and other bits and bobs. +```cpp +// if 200Hz is the middle-register of the instrument +stretch.setFormantBase(200/sampleRate); +``` -For convenience, a copy of the library is included (with its own `LICENSE.txt`) in `dsp/`, but if you're already using this elsewhere then you should remove this copy to avoid versioning issues. +## Compiling + +⚠️ This has mostly been tested with AppleClang (Mac), and MSVC (Windows). We aim to support other compilers though, so please get in touch if you have any problems. + +Enabling `-ffast-math` (or equivalent) is fine, unless you're using Apple Clang 16.0.0 (in which case it will deliberately break, because that version generates incorrect SIMD code). + +The algorithm has a lot of number-crunching, so Debug builds are much slower (up to 10x). If possible, you might want to enable optimisation where Stretch is used, even in Debug builds. + +### Dependencies and `#define`s + +This uses the [Signalsmith Linear](https://github.com/Signalsmith-Audio/linear) library for FFTs and other speedups. There are [flags]([Linear repo](https://github.com/Signalsmith-Audio/linear?tab=readme-ov-file#building)) to enable Accelerate (`SIGNALSMITH_USE_ACCELERATE`) or IPP (`SIGNALSMITH_USE_IPP`). ## License @@ -136,14 +157,18 @@ ## Other environments / languages -There's a Web Audio wrapper in `web/` (using WASM/AudioWorklet). This will remain in-sync with the C++ library. +There's a Web Audio release in [`web/`](web/) (WASM/AudioWorklet), also available on [NPM](npmjs.com/package/signalsmith-stretch). This has its own (higher-level) API, but the stretching algorithm will remain in-sync with the C++ library. + +There's a [Python binding](https://pypi.org/project/python-stretch/) written/published by [Gregorio Andrea Giudici](https://github.com/gregogiudici/python-stretch). This is used as the default pitch/time method in [Audiomentations](https://iver56.github.io/audiomentations/). -There's a [Python binding](https://pypi.org/project/python-stretch/) written/published by [Gregorio Andrea Giudici](https://github.com/gregogiudici/python-stretch), and a [Rust wrapper](https://crates.io/crates/signalsmith-stretch) by [Colin Marc](https://github.com/colinmarc/signalsmith-stretch-rs). +There's a [Rust wrapper](https://crates.io/crates/signalsmith-stretch) by [Colin Marc](https://github.com/colinmarc/signalsmith-stretch-rs). ## Thanks We'd like to particularly thank the following people who sponsored specific features or improvements: +* **Future Audio Workshop**: chunked-computation mode +* **Jochem de Jong**: formant shifting/compensation * **Metronaut**: web audio (JS/WASM) release * **Daniel L Bowling** and the Stanford School of Medicine: web audio improvements Modified: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h Sun May 25 13:24:35 2025 (r23190) +++ trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h Sun May 25 14:04:10 2025 (r23191) @@ -1,63 +1,89 @@ #ifndef SIGNALSMITH_STRETCH_H #define SIGNALSMITH_STRETCH_H -#include "dsp/spectral.h" -#include "dsp/delay.h" -#include "dsp/perf.h" -SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 0); // Check version is compatible +#include "signalsmith-linear/stft.h" // https://github.com/Signalsmith-Audio/linear + #include <vector> +#include <array> #include <algorithm> #include <functional> #include <random> +#include <limits> +#include <type_traits> namespace signalsmith { namespace stretch { -template<typename Sample=float, class RandomEngine=std::default_random_engine> +namespace _impl { + template<bool conjugateSecond=false, typename V> + static std::complex<V> mul(const std::complex<V> &a, const std::complex<V> &b) { + return conjugateSecond ? std::complex<V>{ + b.real()*a.real() + b.imag()*a.imag(), + b.real()*a.imag() - b.imag()*a.real() + } : std::complex<V>{ + a.real()*b.real() - a.imag()*b.imag(), + a.real()*b.imag() + a.imag()*b.real() + }; + } + template<typename V> + static V norm(const std::complex<V> &a) { + V r = a.real(), i = a.imag(); + return r*r + i*i; + } +} + +template<typename Sample=float, class RandomEngine=void> struct SignalsmithStretch { - static constexpr size_t version[3] = {1, 1, 1}; + static constexpr size_t version[3] = {1, 3, 1}; SignalsmithStretch() : randomEngine(std::random_device{}()) {} SignalsmithStretch(long seed) : randomEngine(seed) {} - + int blockSamples() const { - return stft.windowSize(); + return int(stft.blockSamples()); } int intervalSamples() const { - return stft.interval(); + return int(stft.defaultInterval()); } int inputLatency() const { - return stft.windowSize()/2; + return int(stft.analysisLatency()); } int outputLatency() const { - return stft.windowSize() - inputLatency(); + return int(stft.synthesisLatency() + _splitComputation*stft.defaultInterval()); } void reset() { - stft.reset(); - inputBuffer.reset(); + stft.reset(0.1); + stashedInput = stft.input; + stashedOutput = stft.output; + prevInputOffset = -1; channelBands.assign(channelBands.size(), Band()); silenceCounter = 0; didSeek = false; - flushed = true; + blockProcess = {}; + freqEstimateWeighted = freqEstimateWeight = 0; } // Configures using a default preset - void presetDefault(int nChannels, Sample sampleRate) { - configure(nChannels, sampleRate*0.12, sampleRate*0.03); + void presetDefault(int nChannels, Sample sampleRate, bool splitComputation=false) { + configure(nChannels, sampleRate*0.12, sampleRate*0.03, splitComputation); } - void presetCheaper(int nChannels, Sample sampleRate) { - configure(nChannels, sampleRate*0.1, sampleRate*0.04); + void presetCheaper(int nChannels, Sample sampleRate, bool splitComputation=true) { + configure(nChannels, sampleRate*0.1, sampleRate*0.04, splitComputation); } // Manual setup - void configure(int nChannels, int blockSamples, int intervalSamples) { + void configure(int nChannels, int blockSamples, int intervalSamples, bool splitComputation=false) { + _splitComputation = splitComputation; channels = nChannels; - stft.setWindow(stft.kaiser, true); - stft.resize(channels, blockSamples, intervalSamples); - bands = stft.bands(); - inputBuffer.resize(channels, blockSamples + intervalSamples + 1); - timeBuffer.assign(stft.fftSize(), 0); + stft.configure(channels, channels, blockSamples, intervalSamples + 1); + stft.setInterval(intervalSamples, stft.kaiser); + stft.reset(0.1); + stashedInput = stft.input; + stashedOutput = stft.output; + tmpBuffer.resize(blockSamples + intervalSamples); + + bands = int(stft.bands()); channelBands.assign(bands*channels, Band()); peaks.reserve(bands/2); @@ -65,6 +91,9 @@ smoothedEnergy.resize(bands); outputMap.resize(bands); channelPredictions.resize(channels*bands); + + blockProcess = {}; + formantMetric.resize(bands + 2); } /// Frequency multiplier, and optional tonality limit (as multiple of sample-rate) @@ -79,39 +108,76 @@ } void setTransposeSemitones(Sample semitones, Sample tonalityLimit=0) { setTransposeFactor(std::pow(2, semitones/12), tonalityLimit); - customFreqMap = nullptr; } // Sets a custom frequency map - should be monotonically increasing void setFreqMap(std::function<Sample(Sample)> inputToOutput) { customFreqMap = inputToOutput; } + void setFormantFactor(Sample multiplier, bool compensatePitch=false) { + formantMultiplier = multiplier; + invFormantMultiplier = 1/multiplier; + formantCompensation = compensatePitch; + } + void setFormantSemitones(Sample semitones, bool compensatePitch=false) { + setFormantFactor(std::pow(2, semitones/12), compensatePitch); + } + // Rough guesstimate of the fundamental frequency, used for formant analysis. 0 means attempting to detect the pitch + void setFormantBase(Sample baseFreq=0) { + formantBaseFreq = baseFreq; + } + // Provide previous input ("pre-roll"), without affecting the speed calculation. You should ideally feed it one block-length + one interval template<class Inputs> void seek(Inputs &&inputs, int inputSamples, double playbackRate) { - inputBuffer.reset(); + tmpBuffer.resize(0); + tmpBuffer.resize(stft.blockSamples() + stft.defaultInterval()); + + int startIndex = std::max<int>(0, inputSamples - int(tmpBuffer.size())); // start position in input + int padStart = int(tmpBuffer.size() + startIndex) - inputSamples; // start position in tmpBuffer + Sample totalEnergy = 0; for (int c = 0; c < channels; ++c) { auto &&inputChannel = inputs[c]; - auto &&bufferChannel = inputBuffer[c]; - int startIndex = std::max<int>(0, inputSamples - stft.windowSize() - stft.interval()); for (int i = startIndex; i < inputSamples; ++i) { Sample s = inputChannel[i]; totalEnergy += s*s; - bufferChannel[i] = s; + tmpBuffer[i - startIndex + padStart] = s; } + + stft.writeInput(c, tmpBuffer.size(), tmpBuffer.data()); } + stft.moveInput(tmpBuffer.size()); if (totalEnergy >= noiseFloor) { silenceCounter = 0; silenceFirst = true; } - inputBuffer += inputSamples; didSeek = true; - seekTimeFactor = (playbackRate*stft.interval() > 1) ? 1/playbackRate : stft.interval(); + seekTimeFactor = (playbackRate*stft.defaultInterval() > 1) ? 1/playbackRate : stft.defaultInterval(); } - + template<class Inputs, class Outputs> void process(Inputs &&inputs, int inputSamples, Outputs &&outputs, int outputSamples) { +#ifdef SIGNALSMITH_STRETCH_PROFILE_PROCESS_START + SIGNALSMITH_STRETCH_PROFILE_PROCESS_START(inputSamples, outputSamples); +#endif + int prevCopiedInput = 0; + auto copyInput = [&](int toIndex){ + + int length = std::min<int>(int(stft.blockSamples() + stft.defaultInterval()), toIndex - prevCopiedInput); + tmpBuffer.resize(length); + int offset = toIndex - length; + for (int c = 0; c < channels; ++c) { + auto &&inputBuffer = inputs[c]; + for (int i = 0; i < length; ++i) { + tmpBuffer[i] = inputBuffer[i + offset]; + } + stft.writeInput(c, length, tmpBuffer.data()); + } + stft.moveInput(length); + prevCopiedInput = toIndex; + }; + Sample totalEnergy = 0; for (int c = 0; c < channels; ++c) { auto &&inputChannel = inputs[c]; @@ -120,10 +186,13 @@ totalEnergy += s*s; } } + if (totalEnergy < noiseFloor) { - if (silenceCounter >= 2*stft.windowSize()) { - if (silenceFirst) { + if (silenceCounter >= 2*stft.blockSamples()) { + if (silenceFirst) { // first block of silence processing silenceFirst = false; + //stft.reset(); + blockProcess = {}; for (auto &b : channelBands) { b.input = b.prevInput = b.output = 0; b.inputEnergy = 0; @@ -148,15 +217,7 @@ } // Store input in history buffer - for (int c = 0; c < channels; ++c) { - auto &&inputChannel = inputs[c]; - auto &&bufferChannel = inputBuffer[c]; - int startIndex = std::max<int>(0, inputSamples - stft.windowSize() - stft.interval()); - for (int i = startIndex; i < inputSamples; ++i) { - bufferChannel[i] = inputChannel[i]; - } - } - inputBuffer += inputSamples; + copyInput(inputSamples); return; } else { silenceCounter += inputSamples; @@ -165,119 +226,174 @@ silenceCounter = 0; silenceFirst = true; } - + for (int outputIndex = 0; outputIndex < outputSamples; ++outputIndex) { - stft.ensureValid(outputIndex, [&](int outputOffset) { + bool newBlock = blockProcess.samplesSinceLast >= stft.defaultInterval(); + if (newBlock) { + blockProcess.step = 0; + blockProcess.steps = 0; // how many processing steps this block will have + blockProcess.samplesSinceLast = 0; + // Time to process a spectrum! Where should it come from in the input? - int inputOffset = std::round(outputOffset*Sample(inputSamples)/outputSamples) - stft.windowSize(); + int inputOffset = std::round(outputIndex*Sample(inputSamples)/outputSamples); int inputInterval = inputOffset - prevInputOffset; prevInputOffset = inputOffset; + + copyInput(inputOffset); + stashedInput = stft.input; // save the input state, since that's what we'll analyse later + if (_splitComputation) { + stashedOutput = stft.output; // save the current output, and read from it + stft.moveOutput(stft.defaultInterval()); // the actual input jumps forward in time by one interval, ready for the synthesis + } + + blockProcess.newSpectrum = didSeek || (inputInterval > 0); + blockProcess.mappedFrequencies = customFreqMap || freqMultiplier != 1; + if (blockProcess.newSpectrum) { + // make sure the previous input is the correct distance in the past (give or take 1 sample) + blockProcess.reanalysePrev = didSeek || std::abs(inputInterval - int(stft.defaultInterval())) > 1; + if (blockProcess.reanalysePrev) blockProcess.steps += stft.analyseSteps() + 1; - bool newSpectrum = didSeek || (inputInterval > 0); - if (newSpectrum) { - for (int c = 0; c < channels; ++c) { - // Copy from the history buffer, if needed - auto &&bufferChannel = inputBuffer[c]; - for (int i = 0; i < -inputOffset; ++i) { - timeBuffer[i] = bufferChannel[i + inputOffset]; + // analyse a new input + blockProcess.steps += stft.analyseSteps() + 1; + } + + blockProcess.processFormants = formantMultiplier != 1 || (formantCompensation && blockProcess.mappedFrequencies); + + blockProcess.timeFactor = didSeek ? seekTimeFactor : stft.defaultInterval()/std::max<Sample>(1, inputInterval); + didSeek = false; + + updateProcessSpectrumSteps(); + blockProcess.steps += processSpectrumSteps; + + blockProcess.steps += stft.synthesiseSteps() + 1; + } + + size_t processToStep = newBlock ? blockProcess.steps : 0; + if (_splitComputation) { + Sample processRatio = Sample(blockProcess.samplesSinceLast + 1)/stft.defaultInterval(); + processToStep = std::min<size_t>(blockProcess.steps, (blockProcess.steps + 0.999f)*processRatio); + } + + while (blockProcess.step < processToStep) { + size_t step = blockProcess.step++; +#ifdef SIGNALSMITH_STRETCH_PROFILE_PROCESS_STEP + SIGNALSMITH_STRETCH_PROFILE_PROCESS_STEP(step, blockProcess.steps); +#endif + if (blockProcess.newSpectrum) { + if (blockProcess.reanalysePrev) { + // analyse past input + if (step < stft.analyseSteps()) { + stashedInput.swap(stft.input); + stft.analyseStep(step, stft.defaultInterval()); + stashedInput.swap(stft.input); + continue; } - // Copy the rest from the input - auto &&inputChannel = inputs[c]; - for (int i = std::max<int>(0, -inputOffset); i < stft.windowSize(); ++i) { - timeBuffer[i] = inputChannel[i + inputOffset]; + step -= stft.analyseSteps(); + if (step < 1) { + // Copy previous analysis to our band objects + for (int c = 0; c < channels; ++c) { + auto channelBands = bandsForChannel(c); + auto *spectrumBands = stft.spectrum(c); + for (int b = 0; b < bands; ++b) { + channelBands[b].prevInput = spectrumBands[b]; + } + } + continue; } - stft.analyse(c, timeBuffer); + step -= 1; } - flushed = false; // TODO: first block after a flush should be gain-compensated - for (int c = 0; c < channels; ++c) { - auto channelBands = bandsForChannel(c); - auto &&spectrumBands = stft.spectrum[c]; - for (int b = 0; b < bands; ++b) { - channelBands[b].input = spectrumBands[b]; - } + // Analyse latest (stashed) input + if (step < stft.analyseSteps()) { + stashedInput.swap(stft.input); + stft.analyseStep(step); + stashedInput.swap(stft.input); + continue; } - - if (didSeek || inputInterval != stft.interval()) { // make sure the previous input is the correct distance in the past - int prevIntervalOffset = inputOffset - stft.interval(); - for (int c = 0; c < channels; ++c) { - // Copy from the history buffer, if needed - auto &&bufferChannel = inputBuffer[c]; - for (int i = 0; i < std::min(-prevIntervalOffset, stft.windowSize()); ++i) { - timeBuffer[i] = bufferChannel[i + prevIntervalOffset]; - } - // Copy the rest from the input - auto &&inputChannel = inputs[c]; - for (int i = std::max<int>(0, -prevIntervalOffset); i < stft.windowSize(); ++i) { - timeBuffer[i] = inputChannel[i + prevIntervalOffset]; - } - stft.analyse(c, timeBuffer); - } + step -= stft.analyseSteps(); + if (step < 1) { + // Copy analysed spectrum into our band objects for (int c = 0; c < channels; ++c) { auto channelBands = bandsForChannel(c); - auto &&spectrumBands = stft.spectrum[c]; + auto *spectrumBands = stft.spectrum(c); for (int b = 0; b < bands; ++b) { - channelBands[b].prevInput = spectrumBands[b]; + channelBands[b].input = spectrumBands[b]; } } + continue; } + step -= 1; } - - Sample timeFactor = didSeek ? seekTimeFactor : stft.interval()/std::max<Sample>(1, inputInterval); - processSpectrum(newSpectrum, timeFactor); - didSeek = false; - for (int c = 0; c < channels; ++c) { - auto channelBands = bandsForChannel(c); - auto &&spectrumBands = stft.spectrum[c]; - for (int b = 0; b < bands; ++b) { - spectrumBands[b] = channelBands[b].output; + if (step < processSpectrumSteps) { + processSpectrum(step); + continue; + } + step -= processSpectrumSteps; + + if (step < 1) { + // Copy band objects into spectrum + for (int c = 0; c < channels; ++c) { + auto channelBands = bandsForChannel(c); + auto *spectrumBands = stft.spectrum(c); + for (int b = 0; b < bands; ++b) { + spectrumBands[b] = channelBands[b].output; + } } + continue; } - }); + step -= 1; + + if (step < stft.synthesiseSteps()) { + stft.synthesiseStep(step); + continue; + } + } +#ifdef SIGNALSMITH_STRETCH_PROFILE_PROCESS_ENDSTEP + SIGNALSMITH_STRETCH_PROFILE_PROCESS_ENDSTEP(); +#endif + ++blockProcess.samplesSinceLast; + if (_splitComputation) stashedOutput.swap(stft.output); for (int c = 0; c < channels; ++c) { auto &&outputChannel = outputs[c]; - auto &&stftChannel = stft[c]; - outputChannel[outputIndex] = stftChannel[outputIndex]; + Sample v = 0; + stft.readOutput(c, 1, &v); + outputChannel[outputIndex] = v; } + stft.moveOutput(1); + if (_splitComputation) stashedOutput.swap(stft.output); } - - // Store input in history buffer - for (int c = 0; c < channels; ++c) { - auto &&inputChannel = inputs[c]; - auto &&bufferChannel = inputBuffer[c]; - int startIndex = std::max<int>(0, inputSamples - stft.windowSize()); - for (int i = startIndex; i < inputSamples; ++i) { - bufferChannel[i] = inputChannel[i]; - } - } - inputBuffer += inputSamples; - stft += outputSamples; + + copyInput(inputSamples); prevInputOffset -= inputSamples; +#ifdef SIGNALSMITH_STRETCH_PROFILE_PROCESS_END + SIGNALSMITH_STRETCH_PROFILE_PROCESS_END(); +#endif } // Read the remaining output, providing no further input. `outputSamples` should ideally be at least `.outputLatency()` template<class Outputs> void flush(Outputs &&outputs, int outputSamples) { - int plainOutput = std::min<int>(outputSamples, stft.windowSize()); - int foldedBackOutput = std::min<int>(outputSamples, stft.windowSize() - plainOutput); + int plainOutput = std::min<int>(outputSamples, int(stft.blockSamples())); + int foldedBackOutput = std::min<int>(outputSamples, int(stft.blockSamples()) - plainOutput); + stft.finishOutput(1); for (int c = 0; c < channels; ++c) { + tmpBuffer.resize(plainOutput); + stft.readOutput(c, plainOutput, tmpBuffer.data()); auto &&outputChannel = outputs[c]; - auto &&stftChannel = stft[c]; for (int i = 0; i < plainOutput; ++i) { // TODO: plain output should be gain- - outputChannel[i] = stftChannel[i]; + outputChannel[i] = tmpBuffer[i]; } + tmpBuffer.resize(foldedBackOutput); + stft.readOutput(c, plainOutput, foldedBackOutput, tmpBuffer.data()); for (int i = 0; i < foldedBackOutput; ++i) { - outputChannel[outputSamples - 1 - i] -= stftChannel[plainOutput + i]; - } - for (int i = 0; i < plainOutput + foldedBackOutput; ++i) { - stftChannel[i] = 0; + outputChannel[outputSamples - 1 - i] -= tmpBuffer[i]; } } - // Skip the output we just used/cleared - stft += plainOutput + foldedBackOutput; + stft.reset(0.1); + // Reset the phase-vocoder stuff, so the next block gets a fresh start for (int c = 0; c < channels; ++c) { auto channelBands = bandsForChannel(c); @@ -285,31 +401,50 @@ channelBands[b].prevInput = channelBands[b].output = 0; } } - flushed = true; } private: + bool _splitComputation = false; + struct { + size_t samplesSinceLast = std::numeric_limits<size_t>::max(); + size_t steps = 0; + size_t step = 0; + + bool newSpectrum = false; + bool reanalysePrev = false; + bool mappedFrequencies = false; + bool processFormants = false; + Sample timeFactor; + } blockProcess; + using Complex = std::complex<Sample>; static constexpr Sample noiseFloor{1e-15}; static constexpr Sample maxCleanStretch{2}; // time-stretch ratio before we start randomising phases - int silenceCounter = 0; + size_t silenceCounter = 0; bool silenceFirst = true; Sample freqMultiplier = 1, freqTonalityLimit = 0.5; std::function<Sample(Sample)> customFreqMap = nullptr; + + bool formantCompensation = false; // compensate for pitch/freq change + Sample formantMultiplier = 1, invFormantMultiplier = 1; + + using STFT = signalsmith::linear::DynamicSTFT<Sample, false, true>; + STFT stft; + typename STFT::Input stashedInput; + typename STFT::Output stashedOutput; + + std::vector<Sample> tmpBuffer; - signalsmith::spectral::STFT<Sample> stft{0, 1, 1}; - signalsmith::delay::MultiBuffer<Sample> inputBuffer; int channels = 0, bands = 0; int prevInputOffset = -1; - std::vector<Sample> timeBuffer; - bool didSeek = false, flushed = true; + bool didSeek = false; Sample seekTimeFactor = 1; Sample bandToFreq(Sample b) const { - return (b + Sample(0.5))/stft.fftSize(); + return stft.binToFreq(b); } Sample freqToBand(Sample f) const { - return f*stft.fftSize() - Sample(0.5); + return stft.freqToBin(f); } struct Band { @@ -371,10 +506,10 @@ Complex input; Complex makeOutput(Complex phase) { - Sample phaseNorm = std::norm(phase); + Sample phaseNorm = _impl::norm(phase); if (phaseNorm <= noiseFloor) { phase = input; // prediction is too weak, fall back to the input - phaseNorm = std::norm(input) + noiseFloor; + phaseNorm = _impl::norm(input) + noiseFloor; } return phase*std::sqrt(energy/phaseNorm); } @@ -384,49 +519,95 @@ return channelPredictions.data() + c*bands; } - RandomEngine randomEngine; - - void processSpectrum(bool newSpectrum, Sample timeFactor) { + // If RandomEngine=void, use std::default_random_engine; + using RandomEngineImpl = typename std::conditional< + std::is_void<RandomEngine>::value, + std::default_random_engine, + RandomEngine + >::type; + RandomEngineImpl randomEngine; + + size_t processSpectrumSteps = 0; + static constexpr size_t splitMainPrediction = 8; // it's just heavy, since we're blending up to 4 different phase predictions + void updateProcessSpectrumSteps() { + processSpectrumSteps = 0; + if (blockProcess.newSpectrum) processSpectrumSteps += channels; + if (blockProcess.mappedFrequencies) { + processSpectrumSteps += smoothEnergySteps; + processSpectrumSteps += 1; // findPeaks + } + processSpectrumSteps += 1; // updating the output map + processSpectrumSteps += channels; // preliminary phase-vocoder prediction + processSpectrumSteps += splitMainPrediction; + if (blockProcess.newSpectrum) processSpectrumSteps += 1; // .input -> .prevInput + if (blockProcess.processFormants) processSpectrumSteps += 3; + } + void processSpectrum(size_t step) { + Sample timeFactor = blockProcess.timeFactor; + + Sample smoothingBins = Sample(stft.fftSamples())/stft.defaultInterval(); + int longVerticalStep = std::round(smoothingBins); timeFactor = std::max<Sample>(timeFactor, 1/maxCleanStretch); bool randomTimeFactor = (timeFactor > maxCleanStretch); std::uniform_real_distribution<Sample> timeFactorDist(maxCleanStretch*2*randomTimeFactor - timeFactor, timeFactor); - - if (newSpectrum) { - for (int c = 0; c < channels; ++c) { - auto bins = bandsForChannel(c); - Complex rot = std::polar(Sample(1), bandToFreq(0)*stft.interval()*Sample(2*M_PI)); + if (blockProcess.newSpectrum) { + if (step < size_t(channels)) { + int channel = int(step); + auto bins = bandsForChannel(channel); + + Complex rot = std::polar(Sample(1), bandToFreq(0)*stft.defaultInterval()*Sample(2*M_PI)); Sample freqStep = bandToFreq(1) - bandToFreq(0); - Complex rotStep = std::polar(Sample(1), freqStep*stft.interval()*Sample(2*M_PI)); - + Complex rotStep = std::polar(Sample(1), freqStep*stft.defaultInterval()*Sample(2*M_PI)); + for (int b = 0; b < bands; ++b) { auto &bin = bins[b]; - bin.output = signalsmith::perf::mul(bin.output, rot); - bin.prevInput = signalsmith::perf::mul(bin.prevInput, rot); - rot = signalsmith::perf::mul(rot, rotStep); + bin.output = _impl::mul(bin.output, rot); + bin.prevInput = _impl::mul(bin.prevInput, rot); + rot = _impl::mul(rot, rotStep); } + return; } + step -= channels; } + if (blockProcess.mappedFrequencies) { + if (step < smoothEnergySteps) { + smoothEnergy(step, smoothingBins); + return; + } + step -= smoothEnergySteps; + if (step-- == 0) { + findPeaks(); + return; + } + } + if (step-- == 0) { + if (blockProcess.mappedFrequencies) { + updateOutputMap(); + } else { // we're not pitch-shifting, so no need to find peaks etc. + for (int c = 0; c < channels; ++c) { + Band *bins = bandsForChannel(c); + for (int b = 0; b < bands; ++b) { + bins[b].inputEnergy = _impl::norm(bins[b].input); + } + } - Sample smoothingBins = Sample(stft.fftSize())/stft.interval(); - int longVerticalStep = std::round(smoothingBins); - if (customFreqMap || freqMultiplier != 1) { - findPeaks(smoothingBins); - updateOutputMap(); - } else { // we're not pitch-shifting, so no need to find peaks etc. - for (int c = 0; c < channels; ++c) { - Band *bins = bandsForChannel(c); for (int b = 0; b < bands; ++b) { - bins[b].inputEnergy = std::norm(bins[b].input); + outputMap[b] = {Sample(b), 1}; } } - for (int b = 0; b < bands; ++b) { - outputMap[b] = {Sample(b), 1}; + return; + } + if (blockProcess.processFormants) { + if (step < 3) { + updateFormants(step); + return; } + step -= 3; } - // Preliminary output prediction from phase-vocoder - for (int c = 0; c < channels; ++c) { + if (step < size_t(channels)) { + int c = int(step); Band *bins = bandsForChannel(c); auto *predictions = predictionsForChannel(c); for (int b = 0; b < bands; ++b) { @@ -442,137 +623,152 @@ auto &outputBin = bins[b]; Complex prevInput = getFractional<&Band::prevInput>(c, lowIndex, fracIndex); - Complex freqTwist = signalsmith::perf::mul<true>(prediction.input, prevInput); - Complex phase = signalsmith::perf::mul(outputBin.output, freqTwist); + Complex freqTwist = _impl::mul<true>(prediction.input, prevInput); + Complex phase = _impl::mul(outputBin.output, freqTwist); outputBin.output = phase/(std::max(prevEnergy, prediction.energy) + noiseFloor); } + return; } + step -= channels; - // Re-predict using phase differences between frequencies - for (int b = 0; b < bands; ++b) { - // Find maximum-energy channel and calculate that - int maxChannel = 0; - Sample maxEnergy = predictionsForChannel(0)[b].energy; - for (int c = 1; c < channels; ++c) { - Sample e = predictionsForChannel(c)[b].energy; - if (e > maxEnergy) { - maxChannel = c; - maxEnergy = e; + if (step < splitMainPrediction) { + // Re-predict using phase differences between frequencies + size_t chunk = step; + int startB = int(bands*chunk/splitMainPrediction); + int endB = int(bands*(chunk + 1)/splitMainPrediction); + for (int b = startB; b < endB; ++b) { + // Find maximum-energy channel and calculate that + int maxChannel = 0; + Sample maxEnergy = predictionsForChannel(0)[b].energy; + for (int c = 1; c < channels; ++c) { + Sample e = predictionsForChannel(c)[b].energy; + if (e > maxEnergy) { + maxChannel = c; + maxEnergy = e; + } } - } - auto *predictions = predictionsForChannel(maxChannel); - auto &prediction = predictions[b]; - auto *bins = bandsForChannel(maxChannel); - auto &outputBin = bins[b]; - - Complex phase = 0; - auto mapPoint = outputMap[b]; - - // Upwards vertical steps - if (b > 0) { - Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; - Complex downInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - binTimeFactor); - Complex shortVerticalTwist = signalsmith::perf::mul<true>(prediction.input, downInput); + auto *predictions = predictionsForChannel(maxChannel); + auto &prediction = predictions[b]; + auto *bins = bandsForChannel(maxChannel); + auto &outputBin = bins[b]; - auto &downBin = bins[b - 1]; - phase += signalsmith::perf::mul(downBin.output, shortVerticalTwist); - - if (b >= longVerticalStep) { - Complex longDownInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - longVerticalStep*binTimeFactor); - Complex longVerticalTwist = signalsmith::perf::mul<true>(prediction.input, longDownInput); + Complex phase = 0; + auto mapPoint = outputMap[b]; - auto &longDownBin = bins[b - longVerticalStep]; - phase += signalsmith::perf::mul(longDownBin.output, longVerticalTwist); - } - } - // Downwards vertical steps - if (b < bands - 1) { - auto &upPrediction = predictions[b + 1]; - auto &upMapPoint = outputMap[b + 1]; + // Upwards vertical steps + if (b > 0) { + Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; + Complex downInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - binTimeFactor); + Complex shortVerticalTwist = _impl::mul<true>(prediction.input, downInput); - Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; - Complex downInput = getFractional<&Band::input>(maxChannel, upMapPoint.inputBin - binTimeFactor); - Complex shortVerticalTwist = signalsmith::perf::mul<true>(upPrediction.input, downInput); + auto &downBin = bins[b - 1]; + phase += _impl::mul(downBin.output, shortVerticalTwist); + + if (b >= longVerticalStep) { + Complex longDownInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - longVerticalStep*binTimeFactor); + Complex longVerticalTwist = _impl::mul<true>(prediction.input, longDownInput); - auto &upBin = bins[b + 1]; - phase += signalsmith::perf::mul<true>(upBin.output, shortVerticalTwist); - - if (b < bands - longVerticalStep) { - auto &longUpPrediction = predictions[b + longVerticalStep]; - auto &longUpMapPoint = outputMap[b + longVerticalStep]; + auto &longDownBin = bins[b - longVerticalStep]; + phase += _impl::mul(longDownBin.output, longVerticalTwist); + } + } + // Downwards vertical steps + if (b < bands - 1) { + auto &upPrediction = predictions[b + 1]; + auto &upMapPoint = outputMap[b + 1]; + + Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; + Complex downInput = getFractional<&Band::input>(maxChannel, upMapPoint.inputBin - binTimeFactor); + Complex shortVerticalTwist = _impl::mul<true>(upPrediction.input, downInput); + + auto &upBin = bins[b + 1]; + phase += _impl::mul<true>(upBin.output, shortVerticalTwist); + + if (b < bands - longVerticalStep) { + auto &longUpPrediction = predictions[b + longVerticalStep]; + auto &longUpMapPoint = outputMap[b + longVerticalStep]; - Complex longDownInput = getFractional<&Band::input>(maxChannel, longUpMapPoint.inputBin - longVerticalStep*binTimeFactor); - Complex longVerticalTwist = signalsmith::perf::mul<true>(longUpPrediction.input, longDownInput); + Complex longDownInput = getFractional<&Band::input>(maxChannel, longUpMapPoint.inputBin - longVerticalStep*binTimeFactor); + Complex longVerticalTwist = _impl::mul<true>(longUpPrediction.input, longDownInput); - auto &longUpBin = bins[b + longVerticalStep]; - phase += signalsmith::perf::mul<true>(longUpBin.output, longVerticalTwist); + auto &longUpBin = bins[b + longVerticalStep]; + phase += _impl::mul<true>(longUpBin.output, longVerticalTwist); + } } - } - outputBin.output = prediction.makeOutput(phase); - - // All other bins are locked in phase - for (int c = 0; c < channels; ++c) { - if (c != maxChannel) { - auto &channelBin = bandsForChannel(c)[b]; - auto &channelPrediction = predictionsForChannel(c)[b]; - - Complex channelTwist = signalsmith::perf::mul<true>(channelPrediction.input, prediction.input); - Complex channelPhase = signalsmith::perf::mul(outputBin.output, channelTwist); - channelBin.output = channelPrediction.makeOutput(channelPhase); + outputBin.output = prediction.makeOutput(phase); + + // All other bins are locked in phase + for (int c = 0; c < channels; ++c) { + if (c != maxChannel) { + auto &channelBin = bandsForChannel(c)[b]; + auto &channelPrediction = predictionsForChannel(c)[b]; + + Complex channelTwist = _impl::mul<true>(channelPrediction.input, prediction.input); + Complex channelPhase = _impl::mul(outputBin.output, channelTwist); + channelBin.output = channelPrediction.makeOutput(channelPhase); + } } } + return; } + step -= splitMainPrediction; - if (newSpectrum) { - for (auto &bin : channelBands) { - bin.prevInput = bin.input; + if (blockProcess.newSpectrum) { + if (step-- == 0) { + for (auto &bin : channelBands) { + bin.prevInput = bin.input; + } } } } // Produces smoothed energy across all channels - void smoothEnergy(Sample smoothingBins) { + static constexpr size_t smoothEnergySteps = 3; + Sample smoothEnergyState = 0; + void smoothEnergy(size_t step, Sample smoothingBins) { Sample smoothingSlew = 1/(1 + smoothingBins*Sample(0.5)); - for (auto &e : energy) e = 0; - for (int c = 0; c < channels; ++c) { - Band *bins = bandsForChannel(c); + if (step-- == 0) { + for (auto &e : energy) e = 0; + for (int c = 0; c < channels; ++c) { + Band *bins = bandsForChannel(c); + for (int b = 0; b < bands; ++b) { + Sample e = _impl::norm(bins[b].input); + bins[b].inputEnergy = e; // Used for interpolating prediction energy + energy[b] += e; + } + } for (int b = 0; b < bands; ++b) { - Sample e = std::norm(bins[b].input); - bins[b].inputEnergy = e; // Used for interpolating prediction energy - energy[b] += e; + smoothedEnergy[b] = energy[b]; } + smoothEnergyState = 0; + return; } - for (int b = 0; b < bands; ++b) { - smoothedEnergy[b] = energy[b]; + + // The two other steps are repeated smoothing passes, down and up + Sample e = smoothEnergyState; + for (int b = bands - 1; b >= 0; --b) { + e += (smoothedEnergy[b] - e)*smoothingSlew; + smoothedEnergy[b] = e; } - Sample e = 0; - for (int repeat = 0; repeat < 2; ++repeat) { - for (int b = bands - 1; b >= 0; --b) { - e += (smoothedEnergy[b] - e)*smoothingSlew; - smoothedEnergy[b] = e; - } - for (int b = 0; b < bands; ++b) { - e += (smoothedEnergy[b] - e)*smoothingSlew; - smoothedEnergy[b] = e; - } + for (int b = 0; b < bands; ++b) { + e += (smoothedEnergy[b] - e)*smoothingSlew; + smoothedEnergy[b] = e; } + smoothEnergyState = e; } Sample mapFreq(Sample freq) const { if (customFreqMap) return customFreqMap(freq); if (freq > freqTonalityLimit) { - Sample diff = freq - freqTonalityLimit; - return freqTonalityLimit*freqMultiplier + diff; + return freq + (freqMultiplier - 1)*freqTonalityLimit; } return freq*freqMultiplier; } // Identifies spectral peaks using energy across all channels - void findPeaks(Sample smoothingBins) { - smoothEnergy(smoothingBins); - + void findPeaks() { peaks.resize(0); int start = 0; @@ -631,6 +827,118 @@ outputMap[b] = {b + topOffset, 1}; } } + + // If we mapped formants the same way as mapFreq(), this would be the inverse + Sample invMapFormant(Sample freq) const { + if (freq*invFormantMultiplier > freqTonalityLimit) { + return freq + (1 - formantMultiplier)*freqTonalityLimit; + } + return freq*invFormantMultiplier; + } + + Sample freqEstimateWeighted = 0; + Sample freqEstimateWeight = 0; + Sample estimateFrequency() { + // 3 highest peaks in the input + std::array<int, 3> peakIndices{0, 0, 0}; + for (int b = 1; b < bands - 1; ++b) { + Sample e = formantMetric[b]; + // local maxima only + if (e < formantMetric[b - 1] || e <= formantMetric[b + 1]) continue; + + if (e > formantMetric[peakIndices[0]]) { + if (e > formantMetric[peakIndices[1]]) { + if (e > formantMetric[peakIndices[2]]) { + peakIndices = {peakIndices[1], peakIndices[2], b}; + } else { + peakIndices = {peakIndices[1], b, peakIndices[2]}; + } + } else { + peakIndices[0] = b; + } + } + } + + // VERY rough pitch estimation + int peakEstimate = peakIndices[2]; + if (formantMetric[peakIndices[1]] > formantMetric[peakIndices[2]]*0.1) { + int diff = std::abs(peakEstimate - peakIndices[1]); + if (diff > peakEstimate/8 && diff < peakEstimate*7/8) peakEstimate = peakEstimate%diff; + if (formantMetric[peakIndices[0]] > formantMetric[peakIndices[2]]*0.01) { + int diff = std::abs(peakEstimate - peakIndices[0]); + if (diff > peakEstimate/8 && diff < peakEstimate*7/8) peakEstimate = peakEstimate%diff; + } + } + Sample weight = formantMetric[peakIndices[2]]; + // Smooth it out a bit + freqEstimateWeighted += (peakEstimate*weight - freqEstimateWeighted)*0.25; + freqEstimateWeight += (weight - freqEstimateWeight)*0.25; + + return freqEstimateWeighted/(freqEstimateWeight + Sample(1e-30)); + } + + Sample freqEstimate; + + std::vector<Sample> formantMetric; + Sample formantBaseFreq = 0; + void updateFormants(size_t step) { + if (step-- == 0) { + for (auto &e : formantMetric) e = 0; + for (int c = 0; c < channels; ++c) { + Band *bins = bandsForChannel(c); + for (int b = 0; b < bands; ++b) { + formantMetric[b] += bins[b].inputEnergy; + } + } + + freqEstimate = freqToBand(formantBaseFreq); + if (formantBaseFreq <= 0) freqEstimate = estimateFrequency(); + + for (int b = 0; b < bands; ++b) { + formantMetric[b] = std::sqrt(formantMetric[b]); + } + } else if (step-- == 0) { + Sample slew = 1/(freqEstimate*0.5 + 1); + Sample e = 0; + for (size_t repeat = 0; repeat < 2; ++repeat) { + for (int b = bands - 1; b >= 0; --b) { + e += (formantMetric[b] - e)*slew; + formantMetric[b] = e; + } + for (int b = 0; b < bands; ++b) { + e += (formantMetric[b] - e)*slew; + formantMetric[b] = e; + } + } + } else { + auto getFormant = [&](Sample band) -> Sample { + if (band < 0) return 0; + band = std::min<Sample>(band, bands); + int floorBand = std::floor(band); + Sample fracBand = band - floorBand; + Sample low = formantMetric[floorBand], high = formantMetric[floorBand + 1]; + return low + (high - low)*fracBand; + }; + + for (int b = 0; b < bands; ++b) { + Sample inputF = bandToFreq(b); + Sample outputF = formantCompensation ? mapFreq(inputF) : inputF; + outputF = invMapFormant(outputF); + + Sample inputE = formantMetric[b]; + Sample targetE = getFormant(freqToBand(outputF)); + + Sample formantRatio = targetE/(inputE + Sample(1e-30)); + Sample energyRatio = formantRatio*formantRatio; + + for (int c = 0; c < channels; ++c) { + Band *bins = bandsForChannel(c); + // This is what's used to decide the output energy, so this affects the output + bins[b].inputEnergy *= energyRatio; + } + } + } + } }; }} // namespace Added: trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/LICENSE.txt ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/LICENSE.txt Sun May 25 14:04:10 2025 (r23191) @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Signalsmith Audio + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. Added: trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/README.md ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/README.md Sun May 25 14:04:10 2025 (r23191) @@ -0,0 +1,91 @@ +# Signalsmith Linear + +Header-only C++11 wrappers for common things needed by our audio libraries. It's designed for internal use, so [caveat developor](https://en.wikipedia.org/wiki/Caveat_emptor). The goal is to wrap around Accelerate/IPP when available, but still work without it. + +Everything is in `signalsmith::linear::` namespace. + +## FFTs + +```cpp +#include "signalsmith-linear/fft.h" +``` + +This provides real and complex FFTs. They all have `.resize(size_t)`, and `.fft()`/`.ifft()` methods which can take either `std::complex<>` pointers, or a real/imaginary pair ("split-complex" form) for each complex argument. + +The `Pow2FFT<>` and `Pow2RealFFT<>` templates wrap around fast implementations where available. + +The main `FFT<>`, `RealFFT<>` and `ModifiedRealFFT<>` templates wrap around the `Pow2<>` implementations, to add support for multiples of 3 and 5. They provide a static `.fastSizeAbove()` to find the next biggest size. + +### Chunked computation + +The main FFT classes also provide `.steps()` method, and an optional `size_t` first argument to the `.fft()`/`.ifft()` methods, so that computation can be divided up into chunks. + +The computation time for the chunks is not exactly equal, but when you're doing large FFTs periodically (instead of smaller ones regularly) it can help distribute the computation out, without using threads. + +## STFTs + +```cpp +#include "signalsmith-linear/stft.h" +``` + +This provides `DynamicSTFT<>` template, which is configured for any block length (zero padding to a fast size), and a (default!) interval between blocks. It can have a different number of input/output channels, using `.spectrum(c)` to access both. + +### Input and output + +The `.synthesise()` method moves the output time forward, and adds in output block(s) generated from the spectrum. The result can then be queried using `.readOutput()`. + +Input is passed in using `.writeInput()`, but you must move the input time forward using `.moveInput()` before calling `.analyse()`. By default, this will analyse the most recent block of input, but if you passed in a non-zero `extraInputHistory` when configuring, you can analyse input from some time in the past. + +### Window shape and block interval + +It has separate analysis/synthesis windows, which can be changed on-the-fly. Both windows have an "offset" marking the centre of the window, to support asymmetrical setups (for reduced latency). + +The "synthesis interval" (optionally passed to `.synthesise()`) can also change arbitrarily. The output is normalised according to these gaps (and the analysis/synthesis windows) such that regardless of spacing or window shape, it would perfectly reconstruct the input. + +The analysis should remain constant between analysis and synthesis, since it's used for normalising the output properly. If you're synthesising signals from scratch (which have no inherent input windowing), then you should reflect that by setting the analysis window to all 1s. + +### Chunked computation + +Similar to the FFTs' `.steps()` methods, `DynamicSTFT` has `.analyseSteps()`/`.synthesiseSteps()` methods, and `.analyseStep()`/`.synthesiseStep()` to help you spread the computation out over time. + +The window shapes/offsets, input/output and synthesis interval must stay the same until the analysis/synthesis is finished. + +## Expressions + +```cpp +#include "signalsmith-linear/linear.h" +``` + +The main `Linear` class provides expression templates, which wrap around three types of pointer: real, complex, and split-complex. You can wrap these pointers (or `std::vector`s) into `Expression`s using the `()` operator: + +```cpp +Linear linear; + +float *a, *b; +std::complex<float> *c; +size_t size; + +// Once the above variables are set up: +linear(a, size) = linear(b) + linear(c).abs(); + +// Pass in two real pointers to make a split-complex expression (which is often a bit faster). +linear(c, size) = linear(a, b).conj(); +``` + +Implementations may use temporary internal storage. This means it's not thread-safe, it should be a member of your processing class, and you should also call `.reserve???()` during configuration, with the longest vector length you expect to use. + +## Building + +### CMake + +If you're using CMake, include this directory. It will add a `signalsmith-linear` target which doesn't build anything, but linking to this "library" will add the include path. + +### Other + +To use Accelerate on Mac, link the framework and define `SIGNALSMITH_USE_ACCELERATE`: + +``` +g++ -framework Accelerate -DSIGNALSMITH_USE_ACCELERATE +``` + +For IPP, link to `IPP::ippcore` and `IPP::ipps`, and define `SIGNALSMITH_USE_IPP`. Added: trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/fft.h ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ trunk/OpenMPT/include/SignalsmithStretch/signalsmith-linear/fft.h Sun May 25 14:04:10 2025 (r23191) @@ -0,0 +1,1366 @@ +#ifndef SIGNALSMITH_AUDIO_LINEAR_FFT_H +#define SIGNALSMITH_AUDIO_LINEAR_FFT_H + +#include <complex> +#include <vector> +#include <cmath> + +#if defined(__FAST_MATH__) && (__apple_build_version__ >= 16000000) && (__apple_build_version__ <= 16000099) && !defined(SIGNALSMITH_IGNORE_BROKEN_APPLECLANG) +# error Apple Clang 16.0.0 generates incorrect SIMD for ARM. If you HAVE to use this version of Clang, turn off -ffast-math. +#endif + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +namespace signalsmith { namespace linear { + +namespace _impl { + template<class V> + void complexMul(std::complex<V> *a, const std::complex<V> *b, const std::complex<V> *c, size_t size) { + for (size_t i = 0; i < size; ++i) { + auto bi = b[i], ci = c[i]; + a[i] = {bi.real()*ci.real() - bi.imag()*ci.imag(), bi.imag()*ci.real() + bi.real()*ci.imag()}; + } + } + template<class V> + void complexMulConj(std::complex<V> *a, const std::complex<V> *b, const std::complex<V> *c, size_t size) { + for (size_t i = 0; i < size; ++i) { + auto bi = b[i], ci = c[i]; + a[i] = {bi.real()*ci.real() + bi.imag()*ci.imag(), bi.imag()*ci.real() - bi.real()*ci.imag()}; + } + } + template<class V> + void complexMul(V *ar, V *ai, const V *br, const V *bi, const V *cr, const V *ci, size_t size) { + for (size_t i = 0; i < size; ++i) { + V rr = br[i]*cr[i] - bi[i]*ci[i]; + V ri = br[i]*ci[i] + bi[i]*cr[i]; + ar[i] = rr; + ai[i] = ri; + } + } + template<class V> + void complexMulConj(V *ar, V *ai, const V *br, const V *bi, const V *cr, const V *ci, size_t size) { + for (size_t i = 0; i < size; ++i) { + V rr = cr[i]*br[i] + ci[i]*bi[i]; + V ri = cr[i]*bi[i] - ci[i]*br[i]; + ar[i] = rr; + ai[i] = ri; + } + } + + // Input: aStride elements next to each other -> output with bStride + template<size_t aStride, class V> + void interleaveCopy(const V *a, V *b, size_t bStride) { + for (size_t bi = 0; bi < bStride; ++bi) { + const V *offsetA = a + bi*aStride; + V *offsetB = b + bi; + for (size_t ai = 0; ai < aStride; ++ai) { + offsetB[ai*bStride] = offsetA[ai]; + } + } + } + template<class V> + void interleaveCopy(const V *a, V *b, size_t aStride, size_t bStride) { + for (size_t bi = 0; bi < bStride; ++bi) { + const V *offsetA = a + bi*aStride; + V *offsetB = b + bi; + for (size_t ai = 0; ai < aStride; ++ai) { + offsetB[ai*bStride] = offsetA[ai]; + } + } + } + template<size_t aStride, class V> + void interleaveCopy(const V *aReal, const V *aImag, V *bReal, V *bImag, size_t bStride) { + for (size_t bi = 0; bi < bStride; ++bi) { + const V *offsetAr = aReal + bi*aStride; + const V *offsetAi = aImag + bi*aStride; + V *offsetBr = bReal + bi; + V *offsetBi = bImag + bi; + for (size_t ai = 0; ai < aStride; ++ai) { + offsetBr[ai*bStride] = offsetAr[ai]; + offsetBi[ai*bStride] = offsetAi[ai]; + } + } + } + template<class V> + void interleaveCopy(const V *aReal, const V *aImag, V *bReal, V *bImag, size_t aStride, size_t bStride) { + for (size_t bi = 0; bi < bStride; ++bi) { + const V *offsetAr = aReal + bi*aStride; + const V *offsetAi = aImag + bi*aStride; + V *offsetBr = bReal + bi; + V *offsetBi = bImag + bi; + for (size_t ai = 0; ai < aStride; ++ai) { + offsetBr[ai*bStride] = offsetAr[ai]; + offsetBi[ai*bStride] = offsetAi[ai]; + } + } + } +} + +/// Fairly simple and very portable power-of-2 FFT +template<typename Sample> +struct SimpleFFT { + using Complex = std::complex<Sample>; + + SimpleFFT(size_t size=0) { + resize(size); + } + + void resize(size_t size) { + twiddles.resize(size*3/4); + for (size_t i = 0; i < size*3/4; ++i) { + Sample twiddlePhase = -2*M_PI*i/size; + twiddles[i] = std::polar(Sample(1), twiddlePhase); + } + working.resize(size); + } + + void fft(const Complex *time, Complex *freq) { + size_t size = working.size(); + if (size <= 1) { + *freq = *time; + return; + } + fftPass<false>(size, 1, time, freq, working.data()); + } + + void ifft(const Complex *freq, Complex *time) { + size_t size = working.size(); + if (size <= 1) { + *time = *freq; + return; + } + fftPass<true>(size, 1, freq, time, working.data()); + } + + void fft(const Sample *inR, const Sample *inI, Sample *outR, Sample *outI) { + size_t size = working.size(); + if (size <= 1) { + *outR = *inR; + *outI = *inI; + return; + } + Sample *workingR = (Sample *)working.data(), *workingI = workingR + size; + fftPass<false>(size, 1, inR, inI, outR, outI, workingR, workingI); + } + void ifft(const Sample *inR, const Sample *inI, Sample *outR, Sample *outI) { + size_t size = working.size(); + if (size <= 1) { + *outR = *inR; + *outI = *inI; + return; + } + Sample *workingR = (Sample *)working.data(), *workingI = workingR + size; + fftPass<true>(size, 1, inR, inI, outR, outI, workingR, workingI); + } +private: + std::vector<Complex> twiddles; + std::vector<Complex> working; + + template<bool conjB> + static Complex mul(const Complex &a, const Complex &b) { + return conjB ? Complex{ + a.real()*b.real() + a.imag()*b.imag(), + a.imag()*b.real() - a.real()*b.imag() + } : Complex{ + a.real()*b.real() - a.imag()*b.imag(), + a.imag()*b.real() + a.real()*b.imag() + }; + } + + // Calculate a [size]-point FFT, where each element is a block of [stride] values + template<bool inverse> + void fftPass(size_t size, size_t stride, const Complex *input, Complex *output, Complex *working) { + if (size/4 > 1) { + // Calculate four quarter-size FFTs + fftPass<inverse>(size/4, stride*4, input, working, output); + combine4<inverse>(size, stride, working, output); + } else if (size == 4) { + combine4<inverse>(4, stride, input, output); + } else { + // 2-point FFT + for (size_t s = 0; s < stride; ++s) { + Complex a = input[s]; + Complex b = input[s + stride]; + output[s] = a + b; + output[s + stride] = a - b; + } + } + } + + // Combine interleaved results into a single spectrum + template<bool inverse> + void combine4(size_t size, size_t stride, const Complex *input, Complex *output) const { + auto twiddleStep = working.size()/size; + for (size_t i = 0; i < size/4; ++i) { + Complex twiddleB = twiddles[i*twiddleStep]; + Complex twiddleC = twiddles[i*2*twiddleStep]; + Complex twiddleD = twiddles[i*3*twiddleStep]; + + const Complex *inputA = input + 4*i*stride; + const Complex *inputB = input + (4*i + 1)*stride; + const Complex *inputC = input + (4*i + 2)*stride; + const Complex *inputD = input + (4*i + 3)*stride; + Complex *outputA = output + i*stride; + Complex *outputB = output + (i + size/4)*stride; + Complex *outputC = output + (i + size/4*2)*stride; + Complex *outputD = output + (i + size/4*3)*stride; + for (size_t s = 0; s < stride; ++s) { + Complex a = inputA[s]; + Complex b = mul<inverse>(inputB[s], twiddleB); + Complex c = mul<inverse>(inputC[s], twiddleC); + Complex d = mul<inverse>(inputD[s], twiddleD); + Complex ac0 = a + c, ac1 = a - c; + Complex bd0 = b + d, bd1 = inverse ? (b - d) : (d - b); + Complex bd1i = {-bd1.imag(), bd1.real()}; + outputA[s] = ac0 + bd0; + outputB[s] = ac1 + bd1i; + outputC[s] = ac0 - bd0; + outputD[s] = ac1 - bd1i; + } + } + } + + // The same thing, but translated for split-complex input/output + template<bool inverse> + void fftPass(size_t size, size_t stride, const Sample *inputR, const Sample *inputI, Sample *outputR, Sample *outputI, Sample *workingR, Sample *workingI) const { + if (size/4 > 1) { + // Calculate four quarter-size FFTs + fftPass<inverse>(size/4, stride*4, inputR, inputI, workingR, workingI, outputR, outputI); + combine4<inverse>(size, stride, workingR, workingI, outputR, outputI); + } else if (size == 4) { + combine4<inverse>(4, stride, inputR, inputI, outputR, outputI); + } else { + // 2-point FFT + for (size_t s = 0; s < stride; ++s) { + Sample ar = inputR[s], ai = inputI[s]; + Sample br = inputR[s + stride], bi = inputI[s + stride]; + outputR[s] = ar + br; + outputI... [truncated message content] |
From: <sv...@op...> - 2025-05-25 11:25:13
|
Author: manx Date: Sun May 25 13:24:35 2025 New Revision: 23190 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23190 Log: Merged revision(s) 23188 from trunk/OpenMPT: [Fix] build: CI: GitHub: OpenBSD: Fix package install on 7.7. ........ Modified: branches/OpenMPT-1.31/ (props changed) branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml branches/OpenMPT-1.31/.github/workflows/OpenBSD-Makefile.yml Modified: branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml ============================================================================== --- branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml Sun May 25 13:24:14 2025 (r23189) +++ branches/OpenMPT-1.31/.github/workflows/OpenBSD-Autotools.yml Sun May 25 13:24:35 2025 (r23190) @@ -48,8 +48,8 @@ shutdown_vm: false run: | sudo pkg_add -u - sudo pkg_add -I ${{ (matrix.version == '7.6') && 'ghostscript-10.03.1p2' || (matrix.version == '7.5') && 'ghostscript-10.03.1' || (matrix.version == '7.4') && 'ghostscript-10.02.0' || 'ghostscript' }} - sudo pkg_add -I ${{ (matrix.version == '7.6') && 'autoconf-2.72p0 automake-1.16.5' || (matrix.version == '7.5') && 'autoconf-2.71 automake-1.16.5' || (matrix.version == '7.4') && 'autoconf-2.71 automake-1.16.5' || 'autoconf automake' }} + sudo pkg_add -I ${{ (matrix.version == '7.7') && 'ghostscript-10.05.0' || (matrix.version == '7.6') && 'ghostscript-10.03.1p2' || (matrix.version == '7.5') && 'ghostscript-10.03.1' || (matrix.version == '7.4') && 'ghostscript-10.02.0' || 'ghostscript' }} + sudo pkg_add -I ${{ (matrix.version == '7.7') && 'autoconf-2.72p0 automake-1.17' || (matrix.version == '7.6') && 'autoconf-2.72p0 automake-1.16.5' || (matrix.version == '7.5') && 'autoconf-2.71 automake-1.16.5' || (matrix.version == '7.4') && 'autoconf-2.71 automake-1.16.5' || 'autoconf automake' }} sudo pkg_add -I subversion p5-XML-XPath git mawk gmake pkgconf autoconf-archive libtool libltdl help2man doxygen mpg123 libogg libvorbis flac libsndfile pulseaudio portaudio-svn sdl2 - name: Build uses: cross-platform-actions/action@v0.28.0 Modified: branches/OpenMPT-1.31/.github/workflows/OpenBSD-Makefile.yml ============================================================================== --- branches/OpenMPT-1.31/.github/workflows/OpenBSD-Makefile.yml Sun May 25 13:24:14 2025 (r23189) +++ branches/OpenMPT-1.31/.github/workflows/OpenBSD-Makefile.yml Sun May 25 13:24:35 2025 (r23190) @@ -56,8 +56,8 @@ shutdown_vm: false run: | sudo pkg_add -u - sudo pkg_add -I ${{ (matrix.version == '7.6') && 'ghostscript-10.03.1p2' || (matrix.version == '7.5') && 'ghostscript-10.03.1' || (matrix.version == '7.4') && 'ghostscript-10.02.0' || 'ghostscript' }} - sudo pkg_add -I ${{ (matrix.version == '7.6') && 'autoconf-2.72p0 automake-1.16.5' || (matrix.version == '7.5') && 'autoconf-2.71 automake-1.16.5' || (matrix.version == '7.4') && 'autoconf-2.71 automake-1.16.5' || 'autoconf automake' }} + sudo pkg_add -I ${{ (matrix.version == '7.7') && 'ghostscript-10.05.0' || (matrix.version == '7.6') && 'ghostscript-10.03.1p2' || (matrix.version == '7.5') && 'ghostscript-10.03.1' || (matrix.version == '7.4') && 'ghostscript-10.02.0' || 'ghostscript' }} + sudo pkg_add -I ${{ (matrix.version == '7.7') && 'autoconf-2.72p0 automake-1.17' || (matrix.version == '7.6') && 'autoconf-2.72p0 automake-1.16.5' || (matrix.version == '7.5') && 'autoconf-2.71 automake-1.16.5' || (matrix.version == '7.4') && 'autoconf-2.71 automake-1.16.5' || 'autoconf automake' }} sudo pkg_add -I subversion p5-XML-XPath git mawk gmake pkgconf autoconf-archive libtool libltdl help2man doxygen mpg123 libogg libvorbis flac libsndfile pulseaudio portaudio-svn sdl2 - name: Build uses: cross-platform-actions/action@v0.28.0 |
From: <sv...@op...> - 2025-05-25 11:24:26
|
Author: manx Date: Sun May 25 13:24:14 2025 New Revision: 23189 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23189 Log: Merged revision(s) 23188 from trunk/OpenMPT: [Fix] build: CI: GitHub: OpenBSD: Fix package install on 7.7. ........ Modified: branches/OpenMPT-1.32/ (props changed) branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml branches/OpenMPT-1.32/.github/workflows/OpenBSD-Makefile.yml Modified: branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml ============================================================================== --- branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml Sun May 25 13:23:55 2025 (r23188) +++ branches/OpenMPT-1.32/.github/workflows/OpenBSD-Autotools.yml Sun May 25 13:24:14 2025 (r23189) @@ -48,8 +48,8 @@ shutdown_vm: false run: | sudo pkg_add -u - sudo pkg_add -I ${{ (matrix.version == '7.6') && 'ghostscript-10.03.1p2' || (matrix.version == '7.5') && 'ghostscript-10.03.1' || (matrix.version == '7.4') && 'ghostscript-10.02.0' || 'ghostscript' }} - sudo pkg_add -I ${{ (matrix.version == '7.6') && 'autoconf-2.72p0 automake-1.16.5' || (matrix.version == '7.5') && 'autoconf-2.71 automake-1.16.5' || (matrix.version == '7.4') && 'autoconf-2.71 automake-1.16.5' || 'autoconf automake' }} + sudo pkg_add -I ${{ (matrix.version == '7.7') && 'ghostscript-10.05.0' || (matrix.version == '7.6') && 'ghostscript-10.03.1p2' || (matrix.version == '7.5') && 'ghostscript-10.03.1' || (matrix.version == '7.4') && 'ghostscript-10.02.0' || 'ghostscript' }} + sudo pkg_add -I ${{ (matrix.version == '7.7') && 'autoconf-2.72p0 automake-1.17' || (matrix.version == '7.6') && 'autoconf-2.72p0 automake-1.16.5' || (matrix.version == '7.5') && 'autoconf-2.71 automake-1.16.5' || (matrix.version == '7.4') && 'autoconf-2.71 automake-1.16.5' || 'autoconf automake' }} sudo pkg_add -I subversion p5-XML-XPath git mawk gmake pkgconf autoconf-archive libtool libltdl help2man doxygen mpg123 libogg libvorbis flac libsndfile pulseaudio portaudio-svn sdl2 - name: Build uses: cross-platform-actions/action@v0.28.0 Modified: branches/OpenMPT-1.32/.github/workflows/OpenBSD-Makefile.yml ============================================================================== --- branches/OpenMPT-1.32/.github/workflows/OpenBSD-Makefile.yml Sun May 25 13:23:55 2025 (r23188) +++ branches/OpenMPT-1.32/.github/workflows/OpenBSD-Makefile.yml Sun May 25 13:24:14 2025 (r23189) @@ -56,8 +56,8 @@ shutdown_vm: false run: | sudo pkg_add -u - sudo pkg_add -I ${{ (matrix.version == '7.6') && 'ghostscript-10.03.1p2' || (matrix.version == '7.5') && 'ghostscript-10.03.1' || (matrix.version == '7.4') && 'ghostscript-10.02.0' || 'ghostscript' }} - sudo pkg_add -I ${{ (matrix.version == '7.6') && 'autoconf-2.72p0 automake-1.16.5' || (matrix.version == '7.5') && 'autoconf-2.71 automake-1.16.5' || (matrix.version == '7.4') && 'autoconf-2.71 automake-1.16.5' || 'autoconf automake' }} + sudo pkg_add -I ${{ (matrix.version == '7.7') && 'ghostscript-10.05.0' || (matrix.version == '7.6') && 'ghostscript-10.03.1p2' || (matrix.version == '7.5') && 'ghostscript-10.03.1' || (matrix.version == '7.4') && 'ghostscript-10.02.0' || 'ghostscript' }} + sudo pkg_add -I ${{ (matrix.version == '7.7') && 'autoconf-2.72p0 automake-1.17' || (matrix.version == '7.6') && 'autoconf-2.72p0 automake-1.16.5' || (matrix.version == '7.5') && 'autoconf-2.71 automake-1.16.5' || (matrix.version == '7.4') && 'autoconf-2.71 automake-1.16.5' || 'autoconf automake' }} sudo pkg_add -I subversion p5-XML-XPath git mawk gmake pkgconf autoconf-archive libtool libltdl help2man doxygen mpg123 libogg libvorbis flac libsndfile pulseaudio portaudio-svn sdl2 - name: Build uses: cross-platform-actions/action@v0.28.0 |