diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..5b1982f --- /dev/null +++ b/.clang-format @@ -0,0 +1,88 @@ +# Generated from CLion C/C++ Code Style settings +BasedOnStyle: LLVM +Cpp11BracedListStyle: true +AccessModifierOffset: -4 +AlignConsecutiveMacros: true +AlignTrailingComments: false +AlignAfterOpenBracket: Align +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AlignArrayOfStructures: Left +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: None +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Custom +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterExternBlock: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true +# BraceBreakingStyle: Attach +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +ColumnLimit: 0 +CommentPragmas: '^[^ ]' +CompactNamespaces: false +ContinuationIndentWidth: 8 +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +InsertTrailingCommas: Wrapped +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Right +ReflowComments: false +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never \ No newline at end of file diff --git a/.github/workflows/c-codestyle.yml b/.github/workflows/c-codestyle.yml new file mode 100644 index 0000000..9c2f474 --- /dev/null +++ b/.github/workflows/c-codestyle.yml @@ -0,0 +1,32 @@ +name: C Codestyle + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - '**/*.c' + - '.github/workflows/c-codestyle.yml' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + paths: + - '**/*.c' + - '.github/workflows/c-codestyle.yml' + +jobs: + check-c-codestyle: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: sudo apt install clang-format + + - name: Check c codestyle + run: python3 resources/.lint/c/formatter.py -c -v diff --git a/.github/workflows/c-demo.yml b/.github/workflows/c-demo.yml new file mode 100644 index 0000000..e3320e0 --- /dev/null +++ b/.github/workflows/c-demo.yml @@ -0,0 +1,81 @@ +name: C Demo + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'demo/c/**' + - '!demo/c/README.md' + - '.github/workflows/c-demo.yml' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + paths: + - 'demo/c/**' + - '!demo/c/README.md' + - '.github/workflows/c-demo.yml' + +defaults: + run: + working-directory: demo/c + shell: bash + +jobs: + build-github-hosted: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + pv_speaker_platform: linux + - os: windows-latest + pv_speaker_platform: windows + - os: macos-latest + pv_speaker_platform: mac-arm64 + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Create build directory + run: cmake -B ./build -S. -DPV_SPEAKER_PLATFORM="${{ matrix.pv_speaker_platform }}" + + - name: Build demo + run: cmake --build ./build + + - name: Run demo + run: ./build/pv_speaker_demo --show_audio_devices + + build-self-hosted: + runs-on: ${{ matrix.machine }} + strategy: + matrix: + machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, rpi5-64] + include: + - machine: rpi3-32 + pv_speaker_platform: raspberry-pi3 + - machine: rpi3-64 + pv_speaker_platform: raspberry-pi3-64 + - machine: rpi4-32 + pv_speaker_platform: raspberry-pi4 + - machine: rpi4-64 + pv_speaker_platform: raspberry-pi4-64 + - machine: rpi5-64 + pv_speaker_platform: raspberry-pi5-64 + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Create build directory + run: cmake -B ./build -S . -DPV_SPEAKER_PLATFORM="${{ matrix.pv_speaker_platform }}" + + - name: Build demo + run: cmake --build ./build + + - name: Run demo + run: ./build/pv_speaker_demo --show_audio_devices diff --git a/.github/workflows/c.yml b/.github/workflows/c.yml new file mode 100644 index 0000000..9a000b0 --- /dev/null +++ b/.github/workflows/c.yml @@ -0,0 +1,196 @@ +name: C + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'demo/c/**' + - '!demo/c/README.md' + - 'project/**' + - '!project/README.md' + - '.github/workflows/c.yml' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + paths: + - 'demo/c/**' + - '!demo/c/README.md' + - 'project/**' + - '!project/README.md' + - '.github/workflows/c.yml' + +jobs: + build-github-hosted: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-20.04] + include: + - os: ubuntu-20.04 + pv_speaker_platform: linux + output_dir: linux/x86_64 + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Create build directory + run: cmake -B ./build -S . -DPV_SPEAKER_PLATFORM="${{ matrix.pv_speaker_platform }}" -DOUTPUT_DIR="${{ matrix.output_dir }}" + working-directory: project + + - name: Build + run: cmake --build ./build + working-directory: project + + - name: Test + run: ctest --output-on-failure + working-directory: project/build + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.pv_speaker_platform }} + path: | + lib/${{ matrix.output_dir }}/libpv_speaker.* + retention-days: 3 + + build-self-hosted: + runs-on: ${{ matrix.machine }} + strategy: + matrix: + machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, rpi5-32, rpi5-64] + include: + - machine: rpi3-32 + pv_speaker_platform: raspberry-pi3 + output_dir: raspberry-pi/cortex-a53 + - machine: rpi3-64 + pv_speaker_platform: raspberry-pi3-64 + output_dir: raspberry-pi/cortex-a53-aarch64 + - machine: rpi4-32 + pv_speaker_platform: raspberry-pi4 + output_dir: raspberry-pi/cortex-a72 + - machine: rpi4-64 + pv_speaker_platform: raspberry-pi4-64 + output_dir: raspberry-pi/cortex-a72-aarch64 + - machine: rpi5-32 + pv_speaker_platform: raspberry-pi5 + output_dir: raspberry-pi/cortex-a76 + - machine: rpi5-64 + pv_speaker_platform: raspberry-pi5-64 + output_dir: raspberry-pi/cortex-a76-aarch64 + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Create build directory + run: cmake -B ./build -S . -DPV_SPEAKER_PLATFORM="${{ matrix.pv_speaker_platform }}" -DOUTPUT_DIR="${{ matrix.output_dir }}" + working-directory: project + + - name: Build + run: cmake --build ./build + working-directory: project + + - name: Test + run: ctest --output-on-failure + working-directory: project/build + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.pv_speaker_platform }} + path: | + lib/${{ matrix.output_dir }}/libpv_speaker.* + retention-days: 3 + + build-mac: + runs-on: pv-ios + strategy: + matrix: + pv_speaker_platform: [mac-arm64, mac-x86_64] + include: + - pv_speaker_platform: mac-arm64 + output_dir: mac/arm64 + - pv_speaker_platform: mac-x86_64 + output_dir: mac/x86_64 + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Create build directory + run: cmake -B ./build -S . -DPV_SPEAKER_PLATFORM="${{ matrix.pv_speaker_platform }}" -DOUTPUT_DIR="${{ matrix.output_dir }}" + working-directory: project + + - name: Build + run: cmake --build ./build + working-directory: project + + - name: Test + run: ctest --output-on-failure + working-directory: project/build + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.pv_speaker_platform }} + path: | + lib/${{ matrix.output_dir }}/libpv_speaker.* + retention-days: 3 + + build-windows: + runs-on: pv-windows + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Create build directory + run: cmake -B ./build -S . -DPV_SPEAKER_PLATFORM="windows" -DOUTPUT_DIR="windows/amd64" + working-directory: project + shell: pwsh + + - name: Build + run: cmake --build ./build + working-directory: project + shell: pwsh + + - name: Test + run: ctest --output-on-failure + working-directory: project/build + shell: pwsh + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: windows + path: | + lib/windows/amd64/libpv_speaker.* + retention-days: 3 + + collect-artifacts: + runs-on: ubuntu-latest + needs: [build-github-hosted, build-self-hosted, build-mac, build-windows] + + steps: + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + + - name: Make collection directory + run: mkdir -p all-libs/lib + + - name: Collect artifacts + shell: bash + run: | + for DIR in artifacts/*; do cp -a $DIR/* all-libs/lib/; done + + - name: Upload all artifacts + uses: actions/upload-artifact@v3 + with: + name: all-libs + path: all-libs + retention-days: 3 diff --git a/.github/workflows/spell-check.yml b/.github/workflows/spell-check.yml new file mode 100644 index 0000000..8c5eac7 --- /dev/null +++ b/.github/workflows/spell-check.yml @@ -0,0 +1,25 @@ +name: SpellCheck + +on: + workflow_dispatch: + push: + branches: [ main ] + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + +jobs: + markdown: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install CSpell + run: npm install -g cspell + + - name: Run CSpell + run: cspell --config resources/.lint/spell-check/.cspell.json "**/*" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b157b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +build +cmake-build-debug +.DS_Store \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7a355cb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "project/src/miniaudio"] + path = project/src/miniaudio + url = ../../mackron/miniaudio.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..56f59f6 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# PvSpeaker + +Made in Vancouver, Canada by [Picovoice](https://picovoice.ai) + + +[![Twitter URL](https://img.shields.io/twitter/url?label=%40AiPicovoice&style=social&url=https%3A%2F%2Ftwitter.com%2FAiPicovoice)](https://twitter.com/AiPicovoice) + +[![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCAdi9sTCXLosG1XeqDwLx7w?label=YouTube&style=social)](https://www.youtube.com/channel/UCAdi9sTCXLosG1XeqDwLx7w) + +PvSpeaker is an easy-to-use, cross-platform audio player designed for real-time speech audio processing. It allows developers to send raw PCM frames to an audio device's output stream. + +## Table of Contents +- [PvSpeaker](#pvspeaker) + - [Table of Contents](#table-of-contents) + - [Source Code](#source-code) + - [Demos](#demos) + - [C](#c-demo) + +## Source Code + +If you are interested in building PvSpeaker from source or integrating it into an existing C project, the PvSpeaker +source code is located under the [/project](./project) directory. + +## Demos + +If using SSH, clone the repository with: + +```console +git clone --recurse-submodules git@github.com:Picovoice/pvspeaker.git +``` + +If using HTTPS, clone the repository with: + +```console +git clone --recurse-submodules https://github.com/Picovoice/pvspeaker.git +``` + +### C Demo + +Run the following commands to build the demo app: + +```console +cd demo/c +cmake -S . -B build -DPV_SPEAKER_PLATFORM={PV_SPEAKER_PLATFORM} +cmake --build build +``` + +The `{PV_SPEAKER_PLATFORM}` variable will set the compilation flags for the given platform. Exclude this variable +to get a list of possible values. + +Get a list of available audio player devices: +```console +./pv_speaker_demo --show_audio_devices +``` + +Play from a single-channel PCM WAV file with a given audio device index: +```console +./pv_speaker_demo -i test.wav -d 2 +``` + +Hit `Ctrl+C` if you wish to stop audio playback before it completes. If no audio device index (`-d`) is provided, the demo will use the system's default audio player device. + +For more information about the C demo, go to [demo/c](demo/c). \ No newline at end of file diff --git a/demo/c/CMakeLists.txt b/demo/c/CMakeLists.txt new file mode 100644 index 0000000..f2d408a --- /dev/null +++ b/demo/c/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.4) +project(pv_speaker_demo VERSION 1.0.0 DESCRIPTION "Picovoice audio player library demo.") + +if(NOT PV_SPEAKER_PLATFORM) + message(FATAL_ERROR "No `PV_SPEAKER_PLATFORM` value was given. Valid platforms are: \n" + "linux, mac-arm64, mac-x86_64, windows, raspberry-pi3-32, raspberry-pi3-64," + "raspberry-pi4-32, raspberry-pi4-64, raspberry-pi5-32, raspberry-pi5-64") +endif() + +if (${PV_SPEAKER_PLATFORM} STREQUAL "mac-arm64") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/mac/arm64) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "mac-x86_64") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/mac/x86_64) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "linux") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/linux/x86_64) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi3") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/raspberry-pi/cortex-a53) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi3-64") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/raspberry-pi/cortex-a53-aarch64) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi4") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/raspberry-pi/cortex-a72) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi4-64") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/raspberry-pi/cortex-a72-aarch64) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi5") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/raspberry-pi/cortex-a76) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi5-64") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/raspberry-pi/cortex-a76-aarch64) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "windows") + set(PV_SPEAKER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../lib/windows/amd64) +else () + message(FATAL_ERROR "Unknown platform `${PV_SPEAKER_PLATFORM}`.") +endif () + +include_directories(../../project/include) +link_directories(${PV_SPEAKER_LIB_DIR}) +add_executable(pv_speaker_demo pv_speaker_demo.c) +target_link_libraries(pv_speaker_demo pv_speaker) + +add_custom_command(TARGET pv_speaker_demo + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${PV_SPEAKER_LIB_DIR} ${CMAKE_BINARY_DIR} +) \ No newline at end of file diff --git a/demo/c/Preload.CMake b/demo/c/Preload.CMake new file mode 100644 index 0000000..d4b1418 --- /dev/null +++ b/demo/c/Preload.CMake @@ -0,0 +1,5 @@ +if (WIN32) + set(CMAKE_GENERATOR "MinGW Makefiles" CACHE STRING "" FORCE) +else() + set(CMAKE_GENERATOR "Unix Makefiles" CACHE STRING "" FORCE) +endif() \ No newline at end of file diff --git a/demo/c/README.md b/demo/c/README.md new file mode 100644 index 0000000..4ae6557 --- /dev/null +++ b/demo/c/README.md @@ -0,0 +1,52 @@ +# PvSpeaker Demo for C + +This project contains a C command-line demo for PvSpeaker that demonstrates how to use PvSpeaker to play audio from a single-channel PCM WAV file. + +## PvSpeaker + +PvSpeaker is an easy-to-use, cross-platform audio player designed for real-time speech audio processing. It allows developers to send raw PCM frames to an audio device's output stream. + +## Requirements + +- CMake 3.4+. +- C99 compatible compiler. +- **Windows**: MinGW. + +## Compatibility + +- Linux (x86_64) +- macOS (x86_64, arm64) +- Windows (amd64) +- Raspberry Pi (3, 4 and 5) + +## Compiling + +Run the following commands to build the demo app: + +```console +git submodule update --init --recursive +cmake -S . -B build -DPV_SPEAKER_PLATFORM={PV_SPEAKER_PLATFORM} +cmake --build build +``` + +The `{PV_SPEAKER_PLATFORM}` variable will set the compilation flags for the given platform. Exclude this variable +to get a list of possible values. + +## Usage + +To see the usage options for the demo: +```console +./pv_speaker_demo +``` + +Get a list of available audio player devices: +```console +./pv_speaker_demo --show_audio_devices +``` + +Play from a single-channel PCM WAV file with a given audio device index: +```console +./pv_speaker_demo -i test.wav -d 2 +``` + +Hit `Ctrl+C` if you wish to stop audio playback before it completes. If no audio device index (`-d`) is provided, the demo will use the system's default audio player device. diff --git a/demo/c/pv_speaker_demo.c b/demo/c/pv_speaker_demo.c new file mode 100644 index 0000000..ed91fb7 --- /dev/null +++ b/demo/c/pv_speaker_demo.c @@ -0,0 +1,226 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" + file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include + +#include "pv_speaker.h" + +static volatile bool is_interrupted = false; + +void interrupt_handler(int _) { + (void) _; + is_interrupted = true; +} + +static struct option long_options[] = { + {"show_audio_devices", no_argument, NULL, 's'}, + {"input_wav_path", required_argument, NULL, 'i'}, + {"audio_device_index", required_argument, NULL, 'd'} +}; + +static void print_usage(const char *program_name) { + fprintf(stderr, + "Usage : %s -i INPUT_WAV_PATH [-d AUDIO_DEVICE_INDEX]\n" + " %s --show_audio_devices\n", + program_name, + program_name); +} + +static void show_audio_devices(void) { + char **device_list = NULL; + int32_t device_list_length = 0; + + // List devices + pv_speaker_status_t status = pv_speaker_get_available_devices( + &device_list_length, + &device_list); + if (status != PV_SPEAKER_STATUS_SUCCESS) { + fprintf(stderr, "Failed to get audio devices with: %s.\n", pv_speaker_status_to_string(status)); + exit(1); + } + + fprintf(stdout, "Printing devices...\n"); + for (int32_t i = 0; i < device_list_length; i++) { + fprintf(stdout, "index: %d, name: %s\n", i, device_list[i]); + } + + pv_speaker_free_available_devices(device_list_length, device_list); +} + +typedef struct { + uint8_t chunk_id[4]; + uint32_t chunk_size; + uint8_t format[4]; + uint8_t subchunk1_id[4]; + uint32_t subchunk1_size; + uint16_t audio_format; + uint16_t num_channels; + uint32_t sample_rate; + uint32_t byte_rate; + uint16_t block_align; + uint16_t bits_per_sample; + uint8_t subchunk2_id[4]; + uint32_t subchunk2_size; +} wav_header; + +void *read_wav_file(const char *filename, uint32_t *num_samples, uint32_t *sample_rate, uint16_t *bits_per_sample) { + FILE *file = fopen(filename, "rb"); + if (!file) { + perror("Unable to open file"); + return NULL; + } + + wav_header header; + + fread(&header, sizeof(header), 1, file); + + if (header.chunk_id[0] != 'R' || header.chunk_id[1] != 'I' || header.chunk_id[2] != 'F' || header.chunk_id[3] != 'F' || + header.format[0] != 'W' || header.format[1] != 'A' || header.format[2] != 'V' || header.format[3] != 'E') { + fclose(file); + fprintf(stderr, "Invalid WAV file\n"); + return NULL; + } + + if (header.audio_format != 1) { + fclose(file); + fprintf(stderr, "WAV file format must be PCM type\n"); + return NULL; + } + + if (header.num_channels != 1) { + fclose(file); + fprintf(stderr, "WAV file must have a single channel (MONO)\n"); + return NULL; + } + + *sample_rate = header.sample_rate; + + *bits_per_sample = header.bits_per_sample; + + uint32_t bytes_per_sample = header.bits_per_sample / 8; + *num_samples = header.subchunk2_size / bytes_per_sample; + + void *pcm_data = malloc(header.subchunk2_size); + if (!pcm_data) { + perror("Memory allocation failed"); + fclose(file); + return NULL; + } + + fread(pcm_data, header.subchunk2_size, 1, file); + + fclose(file); + + return pcm_data; +} + +int main(int argc, char *argv[]) { + const char *input_wav_path = NULL; + int32_t device_index = -1; + + int c; + while ((c = getopt_long(argc, argv, "si:d:", long_options, NULL)) != -1) { + switch (c) { + case 's': + show_audio_devices(); + return 0; + case 'i': + input_wav_path = optarg; + break; + case 'd': + device_index = (int32_t) strtol(optarg, NULL, 10); + break; + default: + exit(1); + } + } + + if (!input_wav_path) { + print_usage(argv[0]); + exit(1); + } + + signal(SIGINT, interrupt_handler); + fprintf(stdout, "pv_speaker version: %s\n", pv_speaker_version()); + + uint32_t num_samples, sample_rate; + uint16_t bits_per_sample; + void *pcm = read_wav_file(input_wav_path, &num_samples, &sample_rate, &bits_per_sample); + + fprintf(stdout, "Initializing pv_speaker...\n"); + const int32_t frame_length = 512; + pv_speaker_t *speaker = NULL; + pv_speaker_status_t status = pv_speaker_init( + sample_rate, + frame_length, + bits_per_sample, + device_index, + 10, + &speaker); + if (status != PV_SPEAKER_STATUS_SUCCESS) { + fprintf(stderr, "Failed to initialize device with %s.\n", pv_speaker_status_to_string(status)); + exit(1); + } + + pv_speaker_set_debug_logging(speaker, true); + + const char *selected_device = pv_speaker_get_selected_device(speaker); + fprintf(stdout, "Selected device: %s.\n", selected_device); + + status = pv_speaker_start(speaker); + if (status != PV_SPEAKER_STATUS_SUCCESS) { + fprintf(stderr, "Failed to start device with %s.\n", pv_speaker_status_to_string(status)); + exit(1); + } + + fprintf(stdout, "Playing audio...\n"); + if (pcm) { + char *pcmData = (char *) pcm; + for (int i = 0; i < num_samples; i += frame_length) { + bool is_last_frame = i + frame_length >= num_samples; + + status = pv_speaker_write( + speaker, + is_last_frame ? num_samples - i : frame_length, + &pcmData[i * bits_per_sample / 8]); + if (status != PV_SPEAKER_STATUS_SUCCESS) { + fprintf(stderr, "Failed to write with %s.\n", pv_speaker_status_to_string(status)); + exit(1); + } + if (is_interrupted) { + fprintf(stdout, "\nStopped audio...\n"); + break; + } + } + + free(pcm); + } + + status = pv_speaker_stop(speaker); + if (status != PV_SPEAKER_STATUS_SUCCESS) { + fprintf(stderr, "Failed to stop device with %s.\n", pv_speaker_status_to_string(status)); + exit(1); + } + + if (!is_interrupted) { + fprintf(stdout, "Finished playing audio...\n"); + } + + fprintf(stdout, "Deleting pv_speaker...\n"); + pv_speaker_delete(speaker); + + return 0; +} diff --git a/lib/linux/x86_64/libpv_speaker.so b/lib/linux/x86_64/libpv_speaker.so new file mode 100644 index 0000000..c416626 Binary files /dev/null and b/lib/linux/x86_64/libpv_speaker.so differ diff --git a/lib/mac/arm64/libpv_speaker.dylib b/lib/mac/arm64/libpv_speaker.dylib new file mode 100644 index 0000000..798e4db Binary files /dev/null and b/lib/mac/arm64/libpv_speaker.dylib differ diff --git a/lib/mac/x86_64/libpv_speaker.dylib b/lib/mac/x86_64/libpv_speaker.dylib new file mode 100644 index 0000000..0f605c3 Binary files /dev/null and b/lib/mac/x86_64/libpv_speaker.dylib differ diff --git a/lib/raspberry-pi/cortex-a53-aarch64/libpv_speaker.so b/lib/raspberry-pi/cortex-a53-aarch64/libpv_speaker.so new file mode 100644 index 0000000..7bf5f2c Binary files /dev/null and b/lib/raspberry-pi/cortex-a53-aarch64/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a53/libpv_speaker.so b/lib/raspberry-pi/cortex-a53/libpv_speaker.so new file mode 100644 index 0000000..90e064c Binary files /dev/null and b/lib/raspberry-pi/cortex-a53/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a72-aarch64/libpv_speaker.so b/lib/raspberry-pi/cortex-a72-aarch64/libpv_speaker.so new file mode 100644 index 0000000..839746d Binary files /dev/null and b/lib/raspberry-pi/cortex-a72-aarch64/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a72/libpv_speaker.so b/lib/raspberry-pi/cortex-a72/libpv_speaker.so new file mode 100644 index 0000000..eb98c5c Binary files /dev/null and b/lib/raspberry-pi/cortex-a72/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a76-aarch64/libpv_speaker.so b/lib/raspberry-pi/cortex-a76-aarch64/libpv_speaker.so new file mode 100644 index 0000000..d15de7a Binary files /dev/null and b/lib/raspberry-pi/cortex-a76-aarch64/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a76/libpv_speaker.so b/lib/raspberry-pi/cortex-a76/libpv_speaker.so new file mode 100644 index 0000000..55d83f3 Binary files /dev/null and b/lib/raspberry-pi/cortex-a76/libpv_speaker.so differ diff --git a/lib/windows/amd64/libpv_speaker.dll b/lib/windows/amd64/libpv_speaker.dll new file mode 100644 index 0000000..2fb3ee6 Binary files /dev/null and b/lib/windows/amd64/libpv_speaker.dll differ diff --git a/project/CMakeLists.txt b/project/CMakeLists.txt new file mode 100644 index 0000000..db4e873 --- /dev/null +++ b/project/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.4) +project(pv_speaker VERSION 1.0.0 DESCRIPTION "Picovoice audio player library.") + +set(CMAKE_C_STANDARD 99) +set(CMAKE_BUILD_TYPE Release) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden") + +option(PV_BUILD_TESTS "Build and run library tests" ON) + +if(NOT PV_SPEAKER_PLATFORM) + message(FATAL_ERROR "No `PV_SPEAKER_PLATFORM` value was given. Valid platforms are: \n" + "linux, mac-arm64, mac-x86_64, windows, raspberry-pi3-32, raspberry-pi3-64," + "raspberry-pi4-32, raspberry-pi4-64, raspberry-pi5-32, raspberry-pi5-64") +endif() + +if (${PV_SPEAKER_PLATFORM} STREQUAL "mac-arm64") + add_definitions(-D__PV_SPEAKER_PLATFORM_DARWIN__) + set(CMAKE_OSX_ARCHITECTURES "arm64") +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "mac-x86_64") + add_definitions(-D__PV_SPEAKER_PLATFORM_DARWIN__) + set(CMAKE_OSX_ARCHITECTURES "x86_64") +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "linux") + add_definitions(-D__PV_SPEAKER_PLATFORM_LINUX__) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi3") + set(PV_LINK_ATOMIC ON) + add_definitions(-D__PV_SPEAKER_PLATFORM_RASPBERRYPI__) + add_compile_options(-mcpu=cortex-a53 -mtune=cortex-a53 -mfloat-abi=hard -mfpu=neon-fp-armv8) + add_link_options(-mcpu=cortex-a53 -mtune=cortex-a53 -mfloat-abi=hard -mfpu=neon-fp-armv8) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi3-64") + set(PV_LINK_ATOMIC ON) + add_definitions(-D__PV_SPEAKER_PLATFORM_RASPBERRYPI__) + add_compile_options(-mcpu=cortex-a53 -mtune=cortex-a53) + add_link_options(-mcpu=cortex-a53 -mtune=cortex-a53) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi4") + set(PV_LINK_ATOMIC ON) + add_definitions(-D__PV_SPEAKER_PLATFORM_RASPBERRYPI__) + add_compile_options(-mcpu=cortex-a72 -mtune=cortex-a72 -mfloat-abi=hard -mfpu=neon-fp-armv8) + add_link_options(-mcpu=cortex-a72 -mtune=cortex-a72 -mfloat-abi=hard -mfpu=neon-fp-armv8) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi4-64") + set(PV_LINK_ATOMIC ON) + add_definitions(-D__PV_SPEAKER_PLATFORM_RASPBERRYPI__) + add_compile_options(-mcpu=cortex-a72 -mtune=cortex-a72) + add_link_options(-mcpu=cortex-a72 -mtune=cortex-a72) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi5") + set(PV_LINK_ATOMIC ON) + add_definitions(-D__PV_SPEAKER_PLATFORM_RASPBERRYPI__) + add_compile_options(-mcpu=cortex-a76 -mtune=cortex-a76 -mfloat-abi=hard -mfpu=neon-fp-armv8) + add_link_options(-mcpu=cortex-a76 -mtune=cortex-a76 -mfloat-abi=hard -mfpu=neon-fp-armv8) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "raspberry-pi5-64") + set(PV_LINK_ATOMIC ON) + add_definitions(-D__PV_SPEAKER_PLATFORM_RASPBERRYPI__) + add_compile_options(-mcpu=cortex-a76 -mtune=cortex-a76) + add_link_options(-mcpu=cortex-a76 -mtune=cortex-a76) +elseif (${PV_SPEAKER_PLATFORM} STREQUAL "windows") + add_definitions(-D__PV_SPEAKER_PLATFORM_WINDOWS__) +else () + message(FATAL_ERROR "Unknown platform `${PV_SPEAKER_PLATFORM}`.") +endif () + +add_library(pv_speaker_object OBJECT src/pv_circular_buffer.c src/pv_speaker.c) +target_include_directories(pv_speaker_object PUBLIC include) +target_include_directories(pv_speaker_object PRIVATE src/miniaudio) + +add_library(pv_speaker SHARED $) +set_target_properties(pv_speaker PROPERTIES PUBLIC_HEADER include/pv_speaker.h) +target_include_directories(pv_speaker PUBLIC include) + +if (NOT ${PV_SPEAKER_PLATFORM} STREQUAL "windows") + target_link_libraries(pv_speaker PRIVATE pthread dl m) + if(PV_LINK_ATOMIC) + target_link_libraries(pv_speaker PRIVATE atomic) + endif() +endif() + +if(DEFINED OUTPUT_DIR) + add_custom_command(TARGET pv_speaker POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + "${CMAKE_SOURCE_DIR}/../lib/${OUTPUT_DIR}/$" + COMMENT "Copying to output directory.") +endif() + +if (PV_BUILD_TESTS) + enable_testing() + + add_executable(test_circular_buffer test/test_pv_circular_buffer.c src/pv_circular_buffer.c) + target_include_directories(test_circular_buffer PUBLIC include) + add_test( + NAME test_circular_buffer + COMMAND test_circular_buffer + ) + + add_executable(test_speaker test/test_pv_speaker.c) + target_link_libraries(test_speaker pv_speaker) + add_test( + NAME test_speaker + COMMAND test_speaker + ) +endif() \ No newline at end of file diff --git a/project/Preload.CMake b/project/Preload.CMake new file mode 100644 index 0000000..d4b1418 --- /dev/null +++ b/project/Preload.CMake @@ -0,0 +1,5 @@ +if (WIN32) + set(CMAKE_GENERATOR "MinGW Makefiles" CACHE STRING "" FORCE) +else() + set(CMAKE_GENERATOR "Unix Makefiles" CACHE STRING "" FORCE) +endif() \ No newline at end of file diff --git a/project/README.md b/project/README.md new file mode 100644 index 0000000..864a371 --- /dev/null +++ b/project/README.md @@ -0,0 +1,124 @@ +# PvSpeaker Source Project + +## Requirements + +- CMake 3.4+. +- C99 compatible compiler. +- **Windows**: MinGW. + +## Compatibility + +- Linux (x86_64) +- macOS (x86_64, arm64) +- Windows (amd64) +- Raspberry Pi (3, 4 and 5) + +## Compiling + +Run the following commands to build and test (`{OUTPUT_DIR}` can be empty if you wish not to copy): + +```console +git submodule update --init --recursive +cmake -S . -B build -DOUTPUT_DIR={OUTPUT_DIR} -DPV_SPEAKER_PLATFORM={PV_SPEAKER_PLATFORM} +cmake --build build +``` + +The variable `{OUTPUT_DIR}` will be used to select the directory to copy the shared object +after a successful compilation. `{OUTPUT_DIR}` should be a directory **relative** to the [lib](../lib) directory. + +The `{PV_SPEAKER_PLATFORM}` variable will set the compilation flags for the given platform. Exclude this variable +to get a list of possible values. + +## Usage + +1. Create a PvSpeaker object: +```c +#include "pv_speaker.h" + +const int32_t sample_rate = 22050; +const int32_t frame_length = 512; +const int16_t bits_per_sample = 16; +const int32_t device_index = -1; // -1 == default device +const int32_t buffered_frame_count = 10; + +pv_speaker_t *speaker = NULL; +pv_speaker_status_t status = pv_speaker_init( + sample_rate, + frame_length, + bits_per_sample, + device_index, + buffered_frame_count, + &speaker); +if (status != PV_SPEAKER_STATUS_SUCCESS) { + // handle PvSpeaker init error +} +``` + +2. Start the speaker: + +```c +pv_speaker_status_t status = pv_speaker_start(speaker); +if (status != PV_SPEAKER_STATUS_SUCCESS) { + // handle PvSpeaker start error +} +``` + +3. Write frames of audio to the speaker: +```c +if (pcm) { + for (int i = 0; i < num_samples; i += frame_length) { + // must have length equal to or less than `frame_length` that was given to `pv_speaker_init()` + // be sure to handle the last frame properly (i.e. the last frame will likely not have length `frame_length`) + bool is_last_frame = i + frame_length >= num_samples; + int32_t last_frame_length = num_samples - i; + + status = pv_speaker_write( + speaker, + is_last_frame ? last_frame_length : frame_length, + &pcm[i * bits_per_sample / 8]); + if (status != PV_SPEAKER_STATUS_SUCCESS) { + // handle PvSpeaker write error + } + } + + free(pcm); +} +``` + +4. Stop playing: + +```c +pv_speaker_status_t status = pv_speaker_stop(speaker); +if (status != PV_SPEAKER_STATUS_SUCCESS) { + // handle PvSpeaker stop error +} +``` + +5. Release resources used by PvSpeaker: +```c +pv_speaker_delete(speaker); +``` + +### Selecting an Audio Device + +To print a list of available audio devices: +```c +char **device_list = NULL; +int32_t device_list_length = 0; + +pv_speaker_status_t status = pv_speaker_get_available_devices(&device_list_length, &device_list); +if (status != PV_SPEAKER_STATUS_SUCCESS) { + // handle PvSpeaker get audio devices error +} + +fprintf(stdout, "Printing devices...\n"); +for (int32_t i = 0; i < device_list_length; i++) { + fprintf(stdout, "index: %d, name: %s\n", i, device_list[i]); +} + +pv_speaker_free_available_devices(device_list_length, device_list); +``` + +The index of the device in the returned list can be used in `pv_speaker_init()` to select that device for playing. + +Refer to [pv_speaker_demo.c](../demo/c/pv_speaker_demo.c) for a full example of how to use `pv_speaker` to capture audio in C. diff --git a/project/Toolchain.CMake b/project/Toolchain.CMake new file mode 100644 index 0000000..0689d82 --- /dev/null +++ b/project/Toolchain.CMake @@ -0,0 +1,10 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_VERSION 1) + +set(CMAKE_C_COMPILER ${PV_TOOLCHAIN_DIR}/bin/${PV_TOOLCHAIN_PREFIX}-gcc) +set(CMAKE_CXX_COMPILER ${PV_TOOLCHAIN_DIR}/bin/${PV_TOOLCHAIN_PREFIX}-g++) + +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) \ No newline at end of file diff --git a/project/include/pv_circular_buffer.h b/project/include/pv_circular_buffer.h new file mode 100644 index 0000000..fc48ab1 --- /dev/null +++ b/project/include/pv_circular_buffer.h @@ -0,0 +1,107 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" + file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +#ifndef PV_CIRCULAR_BUFFER_H +#define PV_CIRCULAR_BUFFER_H + +#include +#include + +/** +* Forward declaration of pv_circular_buffer object. It handles reading and writing to a circular buffer. +*/ +typedef struct pv_circular_buffer pv_circular_buffer_t; + +/** +* Status codes. +*/ +typedef enum { + PV_CIRCULAR_BUFFER_STATUS_SUCCESS = 0, + PV_CIRCULAR_BUFFER_STATUS_OUT_OF_MEMORY, + PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT, + PV_CIRCULAR_BUFFER_STATUS_WRITE_OVERFLOW, +} pv_circular_buffer_status_t; + +/** +* Constructor for pv_circular_buffer object. +* +* @param element_count Capacity of the buffer to read and write. +* @param element_size Size of each element in the buffer. +* @param object[out] Circular buffer object. +* @return Status Code. Returns PV_CIRCULAR_BUFFER_STATUS_OUT_OF_MEMORY or PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT +* on failure. +*/ +pv_circular_buffer_status_t pv_circular_buffer_init( + int32_t element_count, + int32_t element_size, + pv_circular_buffer_t **object); + +/** +* Destructor for pv_circular_buffer object. +* +* @param object Circular buffer object. +*/ +void pv_circular_buffer_delete(pv_circular_buffer_t *object); + +/** +* Reads and copies the elements to the provided buffer. +* +* @param object Circular buffer object. +* @param buffer[out] A pointer to a pre-allocated buffer to receive the copied data. +* @param buffer_length The maximum number of elements that can be copied into `buffer`. +* @param read_length[out] Actual number of elements read. +* @return Status Code. Returns PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT on failure. +*/ +pv_circular_buffer_status_t pv_circular_buffer_read( + pv_circular_buffer_t *object, + void *buffer, + int32_t buffer_length, + int32_t *read_length); + +/** +* Writes and copies the elements of `buffer` to the object's buffer. Does not write frames if the buffer +* is full and returns PV_CIRCULAR_BUFFER_STATUS_WRITE_OVERFLOW which is not a failure. +* +* @param object Circular buffer object. +* @param buffer A pointer to copy its elements to the object's buffer. +* @param buffer_length The amount of elements to copy. +* @return Status Code. Returns PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT on failure. +*/ +pv_circular_buffer_status_t pv_circular_buffer_write( + pv_circular_buffer_t *object, + const void *buffer, + int32_t buffer_length); + +/** +* Gets the current size of the object's buffer. +* +* @param object Circular buffer object. +* @param count[out] The current size of the buffer. +* @return Status Code. Returns PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT on failure. +*/ +pv_circular_buffer_status_t pv_circular_buffer_get_count(pv_circular_buffer_t *object, int32_t *count); + +/** +* Reset the buffer pointers to start. +* +* @param object Circular buffer object. +*/ +void pv_circular_buffer_reset(pv_circular_buffer_t *object); + +/** +* Provides string representations of status codes. +* +* @param status Status code. +* @return String representation. +*/ +const char *pv_circular_buffer_status_to_string(pv_circular_buffer_status_t status); + +#endif //PV_CIRCULAR_BUFFER_H \ No newline at end of file diff --git a/project/include/pv_speaker.h b/project/include/pv_speaker.h new file mode 100644 index 0000000..209b4bd --- /dev/null +++ b/project/include/pv_speaker.h @@ -0,0 +1,181 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" + file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +#ifndef PV_SPEAKER_H +#define PV_SPEAKER_H + +#include +#include + +#if __PV_PLATFORM_WINDOWS__ + +#define PV_API __attribute__ ((dllexport)) + +#else + +#define PV_API __attribute__((visibility ("default"))) + +#endif + +/** +* Struct representing the PvSpeaker object. +*/ +typedef struct pv_speaker pv_speaker_t; + +/** +* Status codes. +*/ +typedef enum { + PV_SPEAKER_STATUS_SUCCESS = 0, + PV_SPEAKER_STATUS_OUT_OF_MEMORY, + PV_SPEAKER_STATUS_INVALID_ARGUMENT, + PV_SPEAKER_STATUS_INVALID_STATE, + PV_SPEAKER_STATUS_BUFFER_OVERFLOW, + PV_SPEAKER_STATUS_BACKEND_ERROR, + PV_SPEAKER_STATUS_DEVICE_ALREADY_INITIALIZED, + PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED, + PV_SPEAKER_STATUS_IO_ERROR, + PV_SPEAKER_STATUS_RUNTIME_ERROR +} pv_speaker_status_t; + +/** +* Creates a PvSpeaker instance. When finished with the instance, resources should be released +* using the `pv_speaker_delete() function. +* +* @param sample_rate The sample rate of the audio to be played. +* @param frame_length The maximum length of audio frame that will be passed to `pv_speaker_write`. +* @param bits_per_sample The number of bits per sample. +* @param device_index The index of the audio device to use. A value of (-1) will resort to default device. +* @param buffered_frames_count The number of audio frames buffered internally for writing - i.e. internal circular buffer +* will be of size `frame_length` * `buffered_frames_count`. If this value is too low, buffer overflows could occur +* and audio frames could be dropped. A higher value will increase memory usage. +* @param[out] object PvSpeaker object to be initialized. +* @return Status Code. PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_STATUS_BACKEND_ERROR, +* PV_SPEAKER_STATUS_DEVICE_INITIALIZED or PV_SPEAKER_STATUS_OUT_OF_MEMORY on failure. +*/ +PV_API pv_speaker_status_t pv_speaker_init( + int32_t sample_rate, + int32_t frame_length, + int16_t bits_per_sample, + int32_t device_index, + int32_t buffered_frames_count, + pv_speaker_t **object); + +/** +* Releases resources acquired by PvSpeaker. +* +* @param object PvSpeaker object. +*/ +PV_API void pv_speaker_delete(pv_speaker_t *object); + +/** +* Starts playing and buffering audio frames. +* +* @param object PvSpeaker object. +* @returnStatus Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED +* or PV_SPEAKER_STATUS_INVALID_STATE on failure. +*/ +PV_API pv_speaker_status_t pv_speaker_start(pv_speaker_t *object); + +/** +* Stops playing audio. +* +* @param object PvSpeaker object. +* @return Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED +* or PV_SPEAKER_STATUS_INVALID_STATE on failure. +*/ +PV_API pv_speaker_status_t pv_speaker_stop(pv_speaker_t *object); + +/** +* Synchronous call to write frames. Copies amount of frames to `frame` array provided to input. +* Array size must not be greater than the `frame_length` value that was given to `pv_speaker_init()`. +* +* @param object PvSpeaker object. +* @param frame_length Size of the array that is passed in. +* @param frame Pointer to the array that will be written. +* @return Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_INVALID_STATE or PV_SPEAKER_IO_ERROR on failure. +* Returns PV_SPEAKER_STATUS_BUFFER_OVERFLOW if audio frames aren't being written fast enough. This means audio frames will be dropped. +*/ +PV_API pv_speaker_status_t pv_speaker_write(pv_speaker_t *object, int32_t frame_length, void *frame); + +/** +* Enable or disable debug logging for PvSpeaker. Debug logs will indicate when there are overflows in the internal +* frame buffer and when an audio source is generating frames of silence. +* +* @param object PvSpeaker object. +* @param is_debug_logging_enabled Boolean indicating whether the debug logging is enabled or disabled. +*/ +PV_API void pv_speaker_set_debug_logging( + pv_speaker_t *object, + bool is_debug_logging_enabled); + +/** +* Gets whether the given `pv_speaker_t` instance is currently playing audio or not. +* +* @param object PvSpeaker object. +* @returns A boolean indicating whether PvSpeaker is currently playing audio or not. +*/ +PV_API bool pv_speaker_get_is_playing(pv_speaker_t *object); + +/** +* Gets the audio device that the given `pv_speaker_t` instance is using. +* +* @param object PvSpeaker object. +* @return A string containing the name of the current playing device. +*/ +PV_API const char *pv_speaker_get_selected_device(pv_speaker_t *object); + +/** +* Gets the list of available audio devices that can be used for playing. +* Free the returned `device_list` array using `pv_speaker_free_device_list()`. +* +* @param[out] device_list_length The number of available audio devices. +* @param[out] device_list The output array containing the list of available audio devices. +* @return Status Code. Returns PV_SPEAKER_STATUS_OUT_OF_MEMORY, PV_SPEAKER_STATUS_BACKEND_ERROR or +* PV_SPEAKER_STATUS_INVALID_ARGUMENT on failure. +*/ +PV_API pv_speaker_status_t pv_speaker_get_available_devices( + int32_t *device_list_length, + char ***device_list); + +/** +* Frees the device list initialized by `pv_speaker_get_available_devices()`. +* +* @param device_list_length The number of audio devices. +* @param device_list The array containing the list of audio devices. +*/ +PV_API void pv_speaker_free_available_devices( + int32_t device_list_length, + char **device_list); + +/** +* Provides string representations of the given status code. +* +* @param status Status code. +* @return String representation. +*/ +PV_API const char *pv_speaker_status_to_string(pv_speaker_status_t status); + +/** +* Gets the audio sample rate used by PvSpeaker. +* +* @return Sample rate. +*/ +PV_API int32_t pv_speaker_sample_rate(void); + +/** +* Gets the PvSpeaker version. +* +* @return Version. +*/ +PV_API const char *pv_speaker_version(void); + +#endif //PV_SPEAKER_H \ No newline at end of file diff --git a/project/src/miniaudio b/project/src/miniaudio new file mode 160000 index 0000000..4a5b74b --- /dev/null +++ b/project/src/miniaudio @@ -0,0 +1 @@ +Subproject commit 4a5b74bef029b3592c54b6048650ee5f972c1a48 diff --git a/project/src/pv_circular_buffer.c b/project/src/pv_circular_buffer.c new file mode 100644 index 0000000..0c93d60 --- /dev/null +++ b/project/src/pv_circular_buffer.c @@ -0,0 +1,185 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" + file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +#include +#include + +#include "pv_circular_buffer.h" + +struct pv_circular_buffer { + void *buffer; + int32_t capacity; + int32_t count; + int32_t element_size; + int32_t read_index; + int32_t write_index; +}; + +pv_circular_buffer_status_t pv_circular_buffer_init( + int32_t element_count, + int32_t element_size, + pv_circular_buffer_t **object) { + if (element_count <= 0) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + if (element_size <= 0) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + if (!object) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + + *object = NULL; + + pv_circular_buffer_t *o = calloc(1, sizeof(pv_circular_buffer_t)); + if (!o) { + return PV_CIRCULAR_BUFFER_STATUS_OUT_OF_MEMORY; + } + + o->buffer = malloc(element_count * element_size); + if (!(o->buffer)) { + pv_circular_buffer_delete(o); + return PV_CIRCULAR_BUFFER_STATUS_OUT_OF_MEMORY; + } + + o->capacity = element_count; + o->element_size = element_size; + + *object = o; + + return PV_CIRCULAR_BUFFER_STATUS_SUCCESS; +} + +void pv_circular_buffer_delete(pv_circular_buffer_t *object) { + if (object) { + free(object->buffer); + free(object); + } +} + +pv_circular_buffer_status_t pv_circular_buffer_read( + pv_circular_buffer_t *object, + void *buffer, + int32_t buffer_length, + int32_t *read_length) { + if (!object) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + if (!buffer) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + if (!buffer_length) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + if ((buffer_length <= 0) || (buffer_length > object->capacity)) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + + void *dst_ptr = buffer; + const void *src_ptr = (int8_t *) object->buffer + (object->read_index * object->element_size); + + const int32_t available = object->capacity - object->read_index; + const int32_t max_copy = (object->count < buffer_length) ? object->count : buffer_length; + const int32_t to_copy = (max_copy < available) ? max_copy : available; + + memcpy(dst_ptr, src_ptr, to_copy * object->element_size); + + object->read_index = (object->read_index + to_copy) % object->capacity; + + const int32_t remaining = max_copy - to_copy; + if (remaining > 0) { + dst_ptr = (int8_t *) buffer + (to_copy * object->element_size); + src_ptr = object->buffer; + + memcpy(dst_ptr, src_ptr, remaining * object->element_size); + + object->read_index = remaining; + } + + object->count -= max_copy; + + *read_length = max_copy; + + return PV_CIRCULAR_BUFFER_STATUS_SUCCESS; +} + +pv_circular_buffer_status_t pv_circular_buffer_write( + pv_circular_buffer_t *object, + const void *buffer, + int32_t buffer_length) { + if (!object) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + if (!buffer) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + if ((buffer_length <= 0) || (buffer_length > object->capacity)) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + if (object->count + buffer_length > object->capacity) { + return PV_CIRCULAR_BUFFER_STATUS_WRITE_OVERFLOW; + } + + const int32_t available = object->capacity - object->write_index; + const int32_t to_copy = (buffer_length < available) ? buffer_length : available; + + void *dst_ptr = (int8_t *) object->buffer + (object->write_index * object->element_size); + const void *src_ptr = buffer; + + memcpy(dst_ptr, src_ptr, to_copy * object->element_size); + + object->write_index = (object->write_index + to_copy) % object->capacity; + + const int32_t remaining = buffer_length - to_copy; + if (remaining > 0) { + dst_ptr = (int8_t *) object->buffer + (object->write_index * object->element_size); + src_ptr = (int8_t *) buffer + (to_copy * object->element_size); + + memcpy(dst_ptr, src_ptr, remaining * object->element_size); + + object->write_index = remaining; + } + + object->count += buffer_length; + + return PV_CIRCULAR_BUFFER_STATUS_SUCCESS; +} + +pv_circular_buffer_status_t pv_circular_buffer_get_count(pv_circular_buffer_t *object, int32_t *count) { + if (!object) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + + *count = object->count; + + return PV_CIRCULAR_BUFFER_STATUS_SUCCESS; +} + +void pv_circular_buffer_reset(pv_circular_buffer_t *object) { + object->count = 0; + object->read_index = 0; + object->write_index = 0; +} + +const char *pv_circular_buffer_status_to_string(pv_circular_buffer_status_t status) { + static const char *const STRINGS[] = { + "SUCCESS", + "OUT_OF_MEMORY", + "INVALID_ARGUMENT", + "WRITE_OVERFLOW"}; + + int32_t size = sizeof(STRINGS) / sizeof(STRINGS[0]); + if (status < PV_CIRCULAR_BUFFER_STATUS_SUCCESS || status >= (PV_CIRCULAR_BUFFER_STATUS_SUCCESS + size)) { + return NULL; + } + + return STRINGS[status - PV_CIRCULAR_BUFFER_STATUS_SUCCESS]; +} \ No newline at end of file diff --git a/project/src/pv_speaker.c b/project/src/pv_speaker.c new file mode 100644 index 0000000..d9608b1 --- /dev/null +++ b/project/src/pv_speaker.c @@ -0,0 +1,438 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" + file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +#pragma GCC diagnostic push + +#pragma GCC diagnostic ignored "-Wunused-result" + +#define MINIAUDIO_IMPLEMENTATION + +#include "miniaudio.h" + +#pragma GCC diagnostic pop + +#include "pv_circular_buffer.h" +#include "pv_speaker.h" + +#define PV_SPEAKER_DEFAULT_DEVICE_INDEX (-1) + +#define PV_SPEAKER_VERSION "1.0.0" + +static const int32_t WRITE_RETRY_COUNT = 500; +static const int32_t WRITE_SLEEP_MILLI_SECONDS = 2; + +static bool is_stopped_and_empty = false; +static bool is_data_requested_while_empty = false; + +struct pv_speaker { + ma_context context; + ma_device device; + pv_circular_buffer_t *buffer; + int32_t frame_length; + bool is_started; + bool is_debug_logging_enabled; + ma_mutex mutex; +}; + +static void pv_speaker_ma_callback(ma_device *device, void *output, const void *input, ma_uint32 frame_count) { + (void) input; + + pv_speaker_t *object = (pv_speaker_t *) device->pUserData; + + ma_mutex_lock(&object->mutex); + + // this callback being invoked after calling `pv_speaker_stop` and the circular buffer is empty indicates that all + // frames have been passed to the output buffer, and the device can stop without truncating the last frame of audio + if (is_stopped_and_empty) { + is_data_requested_while_empty = true; + ma_mutex_unlock(&object->mutex); + return; + } + + int32_t read_length = 0; + pv_circular_buffer_read(object->buffer, output, (int32_t) frame_count, &read_length); + + ma_mutex_unlock(&object->mutex); +} + +PV_API pv_speaker_status_t pv_speaker_init( + int32_t sample_rate, + int32_t frame_length, + int16_t bits_per_sample, + int32_t device_index, + int32_t buffered_frames_count, + pv_speaker_t **object) { + if (device_index < PV_SPEAKER_DEFAULT_DEVICE_INDEX) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (sample_rate <= 0) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (frame_length <= 0) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (bits_per_sample != 8 && + bits_per_sample != 16 && + bits_per_sample != 24 && + bits_per_sample != 32) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (buffered_frames_count < 1) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (!object) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + + *object = NULL; + + pv_speaker_t *o = calloc(1, sizeof(pv_speaker_t)); + if (!o) { + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } + + ma_result result = ma_context_init(NULL, 0, NULL, &(o->context)); + if (result != MA_SUCCESS) { + pv_speaker_delete(o); + if ((result == MA_NO_BACKEND) || (result == MA_FAILED_TO_INIT_BACKEND)) { + return PV_SPEAKER_STATUS_BACKEND_ERROR; + } else if (result == MA_OUT_OF_MEMORY) { + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } else { + return PV_SPEAKER_STATUS_RUNTIME_ERROR; + } + } + + int16_t ma_format; + switch (bits_per_sample) { + case 8: + ma_format = ma_format_u8; + break; + case 16: + ma_format = ma_format_s16; + break; + case 24: + ma_format = ma_format_s24; + break; + case 32: + ma_format = ma_format_s32; + break; + default: + ma_format = ma_format_unknown; + break; + } + + ma_device_config device_config; + device_config = ma_device_config_init(ma_device_type_playback); + device_config.playback.format = ma_format; + device_config.playback.channels = MA_CHANNEL_MONO; + device_config.sampleRate = sample_rate; + device_config.dataCallback = pv_speaker_ma_callback; + device_config.pUserData = o; + + if (device_index != PV_SPEAKER_DEFAULT_DEVICE_INDEX) { + ma_device_info *playback_info = NULL; + ma_uint32 count = 0; + result = ma_context_get_devices( + &(o->context), + &playback_info, + &count, + NULL, + NULL); + if (result != MA_SUCCESS) { + pv_speaker_delete(o); + if (result == MA_OUT_OF_MEMORY) { + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } else { + return PV_SPEAKER_STATUS_RUNTIME_ERROR; + } + } + if (count == 0) { + pv_speaker_delete(o); + return PV_SPEAKER_STATUS_RUNTIME_ERROR; + } + if (device_index >= count) { + pv_speaker_delete(o); + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + device_config.playback.pDeviceID = &playback_info[device_index].id; + } + + result = ma_device_init(&(o->context), &device_config, &(o->device)); + if (result != MA_SUCCESS) { + pv_speaker_delete(o); + if (result == MA_DEVICE_ALREADY_INITIALIZED) { + return PV_SPEAKER_STATUS_DEVICE_ALREADY_INITIALIZED; + } else if (result == MA_OUT_OF_MEMORY) { + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } else { + return PV_SPEAKER_STATUS_RUNTIME_ERROR; + } + } + + result = ma_mutex_init(&(o->mutex)); + if (result != MA_SUCCESS) { + pv_speaker_delete(o); + if (result == MA_OUT_OF_MEMORY) { + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } else { + return PV_SPEAKER_STATUS_RUNTIME_ERROR; + } + } + + const int32_t buffer_capacity = frame_length * buffered_frames_count; + pv_circular_buffer_status_t status = pv_circular_buffer_init( + buffer_capacity, + bits_per_sample / 8, + &(o->buffer)); + + if (status != PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { + pv_speaker_delete(o); + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } + + o->frame_length = frame_length; + + *object = o; + + return PV_SPEAKER_STATUS_SUCCESS; +} + +PV_API void pv_speaker_delete(pv_speaker_t *object) { + if (object) { + ma_device_uninit(&(object->device)); + ma_context_uninit(&(object->context)); + ma_mutex_uninit(&(object->mutex)); + pv_circular_buffer_delete(object->buffer); + free(object); + } +} + +PV_API pv_speaker_status_t pv_speaker_start(pv_speaker_t *object) { + if (!object) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + + is_stopped_and_empty = false; + is_data_requested_while_empty = false; + + ma_result result = ma_device_start(&(object->device)); + if (result != MA_SUCCESS) { + if (result == MA_DEVICE_NOT_INITIALIZED) { + return PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED; + } else { + // device already started + return PV_SPEAKER_STATUS_INVALID_STATE; + } + } + + object->is_started = true; + + return PV_SPEAKER_STATUS_SUCCESS; +} + +PV_API pv_speaker_status_t pv_speaker_stop(pv_speaker_t *object) { + if (!object) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + + // waits for all frames to be copied to output buffer before stopping + while (!is_stopped_and_empty || !is_data_requested_while_empty) { + ma_mutex_lock(&object->mutex); + int32_t count = 0; + pv_circular_buffer_status_t status = pv_circular_buffer_get_count(object->buffer, &count); + if (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && count == 0) { + is_stopped_and_empty = true; + } else if (status != PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { + ma_mutex_unlock(&object->mutex); + return PV_SPEAKER_STATUS_RUNTIME_ERROR; + } + ma_mutex_unlock(&object->mutex); + ma_sleep(WRITE_SLEEP_MILLI_SECONDS); + } + + ma_result result = ma_device_stop(&(object->device)); + if (result != MA_SUCCESS) { + if (result == MA_DEVICE_NOT_INITIALIZED) { + return PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED; + } else { + // device already stopped + return PV_SPEAKER_STATUS_INVALID_STATE; + } + } + + ma_mutex_lock(&object->mutex); + pv_circular_buffer_reset(object->buffer); + object->is_started = false; + ma_mutex_unlock(&object->mutex); + + return PV_SPEAKER_STATUS_SUCCESS; +} + +PV_API pv_speaker_status_t pv_speaker_write(pv_speaker_t *object, int32_t frame_length, void *frame) { + if (!object) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (!frame) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (frame_length > object->frame_length) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (!(object->is_started)) { + return PV_SPEAKER_STATUS_INVALID_STATE; + } + + for (int32_t i = 0; i < WRITE_RETRY_COUNT; i++) { + ma_mutex_lock(&object->mutex); + + pv_circular_buffer_status_t status = pv_circular_buffer_write( + object->buffer, + frame, + frame_length); + if (status == PV_CIRCULAR_BUFFER_STATUS_WRITE_OVERFLOW && (i == (WRITE_RETRY_COUNT - 1))) { + ma_mutex_unlock(&object->mutex); + return PV_SPEAKER_STATUS_BUFFER_OVERFLOW; + } else if (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { + ma_mutex_unlock(&object->mutex); + return PV_SPEAKER_STATUS_SUCCESS; + } else { + ma_mutex_unlock(&object->mutex); + ma_sleep(WRITE_SLEEP_MILLI_SECONDS); + } + } + + return PV_SPEAKER_STATUS_IO_ERROR; +} + +PV_API void pv_speaker_set_debug_logging( + pv_speaker_t *object, + bool is_debug_logging_enabled) { + if (!object) { + return; + } + + object->is_debug_logging_enabled = is_debug_logging_enabled; +} + +PV_API bool pv_speaker_get_is_playing(pv_speaker_t *object) { + if (!object) { + return false; + } + return object->is_started; +} + +PV_API const char *pv_speaker_get_selected_device(pv_speaker_t *object) { + if (!object) { + return NULL; + } + return object->device.playback.name; +} + +PV_API pv_speaker_status_t pv_speaker_get_available_devices( + int32_t *device_list_length, + char ***device_list) { + if (!device_list_length) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (!device_list) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + + ma_context context; + ma_result result = ma_context_init(NULL, 0, NULL, &context); + if (result != MA_SUCCESS) { + if ((result == MA_NO_BACKEND) || (result == MA_FAILED_TO_INIT_BACKEND)) { + return PV_SPEAKER_STATUS_BACKEND_ERROR; + } else if (result == MA_OUT_OF_MEMORY) { + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } else { + return PV_SPEAKER_STATUS_INVALID_STATE; + } + } + + ma_device_info *playback_info; + ma_uint32 playback_count; + result = ma_context_get_devices( + &context, + NULL, + NULL, + &playback_info, + &playback_count); + if (result != MA_SUCCESS) { + ma_context_uninit(&context); + if (result == MA_OUT_OF_MEMORY) { + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } else { + return PV_SPEAKER_STATUS_INVALID_STATE; + } + } + + char **d = calloc(playback_count, sizeof(char *)); + if (!d) { + ma_context_uninit(&context); + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } + + for (int32_t i = 0; i < (int32_t) playback_count; i++) { + d[i] = strdup(playback_info[i].name); + if (!d[i]) { + for (int32_t j = i - 1; j >= 0; j--) { + free(d[j]); + } + free(d); + ma_context_uninit(&context); + return PV_SPEAKER_STATUS_OUT_OF_MEMORY; + } + } + + ma_context_uninit(&context); + + *device_list_length = (int32_t) playback_count; + *device_list = d; + + return PV_SPEAKER_STATUS_SUCCESS; +} + +PV_API void pv_speaker_free_available_devices( + int32_t device_list_length, + char **device_list) { + if (device_list && (device_list_length > 0)) { + for (int32_t i = 0; i < device_list_length; i++) { + free(device_list[i]); + } + free(device_list); + } +} + +PV_API const char *pv_speaker_status_to_string(pv_speaker_status_t status) { + static const char *const STRINGS[] = { + "SUCCESS", + "OUT_OF_MEMORY", + "INVALID_ARGUMENT", + "INVALID_STATE", + "BACKEND_ERROR", + "DEVICE_INITIALIZED", + "DEVICE_NOT_INITIALIZED", + "IO_ERROR", + "RUNTIME_ERROR"}; + + int32_t size = sizeof(STRINGS) / sizeof(STRINGS[0]); + if (status < PV_SPEAKER_STATUS_SUCCESS || status >= (PV_SPEAKER_STATUS_SUCCESS + size)) { + return NULL; + } + + return STRINGS[status - PV_SPEAKER_STATUS_SUCCESS]; +} + +PV_API const char *pv_speaker_version(void) { + return PV_SPEAKER_VERSION; +} \ No newline at end of file diff --git a/project/test/test_helper.h b/project/test/test_helper.h new file mode 100644 index 0000000..a719dc2 --- /dev/null +++ b/project/test/test_helper.h @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include + +static char error_message[256] = {0}; + +static const char *test_error_message(const char *message, va_list args) { + vsnprintf(error_message, sizeof(error_message) / sizeof(error_message[0]), message, args); + return error_message; +} + +static void check_condition(bool condition, const char *function, int32_t line, const char *message, ...) { + if (condition == 0) { + va_list args; + va_start(args, message); + fprintf(stderr, "%s:%s():at_line %d: %s\n", __FILE__, function, line, test_error_message(message, args)); + va_end(args); + exit(1); + } +} \ No newline at end of file diff --git a/project/test/test_pv_circular_buffer.c b/project/test/test_pv_circular_buffer.c new file mode 100644 index 0000000..5599248 --- /dev/null +++ b/project/test/test_pv_circular_buffer.c @@ -0,0 +1,220 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" + file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +#include "pv_circular_buffer.h" +#include "test_helper.h" + +static void test_pv_circular_buffer_once(void) { + pv_circular_buffer_t *cb; + pv_circular_buffer_status_t status = pv_circular_buffer_init(128, sizeof(int16_t), &cb); + check_condition( + status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Failed to initialize buffer."); + + int16_t in_buffer[] = {5, 7, -20, 35, 70}; + int32_t in_size = sizeof(in_buffer) / sizeof(in_buffer[0]); + + int32_t out_size = in_size; + int16_t *out_buffer = malloc(out_size * sizeof(int16_t)); + check_condition(out_buffer != NULL, __FUNCTION__, __LINE__, "Failed to allocate memory."); + + status = pv_circular_buffer_write(cb, in_buffer, in_size); + check_condition(status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, __FUNCTION__, __LINE__, "Failed to write buffer."); + + int32_t count = 0; + status = pv_circular_buffer_get_count(cb, &count); + check_condition( + (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && count == in_size), + __FUNCTION__, + __LINE__, + "Failed to get correct count after write."); + + int32_t read_length = 0; + status = pv_circular_buffer_read(cb, out_buffer, out_size, &read_length); + check_condition( + (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && read_length == out_size), + __FUNCTION__, + __LINE__, + "Failed to read buffer."); + + status = pv_circular_buffer_get_count(cb, &count); + check_condition( + (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && count == (in_size - out_size)), + __FUNCTION__, + __LINE__, + "Failed to get correct count after read."); + + for (int32_t i = 0; i < in_size; i++) { + check_condition( + in_buffer[i] == out_buffer[i], + __FUNCTION__, + __LINE__, + "Read & write buffers have different values at index %d with values: in_buffer: %d, out_buffer: %d", + i, + in_buffer[i], + out_buffer[i]); + } + + free(out_buffer); + pv_circular_buffer_delete(cb); +} + +static void test_pv_circular_buffer_read_incomplete(void) { + pv_circular_buffer_t *cb; + pv_circular_buffer_status_t status = pv_circular_buffer_init(128, sizeof(int16_t), &cb); + check_condition( + status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Failed to initialize buffer."); + + int32_t out_size = 5; + int16_t *out_buffer = malloc(out_size * sizeof(int16_t)); + check_condition(out_buffer != NULL, __FUNCTION__, __LINE__, "Failed to allocate memory."); + + int32_t read_length = 0; + status = pv_circular_buffer_read(cb, out_buffer, out_size, &read_length); + check_condition( + (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && read_length < out_size), + __FUNCTION__, + __LINE__, + "Expected buffer size to be 0."); + + free(out_buffer); + pv_circular_buffer_delete(cb); +} + +static void test_pv_circular_buffer_write_overflow(void) { + pv_circular_buffer_t *cb; + pv_circular_buffer_status_t status = pv_circular_buffer_init( + 10, + sizeof(int16_t), + &cb); + check_condition( + status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Failed to initialize buffer."); + + int16_t in_buffer[] = {5, 7, -20, 35, 70, 100, 0, 1, -100, 0}; + int32_t in_size = sizeof(in_buffer) / sizeof(in_buffer[0]); + + status = pv_circular_buffer_write(cb, in_buffer, in_size); + check_condition( + status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Failed to write to buffer."); + + status = pv_circular_buffer_write(cb, in_buffer, in_size); + check_condition( + status == PV_CIRCULAR_BUFFER_STATUS_WRITE_OVERFLOW, + __FUNCTION__, + __LINE__, + "Expected write overflow."); + + pv_circular_buffer_delete(cb); +} + +static void test_pv_circular_buffer_read_write(void) { + pv_circular_buffer_t *cb; + pv_circular_buffer_status_t status = pv_circular_buffer_init(2048, sizeof(int16_t), &cb); + check_condition( + status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Failed to initialize buffer."); + + int32_t in_size = 512; + int16_t in_buffer[in_size]; + for (int32_t i = 0; i < in_size; i++) { + in_buffer[i] = (int16_t) ((rand() % (2000 + 1)) - 1000); + } + + int32_t out_size = in_size; + int16_t *out_buffer = malloc(out_size * sizeof(int16_t)); + check_condition(out_buffer != NULL, __FUNCTION__, __LINE__, "Failed to allocate memory."); + + int32_t read_length = 0; + for (int32_t i = 0; i < 10; i++) { + pv_circular_buffer_write(cb, in_buffer, in_size); + pv_circular_buffer_read(cb, out_buffer, out_size, &read_length); + for (int32_t j = 0; j < in_size; j++) { + check_condition( + in_buffer[i] == out_buffer[i], + __FUNCTION__, + __LINE__, + "Read & write buffers have different values at index %d with values: in_buffer: %d, out_buffer: %d", + i, + in_buffer[i], + out_buffer[i]); + } + } + + free(out_buffer); + pv_circular_buffer_delete(cb); +} + +static void test_pv_circular_buffer_read_write_one_by_one(void) { + pv_circular_buffer_t *cb; + pv_circular_buffer_status_t status = pv_circular_buffer_init(12, sizeof(int16_t), &cb); + check_condition( + status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Failed to initialize buffer."); + + int32_t in_size = 64; + int16_t in_buffer[in_size]; + for (int32_t i = 0; i < in_size; i++) { + in_buffer[i] = (int16_t) ((rand() % (2000 + 1)) - 1000); + } + + int32_t out_size = in_size; + int16_t *out_buffer = malloc(out_size * sizeof(int16_t)); + check_condition(out_buffer != NULL, __FUNCTION__, __LINE__, "Failed to allocate memory."); + + int32_t read_length = 0; + for (int32_t i = 0; i < in_size; i++) { + status = pv_circular_buffer_write(cb, in_buffer + i, 1); + check_condition( + status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Failed to write to buffer."); + + status = pv_circular_buffer_read(cb, out_buffer + i, 1, &read_length); + check_condition( + (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && read_length == 1), + __FUNCTION__, + __LINE__, + "Failed to read buffer."); + + check_condition(in_buffer[i] == out_buffer[i], __FUNCTION__, __LINE__, "Buffer have incorrect sizes."); + } + + free(out_buffer); + pv_circular_buffer_delete(cb); +} + +int main() { + srand(time(NULL)); + + test_pv_circular_buffer_once(); + test_pv_circular_buffer_read_incomplete(); + test_pv_circular_buffer_write_overflow(); + test_pv_circular_buffer_read_write(); + test_pv_circular_buffer_read_write_one_by_one(); + + return 0; +} \ No newline at end of file diff --git a/project/test/test_pv_speaker.c b/project/test/test_pv_speaker.c new file mode 100644 index 0000000..a147869 --- /dev/null +++ b/project/test/test_pv_speaker.c @@ -0,0 +1,326 @@ +/* + Copyright 2024 Picovoice Inc. + + You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" + file accompanying this source. + + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +#include "string.h" + +#include "pv_speaker.h" +#include "test_helper.h" + +static void init_test_helper( + int32_t sample_rate, + int32_t frame_length, + int16_t bits_per_sample, + int32_t device_index, + int32_t buffered_frames_count, + pv_speaker_status_t expected_status) { + pv_speaker_t *speaker = NULL; + pv_speaker_status_t status; + + status = pv_speaker_init( + sample_rate, + frame_length, + bits_per_sample, + device_index, + buffered_frames_count, + &speaker); + + check_condition( + status == expected_status, + __FUNCTION__, + __LINE__, + "Speaker initialization returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(expected_status)); + if (speaker) { + pv_speaker_delete(speaker); + } +} + +static void test_pv_speaker_init(void) { + printf("Initialize with valid parameters\n"); + init_test_helper(16000, 512, 16, 0, 10, PV_SPEAKER_STATUS_SUCCESS); + + printf("Initialize with valid parameters (different sample rate)\n"); + init_test_helper(22050, 512, 16, 0, 10, PV_SPEAKER_STATUS_SUCCESS); + + printf("Initialize with valid parameters (different frame length)\n"); + init_test_helper(16000, 256, 16, 0, 10, PV_SPEAKER_STATUS_SUCCESS); + + printf("Initialize with valid parameters (different bits per sample)\n"); + init_test_helper(16000, 512, 8, 0, 10, PV_SPEAKER_STATUS_SUCCESS); + + printf("Initialize with invalid device index (negative)\n"); + init_test_helper(16000, 512, 16, -2, 10, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + + printf("Initialize with invalid device index (too high)\n"); + init_test_helper(16000, 512, 16, 500, 10, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + + printf("Initialize with invalid frame length\n"); + init_test_helper(16000, -1, 16, 0, 10, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + + printf("Initialize with invalid bits per sample\n"); + init_test_helper(16000, 512, -1, -2, 10, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + + printf("Initialize with invalid buffered frames count\n"); + init_test_helper(16000, 512, 16, 0, 0, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + + printf("Initialize with null speaker pointer\n"); + pv_speaker_status_t status = pv_speaker_init(16000, 512, 16, 0, 10, NULL); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "Speaker initialization returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); +} + +static void test_pv_speaker_start_stop(void) { + pv_speaker_t *speaker = NULL; + pv_speaker_status_t status; + int32_t frame_length = 512; + int16_t frame[frame_length]; + char *frame_ptr = (char *) frame; + + status = pv_speaker_init(16000, frame_length, 16, 0, 10, &speaker); + check_condition( + status == PV_SPEAKER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Speaker initialization returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + + printf("Check is_playing on NULL\n"); + bool is_playing = pv_speaker_get_is_playing(NULL); + check_condition( + is_playing == false, + __FUNCTION__, + __LINE__, + "get_is_playing returned true on a NULL object."); + + printf("Check is_playing on before start\n"); + is_playing = pv_speaker_get_is_playing(speaker); + check_condition( + is_playing == false, + __FUNCTION__, + __LINE__, + "get_is_playing returned true - expected false."); + + printf("Call start on null object\n"); + status = pv_speaker_start(NULL); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "Speaker start returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + + printf("Call read before start object\n"); + status = pv_speaker_write(speaker, frame_length, frame_ptr); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_STATE, + __FUNCTION__, + __LINE__, + "Speaker read returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_STATE)); + + printf("Call start on valid speaker object\n"); + status = pv_speaker_start(speaker); + check_condition( + status == PV_SPEAKER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Speaker start returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + + printf("Call write on null speaker\n"); + status = pv_speaker_write(NULL, frame_length, frame_ptr); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "Speaker write returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + + printf("Call write with null frame\n"); + status = pv_speaker_write(speaker, frame_length, NULL); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "Speaker write returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + + printf("Call write with valid args\n"); + status = pv_speaker_write(speaker, frame_length, frame_ptr); + check_condition( + status == PV_SPEAKER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Speaker write returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + + printf("Check is_playing on started speaker\n"); + is_playing = pv_speaker_get_is_playing(speaker); + check_condition( + is_playing == true, + __FUNCTION__, + __LINE__, + "get_is_playing returned false - expected true."); + + printf("Call stop on null speaker object\n"); + status = pv_speaker_stop(NULL); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "Speaker stop returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + + printf("Call stop on valid speaker object\n"); + status = pv_speaker_stop(speaker); + check_condition( + status == PV_SPEAKER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Speaker stop returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + + printf("Check is_playing on stopped speaker\n"); + is_playing = pv_speaker_get_is_playing(speaker); + check_condition( + is_playing == false, + __FUNCTION__, + __LINE__, + "get_is_playing returned true - expected false."); + + pv_speaker_delete(speaker); +} + +static void test_pv_speaker_set_debug_logging(void) { + pv_speaker_t *speaker = NULL; + pv_speaker_status_t status = pv_speaker_init(16000, 512, 16, 0, 10, &speaker); + check_condition( + status == PV_SPEAKER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Speaker initialization returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + + pv_speaker_set_debug_logging(NULL, true); + pv_speaker_set_debug_logging(speaker, true); + + pv_speaker_delete(speaker); +} + +static void test_pv_speaker_get_selected_device(void) { + pv_speaker_t *speaker = NULL; + pv_speaker_status_t status = pv_speaker_init(16000, 512, 16, 0, 10, &speaker); + check_condition( + status == PV_SPEAKER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Speaker initialization returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + + check_condition( + pv_speaker_get_selected_device(NULL) == NULL, + __FUNCTION__, + __LINE__, + "pv_speaker_get_selected_device should have returned NULL."); + + check_condition( + strcmp(pv_speaker_get_selected_device(speaker), "") != 0, + __FUNCTION__, + __LINE__, + "pv_speaker_get_selected_device should have returned a device name"); + + pv_speaker_delete(speaker); +} + +static void test_pv_speaker_get_available_devices(void) { + pv_speaker_status_t status; + int32_t device_list_length = -1; + char **device_list = NULL; + + status = pv_speaker_get_available_devices(NULL, &device_list); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "pv_speaker_get_available_devices returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + + status = pv_speaker_get_available_devices(&device_list_length, NULL); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "pv_speaker_get_available_devices returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + + status = pv_speaker_get_available_devices(&device_list_length, &device_list); + check_condition( + status == PV_SPEAKER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "pv_speaker_get_available_devices returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + check_condition( + device_list_length >= 0, + __FUNCTION__, + __LINE__, + "device_list_length should have been greater than 0, instead got %d", + device_list_length); + check_condition( + device_list != NULL, + __FUNCTION__, + __LINE__, + "device_list should have not been NULL"); + + pv_speaker_free_available_devices(device_list_length, device_list); +} + +static void test_pv_speaker_version(void) { + const char *version = pv_speaker_version(); + check_condition( + strcmp(version, "") != 0, + __FUNCTION__, + __LINE__, + "Version was supposed to be a non-empty string."); +} + +int main() { + srand(time(NULL)); + + test_pv_speaker_get_available_devices(); + test_pv_speaker_version(); + test_pv_speaker_init(); + test_pv_speaker_start_stop(); + test_pv_speaker_set_debug_logging(); + test_pv_speaker_get_selected_device(); + + return 0; +} \ No newline at end of file diff --git a/resources/.lint/c/formatter.py b/resources/.lint/c/formatter.py new file mode 100644 index 0000000..8a615b0 --- /dev/null +++ b/resources/.lint/c/formatter.py @@ -0,0 +1,80 @@ +import fnmatch +import os +import re +import subprocess +from argparse import ArgumentParser + +import sys + +IGNORE_LIST = { + 'project/src/miniaudio', + 'lib' +} + +EXCLUDE_PATTERN = { + '.*ios.*', + '.*node_modules.*', + '.*?/mcu(?!.*src/(pv|main)).*', + '.*build.*', +} + + +def main(): + parser = ArgumentParser() + + parser.add_argument('--verbose', + '-v', + action='store_true', + help='If set, shows the list of processed files') + parser.add_argument('--check-only', + '-c', + action='store_true', + help='If set, checks for warnings only') + + input_args = parser.parse_args() + formatter(input_args.verbose, input_args.check_only) + + +def formatter(verbose, check_only): + if check_only: + cmd = "clang-format --dry-run --Werror --verbose" + else: + cmd = "clang-format -i -style=file --verbose" + print(cmd) + + src_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../../../') + all_files = find('*.c', src_dir) + all_files.extend(find('*.h', src_dir)) + + c_source_files = [file_path for file_path in all_files if + not any(ignored_path in file_path for ignored_path in IGNORE_LIST)] + + c_source_files = [file_path for file_path in c_source_files if + not any(re.match(pattern, file_path, flags=re.IGNORECASE) for pattern in EXCLUDE_PATTERN)] + + c_source_files_num = len(c_source_files) + for index, c_source_file in enumerate(c_source_files): + format_command = f"{cmd} {c_source_file}" + try: + result = subprocess.check_output(format_command, shell=True, stderr=subprocess.STDOUT).decode('utf-8') + except subprocess.CalledProcessError as e: + print(f'Formatter failed with ({e.returncode}):\n{e.output.decode("utf-8")}') + continue + + if verbose: + print(result) + sys.stdout.write(f"Completion: {index / c_source_files_num * 100:.2f}%\r") + sys.stdout.flush() + + +def find(pattern, path): + file_list = [] + for root, dirs, files in os.walk(path): + for name in files: + if fnmatch.fnmatch(name, pattern): + file_list.append(os.path.join(root, name)) + return file_list + + +if __name__ == '__main__': + main() diff --git a/resources/.lint/spell-check/.cspell.json b/resources/.lint/spell-check/.cspell.json new file mode 100644 index 0000000..b0377a5 --- /dev/null +++ b/resources/.lint/spell-check/.cspell.json @@ -0,0 +1,31 @@ +{ + "language": "en", + "dictionaries": [ + "dict" + ], + "dictionaryDefinitions": [ + { + "name": "dict", + "path": "./dict.txt", + "addWords": true + } + ], + "ignorePaths": [ + "**/CMakeLists.txt", + "**/Preload.CMake", + + // submodules + "../../project/src/miniaudio", + + // binaries + "**/*.a", + "**/*.dll", + "**/*.dylib", + "**/*.mp3", + "**/*.node", + "**/*.ppn", + "**/*.so", + "**/*.wasm", + "**/*.wav", + ] +} \ No newline at end of file diff --git a/resources/.lint/spell-check/dict.txt b/resources/.lint/spell-check/dict.txt new file mode 100644 index 0000000..203dbb6 --- /dev/null +++ b/resources/.lint/spell-check/dict.txt @@ -0,0 +1,47 @@ +aarch +argstype +armv +bitwidth +Cdecl +cpvspeaker +CTEST +dlfcn +dlltool +dlopen +dlsym +DOUTPUT +dylib +ERRORLEVEL +FARPROC +fprintf +fvisibility +gendef +getconf +HMODULE +LDFLAGS +libc +libloading +libpv +LPCSTR +lpthread +Makefiles +malloc +miniaudio +napi +NETCOREAPP +Picovoice +pthread +pvspeaker +pvspeakerdemo +repr +RTLD +stdbool +stdint +stdlib +subchunk +typedarray +vtable +wavefile +wavfile +Wunused +xcopy diff --git a/resources/scripts/platform.bat b/resources/scripts/platform.bat new file mode 100644 index 0000000..252560e --- /dev/null +++ b/resources/scripts/platform.bat @@ -0,0 +1,14 @@ +@ECHO OFF + +SET ARCH=%PROCESSOR_ARCHITECTURE% + +2>NUL CALL :CASE_%ARCH% # jump to arch +IF ERRORLEVEL 1 CALL :DEFAULT_CASE + +:CASE_AMD64 +