mirror of
https://github.com/audacity/audacity-project-tools.git
synced 2026-04-10 05:56:29 -05:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40322b0650 | ||
|
|
edd0a14a52 | ||
|
|
956b312035 | ||
|
|
b5013399bc | ||
|
|
99837208db | ||
|
|
601208bb9c | ||
|
|
cd7654c2ef | ||
|
|
516c53fcf2 | ||
|
|
e99cae9ce5 | ||
|
|
cce14f3260 | ||
|
|
24de66669e | ||
|
|
743e8e8615 | ||
|
|
b67cef0611 | ||
|
|
a080523fd8 | ||
|
|
bb1a76604f | ||
|
|
58d7406479 | ||
|
|
0d19b93d3a | ||
|
|
1b447406f5 | ||
|
|
3a1b4b0778 | ||
|
|
84c09abe60 | ||
|
|
b27f3cb580 | ||
|
|
d74083ba4b |
73
.github/workflows/cmake.yml
vendored
73
.github/workflows/cmake.yml
vendored
@@ -6,10 +6,8 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
|
||||
BUILD_TYPE: Release
|
||||
CONAN_USER_HOME: ${{ github.workspace }}/conan-home/
|
||||
CONAN_USER_HOME_SHORT: "${{ github.workspace }}/conan-home/short"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Building ${{ matrix.config.os }}
|
||||
@@ -18,58 +16,51 @@ jobs:
|
||||
matrix:
|
||||
config:
|
||||
- os: ubuntu-latest
|
||||
generator: Unix Makefiles
|
||||
#- os: macos-11
|
||||
# generator: Xcode
|
||||
preset: conan-release
|
||||
- os: macos-latest
|
||||
preset: conan-release
|
||||
- os: windows-2022
|
||||
generator: Visual Studio 17 2022
|
||||
preset: conan-default
|
||||
steps:
|
||||
- name: switch to gcc-11 on linux
|
||||
if: ${{ matrix.config.os == 'ubuntu-latest' }}
|
||||
run: |
|
||||
sudo apt install gcc-11 g++-11
|
||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11 --slave /usr/bin/gcov gcov /usr/bin/gcov-11
|
||||
sudo update-alternatives --set gcc /usr/bin/gcc-11
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2.3.1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
cache: pip
|
||||
#cache-dependency-path: # optional
|
||||
- uses: BSFishy/pip-action@v1
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Conan
|
||||
run: pip install "conan>=2"
|
||||
|
||||
- name: Detect Conan profile
|
||||
run: conan profile detect --force
|
||||
|
||||
- name: Cache Conan packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
packages: conan
|
||||
- name: Cache for .conan
|
||||
id: cache-conan
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-conan-modules
|
||||
with:
|
||||
path: ${{ env.CONAN_USER_HOME }}
|
||||
key: host-${{ matrix.config.os }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('CMakeLists.txt') }}
|
||||
path: ~/.conan2
|
||||
key: conan-${{ matrix.config.os }}-${{ hashFiles('conanfile.txt') }}
|
||||
restore-keys: |
|
||||
host-3-${{ matrix.config.os }}-
|
||||
conan-${{ matrix.config.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: conan install . --output-folder=build/${{ env.BUILD_TYPE }} --build=missing -s build_type=${{ env.BUILD_TYPE }}
|
||||
|
||||
- name: Configure CMake
|
||||
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
|
||||
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
|
||||
run: cmake -B ${{github.workspace}}/build -G"${{ matrix.config.generator }}" -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DCMAKE_CONFIGURATION_TYPES=${{ env.BUILD_TYPE }}
|
||||
run: cmake --preset ${{ matrix.config.preset }}
|
||||
|
||||
- name: Build
|
||||
# Build your program with the given configuration
|
||||
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
|
||||
run: cmake --build build/${{ env.BUILD_TYPE }} --config ${{ env.BUILD_TYPE }}
|
||||
|
||||
- name: Package
|
||||
# Build your program with the given configuration
|
||||
run: |
|
||||
cd build
|
||||
cmake --build . --target package --config "${{ env.BUILD_TYPE }}"
|
||||
working-directory: build/${{ env.BUILD_TYPE }}
|
||||
run: cmake --build . --target package --config ${{ env.BUILD_TYPE }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: audacity-project-tools-${{ matrix.config.os }}
|
||||
path: |
|
||||
build/packages/*
|
||||
!build/packages/_CPack_Packages
|
||||
build/${{ env.BUILD_TYPE }}/packages/*
|
||||
!build/${{ env.BUILD_TYPE }}/packages/_CPack_Packages
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
53
.github/workflows/release.yml
vendored
Normal file
53
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_id:
|
||||
description: 'Workflow run ID to take artifacts from'
|
||||
required: true
|
||||
type: string
|
||||
tag:
|
||||
description: 'Tag name (e.g. v1.0.5)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Mark as pre-release'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
gh run download ${{ inputs.run_id }} --dir release-artifacts
|
||||
|
||||
for dir in release-artifacts/*/; do
|
||||
name=$(basename "$dir")
|
||||
(cd "$dir" && zip -r "../../release-assets/${name}.zip" .)
|
||||
done
|
||||
|
||||
- name: Create tag
|
||||
run: |
|
||||
git tag ${{ inputs.tag }}
|
||||
git push origin ${{ inputs.tag }}
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release create ${{ inputs.tag }} \
|
||||
--title "${{ inputs.tag }}" \
|
||||
${{ inputs.prerelease && '--prerelease' || '' }} \
|
||||
release-assets/*
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -34,3 +34,14 @@
|
||||
build/**
|
||||
.vs/**
|
||||
out/**
|
||||
.venv/**
|
||||
CMakeUserPresets.json
|
||||
|
||||
# Audacity files
|
||||
|
||||
*.aup3
|
||||
*.aup4
|
||||
*.aup3-shm
|
||||
*.aup3-wal
|
||||
*.aup4-shm
|
||||
*.aup4-wal
|
||||
|
||||
108
CMakeLists.txt
108
CMakeLists.txt
@@ -14,108 +14,18 @@ if( CMAKE_GENERATOR MATCHES "Visual Studio" )
|
||||
)
|
||||
endif()
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR})
|
||||
list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR})
|
||||
|
||||
if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake")
|
||||
message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan")
|
||||
file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/master/conan.cmake"
|
||||
"${CMAKE_BINARY_DIR}/conan.cmake"
|
||||
TLS_VERIFY ON)
|
||||
endif()
|
||||
|
||||
include(${CMAKE_BINARY_DIR}/conan.cmake)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
if(WIN32)
|
||||
add_definitions(-DUNICODE -D_UNICODE)
|
||||
endif()
|
||||
|
||||
set(DEPENDENCIES
|
||||
REQUIRES
|
||||
fmt/7.1.3
|
||||
sqlite3/3.40.0
|
||||
sqlitecpp/3.1.1
|
||||
gflags/2.2.2
|
||||
utfcpp/3.2.1
|
||||
boost/1.78.0
|
||||
|
||||
OPTIONS
|
||||
boost:without_atomic=False
|
||||
boost:without_chrono=True
|
||||
boost:without_container=True
|
||||
boost:without_context=True
|
||||
boost:without_contract=True
|
||||
boost:without_coroutine=True
|
||||
boost:without_date_time=True
|
||||
boost:without_exception=True
|
||||
boost:without_fiber=True
|
||||
boost:without_filesystem=False
|
||||
boost:without_graph=True
|
||||
boost:without_graph_parallel=True
|
||||
boost:without_iostreams=True
|
||||
boost:without_json=True
|
||||
boost:without_locale=True
|
||||
boost:without_log=True
|
||||
boost:without_math=True
|
||||
boost:without_mpi=True
|
||||
boost:without_nowide=True
|
||||
boost:without_program_options=True
|
||||
boost:without_python=True
|
||||
boost:without_random=True
|
||||
boost:without_regex=True
|
||||
boost:without_serialization=True
|
||||
boost:without_stacktrace=True
|
||||
boost:without_system=False
|
||||
boost:without_test=True
|
||||
boost:without_thread=True
|
||||
boost:without_timer=True
|
||||
boost:without_type_erasure=True
|
||||
boost:without_wave=True
|
||||
)
|
||||
|
||||
### Setup dependecies
|
||||
if(CMAKE_CONFIGURATION_TYPES)
|
||||
foreach(TYPE ${CMAKE_CONFIGURATION_TYPES})
|
||||
message(STATUS "Configuring packages for ${TYPE}")
|
||||
|
||||
conan_cmake_configure(
|
||||
${DEPENDENCIES}
|
||||
GENERATORS
|
||||
cmake_find_package_multi
|
||||
)
|
||||
|
||||
conan_cmake_autodetect(settings BUILD_TYPE ${TYPE})
|
||||
|
||||
conan_cmake_install(PATH_OR_REFERENCE .
|
||||
BUILD missing
|
||||
SETTINGS ${settings}
|
||||
)
|
||||
endforeach()
|
||||
else()
|
||||
message(STATUS "Configuring packages for ${CMAKE_BUILD_TYPE}")
|
||||
|
||||
conan_cmake_configure(
|
||||
${DEPENDENCIES}
|
||||
GENERATORS
|
||||
cmake_find_package_multi
|
||||
)
|
||||
|
||||
conan_cmake_autodetect(settings)
|
||||
|
||||
conan_cmake_install(PATH_OR_REFERENCE .
|
||||
BUILD missing
|
||||
SETTINGS ${settings}
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(fmt CONFIG)
|
||||
find_package(SQLite3 CONFIG)
|
||||
find_package(SQLiteCpp CONFIG)
|
||||
find_package(gflags CONFIG)
|
||||
find_package(utf8cpp CONFIG)
|
||||
find_package(Boost)
|
||||
find_package(fmt REQUIRED)
|
||||
find_package(SQLite3 REQUIRED)
|
||||
find_package(SQLiteCpp REQUIRED)
|
||||
find_package(gflags REQUIRED)
|
||||
find_package(utf8cpp REQUIRED)
|
||||
find_package(Boost REQUIRED COMPONENTS filesystem system)
|
||||
|
||||
set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin )
|
||||
|
||||
@@ -150,9 +60,9 @@ add_executable(audacity-project-tools
|
||||
target_link_libraries(audacity-project-tools
|
||||
fmt::fmt
|
||||
SQLite::SQLite3
|
||||
SQLiteCpp::SQLiteCpp
|
||||
SQLiteCpp
|
||||
gflags::gflags
|
||||
utf8cpp::utf8cpp
|
||||
utf8cpp
|
||||
Boost::boost
|
||||
Boost::filesystem
|
||||
Boost::system
|
||||
@@ -166,7 +76,7 @@ set(CPACK_PACKAGE_NAME "audacity-project-tools")
|
||||
set(CPACK_PACKAGE_VENDOR "Audacity")
|
||||
set(CPACK_PACKAGE_VERSION_MAJOR 1)
|
||||
set(CPACK_PACKAGE_VERSION_MINOR 0)
|
||||
set(CPACK_PACKAGE_VERSION_PATCH 2)
|
||||
set(CPACK_PACKAGE_VERSION_PATCH 5)
|
||||
set(CPACK_GENERATOR ZIP)
|
||||
SET(CPACK_OUTPUT_FILE_PREFIX packages)
|
||||
|
||||
|
||||
@@ -48,4 +48,4 @@ $ ls
|
||||
|
||||
`audacity-project-tools` requires a C++17 compliant compiler with the complete C++17 library. The build requires <filesystem> and floating-point versions of `from_chars`.
|
||||
|
||||
CMake and Conan are required to configure the project.
|
||||
CMake and Conan are required to configure the project. Conan 2.0 is not supported ATM.
|
||||
|
||||
44
conanfile.txt
Normal file
44
conanfile.txt
Normal file
@@ -0,0 +1,44 @@
|
||||
[requires]
|
||||
fmt/7.1.3
|
||||
sqlite3/3.45.3
|
||||
sqlitecpp/3.3.3
|
||||
gflags/2.2.2
|
||||
utfcpp/3.2.1
|
||||
boost/1.84.0
|
||||
|
||||
[generators]
|
||||
CMakeDeps
|
||||
CMakeToolchain
|
||||
|
||||
[options]
|
||||
boost/*:without_atomic=False
|
||||
boost/*:without_chrono=True
|
||||
boost/*:without_container=True
|
||||
boost/*:without_context=True
|
||||
boost/*:without_contract=True
|
||||
boost/*:without_coroutine=True
|
||||
boost/*:without_date_time=True
|
||||
boost/*:without_exception=True
|
||||
boost/*:without_fiber=True
|
||||
boost/*:without_filesystem=False
|
||||
boost/*:without_graph=True
|
||||
boost/*:without_graph_parallel=True
|
||||
boost/*:without_iostreams=True
|
||||
boost/*:without_json=True
|
||||
boost/*:without_locale=True
|
||||
boost/*:without_log=True
|
||||
boost/*:without_math=True
|
||||
boost/*:without_mpi=True
|
||||
boost/*:without_nowide=True
|
||||
boost/*:without_program_options=True
|
||||
boost/*:without_python=True
|
||||
boost/*:without_random=True
|
||||
boost/*:without_regex=True
|
||||
boost/*:without_serialization=True
|
||||
boost/*:without_stacktrace=True
|
||||
boost/*:without_system=False
|
||||
boost/*:without_test=True
|
||||
boost/*:without_thread=True
|
||||
boost/*:without_timer=True
|
||||
boost/*:without_type_erasure=True
|
||||
boost/*:without_wave=True
|
||||
@@ -1,21 +1 @@
|
||||
bottle==0.12.19
|
||||
certifi==2021.10.8
|
||||
charset-normalizer==2.0.9
|
||||
colorama==0.4.4
|
||||
conan==1.43.2
|
||||
distro==1.6.0
|
||||
fasteners==0.16.3
|
||||
idna==3.3
|
||||
Jinja2==2.11.3
|
||||
MarkupSafe==2.0.1
|
||||
node-semver==0.6.1
|
||||
patch-ng==1.17.4
|
||||
pluginbase==1.0.1
|
||||
Pygments==2.10.0
|
||||
PyJWT==1.7.1
|
||||
python-dateutil==2.8.2
|
||||
PyYAML==5.4.1
|
||||
requests==2.26.0
|
||||
six==1.16.0
|
||||
tqdm==4.62.3
|
||||
urllib3==1.26.7
|
||||
conan>=2
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace
|
||||
|
||||
constexpr int64_t AudacityProjectID = 1096107097;
|
||||
constexpr int64_t MaxSupportedVersion =
|
||||
(3 << 24) + (1 << 16) + (3 << 8) + 0; // 3.1.3.0
|
||||
(3 << 24) + (7 << 16) + (0 << 8) + 0; // 3.7.0.0
|
||||
|
||||
template<typename T> size_t readInt(const std::string& string, size_t offset, T& output)
|
||||
{
|
||||
@@ -112,16 +112,32 @@ AudacityDatabase::AudacityDatabase(
|
||||
mProjectVersion =
|
||||
mDatabase->execAndGet("PRAGMA user_version;").getUInt();
|
||||
|
||||
const auto majorVersion = (mProjectVersion >> 24) & 0xFF;
|
||||
const auto minorVersion = (mProjectVersion >> 16) & 0xFF;
|
||||
const auto patchVersion = (mProjectVersion >> 8) & 0xFF;
|
||||
|
||||
fmt::print(
|
||||
"Project requires Audacity {}.{}.{}\n",
|
||||
(mProjectVersion >> 24) & 0xFF, (mProjectVersion >> 16) & 0xFF,
|
||||
(mProjectVersion >> 8) & 0xFF);
|
||||
"Project requires Audacity {}.{}.{}\n", majorVersion,
|
||||
minorVersion, patchVersion);
|
||||
|
||||
if (majorVersion != 3)
|
||||
{
|
||||
fmt::print("user_version pragma appears to be broken...\n");
|
||||
mProjectVersion = MaxSupportedVersion;
|
||||
}
|
||||
|
||||
if (mProjectVersion > MaxSupportedVersion)
|
||||
throw std::runtime_error("Unsupported project version");
|
||||
fmt::print(
|
||||
"DANGER!!! Unsupported Audacity version detected! Project data might be lost! Proceed with caution!\n");
|
||||
}, true);
|
||||
}
|
||||
|
||||
AudacityDatabase::~AudacityDatabase()
|
||||
{
|
||||
mDatabase.reset();
|
||||
removeJournalFiles(mReadOnly ? mProjectPath : mWritablePath);
|
||||
}
|
||||
|
||||
void AudacityDatabase::reopenReadonlyAsWritable()
|
||||
{
|
||||
if (!mReadOnly)
|
||||
@@ -456,22 +472,26 @@ void AudacityDatabase::extractTrack(
|
||||
waveFile.writeFile();
|
||||
}
|
||||
|
||||
void AudacityDatabase::removeJournalFiles(const std::filesystem::path& dbPath)
|
||||
{
|
||||
auto walFile = dbPath;
|
||||
walFile.replace_extension("aup3-wal");
|
||||
|
||||
if (std::filesystem::exists(walFile))
|
||||
std::filesystem::remove(walFile);
|
||||
|
||||
auto shmFile = dbPath;
|
||||
shmFile.replace_extension("aup3-shm");
|
||||
|
||||
if (std::filesystem::exists(shmFile))
|
||||
std::filesystem::remove(shmFile);
|
||||
}
|
||||
|
||||
void AudacityDatabase::removeOldFiles()
|
||||
{
|
||||
if (std::filesystem::exists(mWritablePath))
|
||||
{
|
||||
std::filesystem::remove(mWritablePath);
|
||||
|
||||
auto walFile = mWritablePath;
|
||||
walFile.replace_extension("aup3-wal");
|
||||
|
||||
if (std::filesystem::exists(walFile))
|
||||
std::filesystem::remove(walFile);
|
||||
|
||||
auto shmFile = mWritablePath;
|
||||
shmFile.replace_extension("aup3-shm");
|
||||
|
||||
if (std::filesystem::exists(shmFile))
|
||||
std::filesystem::remove(shmFile);
|
||||
removeJournalFiles(mWritablePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ class AudacityDatabase final
|
||||
public:
|
||||
explicit AudacityDatabase(
|
||||
const std::filesystem::path& path, RecoveryConfig recoveryConfig);
|
||||
~AudacityDatabase();
|
||||
|
||||
void reopenReadonlyAsWritable();
|
||||
void recoverDatabase();
|
||||
@@ -43,6 +44,7 @@ public:
|
||||
void extractTrack(SampleFormat format, int32_t sampleRate, bool asStereo);
|
||||
|
||||
private:
|
||||
static void removeJournalFiles(const std::filesystem::path& dbPath);
|
||||
void removeOldFiles();
|
||||
|
||||
std::unique_ptr<SQLite::Database> mDatabase;
|
||||
|
||||
@@ -281,17 +281,10 @@ class IdsLookup final
|
||||
public:
|
||||
void store(uint16_t index, std::string value)
|
||||
{
|
||||
const auto size = mIds.size();
|
||||
if (index >= mIds.size())
|
||||
mIds.resize(index + 1);
|
||||
|
||||
if (index == size)
|
||||
mIds.push_back(std::move(value));
|
||||
else
|
||||
{
|
||||
if ((index + 1) < size)
|
||||
mIds.resize(index);
|
||||
|
||||
mIds[index] = std::move(value);
|
||||
}
|
||||
mIds[index] = std::move(value);
|
||||
}
|
||||
|
||||
std::string_view get(uint16_t index)
|
||||
|
||||
@@ -86,6 +86,7 @@ Buffer::read(void* data, size_t offset, size_t size) const noexcept
|
||||
offset = 0;
|
||||
bytesLeft -= chunkSize;
|
||||
outPtr += chunkSize;
|
||||
++chunk;
|
||||
}
|
||||
|
||||
return size;
|
||||
|
||||
@@ -48,12 +48,12 @@ public:
|
||||
return 0;
|
||||
|
||||
const size_t chunkIndex = offset / BUFFER_SIZE;
|
||||
offset = offset - BUFFER_SIZE * chunkIndex;
|
||||
const size_t chunkOffset = offset - BUFFER_SIZE * chunkIndex;
|
||||
|
||||
if (BUFFER_SIZE < (offset + size))
|
||||
if (BUFFER_SIZE < (chunkOffset + size))
|
||||
return read(&data, offset, size);
|
||||
|
||||
const void* ptr = mChunks[chunkIndex].data() + offset;
|
||||
const void* ptr = mChunks[chunkIndex].data() + chunkOffset;
|
||||
|
||||
data = *static_cast<const T*>(ptr);
|
||||
|
||||
|
||||
@@ -48,6 +48,20 @@ void WaveBlock::convertToSilence() noexcept
|
||||
mXMLNode->setAttribute("badblock", true);
|
||||
}
|
||||
|
||||
void WaveBlock::setBlockId(int64_t blockId) noexcept
|
||||
{
|
||||
mBlockId = blockId;
|
||||
mXMLNode->setAttribute("blockid", mBlockId);
|
||||
mXMLNode->setAttribute("badblock", true);
|
||||
}
|
||||
|
||||
void WaveBlock::setStart(int64_t start) noexcept
|
||||
{
|
||||
mStart = start;
|
||||
mXMLNode->setAttribute("start", mStart);
|
||||
mXMLNode->setAttribute("badblock", true);
|
||||
}
|
||||
|
||||
int64_t WaveBlock::getBlockId() const noexcept
|
||||
{
|
||||
return mBlockId;
|
||||
@@ -250,6 +264,66 @@ AudacityProject::~AudacityProject()
|
||||
{
|
||||
}
|
||||
|
||||
bool AudacityProject::containsBlock(int64_t blockId) const
|
||||
{
|
||||
SQLite::Statement query(
|
||||
mDb.DB(), "SELECT 1 FROM sampleblocks WHERE blockid = ?1;");
|
||||
|
||||
query.bind(1, blockId);
|
||||
|
||||
while (query.executeStep())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int AudacityProject::getRealBlockLength(const WaveBlock& block) const
|
||||
{
|
||||
if (block.getBlockId() < 0)
|
||||
return -block.getBlockId();
|
||||
|
||||
SQLite::Statement query (
|
||||
mDb.DB(), "SELECT sampleformat, samples FROM sampleblocks WHERE blockid = ?1;");
|
||||
|
||||
query.bind(1, block.getBlockId());
|
||||
|
||||
while (query.executeStep ())
|
||||
{
|
||||
const int format = query.getColumn(0).getInt();
|
||||
const int64_t blobSize = query.getColumn(1).getBytes();
|
||||
|
||||
if (format != block.getParent()->getFormat())
|
||||
throw std::runtime_error (
|
||||
fmt::format("Unexpected sample format for block {}",
|
||||
block.getBlockId()));
|
||||
|
||||
return blobSize / BytesPerSample(static_cast<SampleFormat>(format));
|
||||
}
|
||||
|
||||
throw std::runtime_error (
|
||||
fmt::format("Block {} not found in the database", block.getBlockId()));
|
||||
}
|
||||
|
||||
AudacityProject::BlockValidationResult
|
||||
AudacityProject::validateBlock(const WaveBlock& block) const
|
||||
{
|
||||
// Creating query every time is slower,
|
||||
// but can withstand error
|
||||
SQLite::Statement query(
|
||||
mDb.DB(), "SELECT sampleformat FROM sampleblocks WHERE blockid = ?1;");
|
||||
|
||||
query.bind(1, block.getBlockId());
|
||||
|
||||
while (query.executeStep())
|
||||
{
|
||||
const int format = query.getColumn(0).getInt();
|
||||
|
||||
return block.getParent()->getFormat() == format ? BlockValidationResult::Ok : BlockValidationResult::Invalid;
|
||||
}
|
||||
|
||||
return BlockValidationResult::Missing;
|
||||
}
|
||||
|
||||
std::set<int64_t> AudacityProject::validateBlocks() const
|
||||
{
|
||||
std::set<int64_t> missingBlocks;
|
||||
@@ -262,51 +336,106 @@ std::set<int64_t> AudacityProject::validateBlocks() const
|
||||
if (missingBlocks.count(block.getBlockId()))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
// Creating query every time is slower,
|
||||
// but can withstand error
|
||||
SQLite::Statement query(
|
||||
mDb.DB(),
|
||||
"SELECT sampleformat, summin, summax, sumrms, summary256, summary64k, samples FROM sampleblocks WHERE blockid = ?1;");
|
||||
|
||||
query.bind(1, block.getBlockId());
|
||||
|
||||
int rowsCount = 0;
|
||||
|
||||
while (query.executeStep())
|
||||
{
|
||||
const int format = query.getColumn(0).getInt();
|
||||
|
||||
if (block.getParent()->getFormat() != format)
|
||||
throw std::runtime_error(fmt::format("Format mismatch for block {}\n", block.getBlockId()));
|
||||
|
||||
++rowsCount;
|
||||
}
|
||||
|
||||
if (rowsCount == 0)
|
||||
throw std::runtime_error("Block not found");
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
if (auto result = validateBlock(block); result != BlockValidationResult::Ok)
|
||||
{
|
||||
missingBlocks.emplace(block.getBlockId());
|
||||
fmt::print("Invalid block {}: {}\n", block.getBlockId(), ex.what());
|
||||
}
|
||||
|
||||
if (result == BlockValidationResult::Invalid)
|
||||
fmt::print("Wrong sample formate for block: {}\n", block.getBlockId());
|
||||
else if (result == BlockValidationResult::Missing)
|
||||
fmt::print("Missing block: {}\n", block.getBlockId());
|
||||
}
|
||||
}
|
||||
|
||||
return missingBlocks;
|
||||
}
|
||||
|
||||
std::set<int64_t> AudacityProject::fixupMissingBlocks()
|
||||
std::set<int64_t> AudacityProject::recoverProject()
|
||||
{
|
||||
auto missingBlocks = validateBlocks();
|
||||
|
||||
for (auto& block : mWaveBlocks)
|
||||
for (auto& sequence : mSequences)
|
||||
{
|
||||
int nextBlockStart = 0;
|
||||
|
||||
bool firstBlock = true;
|
||||
|
||||
for (auto blockPtr : sequence)
|
||||
{
|
||||
if (firstBlock)
|
||||
{
|
||||
firstBlock = false;
|
||||
|
||||
if (blockPtr->getStart() != 0)
|
||||
{
|
||||
fmt::print("Invalid block id for first block in sequence: {}\n", blockPtr->getBlockId());
|
||||
blockPtr->setBlockId(0);
|
||||
}
|
||||
|
||||
nextBlockStart += missingBlocks.count(blockPtr->getBlockId()) ?
|
||||
blockPtr->getLength() :
|
||||
getRealBlockLength(*blockPtr);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (missingBlocks.count(blockPtr->getBlockId()))
|
||||
{
|
||||
// We can only hope that the block start is correct
|
||||
nextBlockStart += blockPtr->getLength();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (blockPtr->getStart() != nextBlockStart)
|
||||
{
|
||||
fmt::print("Invalid block start for block: {}\n", blockPtr->getBlockId());
|
||||
blockPtr->setStart(nextBlockStart);
|
||||
}
|
||||
|
||||
nextBlockStart += getRealBlockLength(*blockPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto blocksCount = mWaveBlocks.size();
|
||||
|
||||
for (size_t index = 0; index < blocksCount; ++index)
|
||||
{
|
||||
auto& block = mWaveBlocks[index];
|
||||
|
||||
if (missingBlocks.count(block.getBlockId()) == 0)
|
||||
continue;
|
||||
|
||||
if (index > 0 && (index + 1) < blocksCount)
|
||||
{
|
||||
auto& prevBlock = mWaveBlocks[index - 1];
|
||||
auto& nextBlock = mWaveBlocks[index + 1];
|
||||
|
||||
if (prevBlock.getParent () == block.getParent () &&
|
||||
nextBlock.getParent () == block.getParent ())
|
||||
{
|
||||
const auto prevBlockId = prevBlock.getBlockId();
|
||||
const auto nextBlockId = nextBlock.getBlockId();
|
||||
|
||||
const auto diff = nextBlockId - prevBlockId;
|
||||
|
||||
if (prevBlockId > 0 && nextBlockId > 0 &&
|
||||
(diff == 2 || diff == 4))
|
||||
{
|
||||
const auto potentialBlockId =
|
||||
(prevBlockId + nextBlockId) / 2;
|
||||
|
||||
if (containsBlock(potentialBlockId))
|
||||
{
|
||||
fmt::print("Recovered block id: {} -> {}\n", block.getBlockId(), potentialBlockId);
|
||||
block.setBlockId(potentialBlockId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt::print("Converting block to silence: start {}\n", block.getStart());
|
||||
block.convertToSilence();
|
||||
}
|
||||
|
||||
@@ -354,15 +483,22 @@ void AudacityProject::removeUnusedBlocks()
|
||||
|
||||
readBlocksList.reset();
|
||||
|
||||
std::set<int64_t> orphanedBlocks;
|
||||
std::set<int64_t> usedBlocks;
|
||||
|
||||
for (const auto block : mWaveBlocks)
|
||||
for (const auto& block : mWaveBlocks)
|
||||
{
|
||||
if (block.isSilence())
|
||||
continue;
|
||||
|
||||
if (availableBlocks.count(block.getBlockId()) == 0)
|
||||
orphanedBlocks.emplace(block.getBlockId());
|
||||
usedBlocks.emplace(block.getBlockId());
|
||||
}
|
||||
|
||||
std::set<int64_t> orphanedBlocks;
|
||||
|
||||
for (auto blockId : availableBlocks)
|
||||
{
|
||||
if (usedBlocks.count(blockId) == 0)
|
||||
orphanedBlocks.emplace(blockId);
|
||||
}
|
||||
|
||||
mDb.reopenReadonlyAsWritable();
|
||||
|
||||
@@ -54,6 +54,9 @@ public:
|
||||
bool isSilence() const noexcept;
|
||||
void convertToSilence() noexcept;
|
||||
|
||||
void setBlockId(int64_t blockId) noexcept;
|
||||
void setStart(int64_t start) noexcept;
|
||||
|
||||
int64_t getBlockId() const noexcept;
|
||||
int64_t getStart() const noexcept;
|
||||
int64_t getLength() const noexcept;
|
||||
@@ -167,9 +170,22 @@ public:
|
||||
AudacityProject(AudacityDatabase& db);
|
||||
~AudacityProject();
|
||||
|
||||
bool containsBlock(int64_t blockId) const;
|
||||
|
||||
enum class BlockValidationResult
|
||||
{
|
||||
Ok,
|
||||
Missing,
|
||||
Invalid
|
||||
};
|
||||
|
||||
int getRealBlockLength(const WaveBlock& block) const;
|
||||
|
||||
BlockValidationResult validateBlock(const WaveBlock& block) const;
|
||||
|
||||
std::set<int64_t> validateBlocks() const;
|
||||
|
||||
std::set<int64_t> fixupMissingBlocks();
|
||||
std::set<int64_t> recoverProject();
|
||||
|
||||
void saveProject();
|
||||
|
||||
|
||||
@@ -34,3 +34,18 @@ uint32_t BytesPerSample(SampleFormat format)
|
||||
throw std::runtime_error(fmt::format("Unsupported format {}", format));
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t DiskBytesPerSample(SampleFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case SampleFormat::Int16:
|
||||
return 2;
|
||||
case SampleFormat::Int24:
|
||||
return 4;
|
||||
case SampleFormat::Float32:
|
||||
return 4;
|
||||
default:
|
||||
throw std::runtime_error(fmt::format("Unsupported format {}", format));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,3 +18,4 @@ enum class SampleFormat
|
||||
SampleFormat SampleFormatFromString(std::string_view format);
|
||||
|
||||
uint32_t BytesPerSample(SampleFormat format);
|
||||
uint32_t DiskBytesPerSample(SampleFormat format);
|
||||
|
||||
@@ -55,7 +55,15 @@ void GetAttributeValue(const AttributeValue& attr, Ret& result)
|
||||
{
|
||||
if constexpr (std::is_same_v<Ret, bool>)
|
||||
{
|
||||
result = arg == "true" || arg == "0";
|
||||
result = arg == "true" || arg == "1";
|
||||
}
|
||||
else if constexpr (std::is_floating_point_v<Ret>)
|
||||
{
|
||||
std::string tmp(arg);
|
||||
if constexpr (std::is_same_v<Ret, float>)
|
||||
result = std::stof(tmp);
|
||||
else
|
||||
result = static_cast<Ret>(std::stod(tmp));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -50,8 +50,6 @@ DEFINE_string(
|
||||
"float",
|
||||
"Sample format for the extracted samples (-extract_sample_blocks, -extract_as_mono_track, -extract_as_stereo_track). Possible values are: int16, int24, float");
|
||||
|
||||
constexpr int64_t AudacityProjectID = 1096107097;
|
||||
constexpr int64_t MaxSupportedVersion = (3 << 24) + (1 << 16) + (3 << 8) + 0; // 3.1.3.0
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -183,7 +181,7 @@ int main(int argc, char **argv)
|
||||
if (project == nullptr)
|
||||
project = std::make_unique<AudacityProject>(projectDatabase);
|
||||
|
||||
project->fixupMissingBlocks();
|
||||
project->recoverProject();
|
||||
}
|
||||
|
||||
if (FLAGS_compact)
|
||||
|
||||
Reference in New Issue
Block a user