diff --git a/CMakeLists.txt b/CMakeLists.txt index 5bb4adc4aa8..ab66888beff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -530,14 +530,14 @@ ENDIF(WANT_SF2) # check for libgig If(WANT_GIG) - PKG_CHECK_MODULES(GIG gig) + find_package(Gig) IF(GIG_FOUND) SET(LMMS_HAVE_GIG TRUE) SET(STATUS_GIG "OK") - ELSE(GIG_FOUND) + ELSE() SET(STATUS_GIG "not found, libgig needed for decoding .gig files") - ENDIF(GIG_FOUND) -ENDIF(WANT_GIG) + ENDIF() +ENDIF() # check for pthreads IF(LMMS_BUILD_LINUX OR LMMS_BUILD_APPLE OR LMMS_BUILD_OPENBSD OR LMMS_BUILD_FREEBSD OR LMMS_BUILD_HAIKU) diff --git a/cmake/modules/FindGig.cmake b/cmake/modules/FindGig.cmake new file mode 100644 index 00000000000..aeb5290a9b0 --- /dev/null +++ b/cmake/modules/FindGig.cmake @@ -0,0 +1,32 @@ +# Copyright (c) 2024 Kevin Zander +# Based on FindPortAudio.cmake, copyright (c) 2023 Dominic Clark +# +# Redistribution and use is allowed according to the terms of the New BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +include(ImportedTargetHelpers) + +find_package_config_mode_with_fallback(gig libgig::libgig + LIBRARY_NAMES "gig" + INCLUDE_NAMES "libgig/gig.h" + PKG_CONFIG gig + PREFIX Gig +) + +determine_version_from_source(Gig_VERSION libgig::libgig [[ + #include + #include + + auto main() -> int + { + const auto version = gig::libraryVersion(); + std::cout << version; + } +]]) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args(Gig + REQUIRED_VARS Gig_LIBRARY Gig_INCLUDE_DIRS + VERSION_VAR Gig_VERSION +) diff --git a/plugins/GigPlayer/CMakeLists.txt b/plugins/GigPlayer/CMakeLists.txt index ccec8c87336..87809293e90 100644 --- a/plugins/GigPlayer/CMakeLists.txt +++ b/plugins/GigPlayer/CMakeLists.txt @@ -3,9 +3,28 @@ if(LMMS_HAVE_GIG) include_directories(SYSTEM ${GIG_INCLUDE_DIRS}) SET(CMAKE_AUTOUIC ON) + # Version checking logic disabled for msvc as vcpkg guarantees gig > 4.3 + if(NOT MSVC) + string(REPLACE "." ";" GIG_VERSION_LIST "${Gig_VERSION}") + list(LENGTH GIG_VERSION_LIST GIG_VERSION_LIST_LENGTH) + list(GET GIG_VERSION_LIST 0 GIG_VERSION_MAJOR) + list(GET GIG_VERSION_LIST 1 GIG_VERSION_MINOR) + list(GET GIG_VERSION_LIST 2 GIG_VERSION_PATCH) + if(GIG_VERSION_LIST_LENGTH GREATER 3) + list(GET GIG_VERSION_LIST 3 GIG_VERSION_REVISION) + endif() + configure_file("lmms_gig.h.in" "${CMAKE_BINARY_DIR}/lmms_gig.h") + endif() + + # FIXME: Make sure we build globally with exceptions enabled and we can remove this # Required for not crashing loading files with libgig add_compile_options("-fexceptions") + # disable deprecated check for mingw-x-libgig + if(LMMS_BUILD_WIN32 AND NOT MSVC) + add_compile_options("-Wno-deprecated") + endif() + link_directories(${GIG_LIBRARY_DIRS}) link_libraries(${GIG_LIBRARIES}) build_plugin(gigplayer @@ -13,5 +32,5 @@ if(LMMS_HAVE_GIG) MOCFILES GigPlayer.h PatchesDialog.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" ) - target_link_libraries(gigplayer SampleRate::samplerate) + target_link_libraries(gigplayer SampleRate::samplerate libgig::libgig) endif(LMMS_HAVE_GIG) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index b72e30b3335..0b7926de6f5 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -55,6 +55,8 @@ #include "embed.h" #include "plugin_export.h" +#include + namespace lmms { @@ -322,6 +324,10 @@ void GigInstrument::playNote( NotePlayHandle * _n, SampleFrame* ) // the preferences) void GigInstrument::play( SampleFrame* _working_buffer ) { + // These must remain thread_local or will cause collisions/bad data with play buffers + thread_local static std::vector sampleData(32768); + thread_local static std::vector convertBuf(32768); + const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); const auto rate = Engine::audioEngine()->outputSampleRate(); @@ -333,89 +339,73 @@ void GigInstrument::play( SampleFrame* _working_buffer ) if( m_instance == nullptr || m_instrument == nullptr ) { - m_synthMutex.unlock(); m_notesMutex.unlock(); + m_synthMutex.unlock(); return; } - for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) - { - // Process notes in the KeyUp state, adding release samples if desired - if( it->state == GigState::KeyUp ) - { - // If there are no samples, we're done - if( it->samples.empty() ) + // TODO: C++20 std::erase_if + // Process and remove if complete + m_notes.erase(std::remove_if(m_notes.begin(), m_notes.end(), + [&](GigNote& gigNote) { + // Process notes in the KeyUp state, adding release samples if desired + if (gigNote.state == GigState::KeyUp) { - it->state = GigState::Completed; - } - else - { - it->state = GigState::PlayingKeyUp; - - // Notify each sample that the key has been released - for (auto& sample : it->samples) + // If there are no samples, we're done + if (gigNote.samples.empty()) { gigNote.state = GigState::Completed; } + else { - sample.adsr.keyup(); - } + gigNote.state = GigState::PlayingKeyUp; - // Add release samples if available - if( it->release == true ) - { - addSamples( *it, true ); + // Notify each sample that the key has been released + for (auto& sample : gigNote.samples) { sample.adsr.keyup(); } + + // Add release samples if available + if (gigNote.release) { addSamples(gigNote, true); } } } - } - // Process notes in the KeyDown state, adding samples for the notes - else if( it->state == GigState::KeyDown ) - { - it->state = GigState::PlayingKeyDown; - addSamples( *it, false ); - } - - // Delete ended samples - for( QList::iterator sample = it->samples.begin(); - sample != it->samples.end(); ++sample ) - { - // Delete if the ADSR for a sample is complete for normal - // notes, or if a release sample, then if we've reached - // the end of the sample - if( sample->sample == nullptr || sample->adsr.done() || - ( it->isRelease == true && - sample->pos >= sample->sample->SamplesTotal - 1 ) ) + // Process notes in the KeyDown state, adding samples for the notes + else if (gigNote.state == GigState::KeyDown) { - sample = it->samples.erase( sample ); - - if( sample == it->samples.end() ) - { - break; - } + gigNote.state = GigState::PlayingKeyDown; + addSamples(gigNote, false); } - } - // Delete ended notes (either in the completed state or all the samples ended) - if( it->state == GigState::Completed || it->samples.empty() ) + // TODO: C++20 std::erase_if + // Delete ended samples + gigNote.samples.erase(std::remove_if(gigNote.samples.begin(), gigNote.samples.end(), + [&](GigSample& sample) { + if (sample.adsr.done()) { return true; } + if (sample.sample == nullptr) { return true; } + if (gigNote.isRelease && sample.pos >= (sample.sample->SamplesTotal - 1)) + { + return true; + } + return false; + }), + gigNote.samples.end() + ); + + // Delete ended notes (either in the completed state or all the samples ended) + return gigNote.state == GigState::Completed || gigNote.samples.empty(); + }), + m_notes.end() + ); + + auto updateWorkingBufferByData = [&](const auto& data) { + for (f_cnt_t i = 0; i < frames; ++i) { - it = m_notes.erase( it ); - - if( it == m_notes.end() ) - { - break; - } + _working_buffer[i][0] += data[i][0]; + _working_buffer[i][1] += data[i][1]; } - } + }; // Fill buffer with portions of the note samples - for (auto& note : m_notes) - { - // Only process the notes if we're in a playing state - if (!(note.state == GigState::PlayingKeyDown || note.state == GigState::PlayingKeyUp )) - { - continue; - } + std::for_each(m_notes.begin(), m_notes.end(), [&](GigNote& note) { + if (note.state != GigState::PlayingKeyDown && note.state != GigState::PlayingKeyUp) { return; } - for (auto& sample : note.samples) - { - if (sample.sample == nullptr || sample.region == nullptr) { continue; } + std::for_each(note.samples.begin(), note.samples.end(), [&](GigSample& sample) { + if (sample.sample == nullptr || sample.region == nullptr) { return; } // Will change if resampling bool resample = false; @@ -440,50 +430,36 @@ void GigInstrument::play( SampleFrame* _working_buffer ) samples = frames / freq_factor + Sample::s_interpolationMargins[m_interpolation]; } + const auto neededCapacity = static_cast::size_type>(samples); + sampleData.resize(std::max(neededCapacity, sampleData.capacity())); + convertBuf.resize(std::max(neededCapacity, convertBuf.capacity())); + // Load this note's data - SampleFrame sampleData[samples]; loadSample(sample, sampleData, samples); // Apply ADSR using a copy so if we don't use these samples when // resampling, the ADSR doesn't get messed up ADSR copy = sample.adsr; - - for( f_cnt_t i = 0; i < samples; ++i ) - { - float amplitude = copy.value(); - sampleData[i][0] *= amplitude; - sampleData[i][1] *= amplitude; - } + const auto amplitude = copy.value(); + std::for_each_n(sampleData.begin(), samples, [&](auto& frame) { + frame[0] *= amplitude; + frame[1] *= amplitude; + }); // Output the data resampling if needed - if( resample == true ) - { - SampleFrame convertBuf[frames]; + // Only output if resampling is successful (note that "used" is output) + if (resample && sample.convertSampleRate(sampleData, convertBuf, samples, frames, freq_factor, used)) - // Only output if resampling is successful (note that "used" is output) - if (sample.convertSampleRate(*sampleData, *convertBuf, samples, frames, freq_factor, used)) - { - for( f_cnt_t i = 0; i < frames; ++i ) - { - _working_buffer[i][0] += convertBuf[i][0]; - _working_buffer[i][1] += convertBuf[i][1]; - } - } - } - else { - for( f_cnt_t i = 0; i < frames; ++i ) - { - _working_buffer[i][0] += sampleData[i][0]; - _working_buffer[i][1] += sampleData[i][1]; - } + updateWorkingBufferByData(convertBuf); } + else { updateWorkingBufferByData(sampleData); } // Update note position with how many samples we actually used sample.pos += used; sample.adsr.inc(used); - } - } + }); + }); m_notesMutex.unlock(); m_synthMutex.unlock(); @@ -499,25 +475,24 @@ void GigInstrument::play( SampleFrame* _working_buffer ) -void GigInstrument::loadSample( GigSample& sample, SampleFrame* sampleData, f_cnt_t samples ) +void GigInstrument::loadSample(GigSample& sample, std::vector& sampleData, gig::file_offset_t samples) { - if( sampleData == nullptr || samples < 1 ) - { - return; - } + // This must remain thread_local or will cause collisions/bad data with play buffers + thread_local static std::vector buffer(32768); + if (samples < 1) { return; } // Determine if we need to loop part of this sample bool loop = false; gig::loop_type_t loopType = gig::loop_type_normal; - f_cnt_t loopStart = 0; - f_cnt_t loopLength = 0; + gig::file_offset_t loopStart = 0; + gig::file_offset_t loopLength = 0; - if( sample.region->pSampleLoops != nullptr ) + if (sample.region->pSampleLoops != nullptr) { - for( uint32_t i = 0; i < sample.region->SampleLoops; ++i ) + for (uint32_t i = 0; i < sample.region->SampleLoops; ++i) { loop = true; - loopType = static_cast( sample.region->pSampleLoops[i].LoopType ); + loopType = static_cast(sample.region->pSampleLoops[i].LoopType); loopStart = sample.region->pSampleLoops[i].LoopStart; loopLength = sample.region->pSampleLoops[i].LoopLength; @@ -526,107 +501,106 @@ void GigInstrument::loadSample( GigSample& sample, SampleFrame* sampleData, f_cn } } - unsigned long allocationsize = samples * sample.sample->FrameSize; - int8_t buffer[allocationsize]; + const auto allocationSize = samples * sample.sample->FrameSize; + const auto neededCapacity = static_cast::size_type>(allocationSize); + buffer.resize(std::max(neededCapacity, buffer.capacity())); // Load the sample in different ways depending on if we're looping or not - if( loop == true && ( sample.pos >= loopStart || sample.pos + samples > loopStart ) ) + if (loop && (sample.pos >= loopStart || sample.pos + samples > loopStart)) { // Calculate the new position based on the type of loop - if( loopType == gig::loop_type_bidirectional ) + if (loopType == gig::loop_type_bidirectional) { - sample.pos = getPingPongIndex( sample.pos, loopStart, loopStart + loopLength ); + const auto loopPos = (sample.pos - loopStart + loopLength) % (loopLength * 2); + sample.pos = (sample.pos < loopStart + loopLength) ? sample.pos + : (loopStart + ((loopPos < loopLength) ? loopLength - loopPos : loopPos - loopLength)); } else { - sample.pos = getLoopedIndex( sample.pos, loopStart, loopStart + loopLength ); + sample.pos = (sample.pos < loopStart + loopLength) ? sample.pos + : loopStart + (sample.pos - loopStart) % loopLength; // TODO: also implement loop_type_backward support } - sample.sample->SetPos( sample.pos ); + sample.sample->SetPos(sample.pos); // Load the samples (based on gig::Sample::ReadAndLoop) even around the end // of a loop boundary wrapping to the beginning of the loop region - long samplestoread = samples; - long samplestoloopend = 0; - long readsamples = 0; - long totalreadsamples = 0; - long loopEnd = loopStart + loopLength; + gig::file_offset_t samplesToRead = samples; + gig::file_offset_t samplesToLoopEnd = 0; + gig::file_offset_t readSamples = 0; + gig::file_offset_t totalReadSamples = 0; + gig::file_offset_t loopEnd = loopStart + loopLength; do { - samplestoloopend = loopEnd - sample.sample->GetPos(); - readsamples = sample.sample->Read( &buffer[totalreadsamples * sample.sample->FrameSize], - std::min( samplestoread, samplestoloopend ) ); - samplestoread -= readsamples; - totalreadsamples += readsamples; - - if( readsamples >= samplestoloopend ) - { - sample.sample->SetPos( loopStart ); - } + samplesToLoopEnd = loopEnd - sample.sample->GetPos(); + readSamples = sample.sample->Read( + &buffer[totalReadSamples * sample.sample->FrameSize], + std::min(samplesToRead, samplesToLoopEnd) + ); + samplesToRead -= readSamples; + totalReadSamples += readSamples; + + if (readSamples >= samplesToLoopEnd) { sample.sample->SetPos(loopStart); } } - while( samplestoread > 0 && readsamples > 0 ); + while (samplesToRead > 0 && readSamples > 0); } else { - sample.sample->SetPos( sample.pos ); + sample.sample->SetPos(sample.pos); - unsigned long size = sample.sample->Read( &buffer, samples ) * sample.sample->FrameSize; - std::memset( (int8_t*) &buffer + size, 0, allocationsize - size ); + auto size = sample.sample->Read(&buffer[0], samples) * sample.sample->FrameSize; + std::fill_n(buffer.begin(), allocationSize - size, 0); } // Convert from 16 or 24 bit into 32-bit float - if( sample.sample->BitDepth == 24 ) // 24 bit + if (sample.sample->BitDepth == 24) // 24 bit { - auto pInt = reinterpret_cast(&buffer); - - for( f_cnt_t i = 0; i < samples; ++i ) + auto pInt = reinterpret_cast(&buffer[0]); + const auto base_offset = 3 * sample.sample->Channels; + const auto factor = 1.0 / 0x100000000 * sample.attenuation; + for (f_cnt_t i = 0; i < samples; ++i) { // libgig gives 24-bit data as little endian, so we must // convert if on a big endian system int32_t valueLeft = swap32IfBE( - ( pInt[ 3 * sample.sample->Channels * i ] << 8 ) | - ( pInt[ 3 * sample.sample->Channels * i + 1 ] << 16 ) | - ( pInt[ 3 * sample.sample->Channels * i + 2 ] << 24 ) ); + (pInt[base_offset * i] << 8) + | (pInt[base_offset * i + 1] << 16) + | (pInt[base_offset * i + 2] << 24) + ); // Store the notes to this buffer before saving to output // so we can fade them out as needed - sampleData[i][0] = 1.0 / 0x100000000 * sample.attenuation * valueLeft; + sampleData[i][0] = factor * valueLeft; - if( sample.sample->Channels == 1 ) + if (sample.sample->Channels == 1) { sampleData[i][1] = sampleData[i][0]; } else { int32_t valueRight = swap32IfBE( - ( pInt[ 3 * sample.sample->Channels * i + 3 ] << 8 ) | - ( pInt[ 3 * sample.sample->Channels * i + 4 ] << 16 ) | - ( pInt[ 3 * sample.sample->Channels * i + 5 ] << 24 ) ); - - sampleData[i][1] = 1.0 / 0x100000000 * sample.attenuation * valueRight; + (pInt[base_offset * i + 3] << 8) + | (pInt[base_offset * i + 4] << 16) + | (pInt[base_offset * i + 5] << 24) + ); + sampleData[i][1] = factor * valueRight; } } } else // 16 bit { - auto pInt = reinterpret_cast(&buffer); - - for( f_cnt_t i = 0; i < samples; ++i ) + auto pInt = reinterpret_cast(&buffer[0]); + const auto factor = 1.0 / 0x10000 * sample.attenuation; + for (f_cnt_t i = 0; i < samples; ++i) { - sampleData[i][0] = 1.0 / 0x10000 * - pInt[ sample.sample->Channels * i ] * sample.attenuation; - - if( sample.sample->Channels == 1 ) - { - sampleData[i][1] = sampleData[i][0]; - } - else - { - sampleData[i][1] = 1.0 / 0x10000 * - pInt[ sample.sample->Channels * i + 1 ] * sample.attenuation; - } + auto l = pInt[sample.sample->Channels * i]; + auto r = pInt[sample.sample->Channels * i + 1]; + sampleData[i][0] = factor * l; + sampleData[i][1] = sample.sample->Channels == 1 + ? sampleData[i][0] + : factor * r; } } } @@ -634,39 +608,6 @@ void GigInstrument::loadSample( GigSample& sample, SampleFrame* sampleData, f_cn -// These two loop index functions taken from SampleBuffer.cpp -f_cnt_t GigInstrument::getLoopedIndex( f_cnt_t index, f_cnt_t startf, f_cnt_t endf ) const -{ - if( index < endf ) - { - return index; - } - - return startf + ( index - startf ) - % ( endf - startf ); -} - - - - -f_cnt_t GigInstrument::getPingPongIndex( f_cnt_t index, f_cnt_t startf, f_cnt_t endf ) const -{ - if( index < endf ) - { - return index; - } - - const f_cnt_t looplen = endf - startf; - const f_cnt_t looppos = ( index - endf ) % ( looplen * 2 ); - - return ( looppos < looplen ) - ? endf - looppos - : startf + ( looppos - looplen ); -} - - - - // A key has been released void GigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { @@ -1182,7 +1123,7 @@ void GigSample::updateSampleRate() -bool GigSample::convertSampleRate( SampleFrame & oldBuf, SampleFrame & newBuf, +bool GigSample::convertSampleRate(std::vector& oldBuf, std::vector& newBuf, f_cnt_t oldSize, f_cnt_t newSize, float freq_factor, f_cnt_t& used ) { if( srcState == nullptr ) @@ -1190,13 +1131,16 @@ bool GigSample::convertSampleRate( SampleFrame & oldBuf, SampleFrame & newBuf, return false; } - SRC_DATA src_data; - src_data.data_in = &oldBuf[0]; - src_data.data_out = &newBuf[0]; - src_data.input_frames = oldSize; - src_data.output_frames = newSize; - src_data.src_ratio = freq_factor; - src_data.end_of_input = 0; + SRC_DATA src_data{ + /* .data_in */ reinterpret_cast(oldBuf.data()), + /* .data_out */ reinterpret_cast(newBuf.data()), + /* .input_frames */ static_cast(oldSize), + /* .output_frames */ static_cast(newSize), + /* .input_frames_used */ 0, + /* .output_frames_gen */ 0, + /* .end_of_input */ 0, + /* .src_ratio */ freq_factor + }; // We don't need to lock this assuming that we're only outputting the // samples in one thread diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 685c7f5469a..0463658367f 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -38,9 +38,14 @@ #include "Knob.h" #include "LcdSpinBox.h" #include "LedCheckBox.h" -#include "gig.h" +#include +#include +#include +#ifndef _MSC_VER +#include "lmms_gig.h" // version specific typedef disabled on msvc +#endif class QLabel; @@ -157,7 +162,7 @@ class GigSample // Needed since libsamplerate stores data internally between calls void updateSampleRate(); - bool convertSampleRate( SampleFrame & oldBuf, SampleFrame & newBuf, + bool convertSampleRate(std::vector& oldBuf, std::vector& newBuf, f_cnt_t oldSize, f_cnt_t newSize, float freq_factor, f_cnt_t& used ); gig::Sample * sample; @@ -166,12 +171,12 @@ class GigSample ADSR adsr; // The position in sample - f_cnt_t pos; + gig::file_offset_t pos; // Whether to change the pitch of the samples, e.g. if there's only one // sample per octave and you want that sample pitch shifted for the rest of // the notes in the octave, this will be true - bool pitchtrack; + //bool pitchtrack; // Used to convert sample rates int interpolation; @@ -213,7 +218,7 @@ class GigNote bool isRelease; // Whether this is a release sample, changes when we delete it GigState state; float frequency; - QList samples; + std::list samples; // Used to determine which note should be released on key up // @@ -294,7 +299,7 @@ public slots: int m_interpolation; // List of all the currently playing notes - QList m_notes; + std::list m_notes; // Used when determining which samples to use uint32_t m_RandomSeed; @@ -312,9 +317,7 @@ public slots: Dimension getDimensions( gig::Region * pRegion, int velocity, bool release ); // Load sample data from the Gig file, looping the sample where needed - void loadSample( GigSample& sample, SampleFrame* sampleData, f_cnt_t samples ); - f_cnt_t getLoopedIndex( f_cnt_t index, f_cnt_t startf, f_cnt_t endf ) const; - f_cnt_t getPingPongIndex( f_cnt_t index, f_cnt_t startf, f_cnt_t endf ) const; + void loadSample(GigSample& sample, std::vector& sampleData, gig::file_offset_t samples); // Add the desired samples to the note, either normal samples or release // samples diff --git a/plugins/GigPlayer/lmms_gig.h.in b/plugins/GigPlayer/lmms_gig.h.in new file mode 100644 index 00000000000..44061ba6c4d --- /dev/null +++ b/plugins/GigPlayer/lmms_gig.h.in @@ -0,0 +1,12 @@ +// todo: remove if gig > 4.1 is guaranteed everywhere + +#define GIG_VERSION_MAJOR @GIG_VERSION_MAJOR@ +#define GIG_VERSION_MINOR @GIG_VERSION_MINOR@ +#define GIG_VERSION_PATCH @GIG_VERSION_PATCH@ +#cmakedefine GIG_VERSION_REVISION @GIG_VERSION_REVISION@ + +#if GIG_VERSION_MAJOR < 4 || (GIG_VERSION_MAJOR == 4 && GIG_VERSION_MINOR == 0) +namespace gig { + typedef unsigned long file_offset_t; +} +#endif diff --git a/vcpkg.json b/vcpkg.json index 1f64409344f..eba399f5685 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -21,6 +21,10 @@ "sndfile" ] }, + { + "name": "libgig", + "default-features": false + }, { "name": "libogg", "default-features": false