├── tests ├── discarded_services │ └── meson.build ├── services │ ├── org.mock.Speech.Provider.service.in │ ├── meson.build │ └── mock_speech_provider.py.in ├── test-simultaneous-init.c ├── test_settings.py ├── test-bus.conf.in ├── test-registry.c ├── test_provider.py ├── test-simple-speak.c ├── test_errors.py ├── test_voice_selection.py ├── test_voices.py ├── test_types.py ├── meson.build ├── test_audio.py ├── test_speak.py └── _common.py ├── .gitignore ├── AUTHORS ├── .devcontainer ├── devcontainer.json └── Dockerfile ├── utils ├── meson.build └── spiel.c ├── subprojects └── libspeechprovider.wrap ├── doc ├── urlmap.js ├── generate_overview.py ├── Spiel.toml.in └── meson.build ├── .clang-format ├── meson_options.txt ├── libspiel ├── org.monotonous.libspiel.gschema.xml ├── spiel.h ├── spiel-voices-list-model.h ├── spiel-provider.h ├── spiel-provider-private.h ├── spiel-voice.h ├── spiel-collect-providers.h ├── spiel-registry.h ├── spiel-utterance.h ├── spiel-version.h.in ├── spiel-provider-src.h ├── spiel-speaker.h ├── meson.build ├── generate_enums.py ├── spiel-voices-list-model.c ├── spiel-provider-src.c ├── spiel-voice.c ├── spiel-utterance.c ├── spiel-collect-providers.c ├── spiel-provider.c └── spiel-registry.c ├── .github └── workflows │ ├── ci.yml │ └── website.yml ├── examples └── spiel-cli ├── README.md └── meson.build /tests/discarded_services/meson.build: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Eitan Isaacson 2 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ubuntu", 3 | "build": { "dockerfile": "Dockerfile" }, 4 | "remoteUser": "ubuntu" 5 | } 6 | -------------------------------------------------------------------------------- /utils/meson.build: -------------------------------------------------------------------------------- 1 | utils_deps = spiel_deps + [ 2 | spiel_lib_dep, 3 | ] 4 | 5 | spiel_cli = executable('spiel', 'spiel.c', 6 | dependencies: utils_deps, 7 | install: true 8 | ) 9 | -------------------------------------------------------------------------------- /tests/services/org.mock.Speech.Provider.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.@service_name@.Speech.Provider 3 | Exec=@service_exec_dir@/mock_speech_provider.py @service_name@ 4 | SystemdService=tracker-store.service 5 | -------------------------------------------------------------------------------- /subprojects/libspeechprovider.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory=libspeechprovider 3 | url=https://github.com/project-spiel/libspeechprovider.git 4 | revision=main 5 | depth=1 6 | 7 | [provide] 8 | dependency_names = speech-provider-1.0 9 | -------------------------------------------------------------------------------- /doc/urlmap.js: -------------------------------------------------------------------------------- 1 | // A map between namespaces and base URLs for their online documentation 2 | baseURLs = [ 3 | [ 'GLib', 'https://docs.gtk.org/glib/' ], 4 | [ 'GObject', 'https://docs.gtk.org/gobject/' ], 5 | [ 'Gio', 'https://docs.gtk.org/gio/' ], 6 | ] 7 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # See https://wiki.apertis.org/Guidelines/Coding_conventions#Code_formatting 2 | BasedOnStyle: GNU 3 | AlwaysBreakAfterDefinitionReturnType: All 4 | BreakBeforeBinaryOperators: None 5 | BinPackParameters: false 6 | SpaceAfterCStyleCast: true 7 | PointerAlignment: Right 8 | SortIncludes: false 9 | ColumnLimit: 80 10 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('tests', type: 'boolean', value: true, description: 'Enable tests') 2 | option('docs', type: 'boolean', value: true, description: 'Enable docs') 3 | option('utils', type: 'boolean', value: true, description: 'Enable CLI utilities') 4 | option('introspection', type: 'boolean', value: true, 5 | description: 'Generate GObject introspection files') -------------------------------------------------------------------------------- /doc/generate_overview.py: -------------------------------------------------------------------------------- 1 | from sys import argv 2 | import re 3 | 4 | f = open(argv[-1], "r").read() 5 | 6 | regex = r"(## Overview.*\n)## Building.*" 7 | 8 | m = re.search(regex, f, re.MULTILINE | re.DOTALL) 9 | 10 | subsection = m.groups()[0] 11 | 12 | # Make headings one level higher 13 | subsection = subsection.replace("## ", "# ") 14 | 15 | print("Title: Overview\n") 16 | print(subsection) 17 | -------------------------------------------------------------------------------- /tests/services/meson.build: -------------------------------------------------------------------------------- 1 | config = configuration_data() 2 | config.set('provider_iface', iface_xml_path) 3 | 4 | configure_file( 5 | input: 'mock_speech_provider.py.in', 6 | output: 'mock_speech_provider.py', 7 | configuration: config 8 | ) 9 | 10 | service_names = ['mock', 'mock2', 'mock3'] 11 | foreach service_name : service_names 12 | c = configuration_data({ 13 | 'service_exec_dir': meson.current_build_dir(), 14 | 'service_name': service_name 15 | }) 16 | configure_file( 17 | input: 'org.mock.Speech.Provider.service.in', 18 | output: 'org.@0@.Speech.Provider.service'.format(service_name), 19 | configuration: c) 20 | endforeach 21 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:oracular 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | ARG USERNAME=ubuntu 5 | 6 | RUN apt-get update \ 7 | && apt-get install -y \ 8 | git \ 9 | sudo \ 10 | meson \ 11 | gi-docgen \ 12 | libgirepository1.0-dev \ 13 | dbus \ 14 | libdbus-glib-1-dev \ 15 | python3-dasbus \ 16 | python3-tap \ 17 | python3-gi \ 18 | libgstreamer1.0-dev \ 19 | libgstreamer-plugins-base1.0-dev \ 20 | gstreamer1.0-plugins-good \ 21 | && apt-get clean -y \ 22 | && apt-get autoremove -y \ 23 | && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ 24 | && chmod 0440 /etc/sudoers.d/$USERNAME 25 | -------------------------------------------------------------------------------- /libspiel/org.monotonous.libspiel.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nothing 6 | Default voice 7 | The default voice to be used 8 | 9 | 10 | {} 11 | Language/voice mapping 12 | Maping of BCP-47 language tags and the voices that should be used. 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/test-simultaneous-init.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static guint speakers_to_init = 10; 5 | 6 | static void 7 | spiel_speaker_new_cb (GObject *object, GAsyncResult *result, gpointer user_data) 8 | { 9 | GMainLoop *loop = user_data; 10 | g_autoptr (GError) error = NULL; 11 | g_autoptr (SpielSpeaker) speaker = spiel_speaker_new_finish (result, &error); 12 | if (--speakers_to_init == 0) 13 | { 14 | g_main_loop_quit (loop); 15 | } 16 | } 17 | 18 | int 19 | main (int argc, char *argv[]) 20 | { 21 | g_autoptr (GMainLoop) loop = g_main_loop_new (NULL, FALSE); 22 | 23 | for (guint i = 0; i < speakers_to_init; i++) 24 | { 25 | spiel_speaker_new (NULL, spiel_speaker_new_cb, loop); 26 | } 27 | 28 | g_main_loop_run (loop); 29 | 30 | return 0; 31 | } -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | from _common import * 2 | 3 | 4 | class SettingsTest(unittest.TestCase): 5 | def test_default_voice(self): 6 | settings = Gio.Settings.new("org.monotonous.libspiel") 7 | 8 | self.assertEqual(settings["default-voice"], None) 9 | settings["default-voice"] = ("org.mock2.Speech.Provider", "ine/hy") 10 | self.assertEqual( 11 | settings["default-voice"], ("org.mock2.Speech.Provider", "ine/hy") 12 | ) 13 | 14 | self.assertEqual(settings["language-voice-mapping"], {}) 15 | settings["language-voice-mapping"] = { 16 | "hy": ("org.mock2.Speech.Provider", "ine/hyw") 17 | } 18 | self.assertEqual( 19 | settings["language-voice-mapping"], 20 | {"hy": ("org.mock2.Speech.Provider", "ine/hyw")}, 21 | ) 22 | 23 | 24 | if __name__ == "__main__": 25 | test_main() 26 | -------------------------------------------------------------------------------- /tests/test-bus.conf.in: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | session 6 | 7 | unix:tmpdir=./ 8 | 9 | @service_dir@ 10 | 11 | 15 | 1000000 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/test-registry.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static void 4 | registry_get_cb (GObject *source, GAsyncResult *result, gpointer user_data) 5 | { 6 | GMainLoop *loop = user_data; 7 | g_autoptr (GError) err = NULL; 8 | g_autoptr (GListModel) providers = NULL; 9 | g_autoptr (GListModel) voices = NULL; 10 | g_autoptr (SpielRegistry) registry = spiel_registry_get_finish (result, &err); 11 | 12 | g_assert_no_error (err); 13 | g_assert (registry != NULL); 14 | 15 | providers = spiel_registry_get_providers (registry); 16 | g_assert (g_list_model_get_n_items (providers) == 3); 17 | 18 | voices = spiel_registry_get_voices (registry); 19 | g_assert (g_list_model_get_n_items (voices) == 8); 20 | 21 | g_main_loop_quit (loop); 22 | } 23 | 24 | static void 25 | test_registry (void) 26 | { 27 | g_autoptr (GMainLoop) loop = g_main_loop_new (NULL, FALSE); 28 | 29 | spiel_registry_get (NULL, registry_get_cb, loop); 30 | g_main_loop_run (loop); 31 | } 32 | 33 | gint 34 | main (gint argc, gchar *argv[]) 35 | { 36 | g_test_init (&argc, &argv, NULL); 37 | g_test_add_func ("/spiel/test_registry", test_registry); 38 | return g_test_run (); 39 | } -------------------------------------------------------------------------------- /libspiel/spiel.h: -------------------------------------------------------------------------------- 1 | /* spiel.h 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | G_BEGIN_DECLS 24 | 25 | #define LIBSPIEL_INSIDE 26 | #include "spiel-dbus-enums.h" 27 | #include "spiel-provider.h" 28 | #include "spiel-speaker.h" 29 | #include "spiel-utterance.h" 30 | #include "spiel-version.h" 31 | #include "spiel-voice.h" 32 | #include "spiel-registry.h" 33 | #undef LIBSPIEL_INSIDE 34 | 35 | G_END_DECLS 36 | -------------------------------------------------------------------------------- /libspiel/spiel-voices-list-model.h: -------------------------------------------------------------------------------- 1 | /* spiel-voices-list-model.h 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | G_BEGIN_DECLS 25 | 26 | #define SPIEL_TYPE_VOICES_LIST_MODEL (spiel_voices_list_model_get_type ()) 27 | 28 | G_DECLARE_FINAL_TYPE (SpielVoicesListModel, 29 | spiel_voices_list_model, 30 | SPIEL, 31 | VOICES_LIST_MODEL, 32 | GObject) 33 | 34 | SpielVoicesListModel *spiel_voices_list_model_new (GListModel *providers); 35 | 36 | G_END_DECLS 37 | -------------------------------------------------------------------------------- /doc/Spiel.toml.in: -------------------------------------------------------------------------------- 1 | [library] 2 | version = "@version@" 3 | browse_url = "https://github.com/project-spiel/libspiel" 4 | repository_url = "https://github.com/project-spiel/libspiel.git" 5 | website_url = "https://project-spiel.org" 6 | docs_url = "https://project-spiel.org/libspiel/" 7 | authors = "Eitan Isaacson" 8 | logo_url = "spiel-logo.svg" 9 | license = "@license@" 10 | description = "A library that interfaces with D-Bus speech synthesis providers." 11 | dependencies = [ "GObject-2.0", "GLib-2.0", "Gio-2.0"] 12 | devhelp = true 13 | search_index = true 14 | 15 | [dependencies."GObject-2.0"] 16 | name = "GObject" 17 | description = "The base type system library" 18 | docs_url = "https://docs.gtk.org/gobject/" 19 | 20 | [dependencies."GLib-2.0"] 21 | name = "GLib" 22 | description = "The base type system library" 23 | docs_url = "https://docs.gtk.org/glib/" 24 | 25 | [dependencies."Gio-2.0"] 26 | name = "GIO" 27 | description = "GObject Interfaces and Objects, Networking, IPC, and I/O" 28 | docs_url = "https://docs.gtk.org/gio/" 29 | 30 | [theme] 31 | name = "basic" 32 | show_index_summary = true 33 | show_class_hierarchy = true 34 | 35 | [source-location] 36 | base_url = "https://github.com/project-spiel/libspiel/blob/main/" 37 | 38 | [extra] 39 | urlmap_file = "urlmap.js" 40 | content_files = ["overview.md"] 41 | content_images = ["spiel-logo.svg"] -------------------------------------------------------------------------------- /tests/test_provider.py: -------------------------------------------------------------------------------- 1 | from _common import * 2 | 3 | 4 | class TestInstallProvider(BaseSpielTest): 5 | def test_providers_speaker_property(self): 6 | speaker = self.wait_for_async_speaker_init() 7 | self.assertEqual( 8 | [p.get_identifier() for p in speaker.props.providers], 9 | [ 10 | "org.mock.Speech.Provider", 11 | "org.mock2.Speech.Provider", 12 | "org.mock3.Speech.Provider", 13 | ], 14 | ) 15 | self.assertEqual( 16 | [len(p.get_voices()) for p in speaker.get_providers()], 17 | [2, 5, 1], 18 | ) 19 | self.assertEqual( 20 | [p.props.name for p in speaker.get_providers()], 21 | ["mock", "mock2", "mock3"], 22 | ) 23 | 24 | def test_install_provider_service(self): 25 | speaker = Spiel.Speaker.new_sync(None) 26 | 27 | self.wait_for_provider_to_go_away("org.mock3.Speech.Provider") 28 | self.uninstall_provider("org.mock3.Speech.Provider") 29 | self.wait_for_voices_changed(speaker, removed=["trk/uz"]) 30 | self.assertEqual(len(speaker.props.voices), 7) 31 | self.install_provider("org.mock3.Speech.Provider") 32 | self.wait_for_voices_changed(speaker, added=["trk/uz"]) 33 | self.assertEqual(len(speaker.props.voices), 8) 34 | 35 | 36 | if __name__ == "__main__": 37 | test_main() 38 | -------------------------------------------------------------------------------- /libspiel/spiel-provider.h: -------------------------------------------------------------------------------- 1 | /* spiel-provider.h 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | #include "spiel-provider-proxy.h" 24 | 25 | G_BEGIN_DECLS 26 | 27 | #define SPIEL_TYPE_PROVIDER (spiel_provider_get_type ()) 28 | 29 | G_DECLARE_FINAL_TYPE (SpielProvider, spiel_provider, SPIEL, PROVIDER, GObject) 30 | 31 | const char *spiel_provider_get_name (SpielProvider *self); 32 | 33 | const char *spiel_provider_get_well_known_name (SpielProvider *self); 34 | 35 | const char *spiel_provider_get_identifier (SpielProvider *self); 36 | 37 | GListModel *spiel_provider_get_voices (SpielProvider *self); 38 | 39 | SpielProviderProxy *spiel_provider_get_proxy (SpielProvider *self); 40 | 41 | G_END_DECLS 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | # Runs on pull requests targeting the default branch 8 | pull_request: 9 | branches: ["main"] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | clang-format: 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Lint 21 | uses: DoozyX/clang-format-lint-action@v0.17 22 | 23 | python-black: 24 | runs-on: ubuntu-24.04 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Lint 29 | uses: psf/black@stable 30 | 31 | deploy: 32 | runs-on: ubuntu-24.04 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v3 36 | - name: Update repos 37 | run: sudo apt update 38 | - name: Install dependencies 39 | run: sudo apt install -y git meson libgirepository1.0-dev dbus libdbus-glib-1-dev python3-dasbus python3-tap python3-gi libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good 40 | - name: Setup Spiel 41 | run: meson setup build -Ddocs=false -Dlibspeechprovider:docs=false 42 | - name: Compile Spiel 43 | run: meson compile -C build 44 | - name: Test Spiel 45 | run: meson test -C build 46 | - uses: actions/upload-artifact@v4 47 | if: failure() 48 | with: 49 | name: Meson_Log 50 | path: build/meson-logs 51 | -------------------------------------------------------------------------------- /examples/spiel-cli: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | 3 | from gi.repository import GLib 4 | import gi 5 | import sys 6 | 7 | gi.require_version("Spiel", "1.0") 8 | from gi.repository import Spiel 9 | 10 | def speak_and_wait(speaker, voice, text): 11 | def _notify_speaking_cb(synth, val): 12 | print("notify speaking") 13 | if not synth.props.speaking: 14 | speaker.disconnect_by_func(_notify_speaking_cb) 15 | speaker.disconnect_by_func(_started_cb) 16 | speaker.disconnect_by_func(_range_started_cb) 17 | speaker.disconnect_by_func(_finished_cb) 18 | loop.quit() 19 | 20 | def _started_cb(synth, utt): 21 | print("utterance-started") 22 | 23 | def _range_started_cb(synth, utt, start, end): 24 | print("range", start, end) 25 | 26 | def _finished_cb(synth, utt): 27 | print("utterance-finished") 28 | 29 | speaker.connect("notify::speaking", _notify_speaking_cb) 30 | speaker.connect("utterance-started", _started_cb) 31 | speaker.connect("range-started", _range_started_cb) 32 | speaker.connect("utterance-finished", _finished_cb) 33 | utterance = Spiel.Utterance(text=text, voice=voice) 34 | speaker.speak(utterance) 35 | 36 | loop = GLib.MainLoop() 37 | loop.run() 38 | 39 | 40 | if __name__ == "__main__": 41 | actual_events = [] 42 | 43 | speaker = Spiel.Speaker.new_sync(None) 44 | for voice in speaker.props.voices: 45 | print(f"{voice.props.name}\t{voice.props.identifier}\t{voice.props.languages}") 46 | speak_and_wait(speaker, voice, sys.argv[-1]) 47 | break; 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Docs & Website 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow one concurrent deployment 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | # Single deploy job since we're just deploying 24 | deploy: 25 | environment: 26 | name: github-pages 27 | url: ${{ steps.deployment.outputs.page_url }} 28 | runs-on: ubuntu-24.04 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | - name: Setup Pages 33 | uses: actions/configure-pages@v4 34 | - name: Update repos 35 | run: sudo apt update 36 | - name: Install dependencies 37 | run: sudo apt install -y git meson gi-docgen libgirepository1.0-dev libdbus-glib-1-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev 38 | - name: Setup Spiel 39 | run: meson setup build -Dtests=false -Dlibspeechprovider:docs=false -Dlibspeechprovider:tests=false 40 | - name: Compile Spiel 41 | run: meson compile -C build 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: './build/doc/libspiel' 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v4 49 | -------------------------------------------------------------------------------- /doc/meson.build: -------------------------------------------------------------------------------- 1 | toml_conf = configuration_data() 2 | toml_conf.set('version', meson.project_version()) 3 | toml_conf.set('license', meson.project_license()) 4 | 5 | source_toml = configure_file( 6 | input: 'Spiel.toml.in', 7 | output: 'Spiel.toml', 8 | configuration: toml_conf, 9 | # install: true, 10 | # install_dir: docs_dir / 'libspiel', 11 | ) 12 | 13 | logo = join_paths(meson.project_source_root(), 'spiel-logo.svg') 14 | 15 | fs = import('fs') 16 | urlmap_doc = fs.copyfile('urlmap.js') 17 | logo_file = fs.copyfile(logo) 18 | 19 | gidocgen = find_program('gi-docgen', required: true) 20 | 21 | gidocgen_common_args = [ 22 | '--quiet', 23 | '--no-namespace-dir', 24 | ] 25 | 26 | if get_option('werror') 27 | gidocgen_common_args += ['--fatal-warnings'] 28 | endif 29 | 30 | python_module = import('python') 31 | python = python_module.find_installation( 32 | 'python3', required : true) 33 | overview_doc = custom_target( 34 | input: join_paths(meson.project_source_root(), 'README.md'), 35 | output: 'overview.md', 36 | command: [python, files('generate_overview.py'), '@INPUT@'], 37 | capture: true 38 | ) 39 | 40 | custom_target('libspiel-doc', 41 | input: [ source_toml, spiel_gir[0] ], 42 | output: 'libspiel', 43 | command: [ 44 | gidocgen, 45 | 'generate', 46 | gidocgen_common_args, 47 | '--config=@INPUT0@', 48 | '--output-dir=@OUTPUT@', 49 | '--content-dir=@0@'.format(meson.current_build_dir()), 50 | '@INPUT1@', 51 | ], 52 | build_by_default: true, 53 | depends: [ overview_doc, urlmap_doc, logo_file ], 54 | install: true, 55 | install_dir: get_option('datadir') / 'doc', 56 | ) 57 | -------------------------------------------------------------------------------- /libspiel/spiel-provider-private.h: -------------------------------------------------------------------------------- 1 | /* spiel-provider-private.h 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include "spiel-provider.h" 22 | 23 | typedef struct _SpielProviderProxy SpielProviderProxy; 24 | typedef struct _SpielVoice SpielVoice; 25 | 26 | SpielProvider *spiel_provider_new (void); 27 | 28 | void spiel_provider_set_proxy (SpielProvider *self, 29 | SpielProviderProxy *provider_proxy); 30 | 31 | SpielVoice *spiel_provider_get_voice_by_id (SpielProvider *self, 32 | const char *voice_id); 33 | 34 | void spiel_provider_set_is_activatable (SpielProvider *self, 35 | gboolean is_activatable); 36 | 37 | gboolean spiel_provider_get_is_activatable (SpielProvider *self); 38 | 39 | gint spiel_provider_compare (SpielProvider *self, 40 | SpielProvider *other, 41 | gpointer user_data); -------------------------------------------------------------------------------- /tests/test-simple-speak.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static GMainLoop *main_loop; 4 | static SpielSpeaker *speaker = NULL; 5 | 6 | static void 7 | speaking_cb (SpielSpeaker *_speaker, GParamSpec *pspec, gpointer user_data) 8 | { 9 | gboolean speaking = FALSE; 10 | g_object_get (speaker, "speaking", &speaking, NULL); 11 | 12 | if (!speaking) 13 | { 14 | g_main_loop_quit (main_loop); 15 | } 16 | } 17 | 18 | static void 19 | speaker_new_cb (GObject *source, GAsyncResult *result, gpointer user_data) 20 | { 21 | GError *err = NULL; 22 | SpielUtterance *utterance = spiel_utterance_new ("hello world"); 23 | gboolean speaking = FALSE; 24 | 25 | speaker = spiel_speaker_new_finish (result, &err); 26 | g_assert_no_error (err); 27 | if (err) 28 | g_error_free (err); 29 | 30 | g_assert (speaker != NULL); 31 | g_assert (utterance != NULL); 32 | 33 | // prevents mock3 from being used 34 | spiel_utterance_set_language (utterance, "hy"); 35 | 36 | g_object_get (speaker, "speaking", &speaking, NULL); 37 | g_assert_false (speaking); 38 | 39 | spiel_speaker_speak (speaker, utterance); 40 | g_object_unref (utterance); 41 | 42 | g_object_get (speaker, "speaking", &speaking, NULL); 43 | g_assert_true (speaking); 44 | 45 | g_signal_connect (speaker, "notify::speaking", G_CALLBACK (speaking_cb), 46 | NULL); 47 | } 48 | 49 | static void 50 | test_speak (void) 51 | { 52 | main_loop = g_main_loop_new (NULL, FALSE); 53 | spiel_speaker_new (NULL, speaker_new_cb, NULL); 54 | g_main_loop_run (main_loop); 55 | g_object_unref (speaker); 56 | } 57 | 58 | gint 59 | main (gint argc, gchar *argv[]) 60 | { 61 | g_test_init (&argc, &argv, NULL); 62 | g_test_add_func ("/spiel/speak", test_speak); 63 | return g_test_run (); 64 | } -------------------------------------------------------------------------------- /libspiel/spiel-voice.h: -------------------------------------------------------------------------------- 1 | /* spiel-voice.h 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include "spiel-dbus-enums.h" 22 | #include 23 | 24 | typedef struct _SpielProvider SpielProvider; 25 | 26 | G_BEGIN_DECLS 27 | 28 | #define SPIEL_TYPE_VOICE (spiel_voice_get_type ()) 29 | 30 | G_DECLARE_FINAL_TYPE (SpielVoice, spiel_voice, SPIEL, VOICE, GObject) 31 | 32 | const char *spiel_voice_get_name (SpielVoice *self); 33 | 34 | const char *spiel_voice_get_identifier (SpielVoice *self); 35 | 36 | SpielProvider *spiel_voice_get_provider (SpielVoice *self); 37 | 38 | const char *const *spiel_voice_get_languages (SpielVoice *self); 39 | 40 | SpielVoiceFeature spiel_voice_get_features (SpielVoice *self); 41 | 42 | const char *spiel_voice_get_output_format (SpielVoice *self); 43 | 44 | void spiel_voice_set_output_format (SpielVoice *self, 45 | const char *output_format); 46 | 47 | guint spiel_voice_hash (SpielVoice *self); 48 | 49 | gboolean spiel_voice_equal (SpielVoice *self, SpielVoice *other); 50 | 51 | gint 52 | spiel_voice_compare (SpielVoice *self, SpielVoice *other, gpointer user_data); 53 | 54 | G_END_DECLS 55 | -------------------------------------------------------------------------------- /libspiel/spiel-collect-providers.h: -------------------------------------------------------------------------------- 1 | /* spiel-collect-providers.h 2 | * 3 | * Copyright (C) 2024 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | typedef struct _SpielProviderProxy SpielProviderProxy; 24 | typedef struct _SpielProvider SpielProvider; 25 | 26 | #define PROVIDER_SUFFIX ".Speech.Provider" 27 | 28 | void spiel_collect_providers (GDBusConnection *connection, 29 | GCancellable *cancellable, 30 | GAsyncReadyCallback callback, 31 | gpointer user_data); 32 | 33 | GHashTable *spiel_collect_providers_finish (GAsyncResult *res, GError **error); 34 | 35 | GHashTable *spiel_collect_providers_sync (GDBusConnection *connection, 36 | GCancellable *cancellable, 37 | GError **error); 38 | 39 | void spiel_collect_provider (GDBusConnection *connection, 40 | GCancellable *cancellable, 41 | const char *provider_name, 42 | GAsyncReadyCallback callback, 43 | gpointer user_data); 44 | 45 | SpielProvider *spiel_collect_provider_finish (GAsyncResult *res, 46 | GError **error); 47 | -------------------------------------------------------------------------------- /libspiel/spiel-registry.h: -------------------------------------------------------------------------------- 1 | /* spiel-registry.h 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | 23 | #include "spiel-provider-proxy.h" 24 | #include "spiel-voice.h" 25 | 26 | typedef struct _SpielVoicesListModel SpielVoicesListModel; 27 | 28 | G_BEGIN_DECLS 29 | 30 | #define SPIEL_TYPE_REGISTRY (spiel_registry_get_type ()) 31 | 32 | G_DECLARE_FINAL_TYPE (SpielRegistry, spiel_registry, SPIEL, REGISTRY, GObject) 33 | 34 | void spiel_registry_get (GCancellable *cancellable, 35 | GAsyncReadyCallback callback, 36 | gpointer user_data); 37 | 38 | SpielRegistry *spiel_registry_get_finish (GAsyncResult *result, GError **error); 39 | 40 | SpielRegistry *spiel_registry_get_sync (GCancellable *cancellable, 41 | GError **error); 42 | 43 | SpielProviderProxy *spiel_registry_get_provider_for_voice (SpielRegistry *self, 44 | SpielVoice *voice); 45 | 46 | SpielVoice *spiel_registry_get_voice_for_utterance (SpielRegistry *self, 47 | SpielUtterance *utterance); 48 | 49 | GListModel *spiel_registry_get_voices (SpielRegistry *self); 50 | 51 | GListModel *spiel_registry_get_providers (SpielRegistry *self); 52 | 53 | G_END_DECLS 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libspiel 2 | 3 | [![ Build & Test ](https://github.com/project-spiel/libspiel/actions/workflows/ci.yml/badge.svg)](https://github.com/project-spiel/libspiel/actions/workflows/ci.yml) [![ Docs & Website ](https://github.com/project-spiel/libspiel/actions/workflows/website.yml/badge.svg)](https://github.com/project-spiel/libspiel/actions/workflows/website.yml) 4 | 5 | ## Overview 6 | 7 | This client library is designed to provide an ergonomic interface to the myriad of potential speech providers that are installed in a given session. The API is inspired by the W3C Web Speech API. It serves several purposes: 8 | * Provide an updated list of installed across all speech providers voices. 9 | * Offer a “speaker” abstraction where utterances can be queued to speak. 10 | * If no voice was explicitly chosen for an utterance, negotiate global user settings and language preferences to choose the most appropriate voice. 11 | 12 | Language bindings are available through GObject Introspection. So this should work for any application, be it in C/C++, Python, Rust, ECMAscript, or Lua. 13 | 14 | A minimal python example would look like this: 15 | ```python 16 | import gi 17 | gi.require_version("Spiel", "1.0") 18 | from gi.repository import GLib, Spiel 19 | 20 | loop = GLib.MainLoop() 21 | 22 | def _notify_speaking_cb(synth, val): 23 | if not synth.props.speaking: 24 | loop.quit() 25 | 26 | speaker = Spiel.Speaker.new_sync(None) 27 | speaker.connect("notify::speaking", _notify_speaking_cb) 28 | 29 | utterance = Spiel.Utterance(text="Hello world.") 30 | speaker.speak(utterance) 31 | 32 | loop.run() 33 | 34 | ``` 35 | 36 | ## Building 37 | 38 | We use the Meson build system for building Spiel. 39 | 40 | ```sh 41 | # Some common options 42 | meson setup build 43 | meson compile -C build 44 | ``` 45 | 46 | Once Spiel is built, to run test the Python example above or a similar GObject client, make sure you are inside the build environment by running `meson devenv -C build`. 47 | 48 | To install libspiel system wide without needing to run `meson devenv`, run `meson install -C build` 49 | 50 | ## Documentation 51 | 52 | There is an [auto-generated API reference](https://project-spiel.org/libspiel/). 53 | -------------------------------------------------------------------------------- /libspiel/spiel-utterance.h: -------------------------------------------------------------------------------- 1 | /* spiel-utterance.h 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include "spiel-voice.h" 22 | #include 23 | 24 | G_BEGIN_DECLS 25 | 26 | #define SPIEL_TYPE_UTTERANCE (spiel_utterance_get_type ()) 27 | 28 | G_DECLARE_FINAL_TYPE ( 29 | SpielUtterance, spiel_utterance, SPIEL, UTTERANCE, GObject) 30 | 31 | SpielUtterance *spiel_utterance_new (const char *text); 32 | 33 | const char *spiel_utterance_get_text (SpielUtterance *self); 34 | 35 | void spiel_utterance_set_text (SpielUtterance *self, const char *text); 36 | 37 | double spiel_utterance_get_pitch (SpielUtterance *self); 38 | 39 | void spiel_utterance_set_pitch (SpielUtterance *self, double pitch); 40 | 41 | double spiel_utterance_get_rate (SpielUtterance *self); 42 | 43 | void spiel_utterance_set_rate (SpielUtterance *self, double rate); 44 | 45 | double spiel_utterance_get_volume (SpielUtterance *self); 46 | 47 | void spiel_utterance_set_volume (SpielUtterance *self, double volume); 48 | 49 | SpielVoice *spiel_utterance_get_voice (SpielUtterance *self); 50 | 51 | void spiel_utterance_set_voice (SpielUtterance *self, SpielVoice *voice); 52 | 53 | const char *spiel_utterance_get_language (SpielUtterance *self); 54 | 55 | void spiel_utterance_set_language (SpielUtterance *self, const char *language); 56 | 57 | void spiel_utterance_set_is_ssml (SpielUtterance *self, gboolean is_ssml); 58 | 59 | gboolean spiel_utterance_get_is_ssml (SpielUtterance *self); 60 | 61 | G_END_DECLS 62 | -------------------------------------------------------------------------------- /libspiel/spiel-version.h.in: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #if !defined(LIBSPIEL_INSIDE) && !defined(SPIEL_COMPILATION) 5 | #error "Only can be included directly." 6 | #endif 7 | 8 | /** 9 | * SECTION:spielversion 10 | * @short_description: spiel version checking 11 | * 12 | * spiel provides macros to check the version of the library 13 | * at compile-time 14 | */ 15 | 16 | /** 17 | * SPIEL_MAJOR_VERSION: 18 | * 19 | * spiel major version component (e.g. 1 if %SPIEL_VERSION is 1.2.3) 20 | */ 21 | #define SPIEL_MAJOR_VERSION (@MAJOR_VERSION @) 22 | 23 | /** 24 | * SPIEL_MINOR_VERSION: 25 | * 26 | * spiel minor version component (e.g. 2 if %SPIEL_VERSION is 1.2.3) 27 | */ 28 | #define SPIEL_MINOR_VERSION (@MINOR_VERSION @) 29 | 30 | /** 31 | * SPIEL_MICRO_VERSION: 32 | * 33 | * spiel micro version component (e.g. 3 if %SPIEL_VERSION is 1.2.3) 34 | */ 35 | #define SPIEL_MICRO_VERSION (@MICRO_VERSION @) 36 | 37 | /** 38 | * SPIEL_VERSION 39 | * 40 | * spiel version. 41 | */ 42 | #define SPIEL_VERSION (@VERSION @) 43 | 44 | /** 45 | * SPIEL_VERSION_S: 46 | * 47 | * spiel version, encoded as a string, useful for printing and 48 | * concatenation. 49 | */ 50 | #define SPIEL_VERSION_S "@VERSION@" 51 | 52 | #define SPIEL_ENCODE_VERSION(major, minor, micro) \ 53 | ((major) << 24 | (minor) << 16 | (micro) << 8) 54 | 55 | /** 56 | * SPIEL_VERSION_HEX: 57 | * 58 | * spiel version, encoded as an hexadecimal number, useful for 59 | * integer comparisons. 60 | */ 61 | #define SPIEL_VERSION_HEX \ 62 | (SPIEL_ENCODE_VERSION (SPIEL_MAJOR_VERSION, SPIEL_MINOR_VERSION, \ 63 | SPIEL_MICRO_VERSION)) 64 | 65 | /** 66 | * SPIEL_CHECK_VERSION: 67 | * @major: required major version 68 | * @minor: required minor version 69 | * @micro: required micro version 70 | * 71 | * Compile-time version checking. Evaluates to %TRUE if the version 72 | * of spiel is greater than the required one. 73 | */ 74 | #define SPIEL_CHECK_VERSION(major, minor, micro) \ 75 | (SPIEL_MAJOR_VERSION > (major) || \ 76 | (SPIEL_MAJOR_VERSION == (major) && SPIEL_MINOR_VERSION > (minor)) || \ 77 | (SPIEL_MAJOR_VERSION == (major) && SPIEL_MINOR_VERSION == (minor) && \ 78 | SPIEL_MICRO_VERSION >= (micro))) 79 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | from _common import * 2 | 3 | 4 | class TestSpeak(BaseSpielTest): 5 | def test_provider_dies(self): 6 | speaker = Spiel.Speaker.new_sync(None) 7 | 8 | utterance = Spiel.Utterance(text="die") 9 | # XXX: fdsrc goes to playing state even when no 10 | # data is written to the pipe. So we use a spielsrc. 11 | utterance.props.voice = self.get_voice( 12 | speaker, "org.mock2.Speech.Provider", "gmw/en-US" 13 | ) 14 | 15 | expected_error = ( 16 | "g-dbus-error-quark", 17 | int(Gio.DBusError.NO_REPLY), 18 | "GDBus.Error:org.freedesktop.DBus.Error.NoReply: " 19 | "Message recipient disconnected from message bus without replying", 20 | ) 21 | expected_events = [ 22 | ["notify:speaking", True], 23 | ["utterance-error", utterance, expected_error], 24 | ["notify:speaking", False], 25 | ] 26 | 27 | actual_events = self.capture_speak_sequence(speaker, utterance) 28 | 29 | self.assertEqual(actual_events, expected_events) 30 | 31 | def test_bad_voice(self): 32 | self.mock_service.SetInfinite(True) 33 | speaker = Spiel.Speaker.new_sync(None) 34 | 35 | voices = [ 36 | self.get_voice(speaker, *provider_identifier_and_id) 37 | for provider_identifier_and_id in [ 38 | ("org.mock2.Speech.Provider", "ine/hyw"), 39 | ("org.mock2.Speech.Provider", "gmw/en-GB-scotland#misconfigured"), 40 | ("org.mock2.Speech.Provider", "gmw/en-GB-x-gbclan"), 41 | ] 42 | ] 43 | 44 | [one, two, three] = [ 45 | Spiel.Utterance(text="hello world, how are you?", voice=voice) 46 | for voice in voices 47 | ] 48 | 49 | expected_error = ( 50 | "spiel-error-quark", 51 | int(Spiel.Error.MISCONFIGURED_VOICE), 52 | "Voice output format not set correctly: 'nuthin'", 53 | ) 54 | expected_events = [ 55 | ["notify:speaking", True], 56 | ["utterance-started", one], 57 | ["utterance-finished", one], 58 | ["utterance-error", two, expected_error], 59 | ["utterance-started", three], 60 | ["utterance-finished", three], 61 | ["notify:speaking", False], 62 | ] 63 | 64 | actual_events = self.capture_speak_sequence(speaker, one, two, three) 65 | 66 | self.assertEqual(actual_events, expected_events) 67 | 68 | 69 | if __name__ == "__main__": 70 | test_main() 71 | -------------------------------------------------------------------------------- /libspiel/spiel-provider-src.h: -------------------------------------------------------------------------------- 1 | /* spiel-provider-src.c 2 | * 3 | * Copyright (C) 2024 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #ifndef __SPIEL_PROVIDER_SRC_H__ 20 | #define __SPIEL_PROVIDER_SRC_H__ 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | G_BEGIN_DECLS 27 | 28 | #define SPIEL_TYPE_PROVIDER_SRC (spiel_provider_src_get_type ()) 29 | #define SPIEL_PROVIDER_SRC(obj) \ 30 | (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPIEL_TYPE_PROVIDER_SRC, \ 31 | SpielProviderSrc)) 32 | #define SPIEL_PROVIDER_SRC_CLASS(klass) \ 33 | (G_TYPE_CHECK_CLASS_CAST ((klass), SPIEL_TYPE_PROVIDER_SRC, \ 34 | SpielProviderSrcClass)) 35 | #define GST_IS_FD_SRC(obj) \ 36 | (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPIEL_TYPE_PROVIDER_SRC)) 37 | #define GST_IS_FD_SRC_CLASS(klass) \ 38 | (G_TYPE_CHECK_CLASS_TYPE ((klass), SPIEL_TYPE_PROVIDER_SRC)) 39 | 40 | typedef struct _SpielProviderSrc SpielProviderSrc; 41 | typedef struct _SpielProviderSrcClass SpielProviderSrcClass; 42 | 43 | /** 44 | * SpielProviderSrc: 45 | * 46 | * Opaque #SpielProviderSrc data structure. 47 | */ 48 | struct _SpielProviderSrc 49 | { 50 | GstPushSrc element; 51 | 52 | /* fd and flag indicating whether fd is seekable */ 53 | gint fd; 54 | 55 | gulong curoffset; /* current offset in file */ 56 | 57 | SpeechProviderStreamReader *reader; 58 | }; 59 | 60 | struct _SpielProviderSrcClass 61 | { 62 | GstPushSrcClass parent_class; 63 | }; 64 | 65 | G_GNUC_INTERNAL GType spiel_provider_src_get_type (void); 66 | 67 | SpielProviderSrc *spiel_provider_src_new (gint fd); 68 | 69 | G_END_DECLS 70 | 71 | #endif /* __SPIEL_PROVIDER_SRC_H__ */ 72 | -------------------------------------------------------------------------------- /libspiel/spiel-speaker.h: -------------------------------------------------------------------------------- 1 | /* spiel-speaker.h 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | 24 | #include "spiel-utterance.h" 25 | 26 | G_BEGIN_DECLS 27 | 28 | #define SPIEL_TYPE_SPEAKER (spiel_speaker_get_type ()) 29 | 30 | G_DECLARE_FINAL_TYPE (SpielSpeaker, spiel_speaker, SPIEL, SPEAKER, GObject) 31 | 32 | void spiel_speaker_new (GCancellable *cancellable, 33 | GAsyncReadyCallback callback, 34 | gpointer user_data); 35 | 36 | SpielSpeaker *spiel_speaker_new_finish (GAsyncResult *result, GError **error); 37 | 38 | SpielSpeaker *spiel_speaker_new_sync (GCancellable *cancellable, 39 | GError **error); 40 | 41 | void spiel_speaker_speak (SpielSpeaker *self, SpielUtterance *utterance); 42 | 43 | void spiel_speaker_pause (SpielSpeaker *self); 44 | 45 | void spiel_speaker_resume (SpielSpeaker *self); 46 | 47 | void spiel_speaker_cancel (SpielSpeaker *self); 48 | 49 | GListModel *spiel_speaker_get_voices (SpielSpeaker *self); 50 | 51 | GListModel *spiel_speaker_get_providers (SpielSpeaker *self); 52 | 53 | GQuark spiel_error_quark (void); 54 | 55 | /** 56 | * SPIEL_ERROR: 57 | * 58 | * Domain for `SpielSpeaker` errors. 59 | */ 60 | #define SPIEL_ERROR spiel_error_quark () 61 | 62 | /** 63 | * SpielError: 64 | * @SPIEL_ERROR_NO_PROVIDERS: No speech providers are available 65 | * @SPIEL_ERROR_MISCONFIGURED_VOICE: Voice is not configured correctly 66 | * @SPIEL_ERROR_PROVIDER_UNEXPECTEDLY_DIED: Speech provider unexpectedly 67 | * died 68 | * @SPIEL_ERROR_INTERNAL_PROVIDER_FAILURE: Internal error in speech 69 | * provider 70 | * 71 | * Error codes in the `SPIEL_ERROR` domain that can be emitted in the 72 | * `utterance-error` signal. 73 | */ 74 | typedef enum /**/ 75 | { 76 | SPIEL_ERROR_NO_PROVIDERS, 77 | SPIEL_ERROR_MISCONFIGURED_VOICE, 78 | SPIEL_ERROR_PROVIDER_UNEXPECTEDLY_DIED, 79 | SPIEL_ERROR_INTERNAL_PROVIDER_FAILURE, 80 | } SpielError; 81 | 82 | G_END_DECLS 83 | -------------------------------------------------------------------------------- /tests/test_voice_selection.py: -------------------------------------------------------------------------------- 1 | from _common import * 2 | 3 | 4 | class TestSpeak(BaseSpielTest): 5 | def test_lang_settings(self): 6 | settings = Gio.Settings.new("org.monotonous.libspiel") 7 | settings["default-voice"] = ("org.mock.Speech.Provider", "ine/hy") 8 | settings["language-voice-mapping"] = { 9 | "en": ("org.mock2.Speech.Provider", "gmw/en-GB-x-gbclan") 10 | } 11 | 12 | utterance = Spiel.Utterance(text="hello world, how are you?", language="en-us") 13 | 14 | speechSynthesis = Spiel.Speaker.new_sync(None) 15 | self.wait_for_speaking_done( 16 | speechSynthesis, lambda: speechSynthesis.speak(utterance) 17 | ) 18 | self.assertEqual(utterance.props.voice.props.name, "English (Lancaster)") 19 | 20 | def test_lang_no_settings(self): 21 | utterance = Spiel.Utterance(text="hello world, how are you?", language="hy") 22 | 23 | speechSynthesis = Spiel.Speaker.new_sync(None) 24 | self.wait_for_speaking_done( 25 | speechSynthesis, lambda: speechSynthesis.speak(utterance) 26 | ) 27 | self.assertIn("hy", utterance.props.voice.props.languages) 28 | 29 | def test_default_voice(self): 30 | speechSynthesis = Spiel.Speaker.new_sync(None) 31 | settings = Gio.Settings.new("org.monotonous.libspiel") 32 | settings["default-voice"] = ("org.mock.Speech.Provider", "ine/hy") 33 | 34 | utterance = Spiel.Utterance(text="hello world, how are you?", language="hy") 35 | 36 | speechSynthesis = Spiel.Speaker.new_sync(None) 37 | self.wait_for_speaking_done( 38 | speechSynthesis, lambda: speechSynthesis.speak(utterance) 39 | ) 40 | self.assertIn("hy", utterance.props.voice.props.languages) 41 | self.assertEqual(utterance.props.voice.props.name, "Armenian (East Armenia)") 42 | 43 | def _test_speak_with_voice(self, speechSynthesis, voice): 44 | utterance = Spiel.Utterance(text="hello world, how are you?", voice=voice) 45 | speechSynthesis.speak(utterance) 46 | args = self.mock_iface( 47 | voice.props.provider.get_identifier() 48 | ).GetLastSpeakArguments() 49 | self.assertEqual(str(args[2]), voice.props.identifier) 50 | 51 | def test_speak_with_voice_sync(self): 52 | speechSynthesis = Spiel.Speaker.new_sync(None) 53 | voice = self.get_voice(speechSynthesis, "org.mock.Speech.Provider", "sit/yue") 54 | self._test_speak_with_voice(speechSynthesis, voice) 55 | 56 | def test_speak_with_voice_sync_autoexit(self): 57 | speechSynthesis = Spiel.Speaker.new_sync(None) 58 | voice = self.get_voice(speechSynthesis, "org.mock3.Speech.Provider", "trk/uz") 59 | self.wait_for_provider_to_go_away("org.mock3.Speech.Provider") 60 | self._test_speak_with_voice(speechSynthesis, voice) 61 | 62 | def test_speak_with_voice_async(self): 63 | speechSynthesis = self.wait_for_async_speaker_init() 64 | voice = self.get_voice(speechSynthesis, "org.mock.Speech.Provider", "sit/yue") 65 | self._test_speak_with_voice(speechSynthesis, voice) 66 | 67 | 68 | if __name__ == "__main__": 69 | test_main() 70 | -------------------------------------------------------------------------------- /tests/test_voices.py: -------------------------------------------------------------------------------- 1 | from _common import * 2 | 3 | 4 | class TestVoices(BaseSpielTest): 5 | def test_voice_features(self): 6 | speechSynthesis = Spiel.Speaker.new_sync(None) 7 | voice = self.get_voice(speechSynthesis, "org.mock.Speech.Provider", "sit/yue") 8 | self.assertTrue(voice.get_features() & Spiel.VoiceFeature.SSML_SAY_AS_CARDINAL) 9 | voice = self.get_voice(speechSynthesis, "org.mock.Speech.Provider", "ine/hy") 10 | self.assertEqual(voice.get_features(), 0) 11 | 12 | def test_get_async_voices(self): 13 | speechSynthesis = self.wait_for_async_speaker_init() 14 | self._test_get_voices(speechSynthesis) 15 | 16 | def test_get_sync_voices(self): 17 | speechSynthesis = Spiel.Speaker.new_sync(None) 18 | self._test_get_voices(speechSynthesis) 19 | 20 | def _test_get_voices(self, speechSynthesis, expected_voices=STANDARD_VOICES): 21 | voices = speechSynthesis.props.voices 22 | voices_info = [ 23 | [ 24 | v.props.provider.props.identifier, 25 | v.props.name, 26 | v.props.identifier, 27 | v.props.languages, 28 | ] 29 | for v in voices 30 | ] 31 | _expected_voices = expected_voices[:] 32 | _expected_voices.sort(key=lambda v: "-".join(v[:3])) 33 | self.assertEqual( 34 | voices_info, 35 | _expected_voices, 36 | ) 37 | 38 | def test_add_voice(self): 39 | speechSynthesis = self.wait_for_async_speaker_init() 40 | self._test_get_voices(speechSynthesis) 41 | self.mock_service.AddVoice("Hebrew", "he", ["he", "he-il"]) 42 | self.wait_for_voices_changed(speechSynthesis, added=["he"]) 43 | self._test_get_voices( 44 | speechSynthesis, 45 | STANDARD_VOICES 46 | + [ 47 | [ 48 | "org.mock.Speech.Provider", 49 | "Hebrew", 50 | "he", 51 | ["he", "he-il"], 52 | ] 53 | ], 54 | ) 55 | self.mock_service.RemoveVoice("he") 56 | self.wait_for_voices_changed(speechSynthesis, removed=["he"]) 57 | self._test_get_voices(speechSynthesis) 58 | 59 | def test_add_voice_from_inactive(self): 60 | speechSynthesis = self.wait_for_async_speaker_init() 61 | self.wait_for_provider_to_go_away("org.mock3.Speech.Provider") 62 | self.mock_iface("org.mock3.Speech.Provider").AddVoice( 63 | "Arabic", "ar", ["ar", "ar-ps", "ar-eg"] 64 | ) 65 | self.wait_for_voices_changed(speechSynthesis, added=["ar"]) 66 | self._test_get_voices( 67 | speechSynthesis, 68 | STANDARD_VOICES 69 | + [ 70 | [ 71 | "org.mock3.Speech.Provider", 72 | "Arabic", 73 | "ar", 74 | ["ar", "ar-ps", "ar-eg"], 75 | ] 76 | ], 77 | ) 78 | self.wait_for_provider_to_go_away("org.mock3.Speech.Provider") 79 | self.mock_iface("org.mock3.Speech.Provider").RemoveVoice("ar") 80 | self.wait_for_voices_changed(speechSynthesis, removed=["ar"]) 81 | self._test_get_voices(speechSynthesis) 82 | 83 | 84 | if __name__ == "__main__": 85 | test_main() 86 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('spiel', 'c', 2 | version: '1.0.5', 3 | meson_version: '>= 0.64.0', 4 | default_options: [ 'warning_level=2', 'werror=false', 'c_std=gnu11', ], 5 | license: 'LGPL-2.1-or-later', 6 | ) 7 | 8 | api_version = '1.0' 9 | 10 | cc = meson.get_compiler('c') 11 | 12 | config_h = configuration_data() 13 | config_h.set_quoted('PACKAGE_VERSION', meson.project_version()) 14 | configure_file(output: 'config.h', configuration: config_h) 15 | add_project_arguments(['-I' + meson.project_build_root()], language: 'c') 16 | 17 | project_c_args = [] 18 | test_c_args = [ 19 | '-Wcast-align', 20 | '-Wdeclaration-after-statement', 21 | '-Werror=address', 22 | '-Werror=array-bounds', 23 | '-Werror=empty-body', 24 | '-Werror=implicit', 25 | '-Werror=implicit-function-declaration', 26 | '-Werror=incompatible-pointer-types', 27 | '-Werror=init-self', 28 | '-Werror=int-conversion', 29 | '-Werror=int-to-pointer-cast', 30 | '-Werror=main', 31 | '-Werror=misleading-indentation', 32 | '-Werror=missing-braces', 33 | '-Werror=missing-include-dirs', 34 | '-Werror=nonnull', 35 | '-Werror=overflow', 36 | '-Werror=parenthesis', 37 | '-Werror=pointer-arith', 38 | '-Werror=pointer-to-int-cast', 39 | '-Werror=redundant-decls', 40 | '-Werror=return-type', 41 | '-Werror=sequence-point', 42 | '-Werror=shadow', 43 | '-Werror=strict-prototypes', 44 | '-Werror=trigraphs', 45 | '-Werror=undef', 46 | '-Werror=write-strings', 47 | '-Wformat-nonliteral', 48 | '-Wignored-qualifiers', 49 | '-Wimplicit-function-declaration', 50 | '-Wlogical-op', 51 | '-Wmissing-declarations', 52 | '-Wmissing-format-attribute', 53 | '-Wmissing-include-dirs', 54 | '-Wmissing-noreturn', 55 | '-Wnested-externs', 56 | '-Wno-cast-function-type', 57 | '-Wno-dangling-pointer', 58 | '-Wno-missing-field-initializers', 59 | '-Wno-sign-compare', 60 | '-Wno-unused-parameter', 61 | '-Wold-style-definition', 62 | '-Wpointer-arith', 63 | '-Wredundant-decls', 64 | '-Wstrict-prototypes', 65 | '-Wswitch-default', 66 | '-Wswitch-enum', 67 | '-Wundef', 68 | '-Wuninitialized', 69 | '-Wunused', 70 | '-fno-strict-aliasing', 71 | ['-Werror=format-security', '-Werror=format=2'], 72 | ] 73 | if get_option('buildtype') != 'plain' 74 | test_c_args += '-fstack-protector-strong' 75 | endif 76 | foreach arg: test_c_args 77 | if cc.has_multi_arguments(arg) 78 | project_c_args += arg 79 | endif 80 | endforeach 81 | add_project_arguments(project_c_args, language: 'c') 82 | 83 | # Synced dependency versions 84 | glib_version = '>= 2.76' 85 | gst_version = '>= 1.0' 86 | speechprovider_version = '>= 1.0.3' 87 | 88 | introspection = get_option('introspection') 89 | 90 | speechprovider_options = ['docs=false'] 91 | if not introspection 92 | speechprovider_options += ['introspection=false'] 93 | endif 94 | 95 | speechprovider_dep = dependency('speech-provider-1.0', version: speechprovider_version, required : false) 96 | if speechprovider_dep.found() 97 | iface_xml = speechprovider_dep.get_variable('iface_xml') 98 | iface_xml_path = iface_xml 99 | system_installed_speechprovider = true 100 | else 101 | speechprovider_proj = subproject('libspeechprovider', default_options: speechprovider_options) 102 | iface_xml = speechprovider_proj.get_variable('iface_xml') 103 | iface_xml_path = iface_xml.full_path() 104 | speechprovider_dep = speechprovider_proj.get_variable('speech_provider_lib_dep') 105 | system_installed_speechprovider = false 106 | endif 107 | 108 | 109 | subdir('libspiel') 110 | 111 | if get_option('tests') 112 | subdir('tests') 113 | endif 114 | 115 | if get_option('utils') 116 | subdir('utils') 117 | endif 118 | 119 | if get_option('docs') and introspection 120 | subdir('doc') 121 | endif 122 | 123 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | from _common import * 2 | 3 | 4 | class TestTypes(BaseSpielTest): 5 | def test_speaker(self): 6 | def _cb(*args): 7 | pass 8 | 9 | speechSynthesis = Spiel.Speaker() 10 | self.assertFalse(speechSynthesis.props.speaking) 11 | self.assertFalse(speechSynthesis.props.paused) 12 | speechSynthesis.connect("notify::speaking", _cb) 13 | speechSynthesis.connect("notify::paused", _cb) 14 | speechSynthesis.connect("utterance-started", _cb) 15 | speechSynthesis.connect("range-started", _cb) 16 | speechSynthesis.connect("utterance-finished", _cb) 17 | speechSynthesis.connect("utterance-canceled", _cb) 18 | 19 | def test_voice(self): 20 | voice = Spiel.Voice( 21 | name="English", 22 | identifier="en", 23 | languages=["en", "es", "he"], 24 | features=Spiel.VoiceFeature.SSML_SAY_AS_CARDINAL, 25 | ) 26 | self.assertEqual(voice.props.name, "English") 27 | self.assertEqual(voice.get_name(), "English") 28 | self.assertEqual(voice.props.identifier, "en") 29 | self.assertEqual(voice.get_identifier(), "en") 30 | self.assertEqual(voice.props.languages, ["en", "es", "he"]) 31 | self.assertEqual(voice.get_languages(), ["en", "es", "he"]) 32 | self.assertEqual(voice.get_features(), Spiel.VoiceFeature.SSML_SAY_AS_CARDINAL) 33 | 34 | def test_utterance(self): 35 | utterance = Spiel.Utterance(text="bye") 36 | self.assertIsNotNone(utterance) 37 | self.assertEqual(utterance.props.text, "bye") 38 | self.assertEqual(utterance.get_text(), "bye") 39 | utterance.set_property("text", "hi") 40 | self.assertEqual(utterance.props.text, "hi") 41 | utterance.set_text("yo") 42 | self.assertEqual(utterance.get_text(), "yo") 43 | self.assertEqual(utterance.props.volume, 1) 44 | self.assertEqual(utterance.get_volume(), 1) 45 | self.assertEqual(utterance.props.rate, 1) 46 | self.assertEqual(utterance.get_rate(), 1) 47 | self.assertEqual(utterance.props.pitch, 1) 48 | self.assertEqual(utterance.get_pitch(), 1) 49 | utterance.props.volume = 0.5 50 | self.assertEqual(utterance.props.volume, 0.5) 51 | self.assertEqual(utterance.get_volume(), 0.5) 52 | utterance.set_volume(0.334) 53 | self.assertEqual(utterance.get_property("volume"), 0.334) 54 | utterance.set_property("pitch", 2) 55 | self.assertEqual(utterance.get_pitch(), 2) 56 | utterance.set_pitch(2.2) 57 | self.assertEqual(utterance.props.pitch, 2.2) 58 | utterance.set_property("rate", 0.25) 59 | self.assertEqual(utterance.get_rate(), 0.25) 60 | utterance.set_rate(0.1) 61 | self.assertEqual(utterance.props.rate, 0.1) 62 | self.assertEqual(utterance.props.voice, None) 63 | self.assertEqual(utterance.get_voice(), None) 64 | voice = Spiel.Voice(name="English", identifier="en") 65 | utterance.set_property("voice", voice) 66 | self.assertEqual(utterance.props.voice.props.name, "English") 67 | self.assertEqual(utterance.get_voice(), voice) 68 | self.assertEqual(utterance.props.language, None) 69 | self.assertEqual(utterance.get_language(), None) 70 | utterance.set_property("language", "en-gb") 71 | self.assertEqual(utterance.props.language, "en-gb") 72 | utterance.set_language("en-us") 73 | self.assertEqual(utterance.get_language(), "en-us") 74 | self.assertFalse(utterance.props.is_ssml) 75 | self.assertFalse(utterance.get_is_ssml()) 76 | utterance.set_is_ssml(True) 77 | self.assertTrue(utterance.props.is_ssml) 78 | self.assertTrue(utterance.get_is_ssml()) 79 | 80 | 81 | if __name__ == "__main__": 82 | test_main() 83 | -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | service_dir = join_paths(meson.current_build_dir(), 'services') 2 | 3 | test_env = environment() 4 | 5 | test_env.prepend('GI_TYPELIB_PATH', meson.project_build_root() / 'libspiel', separator: ':') 6 | test_env.prepend('LD_LIBRARY_PATH', meson.project_build_root() / 'libspiel', separator: ':') 7 | 8 | if not system_installed_speechprovider 9 | speechprovider_libdir = meson.project_build_root() / 'subprojects' / 'libspeechprovider' / 'libspeechprovider' 10 | test_env.prepend('GI_TYPELIB_PATH', speechprovider_libdir, separator: ':') 11 | test_env.prepend('LD_LIBRARY_PATH', speechprovider_libdir, separator: ':') 12 | else 13 | test_env.prepend('GI_TYPELIB_PATH', speechprovider_dep.get_variable('libdir') / 'girepository-1.0', separator: ':') 14 | test_env.prepend('GI_TYPELIB_PATH', speechprovider_dep.get_variable('libdir'), separator: ':') 15 | endif 16 | 17 | test_env.set('G_DEBUG', 'fatal-warnings') 18 | test_env.set('TEST_SERVICE_DIR', service_dir) 19 | test_env.set('TEST_DISCARDED_SERVICE_DIR', join_paths(meson.current_build_dir(), 'discarded_services')) 20 | test_env.set('GSETTINGS_SCHEMA_DIR', join_paths(meson.project_build_root(), 'libspiel')) 21 | test_env.set('GSETTINGS_BACKEND', 'memory') 22 | test_env.set('SPIEL_TEST', '1') 23 | 24 | python_module = import('python') 25 | dbus_run_session = find_program('dbus-run-session', required : false) 26 | python = python_module.find_installation( 27 | 'python3', required : false, modules: ['tap', 'dasbus']) 28 | 29 | tests = ['test_speak.py', 30 | 'test_types.py', 31 | 'test_voices.py', 32 | 'test_settings.py', 33 | 'test_voice_selection.py', 34 | 'test_errors.py', 35 | 'test_audio.py'] 36 | 37 | newer_dbus = dependency('dbus-1', version : '>=1.14.4', required : false) 38 | if newer_dbus.found() 39 | tests += ['test_provider.py'] 40 | endif 41 | 42 | conf = configuration_data() 43 | conf.set('service_dir', service_dir) 44 | test_bus_conf_file = configure_file( 45 | input: 'test-bus.conf.in', 46 | output: 'test-bus.conf', 47 | configuration: conf) 48 | 49 | test_deps = spiel_deps + [ 50 | spiel_lib_dep, 51 | ] 52 | 53 | test_simple_speak = executable('test-simple-speak', 'test-simple-speak.c', 54 | dependencies: test_deps 55 | ) 56 | 57 | test_simultaneous_init = executable( 58 | 'test-simultaneous-init', 59 | 'test-simultaneous-init.c', 60 | dependencies: test_deps 61 | ) 62 | 63 | test_registry = executable('test-registry', 'test-registry.c', 64 | dependencies: test_deps 65 | ) 66 | 67 | if dbus_run_session.found() and introspection 68 | if python.found() 69 | test_deps = [spiel_gir, spiel_schema] 70 | if not system_installed_speechprovider 71 | test_deps += [speechprovider_proj.get_variable('speech_provider_gir')] 72 | endif 73 | 74 | foreach test_name : tests 75 | test( 76 | test_name, dbus_run_session, 77 | args : [ 78 | '--config-file=@0@'.format(join_paths(meson.current_build_dir(), 'test-bus.conf')), 79 | '--', 80 | python.full_path(), 81 | files(test_name) 82 | ], 83 | env : test_env, 84 | protocol : 'tap', 85 | depends: test_deps, 86 | is_parallel : test_name != 'test_provider.py' 87 | ) 88 | endforeach 89 | endif 90 | 91 | test('test-simple-speak', dbus_run_session, 92 | args : [ 93 | '--config-file=@0@'.format(join_paths(meson.current_build_dir(), 'test-bus.conf')), 94 | '--', test_simple_speak 95 | ], 96 | env : test_env 97 | ) 98 | 99 | test('test-simultaneous-init', dbus_run_session, 100 | args : [ 101 | '--config-file=@0@'.format(join_paths(meson.current_build_dir(), 'test-bus.conf')), 102 | '--', test_simultaneous_init 103 | ], 104 | env : test_env 105 | )\ 106 | 107 | test('test-registry', dbus_run_session, 108 | args : [ 109 | '--config-file=@0@'.format(join_paths(meson.current_build_dir(), 'test-bus.conf')), 110 | '--', test_registry 111 | ], 112 | env : test_env 113 | ) 114 | endif 115 | 116 | 117 | subdir('services') 118 | subdir('discarded_services') -------------------------------------------------------------------------------- /libspiel/meson.build: -------------------------------------------------------------------------------- 1 | gnome = import('gnome') 2 | 3 | spiel_iface_sources = gnome.gdbus_codegen( 4 | 'spiel-provider-proxy', 5 | iface_xml, 6 | interface_prefix: 'org.freedesktop.Speech.', 7 | namespace: 'Spiel', 8 | annotations : [ 9 | ['org.freedesktop.Speech.Provider', 'org.gtk.GDBus.C.Name', 'ProviderProxy'] 10 | ], 11 | install_header : true, 12 | extra_args: '--glib-min-required=2.64') 13 | 14 | python_module = import('python') 15 | python = python_module.find_installation( 16 | 'python3', required : true) 17 | generate_enums = files(meson.current_source_dir() / 'generate_enums.py') 18 | spiel_dbus_enums_ch = custom_target( 19 | input: iface_xml, 20 | output: ['spiel-dbus-enums.c', 'spiel-dbus-enums.h'], 21 | command: [python, generate_enums, 'spiel', iface_xml, '@OUTPUT@'], 22 | install: true, 23 | install_dir: join_paths(get_option('includedir'), 'spiel') 24 | ) 25 | 26 | spiel_public_sources = [ 27 | 'spiel-utterance.c', 28 | 'spiel-voice.c', 29 | 'spiel-speaker.c', 30 | 'spiel-provider.c', 31 | 'spiel-voices-list-model.c', 32 | ] 33 | 34 | spiel_sources = [ 35 | spiel_public_sources, 36 | 'spiel-registry.c', 37 | 'spiel-collect-providers.c', 38 | 'spiel-provider-src.c', 39 | spiel_iface_sources, 40 | spiel_dbus_enums_ch[0], 41 | ] 42 | 43 | spiel_public_headers = [ 44 | 'spiel.h', 45 | 'spiel-utterance.h', 46 | 'spiel-voice.h', 47 | 'spiel-speaker.h', 48 | 'spiel-provider.h', 49 | 'spiel-registry.h', 50 | 'spiel-voices-list-model.h', 51 | ] 52 | 53 | spiel_headers = [ 54 | spiel_public_headers, 55 | 'spiel-registry.h', 56 | 'spiel-collect-providers.h', 57 | 'spiel-provider-src.h', 58 | 'spiel-provider-private.h', 59 | ] 60 | 61 | version_split = meson.project_version().split('.') 62 | version_conf = configuration_data() 63 | version_conf.set('VERSION', meson.project_version()) 64 | version_conf.set('MAJOR_VERSION', version_split[0]) 65 | version_conf.set('MINOR_VERSION', version_split[1]) 66 | version_conf.set('MICRO_VERSION', version_split[2]) 67 | 68 | spiel_version_h = configure_file( 69 | input: 'spiel-version.h.in', 70 | output: 'spiel-version.h', 71 | configuration: version_conf, 72 | install: true, 73 | install_dir: join_paths(get_option('includedir'), 'spiel') 74 | ) 75 | 76 | spiel_lib_generated = [ 77 | spiel_version_h, 78 | spiel_dbus_enums_ch, 79 | ] 80 | 81 | spiel_deps = [ 82 | dependency('gio-2.0', version: glib_version), 83 | dependency('gio-unix-2.0', version: glib_version), 84 | dependency('gstreamer-1.0', version: gst_version), 85 | dependency('gstreamer-audio-1.0', version: gst_version), 86 | dependency('speech-provider-1.0', version: speechprovider_version), 87 | ] 88 | 89 | spiel_lib = shared_library('spiel-' + api_version, 90 | spiel_sources, 91 | dependencies: spiel_deps, 92 | version: meson.project_version(), 93 | install: true, 94 | ) 95 | 96 | spiel_lib_dep = declare_dependency( 97 | sources: spiel_lib_generated, 98 | dependencies: spiel_deps, 99 | link_with: spiel_lib, 100 | include_directories: include_directories('.'), 101 | ) 102 | 103 | install_headers(spiel_public_headers, subdir: 'spiel') 104 | 105 | pkg = import('pkgconfig') 106 | pkg.generate( 107 | description: 'A shared library for speech synthesis clients', 108 | libraries: spiel_lib, 109 | name: 'spiel', 110 | filebase: 'spiel-' + api_version, 111 | version: meson.project_version(), 112 | subdirs: 'spiel', 113 | requires: 'gio-2.0' 114 | ) 115 | 116 | if introspection 117 | spiel_gir = gnome.generate_gir(spiel_lib, 118 | sources: spiel_public_headers + spiel_public_sources + spiel_dbus_enums_ch[1], 119 | nsversion: api_version, 120 | namespace: 'Spiel', 121 | header: 'spiel/spiel.h', 122 | symbol_prefix: 'spiel', 123 | identifier_prefix: 'Spiel', 124 | includes: [ 'Gio-2.0' ], 125 | install: true, 126 | export_packages: 'spiel', 127 | ) 128 | endif 129 | 130 | schemas_dir = join_paths(get_option('datadir'), 'glib-2.0/schemas') 131 | install_data('org.monotonous.libspiel.gschema.xml', 132 | install_dir: schemas_dir 133 | ) 134 | 135 | spiel_schema = gnome.compile_schemas(build_by_default: true, depend_files: 'org.monotonous.libspiel.gschema.xml') 136 | 137 | gnome.post_install(glib_compile_schemas: true) 138 | 139 | compile_schemas = find_program('glib-compile-schemas', required: false) 140 | if compile_schemas.found() 141 | # meson.add_install_script('glib-compile-schemas', join_paths(get_option('prefix'), schemas_dir)) 142 | test('Validate schema file', compile_schemas, 143 | args: ['--strict', '--dry-run', meson.current_source_dir()] 144 | ) 145 | endif 146 | 147 | -------------------------------------------------------------------------------- /tests/test_audio.py: -------------------------------------------------------------------------------- 1 | from _common import * 2 | 3 | import gi 4 | 5 | gi.require_version("Gst", "1.0") 6 | from gi.repository import Gst 7 | 8 | Gst.init(None) 9 | 10 | 11 | class TestSpeak(BaseSpielTest): 12 | def _setup_custom_sink(self): 13 | bin = Gst.Bin.new("bin") 14 | level = Gst.ElementFactory.make("level", "level") 15 | bin.add(level) 16 | sink = Gst.ElementFactory.make("fakesink", "sink") 17 | bin.add(sink) 18 | level.link(sink) 19 | 20 | level.set_property("post-messages", True) 21 | # level.set_property("interval", 100) 22 | sink.set_property("sync", True) 23 | 24 | pad = level.get_static_pad("sink") 25 | ghostpad = Gst.GhostPad.new("sink", pad) 26 | bin.add_pad(ghostpad) 27 | 28 | speechSynthesis = Spiel.Speaker.new_sync(None) 29 | speechSynthesis.props.sink = bin 30 | 31 | pipeline = bin.get_parent() 32 | bus = pipeline.get_bus() 33 | 34 | return speechSynthesis, bus 35 | 36 | def test_max_volume(self): 37 | loop = GLib.MainLoop() 38 | 39 | def _on_message(bus, message): 40 | info = message.get_structure() 41 | if info.get_name() == "level": 42 | self.assertGreater(info.get_value("rms")[0], -5) 43 | loop.quit() 44 | 45 | speechSynthesis, bus = self._setup_custom_sink() 46 | 47 | bus.connect("message::element", _on_message) 48 | 49 | utterance = Spiel.Utterance(text="hello world, how are you?") 50 | utterance.props.volume = 1 51 | utterance.props.voice = self.get_voice( 52 | speechSynthesis, "org.mock2.Speech.Provider", "gmw/en-US" 53 | ) 54 | 55 | speechSynthesis.speak(utterance) 56 | 57 | loop.run() 58 | 59 | def test_half_volume(self): 60 | loop = GLib.MainLoop() 61 | 62 | def _on_message(bus, message): 63 | info = message.get_structure() 64 | if info.get_name() == "level": 65 | self.assertLess(info.get_value("rms")[0], -5) 66 | loop.quit() 67 | 68 | speechSynthesis, bus = self._setup_custom_sink() 69 | 70 | bus.connect("message::element", _on_message) 71 | 72 | utterance = Spiel.Utterance(text="hello world, how are you?") 73 | utterance.props.volume = 0.5 74 | utterance.props.voice = self.get_voice( 75 | speechSynthesis, "org.mock2.Speech.Provider", "gmw/en-US" 76 | ) 77 | speechSynthesis.speak(utterance) 78 | 79 | loop.run() 80 | 81 | def test_change_volume(self): 82 | loop = GLib.MainLoop() 83 | 84 | levels = [] 85 | 86 | def _on_message(bus, message): 87 | info = message.get_structure() 88 | if info.get_name() == "level": 89 | level = info.get_value("rms")[0] 90 | if level > -5: 91 | levels.append(level) 92 | utterance.props.volume = 0.5 93 | else: 94 | levels.append(level) 95 | loop.quit() 96 | 97 | speechSynthesis, bus = self._setup_custom_sink() 98 | 99 | bus.connect("message::element", _on_message) 100 | 101 | utterance = Spiel.Utterance(text="hello world, how are you?") 102 | utterance.props.volume = 1.0 103 | utterance.props.voice = self.get_voice( 104 | speechSynthesis, "org.mock2.Speech.Provider", "gmw/en-US" 105 | ) 106 | speechSynthesis.speak(utterance) 107 | 108 | loop.run() 109 | self.assertGreater(levels[0], -5) 110 | self.assertLess(levels[1], -5) 111 | 112 | def test_queue(self): 113 | # Tests the proper disposal/closing of 'audio/x-spiel' utterances in a queue 114 | speaker = Spiel.Speaker.new_sync(None) 115 | 116 | sink = Gst.ElementFactory.make("autoaudiosink", "sink") 117 | # Override usual test fakesink 118 | speaker.props.sink = sink 119 | 120 | voice = self.get_voice(speaker, "org.mock2.Speech.Provider", "gmw/en-US") 121 | [one, two] = [ 122 | Spiel.Utterance(text=text, voice=voice) for text in ["silent", "silent"] 123 | ] 124 | 125 | expected_events = [ 126 | ["notify:speaking", True], 127 | ["utterance-started", one], 128 | ["utterance-finished", one], 129 | ["utterance-started", two], 130 | ["utterance-finished", two], 131 | ["notify:speaking", False], 132 | ] 133 | 134 | actual_events = self.capture_speak_sequence(speaker, one, two) 135 | 136 | self.assertEqual(actual_events, expected_events) 137 | 138 | 139 | if __name__ == "__main__": 140 | test_main() 141 | -------------------------------------------------------------------------------- /libspiel/generate_enums.py: -------------------------------------------------------------------------------- 1 | # XXX: Canonical copy lives in libspeechprovider 2 | 3 | import xml.parsers.expat 4 | import sys 5 | import pathlib 6 | 7 | 8 | class DBusXMLEnumParser: 9 | def __init__(self, xml_data, prefix, infile, outfile): 10 | self.prefix = prefix 11 | self._parser = xml.parsers.expat.ParserCreate() 12 | self._parser.StartElementHandler = self.handle_start_element 13 | self._parser.EndElementHandler = self.handle_end_element 14 | self._parser.CharacterDataHandler = self.handle_char_data 15 | 16 | self._enum_data = None 17 | self._in_docstring = False 18 | self._in_enum_value = False 19 | 20 | self._poutfile = pathlib.Path(outfile) 21 | self._header = self._poutfile.suffix == ".h" 22 | self._writer = self._poutfile.open("w") 23 | self._writer.write(f"/* Autogenerated from {pathlib.Path(infile).name} */\n\n") 24 | 25 | if self._header: 26 | self._writer.write("#pragma once\n\n") 27 | self._writer.write("#include \n\n") 28 | self._writer.write("G_BEGIN_DECLS\n\n") 29 | else: 30 | self._writer.write(f'#include "{self._poutfile.stem}.h"\n\n') 31 | self._parser.Parse(xml_data) 32 | 33 | if self._header: 34 | self._writer.write("\nG_END_DECLS\n\n") 35 | self._writer.close() 36 | 37 | def print_enum_impl(self): 38 | full_name = f"{self.prefix}_{self._enum_data['name']}" 39 | camel_case_name = "".join([a.capitalize() for a in full_name.split("_")]) 40 | self._writer.write(f"GType\n{full_name.lower()}_get_type (void)\n{{\n") 41 | self._writer.write(" static gsize g_define_type_id__volatile = 0;\n\n") 42 | self._writer.write( 43 | " if (g_once_init_enter (&g_define_type_id__volatile))\n {\n" 44 | ) 45 | if self._enum_data["is_flags"]: 46 | self._writer.write(" static const GFlagsValue values[] = {\n") 47 | else: 48 | self._writer.write(" static const GEnumValue values[] = {\n") 49 | values = [] 50 | for val in self._enum_data["values"]: 51 | dashified = "-".join( 52 | val["name"][len(self._enum_data["name"]) + 1 :].lower().split("_") 53 | ) 54 | triad = [ 55 | f"{self.prefix.upper()}_{val['name']}", 56 | f"\"{self.prefix.upper()}_{val['name']}\"", 57 | f'"{dashified}"', 58 | ] 59 | values.append(triad) 60 | values.append(["0", "NULL", "NULL"]) 61 | a = [f" {{ {', '.join(triad)} }}" for triad in values] 62 | self._writer.write(",\n".join(a)) 63 | self._writer.write("\n };\n") 64 | self._writer.write(" GType g_define_type_id =\n") 65 | if self._enum_data["is_flags"]: 66 | self._writer.write( 67 | f' g_flags_register_static (g_intern_static_string ("{camel_case_name}"), values);\n' 68 | ) 69 | else: 70 | self._writer.write( 71 | f' g_enum_register_static (g_intern_static_string ("{camel_case_name}"), values);\n' 72 | ) 73 | self._writer.write( 74 | " g_once_init_leave (&g_define_type_id__volatile, g_define_type_id);\n" 75 | ) 76 | self._writer.write(" }\n\n") 77 | self._writer.write(" return g_define_type_id__volatile;\n") 78 | self._writer.write("}\n\n") 79 | 80 | def print_enum_header(self): 81 | full_name = f"{self.prefix}_{self._enum_data['name']}" 82 | camel_case_name = "".join([a.capitalize() for a in full_name.split("_")]) 83 | self._writer.write("\n/**\n") 84 | self._writer.write(f" * {camel_case_name}:\n") 85 | for val in self._enum_data["values"]: 86 | self._writer.write( 87 | f" * @{self.prefix.upper()}_{val['name']}: {' '.join(val['docstring'])}\n" 88 | ) 89 | self._writer.write(" *\n") 90 | self._writer.write(f" * {' '.join(self._enum_data['docstring'])}\n") 91 | self._writer.write(" */\n") 92 | 93 | self._writer.write(f"typedef enum /**/\n") 94 | self._writer.write("{\n") 95 | for val in self._enum_data["values"]: 96 | self._writer.write( 97 | f" {self.prefix.upper()}_{val['name']} = {val['value']},\n" 98 | ) 99 | self._writer.write("} %s;\n\n" % camel_case_name) 100 | self._writer.write( 101 | f"\nGType {full_name.lower()}_get_type (void) G_GNUC_CONST;\n" 102 | ) 103 | self._writer.write( 104 | f"#define {self.prefix.upper()}_TYPE_{self._enum_data['name']} ({full_name.lower()}_get_type ())\n" 105 | ) 106 | 107 | def handle_start_element(self, name, attrs): 108 | if name == "tp:enum" or name == "tp:flags": 109 | self._enum_data = { 110 | "name": attrs["name"], 111 | "values": [], 112 | "docstring": [], 113 | "is_flags": name == "tp:flags", 114 | } 115 | elif name == "tp:enumvalue" or name == "tp:flag": 116 | if (name == "tp:flag") != (self._enum_data["is_flags"]): 117 | raise Exception("Flags embedded in enum or vice-versa") 118 | self._enum_data["values"].append( 119 | { 120 | "name": f"{self._enum_data['name']}_{attrs['suffix']}", 121 | "value": attrs["value"], 122 | "docstring": [], 123 | } 124 | ) 125 | self._in_enum_value = True 126 | elif name == "tp:docstring": 127 | self._in_docstring = True 128 | 129 | def handle_char_data(self, data): 130 | text = data.strip() 131 | if not text: 132 | return 133 | if self._in_docstring and self._in_enum_value: 134 | self._enum_data["values"][-1]["docstring"].append(text) 135 | else: 136 | self._enum_data["docstring"].append(text) 137 | 138 | def handle_end_element(self, name): 139 | if name == "tp:enum" or name == "tp:flags": 140 | if self._header: 141 | self.print_enum_header() 142 | else: 143 | self.print_enum_impl() 144 | self._enum_data = None 145 | elif name == "tp:enumvalue" or name == "tp:flag": 146 | self._in_enum_value = False 147 | elif name == "tp:docstring": 148 | self._in_docstring = False 149 | 150 | 151 | if __name__ == "__main__": 152 | prefix, infile, source, header = sys.argv[-4:] 153 | xml_data = open(infile).read() 154 | for outfile in [source, header]: 155 | parser = DBusXMLEnumParser(xml_data, prefix, infile, outfile) 156 | -------------------------------------------------------------------------------- /tests/test_speak.py: -------------------------------------------------------------------------------- 1 | from _common import * 2 | 3 | 4 | class TestSpeak(BaseSpielTest): 5 | def test_speak(self): 6 | speaker = Spiel.Speaker.new_sync(None) 7 | 8 | utterance = Spiel.Utterance(text="hello world, how are you?") 9 | utterance.props.voice = self.get_voice( 10 | speaker, "org.mock2.Speech.Provider", "gmw/en-US" 11 | ) 12 | 13 | expected_events = [ 14 | ["notify:speaking", True], 15 | ["utterance-started", utterance], 16 | ["word-started", utterance, 0, 6], 17 | ["sentence-started", utterance, 6, 13], 18 | ["word-started", utterance, 6, 13], 19 | ["word-started", utterance, 13, 17], 20 | ["sentence-started", utterance, 17, 21], 21 | ["word-started", utterance, 17, 21], 22 | ["word-started", utterance, 21, 25], 23 | ["utterance-finished", utterance], 24 | ["notify:speaking", False], 25 | ] 26 | 27 | actual_events = self.capture_speak_sequence(speaker, utterance) 28 | 29 | self.assertEqual(actual_events, expected_events) 30 | 31 | def test_queue(self): 32 | speaker = Spiel.Speaker.new_sync(None) 33 | [one, two, three] = [ 34 | Spiel.Utterance(text=text) for text in ["one", "two", "three"] 35 | ] 36 | 37 | expected_events = [ 38 | ["notify:speaking", True], 39 | ["utterance-started", one], 40 | ["utterance-finished", one], 41 | ["utterance-started", two], 42 | ["utterance-finished", two], 43 | ["utterance-started", three], 44 | ["utterance-finished", three], 45 | ["notify:speaking", False], 46 | ] 47 | 48 | actual_events = self.capture_speak_sequence(speaker, one, two, three) 49 | 50 | self.assertEqual(actual_events, expected_events) 51 | 52 | def test_pause(self): 53 | def _started_cb(_speaker, utt): 54 | _speaker.pause() 55 | 56 | def _notify_paused_cb(_speaker, val): 57 | if _speaker.props.paused: 58 | GLib.idle_add(lambda: _speaker.resume()) 59 | else: 60 | self.mock_service.End() 61 | 62 | self.mock_service.SetInfinite(True) 63 | speaker = Spiel.Speaker.new_sync(None) 64 | speaker.connect("utterance-started", _started_cb) 65 | speaker.connect("notify::paused", _notify_paused_cb) 66 | 67 | utterance = Spiel.Utterance(text="hello world, how are you?") 68 | 69 | expected_events = [ 70 | ["notify:speaking", True], 71 | ["utterance-started", utterance], 72 | ["notify:paused", True], 73 | ["notify:paused", False], 74 | ["utterance-finished", utterance], 75 | ["notify:speaking", False], 76 | ] 77 | 78 | actual_events = self.capture_speak_sequence(speaker, utterance) 79 | 80 | self.assertEqual(actual_events, expected_events) 81 | 82 | def test_cancel(self): 83 | def _started_cb(_speaker, utt): 84 | GLib.idle_add(lambda: _speaker.cancel()) 85 | 86 | self.mock_service.SetInfinite(True) 87 | 88 | speaker = Spiel.Speaker.new_sync(None) 89 | speaker.connect("utterance-started", _started_cb) 90 | 91 | [one, two, three] = [ 92 | Spiel.Utterance(text=text) for text in ["one", "two", "three"] 93 | ] 94 | 95 | expected_events = [ 96 | ["notify:speaking", True], 97 | ["utterance-started", one], 98 | ["utterance-canceled", one], 99 | ["notify:speaking", False], 100 | ] 101 | actual_events = self.capture_speak_sequence(speaker, one, two, three) 102 | 103 | self.assertEqual(actual_events, expected_events) 104 | 105 | def test_pause_and_cancel(self): 106 | def _started_cb(_speaker, utt): 107 | GLib.idle_add(lambda: _speaker.pause()) 108 | 109 | def _notify_paused_cb(_speaker, val): 110 | GLib.idle_add(lambda: _speaker.cancel()) 111 | 112 | actual_events = [] 113 | 114 | self.mock_service.SetInfinite(True) 115 | speaker = Spiel.Speaker.new_sync(None) 116 | speaker.connect("utterance-started", _started_cb) 117 | speaker.connect("notify::paused", _notify_paused_cb) 118 | 119 | utterance = Spiel.Utterance(text="hello world, how are you?") 120 | 121 | expected_events = [ 122 | ["notify:speaking", True], 123 | ["utterance-started", utterance], 124 | ["notify:paused", True], 125 | ["utterance-canceled", utterance], 126 | ["notify:speaking", False], 127 | ] 128 | 129 | actual_events = self.capture_speak_sequence(speaker, utterance) 130 | 131 | self.assertEqual(actual_events, expected_events) 132 | 133 | def test_pause_then_speak(self): 134 | def _notify_paused_cb(_speaker, val): 135 | if _speaker.props.paused: 136 | GLib.idle_add(lambda: _speaker.resume()) 137 | 138 | speaker = Spiel.Speaker.new_sync(None) 139 | speaker.connect("notify::paused", _notify_paused_cb) 140 | GLib.idle_add(lambda: speaker.pause()) 141 | 142 | utterance = Spiel.Utterance(text="hello world, how are you?") 143 | 144 | expected_events = [ 145 | ["notify:paused", True], 146 | ["notify:speaking", True], 147 | ["notify:paused", False], 148 | ["utterance-started", utterance], 149 | ["utterance-finished", utterance], 150 | ["notify:speaking", False], 151 | ] 152 | 153 | actual_events = self.capture_speak_sequence(speaker, utterance) 154 | 155 | self.assertEqual(actual_events, expected_events) 156 | 157 | def test_is_ssml(self): 158 | speaker = Spiel.Speaker.new_sync(None) 159 | 160 | utterance = Spiel.Utterance(text="hello world, how are you?", language="hy") 161 | self.wait_for_speaking_done(speaker, lambda: speaker.speak(utterance)) 162 | is_ssml = self.mock_service.GetLastSpeakArguments()[5] 163 | self.assertFalse(is_ssml) 164 | 165 | utterance = Spiel.Utterance( 166 | text="hello world, how are you?", language="hy", is_ssml=True 167 | ) 168 | self.wait_for_speaking_done(speaker, lambda: speaker.speak(utterance)) 169 | is_ssml = self.mock_service.GetLastSpeakArguments()[5] 170 | self.assertTrue(is_ssml) 171 | 172 | def test_provide_language(self): 173 | speaker = Spiel.Speaker.new_sync(None) 174 | 175 | utterance = Spiel.Utterance(text="hello world, how are you?", language="hy") 176 | self.wait_for_speaking_done(speaker, lambda: speaker.speak(utterance)) 177 | lang = self.mock_service.GetLastSpeakArguments()[6] 178 | self.assertEqual(lang, "hy") 179 | 180 | 181 | if __name__ == "__main__": 182 | test_main() 183 | -------------------------------------------------------------------------------- /utils/spiel.c: -------------------------------------------------------------------------------- 1 | /* spiel.c 2 | * 3 | * Copyright (C) 2024 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | static gboolean list_voices = FALSE; 23 | static gboolean list_providers = FALSE; 24 | static const char *voice_id = NULL; 25 | static const char *provider_id = NULL; 26 | static const char *lang = NULL; 27 | static double pitch = 1; 28 | static double rate = 1; 29 | static double volume = 1; 30 | static gboolean is_ssml = FALSE; 31 | 32 | static GOptionEntry entries[] = { 33 | { "list-voices", 'V', 0, G_OPTION_ARG_NONE, &list_voices, 34 | "List available voices", NULL }, 35 | { "list-providers", 'P', 0, G_OPTION_ARG_NONE, &list_providers, 36 | "List available speech providers", NULL }, 37 | { "voice", 'v', 0, G_OPTION_ARG_STRING, &voice_id, 38 | "Voice ID to use with utterance (should specify provider too)", NULL }, 39 | { "provider", 'p', 0, G_OPTION_ARG_STRING, &provider_id, 40 | "Provider ID of voice to use with utterance", NULL }, 41 | { "language", 'l', 0, G_OPTION_ARG_STRING, &lang, 42 | "Language to use with utterance (specifying a voice overrides this)", 43 | NULL }, 44 | { "pitch", 0, 0, G_OPTION_ARG_DOUBLE, &pitch, 45 | "Pitch of utterance (default: 1.0 range: [0.0 - 2.0])", NULL }, 46 | { "rate", 0, 0, G_OPTION_ARG_DOUBLE, &rate, 47 | "Rate of utterance (default: 1.0 range: [0.1 - 10.0])", NULL }, 48 | { "volume", 0, 0, G_OPTION_ARG_DOUBLE, &volume, 49 | "Volume of utterance (default: 1.0 range: [0.1 - 1.0])", NULL }, 50 | { "ssml", 0, 0, G_OPTION_ARG_NONE, &is_ssml, "Utterance is SSML markup", 51 | NULL }, 52 | { NULL } 53 | }; 54 | 55 | static void 56 | do_list_voices (SpielSpeaker *speaker) 57 | { 58 | GListModel *voices = spiel_speaker_get_voices (speaker); 59 | guint voices_count = g_list_model_get_n_items (voices); 60 | 61 | g_print ("%-25s %-10s %-10s %s\n", "NAME", "LANGUAGES", "IDENTIFIER", 62 | "PROVIDER"); 63 | for (guint i = 0; i < voices_count; i++) 64 | { 65 | g_autoptr (SpielVoice) voice = 66 | SPIEL_VOICE (g_list_model_get_object (voices, i)); 67 | g_autoptr (SpielProvider) provider = spiel_voice_get_provider (voice); 68 | g_autofree char *languages = 69 | g_strjoinv (",", (char **) spiel_voice_get_languages (voice)); 70 | g_print ("%-25s %-10s %-10s %s\n", spiel_voice_get_name (voice), 71 | languages, spiel_voice_get_identifier (voice), 72 | spiel_provider_get_identifier (provider)); 73 | } 74 | } 75 | 76 | static void 77 | do_list_providers (SpielSpeaker *speaker) 78 | { 79 | GListModel *providers = spiel_speaker_get_providers (speaker); 80 | guint providers_count = g_list_model_get_n_items (providers); 81 | 82 | g_print ("%-30s %s\n", "NAME", "IDENTIFIER"); 83 | for (guint i = 0; i < providers_count; i++) 84 | { 85 | g_autoptr (SpielProvider) provider = 86 | SPIEL_PROVIDER (g_list_model_get_object (providers, i)); 87 | g_print ("%-30s %s\n", spiel_provider_get_name (provider), 88 | spiel_provider_get_identifier (provider)); 89 | } 90 | } 91 | 92 | static void 93 | speaking_cb (SpielSpeaker *speaker, GParamSpec *pspec, gpointer user_data) 94 | { 95 | GMainLoop *loop = user_data; 96 | gboolean speaking = FALSE; 97 | g_object_get (speaker, "speaking", &speaking, NULL); 98 | 99 | if (!speaking) 100 | { 101 | g_main_loop_quit (loop); 102 | } 103 | } 104 | 105 | static SpielVoice * 106 | find_voice (SpielSpeaker *speaker) 107 | { 108 | if (!voice_id && provider_id) 109 | { 110 | // Get first voice of provider 111 | GListModel *providers = spiel_speaker_get_providers (speaker); 112 | g_autoptr (SpielProvider) provider = 113 | SPIEL_PROVIDER (g_list_model_get_object (providers, 0)); 114 | if (provider) 115 | { 116 | GListModel *voices = spiel_provider_get_voices (provider); 117 | return SPIEL_VOICE (g_list_model_get_object (voices, 0)); 118 | } 119 | 120 | return NULL; 121 | } 122 | 123 | if (voice_id) 124 | { 125 | GListModel *voices = spiel_speaker_get_voices (speaker); 126 | guint voices_count = g_list_model_get_n_items (voices); 127 | 128 | for (guint i = 0; i < voices_count; i++) 129 | { 130 | g_autoptr (SpielVoice) voice = 131 | SPIEL_VOICE (g_list_model_get_object (voices, i)); 132 | g_autoptr (SpielProvider) provider = spiel_voice_get_provider (voice); 133 | if (g_str_equal (voice_id, spiel_voice_get_identifier (voice)) && 134 | (!provider_id || 135 | g_str_equal (provider_id, 136 | spiel_provider_get_identifier (provider)))) 137 | { 138 | return g_steal_pointer (&voice); 139 | } 140 | } 141 | } 142 | 143 | return NULL; 144 | } 145 | 146 | static void 147 | do_speak (SpielSpeaker *speaker, const char *utterance_text) 148 | { 149 | g_autoptr (GMainLoop) loop = g_main_loop_new (NULL, FALSE); 150 | g_autoptr (SpielUtterance) utterance = spiel_utterance_new (utterance_text); 151 | g_autoptr (SpielVoice) voice = find_voice (speaker); 152 | 153 | if (voice) 154 | { 155 | spiel_utterance_set_voice (utterance, voice); 156 | } 157 | 158 | if (lang) 159 | { 160 | spiel_utterance_set_language (utterance, lang); 161 | } 162 | 163 | spiel_utterance_set_pitch (utterance, pitch); 164 | spiel_utterance_set_rate (utterance, rate); 165 | spiel_utterance_set_volume (utterance, volume); 166 | spiel_utterance_set_is_ssml (utterance, is_ssml); 167 | 168 | g_signal_connect (speaker, "notify::speaking", G_CALLBACK (speaking_cb), 169 | loop); 170 | 171 | spiel_speaker_speak (speaker, utterance); 172 | 173 | g_main_loop_run (loop); 174 | } 175 | 176 | int 177 | main (int argc, char *argv[]) 178 | { 179 | g_autoptr (GError) error = NULL; 180 | g_autoptr (GOptionContext) context; 181 | g_autoptr (SpielSpeaker) speaker; 182 | 183 | context = g_option_context_new ("- command line speech synthesis"); 184 | g_option_context_add_main_entries (context, entries, NULL); 185 | 186 | if (!g_option_context_parse (context, &argc, &argv, &error)) 187 | { 188 | g_print ("option parsing failed: %s\n", error->message); 189 | return 1; 190 | } 191 | 192 | speaker = spiel_speaker_new_sync (NULL, &error); 193 | if (!speaker) 194 | { 195 | g_print ("failed in instantiate speaker: %s\n", error->message); 196 | return 1; 197 | } 198 | 199 | if (list_voices) 200 | { 201 | do_list_voices (speaker); 202 | return 0; 203 | } 204 | 205 | if (list_providers) 206 | { 207 | do_list_providers (speaker); 208 | return 0; 209 | } 210 | 211 | do_speak (speaker, argv[argc - 1]); 212 | } 213 | -------------------------------------------------------------------------------- /libspiel/spiel-voices-list-model.c: -------------------------------------------------------------------------------- 1 | /* spiel-utterance.c 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #include "spiel-voices-list-model.h" 20 | 21 | #include "spiel-provider-private.h" 22 | #include "spiel-voice.h" 23 | 24 | /** 25 | * SpielVoicesListModel: 26 | * 27 | * Represents an aggregate of all the voices available to Spiel. 28 | * 29 | * 30 | */ 31 | 32 | struct _SpielVoicesListModel 33 | { 34 | GObject parent_instance; 35 | 36 | GListModel *providers; 37 | GListStore *mirrored_providers; // XXX: Used to track removed providers 38 | }; 39 | 40 | static void spiel_voices_list_model_iface_init (GListModelInterface *iface); 41 | 42 | G_DEFINE_TYPE_WITH_CODE ( 43 | SpielVoicesListModel, 44 | spiel_voices_list_model, 45 | G_TYPE_OBJECT, 46 | G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, 47 | spiel_voices_list_model_iface_init)); 48 | 49 | static void handle_providers_changed (GListModel *providers, 50 | guint position, 51 | guint removed, 52 | guint added, 53 | SpielVoicesListModel *self); 54 | 55 | SpielVoicesListModel * 56 | spiel_voices_list_model_new (GListModel *providers) 57 | { 58 | SpielVoicesListModel *self = 59 | g_object_new (SPIEL_TYPE_VOICES_LIST_MODEL, NULL); 60 | 61 | g_assert (G_IS_LIST_MODEL (providers)); 62 | g_assert_cmpint (g_list_model_get_n_items (providers), ==, 0); 63 | 64 | self->providers = g_object_ref (providers); 65 | g_signal_connect (self->providers, "items-changed", 66 | G_CALLBACK (handle_providers_changed), self); 67 | return self; 68 | } 69 | 70 | static void 71 | spiel_voices_list_model_finalize (GObject *object) 72 | { 73 | SpielVoicesListModel *self = (SpielVoicesListModel *) object; 74 | 75 | g_signal_handlers_disconnect_by_func ( 76 | self->providers, G_CALLBACK (handle_providers_changed), self); 77 | 78 | g_clear_object (&(self->providers)); 79 | 80 | g_clear_object (&(self->mirrored_providers)); 81 | 82 | G_OBJECT_CLASS (spiel_voices_list_model_parent_class)->finalize (object); 83 | } 84 | 85 | static void 86 | spiel_voices_list_model_class_init (SpielVoicesListModelClass *klass) 87 | { 88 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 89 | 90 | object_class->finalize = spiel_voices_list_model_finalize; 91 | } 92 | 93 | static void 94 | spiel_voices_list_model_init (SpielVoicesListModel *self) 95 | { 96 | self->providers = NULL; 97 | self->mirrored_providers = g_list_store_new (SPIEL_TYPE_PROVIDER); 98 | } 99 | 100 | static GType 101 | spiel_voices_list_model_get_item_type (GListModel *list) 102 | { 103 | return SPIEL_TYPE_VOICE; 104 | } 105 | 106 | static guint 107 | spiel_voices_list_model_get_n_items (GListModel *list) 108 | { 109 | SpielVoicesListModel *self = SPIEL_VOICES_LIST_MODEL (list); 110 | guint total = 0; 111 | guint providers_count = g_list_model_get_n_items (self->providers); 112 | 113 | for (guint i = 0; i < providers_count; i++) 114 | { 115 | g_autoptr (SpielProvider) provider = 116 | SPIEL_PROVIDER (g_list_model_get_object (self->providers, i)); 117 | GListModel *voices = G_LIST_MODEL (spiel_provider_get_voices (provider)); 118 | total += g_list_model_get_n_items (voices); 119 | } 120 | 121 | return total; 122 | } 123 | 124 | static gpointer 125 | spiel_voices_list_model_get_item (GListModel *list, guint position) 126 | { 127 | SpielVoicesListModel *self = SPIEL_VOICES_LIST_MODEL (list); 128 | guint total = 0; 129 | guint providers_count = g_list_model_get_n_items (self->providers); 130 | 131 | for (guint i = 0; i < providers_count; i++) 132 | { 133 | g_autoptr (SpielProvider) provider = 134 | SPIEL_PROVIDER (g_list_model_get_object (self->providers, i)); 135 | GListModel *voices = spiel_provider_get_voices (provider); 136 | guint voice_count = g_list_model_get_n_items (voices); 137 | 138 | if (position >= total && position < (total + voice_count)) 139 | { 140 | return g_list_model_get_item (voices, position - total); 141 | } 142 | total += voice_count; 143 | } 144 | 145 | return NULL; 146 | } 147 | 148 | static void 149 | spiel_voices_list_model_iface_init (GListModelInterface *iface) 150 | { 151 | iface->get_item_type = spiel_voices_list_model_get_item_type; 152 | iface->get_n_items = spiel_voices_list_model_get_n_items; 153 | iface->get_item = spiel_voices_list_model_get_item; 154 | } 155 | 156 | /* Notifications and such */ 157 | 158 | static void 159 | handle_voices_changed (GListModel *voices, 160 | guint position, 161 | guint removed, 162 | guint added, 163 | SpielVoicesListModel *self) 164 | { 165 | guint offset = 0; 166 | guint providers_count = g_list_model_get_n_items (self->providers); 167 | 168 | for (guint i = 0; i < providers_count; i++) 169 | { 170 | g_autoptr (SpielProvider) provider = 171 | SPIEL_PROVIDER (g_list_model_get_object (self->providers, i)); 172 | GListModel *provider_voices = spiel_provider_get_voices (provider); 173 | if (voices == provider_voices) 174 | { 175 | g_list_model_items_changed (G_LIST_MODEL (self), position + offset, 176 | removed, added); 177 | break; 178 | } 179 | offset += g_list_model_get_n_items (provider_voices); 180 | } 181 | } 182 | 183 | static void 184 | _connect_signals (SpielVoicesListModel *self, SpielProvider *provider) 185 | { 186 | GListModel *voices = spiel_provider_get_voices (provider); 187 | g_signal_connect (voices, "items-changed", G_CALLBACK (handle_voices_changed), 188 | self); 189 | } 190 | 191 | static void 192 | _disconnect_signals (SpielVoicesListModel *self, SpielProvider *provider) 193 | { 194 | GListModel *voices = spiel_provider_get_voices (provider); 195 | g_signal_handlers_disconnect_by_func ( 196 | voices, G_CALLBACK (handle_voices_changed), self); 197 | } 198 | 199 | static void 200 | handle_providers_changed (GListModel *providers, 201 | guint position, 202 | guint removed, 203 | guint added, 204 | SpielVoicesListModel *self) 205 | { 206 | guint removed_voices_count = 0; 207 | guint added_voices_count = 0; 208 | guint offset = 0; 209 | 210 | for (guint i = position; i < position + removed; i++) 211 | { 212 | g_autoptr (SpielProvider) provider = SPIEL_PROVIDER ( 213 | g_list_model_get_object (G_LIST_MODEL (self->mirrored_providers), i)); 214 | GListModel *voices = spiel_provider_get_voices (provider); 215 | removed_voices_count += g_list_model_get_n_items (voices); 216 | _disconnect_signals (self, provider); 217 | } 218 | 219 | g_list_store_splice (self->mirrored_providers, position, removed, NULL, 0); 220 | 221 | for (guint i = position; i < position + added; i++) 222 | { 223 | g_autoptr (SpielProvider) provider = 224 | SPIEL_PROVIDER (g_list_model_get_object (self->providers, i)); 225 | GListModel *voices = spiel_provider_get_voices (provider); 226 | added_voices_count += g_list_model_get_n_items (voices); 227 | _connect_signals (self, provider); 228 | g_list_store_insert (self->mirrored_providers, i, provider); 229 | } 230 | 231 | for (guint i = 0; i < position; i++) 232 | { 233 | g_autoptr (SpielProvider) provider = 234 | SPIEL_PROVIDER (g_list_model_get_object (self->providers, i)); 235 | GListModel *voices = spiel_provider_get_voices (provider); 236 | offset += g_list_model_get_n_items (voices); 237 | } 238 | 239 | g_list_model_items_changed (G_LIST_MODEL (self), offset, removed_voices_count, 240 | added_voices_count); 241 | } 242 | -------------------------------------------------------------------------------- /libspiel/spiel-provider-src.c: -------------------------------------------------------------------------------- 1 | /* spiel-provider-src.c 2 | * 3 | * Copyright (C) 2024 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #include "spiel-provider-src.h" 23 | 24 | static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ( 25 | "src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY); 26 | 27 | #define DEFAULT_FD 0 28 | 29 | enum 30 | { 31 | PROP_0, 32 | 33 | PROP_FD, 34 | 35 | PROP_LAST 36 | }; 37 | 38 | #define spiel_provider_src_parent_class parent_class 39 | G_DEFINE_TYPE (SpielProviderSrc, spiel_provider_src, GST_TYPE_PUSH_SRC); 40 | 41 | SpielProviderSrc * 42 | spiel_provider_src_new (gint fd) 43 | { 44 | return g_object_new (SPIEL_TYPE_PROVIDER_SRC, "fd", fd, NULL); 45 | } 46 | 47 | static void spiel_provider_src_set_property (GObject *object, 48 | guint prop_id, 49 | const GValue *value, 50 | GParamSpec *pspec); 51 | static void spiel_provider_src_get_property (GObject *object, 52 | guint prop_id, 53 | GValue *value, 54 | GParamSpec *pspec); 55 | static void spiel_provider_src_dispose (GObject *obj); 56 | 57 | static gboolean spiel_provider_src_start (GstBaseSrc *bsrc); 58 | static gboolean spiel_provider_src_stop (GstBaseSrc *bsrc); 59 | static gboolean spiel_provider_src_unlock (GstBaseSrc *bsrc); 60 | static gboolean spiel_provider_src_unlock_stop (GstBaseSrc *bsrc); 61 | static gboolean spiel_provider_src_get_size (GstBaseSrc *src, guint64 *size); 62 | 63 | static GstFlowReturn spiel_provider_src_create (GstPushSrc *psrc, 64 | GstBuffer **outbuf); 65 | 66 | static void 67 | spiel_provider_src_class_init (SpielProviderSrcClass *klass) 68 | { 69 | GObjectClass *gobject_class; 70 | GstElementClass *gstelement_class; 71 | GstBaseSrcClass *gstbasesrc_class; 72 | GstPushSrcClass *gstpush_src_class; 73 | 74 | gobject_class = G_OBJECT_CLASS (klass); 75 | gstelement_class = GST_ELEMENT_CLASS (klass); 76 | gstbasesrc_class = GST_BASE_SRC_CLASS (klass); 77 | gstpush_src_class = GST_PUSH_SRC_CLASS (klass); 78 | 79 | gobject_class->set_property = spiel_provider_src_set_property; 80 | gobject_class->get_property = spiel_provider_src_get_property; 81 | gobject_class->dispose = spiel_provider_src_dispose; 82 | 83 | g_object_class_install_property ( 84 | gobject_class, PROP_FD, 85 | g_param_spec_int ("fd", "fd", "An open file descriptor to read from", 0, 86 | G_MAXINT, DEFAULT_FD, 87 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | 88 | G_PARAM_CONSTRUCT_ONLY)); 89 | 90 | gst_element_class_set_static_metadata ( 91 | gstelement_class, "Spiel Provider Source", "Source", 92 | "Read specialized audio/event chunks from pipe", 93 | "Eitan Isaacson "); 94 | gst_element_class_add_static_pad_template (gstelement_class, &srctemplate); 95 | 96 | gstbasesrc_class->start = GST_DEBUG_FUNCPTR (spiel_provider_src_start); 97 | gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (spiel_provider_src_stop); 98 | gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR (spiel_provider_src_unlock); 99 | gstbasesrc_class->unlock_stop = 100 | GST_DEBUG_FUNCPTR (spiel_provider_src_unlock_stop); 101 | gstbasesrc_class->get_size = GST_DEBUG_FUNCPTR (spiel_provider_src_get_size); 102 | 103 | gstpush_src_class->create = GST_DEBUG_FUNCPTR (spiel_provider_src_create); 104 | } 105 | 106 | static void 107 | spiel_provider_src_init (SpielProviderSrc *spsrc) 108 | { 109 | spsrc->curoffset = 0; 110 | spsrc->reader = NULL; 111 | } 112 | 113 | static void 114 | spiel_provider_src_dispose (GObject *obj) 115 | { 116 | SpielProviderSrc *src = SPIEL_PROVIDER_SRC (obj); 117 | 118 | g_clear_object (&src->reader); 119 | 120 | G_OBJECT_CLASS (parent_class)->dispose (obj); 121 | } 122 | 123 | static gboolean 124 | spiel_provider_src_start (GstBaseSrc *bsrc) 125 | { 126 | SpielProviderSrc *src = SPIEL_PROVIDER_SRC (bsrc); 127 | gboolean got_header = 128 | speech_provider_stream_reader_get_stream_header (src->reader); 129 | return got_header; 130 | } 131 | 132 | static gboolean 133 | spiel_provider_src_stop (GstBaseSrc *bsrc) 134 | { 135 | // XXX: Do we need this? 136 | return TRUE; 137 | } 138 | 139 | static gboolean 140 | spiel_provider_src_unlock (GstBaseSrc *bsrc) 141 | { 142 | // XXX: Do we need this? 143 | return TRUE; 144 | } 145 | 146 | static gboolean 147 | spiel_provider_src_unlock_stop (GstBaseSrc *bsrc) 148 | { 149 | // XXX: Do we need this? 150 | return TRUE; 151 | } 152 | 153 | static void 154 | spiel_provider_src_set_property (GObject *object, 155 | guint prop_id, 156 | const GValue *value, 157 | GParamSpec *pspec) 158 | { 159 | SpielProviderSrc *src = SPIEL_PROVIDER_SRC (object); 160 | 161 | switch (prop_id) 162 | { 163 | case PROP_FD: 164 | 165 | GST_OBJECT_LOCK (object); 166 | g_assert (src->reader == NULL); 167 | src->fd = g_value_get_int (value); 168 | src->reader = speech_provider_stream_reader_new (src->fd); 169 | GST_OBJECT_UNLOCK (object); 170 | break; 171 | default: 172 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 173 | break; 174 | } 175 | } 176 | 177 | static void 178 | spiel_provider_src_get_property (GObject *object, 179 | guint prop_id, 180 | GValue *value, 181 | GParamSpec *pspec) 182 | { 183 | SpielProviderSrc *src = SPIEL_PROVIDER_SRC (object); 184 | 185 | switch (prop_id) 186 | { 187 | case PROP_FD: 188 | g_value_set_int (value, src->fd); 189 | break; 190 | default: 191 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 192 | break; 193 | } 194 | } 195 | 196 | static GstFlowReturn 197 | spiel_provider_src_create (GstPushSrc *psrc, GstBuffer **outbuf) 198 | { 199 | SpielProviderSrc *src; 200 | 201 | src = SPIEL_PROVIDER_SRC (psrc); 202 | 203 | while (TRUE) 204 | { 205 | guint8 *chunk = NULL; 206 | guint32 chunk_size = 0; 207 | SpeechProviderEventType event_type = SPEECH_PROVIDER_EVENT_TYPE_NONE; 208 | guint32 range_start = 0; 209 | guint32 range_end = 0; 210 | g_autofree char *mark_name = NULL; 211 | gboolean got_event, got_audio; 212 | got_event = speech_provider_stream_reader_get_event ( 213 | src->reader, &event_type, &range_start, &range_end, &mark_name); 214 | if (got_event) 215 | { 216 | gst_element_post_message ( 217 | GST_ELEMENT_CAST (src), 218 | gst_message_new_element ( 219 | GST_OBJECT_CAST (src), 220 | gst_structure_new ("SpielGoingToSpeak", "event_type", 221 | G_TYPE_UINT, event_type, "range_start", 222 | G_TYPE_UINT, range_start, "range_end", 223 | G_TYPE_UINT, range_end, "mark_name", 224 | G_TYPE_STRING, mark_name, NULL))); 225 | } 226 | 227 | got_audio = speech_provider_stream_reader_get_audio (src->reader, &chunk, 228 | &chunk_size); 229 | if (got_audio && chunk_size > 0) 230 | { 231 | GstBuffer *buf = gst_buffer_new_wrapped (chunk, chunk_size); 232 | 233 | GST_BUFFER_OFFSET (buf) = src->curoffset; 234 | GST_BUFFER_TIMESTAMP (buf) = GST_CLOCK_TIME_NONE; 235 | src->curoffset += chunk_size; 236 | 237 | *outbuf = buf; 238 | 239 | return GST_FLOW_OK; 240 | } 241 | 242 | if (!got_audio && !got_event) 243 | { 244 | GST_DEBUG_OBJECT (psrc, "Read 0 bytes. EOS."); 245 | return GST_FLOW_EOS; 246 | } 247 | } 248 | 249 | return GST_FLOW_OK; 250 | } 251 | 252 | static gboolean 253 | spiel_provider_src_get_size (GstBaseSrc *bsrc, guint64 *size) 254 | { 255 | // XXX: Get rid of this? 256 | return FALSE; 257 | } 258 | -------------------------------------------------------------------------------- /tests/_common.py: -------------------------------------------------------------------------------- 1 | import unittest, os, dbus 2 | import dbus.mainloop.glib 3 | from gi.repository import GLib, Gio 4 | 5 | import gi 6 | 7 | gi.require_version("Spiel", "1.0") 8 | from gi.repository import Spiel 9 | 10 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 11 | 12 | LOG_EVENTS = False 13 | 14 | STANDARD_VOICES = [ 15 | ["org.mock2.Speech.Provider", "English (Great Britain)", "gmw/en", ["en-gb", "en"]], 16 | [ 17 | "org.mock2.Speech.Provider", 18 | "English (Scotland)", 19 | "gmw/en-GB-scotland#misconfigured", 20 | ["en-gb-scotland", "en"], 21 | ], 22 | [ 23 | "org.mock2.Speech.Provider", 24 | "English (Lancaster)", 25 | "gmw/en-GB-x-gbclan", 26 | ["en-gb-x-gbclan", "en-gb", "en"], 27 | ], 28 | ["org.mock2.Speech.Provider", "English (America)", "gmw/en-US", ["en-us", "en"]], 29 | [ 30 | "org.mock.Speech.Provider", 31 | "Armenian (East Armenia)", 32 | "ine/hy", 33 | ["hy", "hy-arevela"], 34 | ], 35 | [ 36 | "org.mock2.Speech.Provider", 37 | "Armenian (West Armenia)", 38 | "ine/hyw", 39 | ["hyw", "hy-arevmda", "hy"], 40 | ], 41 | [ 42 | "org.mock.Speech.Provider", 43 | "Chinese (Cantonese)", 44 | "sit/yue", 45 | ["yue", "zh-yue", "zh"], 46 | ], 47 | ["org.mock3.Speech.Provider", "Uzbek", "trk/uz", ["uz"]], 48 | ] 49 | 50 | 51 | class BaseSpielTest(unittest.TestCase): 52 | def __init__(self, *args): 53 | super().__init__(*args) 54 | 55 | def setUp(self): 56 | self.mock_service = self.mock_iface("org.mock.Speech.Provider") 57 | self.mock_service.SetInfinite(False) 58 | self.mock_service.FlushTasks() 59 | 60 | def tearDown(self): 61 | settings = Gio.Settings.new("org.monotonous.libspiel") 62 | settings["default-voice"] = None 63 | settings["language-voice-mapping"] = {} 64 | discarded_dir = os.environ["TEST_DISCARDED_SERVICE_DIR"] 65 | service_dir = os.environ["TEST_SERVICE_DIR"] 66 | for fname in os.listdir(discarded_dir): 67 | os.rename( 68 | os.path.join(discarded_dir, fname), os.path.join(service_dir, fname) 69 | ) 70 | 71 | def mock_iface(self, provider_name): 72 | session_bus = dbus.SessionBus() 73 | proxy = session_bus.get_object( 74 | provider_name, 75 | f"/{'/'.join(provider_name.split('.'))}", 76 | ) 77 | return dbus.Interface( 78 | proxy, dbus_interface="org.freedesktop.Speech.MockProvider" 79 | ) 80 | 81 | def kill_provider(self, provider_name): 82 | try: 83 | self.mock_iface(provider_name).KillMe() 84 | except: 85 | pass 86 | 87 | def list_active_providers(self): 88 | session_bus = dbus.SessionBus() 89 | bus_obj = session_bus.get_object( 90 | "org.freedesktop.DBus", "/org/freedesktop/DBus" 91 | ) 92 | iface = dbus.Interface(bus_obj, "org.freedesktop.DBus") 93 | speech_providers = filter( 94 | lambda s: s.endswith(".Speech.Provider"), 95 | iface.ListNames(), 96 | ) 97 | return [str(s) for s in speech_providers] 98 | 99 | def wait_for_async_speaker_init(self): 100 | def _init_cb(source, result, user_data): 101 | user_data.append(Spiel.Speaker.new_finish(result)) 102 | loop.quit() 103 | 104 | speakerContainer = [] 105 | Spiel.Speaker.new(None, _init_cb, speakerContainer) 106 | loop = GLib.MainLoop() 107 | loop.run() 108 | return speakerContainer[0] 109 | 110 | def wait_for_provider_to_go_away(self, name): 111 | def _cb(*args): 112 | self.assertNotIn(name, self.list_active_providers()) 113 | session_bus.remove_signal_receiver( 114 | _cb, 115 | bus_name="org.freedesktop.DBus", 116 | dbus_interface="org.freedesktop.DBus", 117 | signal_name="NameOwnerChanged", 118 | path="/org/freedesktop/DBus", 119 | arg0=name, 120 | ) 121 | GLib.idle_add(loop.quit) 122 | 123 | speech_providers = self.list_active_providers() 124 | if name not in speech_providers: 125 | return 126 | 127 | session_bus = dbus.SessionBus() 128 | sig_match = session_bus.add_signal_receiver( 129 | _cb, 130 | bus_name="org.freedesktop.DBus", 131 | dbus_interface="org.freedesktop.DBus", 132 | signal_name="NameOwnerChanged", 133 | path="/org/freedesktop/DBus", 134 | arg0=name, 135 | ) 136 | loop = GLib.MainLoop() 137 | loop.run() 138 | 139 | def wait_for_voices_changed(self, speaker, added=[], removed=[]): 140 | voices = speaker.props.voices 141 | 142 | def _cb(*args): 143 | voice_ids = [v.props.identifier for v in voices] 144 | for a in added: 145 | if a not in voice_ids: 146 | return 147 | for r in removed: 148 | if r in voice_ids: 149 | return 150 | voices.disconnect_by_func(_cb) 151 | loop.quit() 152 | 153 | voices.connect("items-changed", _cb) 154 | loop = GLib.MainLoop() 155 | loop.run() 156 | 157 | def wait_for_speaking_done(self, speaker, action): 158 | def _cb(*args): 159 | if not speaker.props.speaking: 160 | speaker.disconnect_by_func(_cb) 161 | loop.quit() 162 | 163 | speaker.connect("notify::speaking", _cb) 164 | action() 165 | loop = GLib.MainLoop() 166 | loop.run() 167 | 168 | def uninstall_provider(self, name): 169 | src = os.path.join( 170 | os.environ["TEST_SERVICE_DIR"], f"{name}{os.path.extsep}service" 171 | ) 172 | dest = os.path.join( 173 | os.environ["TEST_DISCARDED_SERVICE_DIR"], f"{name}{os.path.extsep}service" 174 | ) 175 | os.rename(src, dest) 176 | 177 | def install_provider(self, name): 178 | src = os.path.join( 179 | os.environ["TEST_DISCARDED_SERVICE_DIR"], f"{name}{os.path.extsep}service" 180 | ) 181 | dest = os.path.join( 182 | os.environ["TEST_SERVICE_DIR"], f"{name}{os.path.extsep}service" 183 | ) 184 | os.rename(src, dest) 185 | 186 | def get_voice(self, synth, provider_identifier, voice_id): 187 | for v in synth.props.voices: 188 | if ( 189 | v.props.provider.get_identifier() == provider_identifier 190 | and v.props.identifier == voice_id 191 | ): 192 | return v 193 | 194 | def capture_speak_sequence(self, speaker, *utterances): 195 | event_sequence = [] 196 | 197 | def _append_to_sequence(signal_and_args): 198 | event_sequence.append(signal_and_args) 199 | if LOG_EVENTS: 200 | print(signal_and_args) 201 | 202 | def _notify_speaking_cb(synth, val): 203 | _append_to_sequence(["notify:speaking", synth.props.speaking]) 204 | if not synth.props.speaking: 205 | loop.quit() 206 | 207 | def _notify_paused_cb(synth, val): 208 | _append_to_sequence(["notify:paused", synth.props.paused]) 209 | 210 | def _utterance_started_cb(synth, utt): 211 | _append_to_sequence(["utterance-started", utt]) 212 | 213 | def _utterance_word_started_cb(synth, utt, start, end): 214 | _append_to_sequence(["word-started", utt, start, end]) 215 | 216 | def _utterance_sentence_started_cb(synth, utt, start, end): 217 | _append_to_sequence(["sentence-started", utt, start, end]) 218 | 219 | def _utterance_canceled_cb(synth, utt): 220 | _append_to_sequence(["utterance-canceled", utt]) 221 | 222 | def _utterance_finished_cb(synth, utt): 223 | _append_to_sequence(["utterance-finished", utt]) 224 | 225 | def _utterance_error_cb(synth, utt, error): 226 | _append_to_sequence( 227 | ["utterance-error", utt, (error.domain, error.code, error.message)] 228 | ) 229 | if not synth.props.speaking: 230 | loop.quit() 231 | 232 | speaker.connect("notify::speaking", _notify_speaking_cb) 233 | speaker.connect("notify::paused", _notify_paused_cb) 234 | speaker.connect("utterance-started", _utterance_started_cb) 235 | speaker.connect("utterance-canceled", _utterance_canceled_cb) 236 | speaker.connect("utterance-finished", _utterance_finished_cb) 237 | speaker.connect("utterance-error", _utterance_error_cb) 238 | speaker.connect("word-started", _utterance_word_started_cb) 239 | speaker.connect("sentence-started", _utterance_sentence_started_cb) 240 | 241 | def do_speak(): 242 | for utterance in utterances: 243 | speaker.speak(utterance) 244 | 245 | GLib.idle_add(do_speak) 246 | 247 | loop = GLib.MainLoop() 248 | loop.run() 249 | 250 | return event_sequence 251 | 252 | 253 | def test_main(): 254 | from tap.runner import TAPTestRunner 255 | 256 | runner = TAPTestRunner() 257 | runner.set_stream(True) 258 | unittest.main(testRunner=runner) 259 | -------------------------------------------------------------------------------- /tests/services/mock_speech_provider.py.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import gi 4 | 5 | gi.require_version("Gst", "1.0") 6 | gi.require_version("SpeechProvider", "1.0") 7 | from gi.repository import GLib, Gst, SpeechProvider 8 | 9 | from dasbus.connection import SessionMessageBus 10 | from dasbus.unix import GLibServerUnix 11 | from dasbus.server.property import PropertiesInterface 12 | from xml.dom.minidom import parse, parseString 13 | import re 14 | import os 15 | from os import getcwd 16 | from sys import argv 17 | 18 | Gst.init(None) 19 | 20 | NAME = argv[-1] if len(argv) > 1 else "mock" 21 | 22 | AUTOEXIT = NAME == "mock3" 23 | 24 | VOICES = { 25 | "mock": [ 26 | { 27 | "name": "Chinese (Cantonese)", 28 | "output_format": "audio/x-raw,format=S16LE,channels=1,rate=22050", 29 | "identifier": "sit/yue", 30 | "features": SpeechProvider.VoiceFeature.SSML_SAY_AS_CARDINAL 31 | | SpeechProvider.VoiceFeature.SSML_SAY_AS_ORDINAL, 32 | "languages": ["yue", "zh-yue", "zh"], 33 | }, 34 | { 35 | "name": "Armenian (East Armenia)", 36 | "output_format": "audio/x-raw,format=S16LE,channels=1,rate=22050", 37 | "identifier": "ine/hy", 38 | "features": 0, 39 | "languages": ["hy", "hy-arevela"], 40 | }, 41 | ], 42 | "mock2": [ 43 | { 44 | "name": "Armenian (West Armenia)", 45 | "output_format": "audio/x-raw,format=S16LE,channels=1,rate=22050", 46 | "identifier": "ine/hyw", 47 | "features": 0, 48 | "languages": ["hyw", "hy-arevmda", "hy"], 49 | }, 50 | { 51 | "name": "English (Scotland)", 52 | "output_format": "nuthin", 53 | "identifier": "gmw/en-GB-scotland#misconfigured", 54 | "features": 0, 55 | "languages": ["en-gb-scotland", "en"], 56 | }, 57 | { 58 | "name": "English (Lancaster)", 59 | "output_format": "audio/x-raw,format=S16LE,channels=1,rate=22050", 60 | "identifier": "gmw/en-GB-x-gbclan", 61 | "features": 0, 62 | "languages": ["en-gb-x-gbclan", "en-gb", "en"], 63 | }, 64 | { 65 | "name": "English (America)", 66 | "output_format": "audio/x-spiel,format=S16LE,channels=1,rate=22050", 67 | "identifier": "gmw/en-US", 68 | "features": 0, 69 | "languages": ["en-us", "en"], 70 | }, 71 | { 72 | "name": "English (Great Britain)", 73 | "output_format": "audio/x-raw,format=S16LE,channels=1,rate=22050", 74 | "identifier": "gmw/en", 75 | "features": 0, 76 | "languages": ["en-gb", "en"], 77 | }, 78 | ], 79 | "mock3": [ 80 | { 81 | "name": "Uzbek", 82 | "output_format": "audio/x-raw,format=S16LE,channels=1,rate=22050", 83 | "identifier": "trk/uz", 84 | "features": 0, 85 | "languages": ["uz"], 86 | }, 87 | ], 88 | } 89 | 90 | 91 | class RawSynthStream(object): 92 | def __init__(self, fd, text, indefinite, silent): 93 | elements = [ 94 | "audiotestsrc num-buffers=%d wave=%s name=src" 95 | % (-1 if indefinite else 10, "silence" if silent else "sine"), 96 | "audioconvert", 97 | "audio/x-raw,format=S16LE,channels=1,rate=22050", 98 | "fdsink name=sink", 99 | ] 100 | self._pipeline = Gst.parse_launch(" ! ".join(elements)) 101 | self._pipeline.get_by_name("sink").set_property("fd", fd) 102 | bus = self._pipeline.get_bus() 103 | bus.add_signal_watch() 104 | bus.connect("message::eos", self._on_eos) 105 | 106 | def start(self): 107 | self._pipeline.set_state(Gst.State.PLAYING) 108 | 109 | def end(self): 110 | src = self._pipeline.get_by_name("src") 111 | src.set_state(Gst.State.NULL) 112 | raw_fd = self._pipeline.get_by_name("sink").get_property("fd") 113 | os.close(raw_fd) 114 | 115 | def _on_eos(self, *args): 116 | src = self._pipeline.get_by_name("src") 117 | raw_fd = self._pipeline.get_by_name("sink").get_property("fd") 118 | os.close(raw_fd) 119 | src.set_state(Gst.State.NULL) 120 | 121 | 122 | class SpielSynthStream(object): 123 | def __init__(self, fd, text, indefinite, silent): 124 | self.ranges = ranges = [ 125 | [m.start(), m.end()] for m in re.finditer(r".*?\b\W\s?", text, re.MULTILINE) 126 | ] 127 | num_buffers = -1 128 | if not indefinite: 129 | num_buffers = max(10, len(self.ranges) + 1) 130 | elements = [ 131 | "audiotestsrc num-buffers=%d wave=%s name=src" 132 | % (-1 if indefinite else 10, "silence" if silent else "sine"), 133 | "audioconvert", 134 | "audio/x-raw,format=S16LE,channels=1,rate=22050", 135 | "appsink emit-signals=True name=sink", 136 | ] 137 | self._pipeline = Gst.parse_launch(" ! ".join(elements)) 138 | sink = self._pipeline.get_by_name("sink") 139 | sink.connect("new-sample", self._on_new_sample) 140 | sink.connect("eos", self._on_eos) 141 | 142 | self.stream_writer = SpeechProvider.StreamWriter.new(fd) 143 | 144 | def _on_new_sample(self, sink): 145 | sample = sink.emit("pull-sample") 146 | buffer = sample.get_buffer() 147 | b = buffer.extract_dup(0, buffer.get_size()) 148 | # Some chaos 149 | self.stream_writer.send_audio(b"") 150 | if self.ranges: 151 | start, end = self.ranges.pop(0) 152 | if len(self.ranges) % 2: 153 | self.stream_writer.send_event( 154 | SpeechProvider.EventType.SENTENCE, start, end, "" 155 | ) 156 | self.stream_writer.send_event(SpeechProvider.EventType.WORD, start, end, "") 157 | self.stream_writer.send_audio(b) 158 | 159 | return Gst.FlowReturn.OK 160 | 161 | def end(self): 162 | self._pipeline.set_state(Gst.State.NULL) 163 | self.stream_writer.close() 164 | 165 | def start(self): 166 | self.stream_writer.send_stream_header() 167 | self._pipeline.set_state(Gst.State.PLAYING) 168 | 169 | def _on_eos(self, *args): 170 | self.stream_writer.close() 171 | 172 | 173 | class SomeObject(PropertiesInterface): 174 | __dbus_xml__ = """ 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | """ 202 | 203 | def __init__(self): 204 | self._last_speak_args = [0, "", "", 0, 0, 0, ""] 205 | self._infinite = False 206 | self._stream = None 207 | self._voices = VOICES[NAME][:] 208 | super().__init__() 209 | 210 | def Synthesize(self, fd, utterance, voice_id, pitch, rate, is_ssml, language): 211 | if utterance == "die": 212 | # special utterance text that makes us die 213 | self.byebye() 214 | self._last_speak_args = (fd, utterance, voice_id, pitch, rate, is_ssml, language) 215 | voice = dict([[v["identifier"], v] for v in self._voices])[voice_id] 216 | output_format = voice["output_format"] 217 | synthstream_cls = RawSynthStream 218 | if output_format.startswith("audio/x-spiel"): 219 | synthstream_cls = SpielSynthStream 220 | 221 | self.stream = synthstream_cls( 222 | fd, utterance, self._infinite, utterance == "silent" 223 | ) 224 | self.stream.start() 225 | 226 | @property 227 | def Voices(self): 228 | if AUTOEXIT: 229 | GLib.timeout_add(500, self.byebye) 230 | voices = [ 231 | ( 232 | v["name"], 233 | v["identifier"], 234 | v["output_format"], 235 | v["features"], 236 | v["languages"], 237 | ) 238 | for v in self._voices 239 | ] 240 | return voices 241 | 242 | @property 243 | def Name(self): 244 | return NAME 245 | 246 | def GetLastSpeakArguments(self): 247 | return self._last_speak_args 248 | 249 | def FlushTasks(self): 250 | self.stream = None 251 | self._last_speak_args = [0, "", "", 0, 0, 0] 252 | 253 | def SetInfinite(self, val): 254 | self._infinite = val 255 | 256 | def End(self): 257 | if self.stream: 258 | self.stream.end() 259 | 260 | def AddVoice(self, name, identifier, languages): 261 | self._voices.append( 262 | { 263 | "name": name, 264 | "identifier": identifier, 265 | "output_format": "audio/x-raw,format=S16LE,channels=1,rate=22050", 266 | "features": 0, 267 | "languages": languages, 268 | } 269 | ) 270 | GLib.idle_add(self.report_changed_voices) 271 | 272 | def RemoveVoice(self, identifier): 273 | self._voices = [v for v in self._voices if v["identifier"] != identifier] 274 | GLib.idle_add(self.report_changed_voices) 275 | 276 | def report_changed_voices(self): 277 | self.report_changed_property("Voices") 278 | self.flush_changes() 279 | 280 | def byebye(self): 281 | exit() 282 | 283 | 284 | # Add speech provider interface 285 | dbus_xml = parseString(SomeObject.__dbus_xml__) 286 | node = dbus_xml.getElementsByTagName("node")[0] 287 | provider_iface_xml = parse("@provider_iface@").getElementsByTagName("interface")[0] 288 | provider_iface_xml.parentNode.removeChild(provider_iface_xml) 289 | node.appendChild(provider_iface_xml) 290 | SomeObject.__dbus_xml__ = dbus_xml.toxml() 291 | 292 | if __name__ == "__main__": 293 | bus = SessionMessageBus() 294 | bus.publish_object( 295 | f"/org/{NAME}/Speech/Provider", SomeObject(), server=GLibServerUnix 296 | ) 297 | bus.register_service(f"org.{NAME}.Speech.Provider") 298 | 299 | mainloop = GLib.MainLoop() 300 | mainloop.run() 301 | -------------------------------------------------------------------------------- /libspiel/spiel-voice.c: -------------------------------------------------------------------------------- 1 | /* spiel-voice.c 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #include "spiel.h" 20 | 21 | #include "spiel-provider.h" 22 | #include "spiel-voice.h" 23 | 24 | /** 25 | * SpielVoice: 26 | * 27 | * Represents a voice implemented by a speech provider. 28 | * 29 | * A DBus speech provider advertises a list of voices that it implements. 30 | * Each voice will have human-readable name, a unique identifier, and a list 31 | * of languages supported by the voice. 32 | * 33 | */ 34 | 35 | struct _SpielVoice 36 | { 37 | GObject parent_instance; 38 | char *name; 39 | char *identifier; 40 | char **languages; 41 | char *output_format; 42 | SpielVoiceFeature features; 43 | GWeakRef provider; 44 | }; 45 | 46 | G_DEFINE_FINAL_TYPE (SpielVoice, spiel_voice, G_TYPE_OBJECT) 47 | 48 | enum 49 | { 50 | PROP_0, 51 | PROP_NAME, 52 | PROP_IDENTIFIER, 53 | PROP_LANGUAGES, 54 | PROP_PROVIDER, 55 | PROP_FEATURES, 56 | N_PROPS 57 | }; 58 | 59 | static GParamSpec *properties[N_PROPS]; 60 | 61 | /** 62 | * spiel_voice_get_name: (get-property name) 63 | * @self: a `SpielVoice` 64 | * 65 | * Gets the name. 66 | * 67 | * Returns: (transfer none) (not nullable): the name 68 | * 69 | * Since: 1.0 70 | */ 71 | const char * 72 | spiel_voice_get_name (SpielVoice *self) 73 | { 74 | g_return_val_if_fail (SPIEL_IS_VOICE (self), NULL); 75 | 76 | return self->name; 77 | } 78 | 79 | /** 80 | * spiel_voice_get_identifier: (get-property identifier) 81 | * @self: a `SpielVoice` 82 | * 83 | * Gets the identifier, unique to [property@Spiel.Voice:provider]. 84 | * 85 | * Returns: (transfer none) (not nullable): the identifier 86 | * 87 | * Since: 1.0 88 | */ 89 | const char * 90 | spiel_voice_get_identifier (SpielVoice *self) 91 | { 92 | g_return_val_if_fail (SPIEL_IS_VOICE (self), NULL); 93 | 94 | return self->identifier; 95 | } 96 | 97 | /** 98 | * spiel_voice_get_provider: (get-property provider) 99 | * @self: a `SpielVoice` 100 | * 101 | * Gets the provider associated with this voice 102 | * 103 | * Returns: (transfer full) (nullable): a `SpielProvider` 104 | * 105 | * Since: 1.0 106 | */ 107 | SpielProvider * 108 | spiel_voice_get_provider (SpielVoice *self) 109 | { 110 | g_return_val_if_fail (SPIEL_IS_VOICE (self), NULL); 111 | 112 | return g_weak_ref_get (&self->provider); 113 | } 114 | 115 | /** 116 | * spiel_voice_get_languages: (get-property languages) 117 | * @self: a `SpielVoice` 118 | * 119 | * Gets the list of supported languages. 120 | * 121 | * Returns: (transfer none) (not nullable): a list of BCP 47 tags 122 | * 123 | * Since: 1.0 124 | */ 125 | const char *const * 126 | spiel_voice_get_languages (SpielVoice *self) 127 | { 128 | g_return_val_if_fail (SPIEL_IS_VOICE (self), NULL); 129 | 130 | return (const char *const *) self->languages; 131 | } 132 | 133 | /** 134 | * spiel_voice_get_features: (get-property features) 135 | * @self: a `SpielVoice` 136 | * 137 | * Gets the list of supported features. 138 | * 139 | * Returns: a bit-field of `SpielVoiceFeature` 140 | * 141 | * Since: 1.0 142 | */ 143 | SpielVoiceFeature 144 | spiel_voice_get_features (SpielVoice *self) 145 | { 146 | g_return_val_if_fail (SPIEL_IS_VOICE (self), SPIEL_VOICE_FEATURE_NONE); 147 | 148 | return self->features; 149 | } 150 | 151 | /** 152 | * spiel_voice_get_output_format: (get-property output-format) 153 | * @self: a `SpielVoice` 154 | * 155 | * Gets the output format. 156 | * 157 | * Since: 1.0 158 | */ 159 | const char * 160 | spiel_voice_get_output_format (SpielVoice *self) 161 | { 162 | g_return_val_if_fail (SPIEL_IS_VOICE (self), NULL); 163 | 164 | return self->output_format; 165 | } 166 | 167 | /** 168 | * spiel_voice_set_output_format: (set-property output-format) 169 | * @self: a `SpielVoice` 170 | * @output_format: (not nullable): an output format string. 171 | * 172 | * Sets the audio output format. 173 | * 174 | * Since: 1.0 175 | */ 176 | void 177 | spiel_voice_set_output_format (SpielVoice *self, const char *output_format) 178 | { 179 | g_return_if_fail (SPIEL_IS_VOICE (self)); 180 | g_return_if_fail (output_format != NULL && *output_format != '\0'); 181 | 182 | g_clear_pointer (&self->output_format, g_free); 183 | self->output_format = g_strdup (output_format); 184 | } 185 | 186 | /** 187 | * spiel_voice_hash: 188 | * @self: (not nullable): a `SpielVoice` 189 | * 190 | * Converts a [class@Spiel.Voice] to a hash value. 191 | * 192 | * Returns: a hash value corresponding to @self 193 | * 194 | * Since: 1.0 195 | */ 196 | guint 197 | spiel_voice_hash (SpielVoice *self) 198 | { 199 | g_autoptr (SpielProvider) provider = NULL; 200 | guint hash = 0; 201 | 202 | g_return_val_if_fail (SPIEL_IS_VOICE (self), 0); 203 | 204 | provider = spiel_voice_get_provider (self); 205 | hash = g_str_hash (self->name); 206 | hash = (hash << 5) - hash + g_str_hash (self->identifier); 207 | if (provider) 208 | { 209 | hash = (hash << 5) - hash + 210 | g_str_hash (spiel_provider_get_identifier (provider)); 211 | } 212 | 213 | for (char **language = self->languages; *language; language++) 214 | { 215 | hash = (hash << 5) - hash + g_str_hash (*language); 216 | } 217 | 218 | return hash; 219 | } 220 | 221 | /** 222 | * spiel_voice_equal: 223 | * @self: (not nullable): a `SpielVoice` 224 | * @other: (not nullable): a `SpielVoice` to compare with @self 225 | * 226 | * Compares the two [class@Spiel.Voice] values and returns %TRUE if equal. 227 | * 228 | * Returns: %TRUE if the two voices match. 229 | * 230 | * Since: 1.0 231 | */ 232 | gboolean 233 | spiel_voice_equal (SpielVoice *self, SpielVoice *other) 234 | { 235 | g_autoptr (SpielProvider) self_provider = NULL; 236 | g_autoptr (SpielProvider) other_provider = NULL; 237 | 238 | g_return_val_if_fail (SPIEL_IS_VOICE (self), FALSE); 239 | g_return_val_if_fail (SPIEL_IS_VOICE (other), FALSE); 240 | 241 | self_provider = g_weak_ref_get (&self->provider); 242 | other_provider = g_weak_ref_get (&other->provider); 243 | 244 | if (self_provider != other_provider) 245 | { 246 | return FALSE; 247 | } 248 | 249 | if (!g_str_equal (self->name, other->name)) 250 | { 251 | return FALSE; 252 | } 253 | 254 | if (!g_str_equal (self->identifier, other->identifier)) 255 | { 256 | return FALSE; 257 | } 258 | 259 | if (!g_strv_equal ((const gchar *const *) self->languages, 260 | (const gchar *const *) other->languages)) 261 | { 262 | return FALSE; 263 | } 264 | 265 | return TRUE; 266 | } 267 | 268 | /** 269 | * spiel_voice_compare: 270 | * @self: (not nullable): a `SpielVoice` 271 | * @other: (not nullable): a `SpielVoice` to compare with @self 272 | * @user_data: user-defined callback data 273 | * 274 | * Compares the two [class@Spiel.Voice] values and returns a negative integer 275 | * if the first value comes before the second, 0 if they are equal, or a 276 | * positive integer if the first value comes after the second. 277 | * 278 | * Returns: an integer indicating order 279 | * 280 | * Since: 1.0 281 | */ 282 | gint 283 | spiel_voice_compare (SpielVoice *self, SpielVoice *other, gpointer user_data) 284 | { 285 | g_autoptr (SpielProvider) self_provider = NULL; 286 | g_autoptr (SpielProvider) other_provider = NULL; 287 | int cmp = 0; 288 | 289 | g_return_val_if_fail (SPIEL_IS_VOICE (self), 0); 290 | g_return_val_if_fail (SPIEL_IS_VOICE (other), 0); 291 | 292 | self_provider = g_weak_ref_get (&self->provider); 293 | other_provider = g_weak_ref_get (&other->provider); 294 | 295 | if ((cmp = g_strcmp0 ( 296 | self_provider ? spiel_provider_get_identifier (self_provider) : "", 297 | other_provider ? spiel_provider_get_identifier (other_provider) 298 | : ""))) 299 | { 300 | return cmp; 301 | } 302 | 303 | if ((cmp = g_strcmp0 (self->name, other->name))) 304 | { 305 | return cmp; 306 | } 307 | 308 | if ((cmp = g_strcmp0 (self->identifier, other->identifier))) 309 | { 310 | return cmp; 311 | } 312 | 313 | return cmp; 314 | } 315 | 316 | static void 317 | spiel_voice_finalize (GObject *object) 318 | { 319 | SpielVoice *self = (SpielVoice *) object; 320 | 321 | g_free (self->name); 322 | g_free (self->identifier); 323 | g_strfreev (self->languages); 324 | g_free (self->output_format); 325 | g_weak_ref_clear (&self->provider); 326 | 327 | G_OBJECT_CLASS (spiel_voice_parent_class)->finalize (object); 328 | } 329 | 330 | static void 331 | spiel_voice_get_property (GObject *object, 332 | guint prop_id, 333 | GValue *value, 334 | GParamSpec *pspec) 335 | { 336 | SpielVoice *self = SPIEL_VOICE (object); 337 | 338 | switch (prop_id) 339 | { 340 | case PROP_NAME: 341 | g_value_set_string (value, self->name); 342 | break; 343 | case PROP_IDENTIFIER: 344 | g_value_set_string (value, self->identifier); 345 | break; 346 | case PROP_LANGUAGES: 347 | g_value_set_boxed (value, self->languages); 348 | break; 349 | case PROP_PROVIDER: 350 | g_value_take_object (value, spiel_voice_get_provider (self)); 351 | break; 352 | case PROP_FEATURES: 353 | g_value_set_flags (value, self->features); 354 | break; 355 | default: 356 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 357 | } 358 | } 359 | 360 | static void 361 | spiel_voice_set_property (GObject *object, 362 | guint prop_id, 363 | const GValue *value, 364 | GParamSpec *pspec) 365 | { 366 | SpielVoice *self = SPIEL_VOICE (object); 367 | 368 | switch (prop_id) 369 | { 370 | case PROP_NAME: 371 | g_clear_pointer (&self->name, g_free); 372 | self->name = g_value_dup_string (value); 373 | break; 374 | case PROP_IDENTIFIER: 375 | g_clear_pointer (&self->identifier, g_free); 376 | self->identifier = g_value_dup_string (value); 377 | break; 378 | case PROP_LANGUAGES: 379 | g_strfreev (self->languages); 380 | self->languages = g_value_dup_boxed (value); 381 | break; 382 | case PROP_PROVIDER: 383 | g_weak_ref_set (&self->provider, g_value_get_object (value)); 384 | break; 385 | case PROP_FEATURES: 386 | self->features = g_value_get_flags (value); 387 | break; 388 | default: 389 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 390 | } 391 | } 392 | 393 | static void 394 | spiel_voice_class_init (SpielVoiceClass *klass) 395 | { 396 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 397 | 398 | object_class->finalize = spiel_voice_finalize; 399 | object_class->get_property = spiel_voice_get_property; 400 | object_class->set_property = spiel_voice_set_property; 401 | 402 | /** 403 | * SpielVoice:name: (getter get_name) 404 | * 405 | * A human readable name for the voice. Not guaranteed to be unique. 406 | * May, or may not, be localized by the speech provider. 407 | * 408 | * Since: 1.0 409 | */ 410 | properties[PROP_NAME] = g_param_spec_string ( 411 | "name", NULL, NULL, NULL /* default value */, 412 | G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 413 | 414 | /** 415 | * SpielVoice:identifier: (getter get_identifier) 416 | * 417 | * A unique identifier of the voice. The uniqueness should be considered 418 | * in the scope of the provider (ie. two providers can use the same 419 | * identifier). 420 | * 421 | * Since: 1.0 422 | */ 423 | properties[PROP_IDENTIFIER] = g_param_spec_string ( 424 | "identifier", NULL, NULL, NULL /* default value */, 425 | G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 426 | 427 | /** 428 | * SpielVoice:languages: (getter get_languages) 429 | * 430 | * A list of supported languages encoded as BCP 47 tags. 431 | * 432 | * Since: 1.0 433 | */ 434 | properties[PROP_LANGUAGES] = g_param_spec_boxed ( 435 | "languages", NULL, NULL, G_TYPE_STRV, 436 | G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 437 | 438 | /** 439 | * SpielVoice:provider: (getter get_provider) 440 | * 441 | * The speech provider that implements this voice. 442 | * 443 | * Since: 1.0 444 | */ 445 | properties[PROP_PROVIDER] = g_param_spec_object ( 446 | "provider", NULL, NULL, SPIEL_TYPE_PROVIDER, 447 | G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 448 | 449 | /** 450 | * SpielVoice:features: (getter get_features) 451 | * 452 | * A bitfield of supported features. 453 | * 454 | * Since: 1.0 455 | */ 456 | properties[PROP_FEATURES] = g_param_spec_flags ( 457 | "features", NULL, NULL, SPIEL_TYPE_VOICE_FEATURE, 458 | SPIEL_VOICE_FEATURE_NONE, 459 | G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 460 | 461 | g_object_class_install_properties (object_class, N_PROPS, properties); 462 | } 463 | 464 | static void 465 | spiel_voice_init (SpielVoice *self) 466 | { 467 | self->name = NULL; 468 | self->identifier = NULL; 469 | self->languages = NULL; 470 | self->output_format = NULL; 471 | self->features = 0; 472 | g_weak_ref_init (&self->provider, NULL); 473 | } 474 | -------------------------------------------------------------------------------- /libspiel/spiel-utterance.c: -------------------------------------------------------------------------------- 1 | /* spiel-utterance.c 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #include "spiel.h" 20 | 21 | #include "spiel-utterance.h" 22 | 23 | /** 24 | * SpielUtterance: 25 | * 26 | * Represents an utterance to be spoken by a `SpielSpeaker`. 27 | * 28 | * An utterance consists of the text to be spoken and other properties that 29 | * affect the speech, like rate, pitch or voice used. 30 | * 31 | * Since: 1.0 32 | */ 33 | 34 | struct _SpielUtterance 35 | { 36 | GObject parent_instance; 37 | char *text; 38 | double pitch; 39 | double rate; 40 | double volume; 41 | SpielVoice *voice; 42 | char *language; 43 | gboolean is_ssml; 44 | }; 45 | 46 | G_DEFINE_FINAL_TYPE (SpielUtterance, spiel_utterance, G_TYPE_OBJECT) 47 | 48 | enum 49 | { 50 | PROP_0, 51 | PROP_TEXT, 52 | PROP_PITCH, 53 | PROP_RATE, 54 | PROP_VOLUME, 55 | PROP_VOICE, 56 | PROP_LANGUAGE, 57 | PROP_IS_SSML, 58 | N_PROPS 59 | }; 60 | 61 | static GParamSpec *properties[N_PROPS]; 62 | 63 | /** 64 | * spiel_utterance_new: (constructor) 65 | * @text: (nullable): The utterance text to be spoken. 66 | * 67 | * Creates a new [class@Spiel.Utterance]. 68 | * 69 | * Returns: The new `SpielUtterance`. 70 | * 71 | * Since: 1.0 72 | */ 73 | SpielUtterance * 74 | spiel_utterance_new (const char *text) 75 | { 76 | return g_object_new (SPIEL_TYPE_UTTERANCE, "text", text, NULL); 77 | } 78 | 79 | /** 80 | * spiel_utterance_get_text: (get-property text) 81 | * @self: a `SpielUtterance` 82 | * 83 | * Gets the text spoken in this utterance. 84 | * 85 | * Returns: (transfer none): the text 86 | * 87 | * Since: 1.0 88 | */ 89 | const char * 90 | spiel_utterance_get_text (SpielUtterance *self) 91 | { 92 | 93 | g_return_val_if_fail (SPIEL_IS_UTTERANCE (self), NULL); 94 | 95 | return self->text; 96 | } 97 | 98 | /** 99 | * spiel_utterance_set_text: (set-property text) 100 | * @self: a `SpielUtterance` 101 | * @text: the text to assign to this utterance 102 | * 103 | * Sets the text to be spoken by this utterance. 104 | * 105 | * Since: 1.0 106 | */ 107 | void 108 | spiel_utterance_set_text (SpielUtterance *self, const char *text) 109 | { 110 | g_return_if_fail (SPIEL_IS_UTTERANCE (self)); 111 | 112 | g_free (self->text); 113 | self->text = g_strdup (text); 114 | g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TEXT]); 115 | } 116 | 117 | /** 118 | * spiel_utterance_get_pitch: (get-property pitch) 119 | * @self: a `SpielUtterance` 120 | * 121 | * Gets the pitch used in this utterance. 122 | * 123 | * Returns: the pitch value 124 | * 125 | * Since: 1.0 126 | */ 127 | double 128 | spiel_utterance_get_pitch (SpielUtterance *self) 129 | { 130 | g_return_val_if_fail (SPIEL_IS_UTTERANCE (self), 1.0); 131 | 132 | return self->pitch; 133 | } 134 | 135 | /** 136 | * spiel_utterance_set_pitch: (set-property pitch) 137 | * @self: a `SpielUtterance` 138 | * @pitch: a pitch to assign to this utterance 139 | * 140 | * Sets a pitch on this utterance. 141 | * 142 | * Since: 1.0 143 | */ 144 | void 145 | spiel_utterance_set_pitch (SpielUtterance *self, double pitch) 146 | { 147 | g_return_if_fail (SPIEL_IS_UTTERANCE (self)); 148 | 149 | self->pitch = pitch; 150 | g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PITCH]); 151 | } 152 | 153 | /** 154 | * spiel_utterance_get_rate: (get-property rate) 155 | * @self: a `SpielUtterance` 156 | * 157 | * Gets the rate used in this utterance. 158 | * 159 | * Returns: the rate value 160 | * 161 | * Since: 1.0 162 | */ 163 | double 164 | spiel_utterance_get_rate (SpielUtterance *self) 165 | { 166 | g_return_val_if_fail (SPIEL_IS_UTTERANCE (self), 1.0); 167 | 168 | return self->rate; 169 | } 170 | 171 | /** 172 | * spiel_utterance_set_rate: (set-property rate) 173 | * @self: a `SpielUtterance` 174 | * @rate: a rate to assign to this utterance 175 | * 176 | * Sets a rate on this utterance. 177 | * 178 | * Since: 1.0 179 | */ 180 | void 181 | spiel_utterance_set_rate (SpielUtterance *self, double rate) 182 | { 183 | g_return_if_fail (SPIEL_IS_UTTERANCE (self)); 184 | 185 | self->rate = rate; 186 | g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_RATE]); 187 | } 188 | 189 | /** 190 | * spiel_utterance_get_volume: (get-property volume) 191 | * @self: a `SpielUtterance` 192 | * 193 | * Gets the volume used in this utterance. 194 | * 195 | * Returns: the volume value 196 | * 197 | * Since: 1.0 198 | */ 199 | double 200 | spiel_utterance_get_volume (SpielUtterance *self) 201 | { 202 | g_return_val_if_fail (SPIEL_IS_UTTERANCE (self), 1.0); 203 | 204 | return self->volume; 205 | } 206 | 207 | /** 208 | * spiel_utterance_set_volume: (set-property volume) 209 | * @self: a `SpielUtterance` 210 | * @volume: a volume to assign to this utterance 211 | * 212 | * Sets a volume on this utterance. 213 | * 214 | * Since: 1.0 215 | */ 216 | void 217 | spiel_utterance_set_volume (SpielUtterance *self, double volume) 218 | { 219 | g_return_if_fail (SPIEL_IS_UTTERANCE (self)); 220 | 221 | self->volume = volume; 222 | g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VOLUME]); 223 | } 224 | 225 | /** 226 | * spiel_utterance_get_voice: (get-property voice) 227 | * @self: a `SpielUtterance` 228 | * 229 | * Gets the voice used in this utterance 230 | * 231 | * Returns: (transfer none) (nullable): a `SpielVoice` 232 | * 233 | * Since: 1.0 234 | */ 235 | SpielVoice * 236 | spiel_utterance_get_voice (SpielUtterance *self) 237 | { 238 | g_return_val_if_fail (SPIEL_IS_UTTERANCE (self), NULL); 239 | 240 | return self->voice; 241 | } 242 | 243 | /** 244 | * spiel_utterance_set_voice: (set-property voice) 245 | * @self: a `SpielUtterance` 246 | * @voice: a `SpielVoice` to assign to this utterance 247 | * 248 | * Sets a voice on this utterance. 249 | * 250 | * Since: 1.0 251 | */ 252 | void 253 | spiel_utterance_set_voice (SpielUtterance *self, SpielVoice *voice) 254 | { 255 | g_return_if_fail (SPIEL_IS_UTTERANCE (self)); 256 | g_return_if_fail (voice == NULL || SPIEL_IS_VOICE (voice)); 257 | 258 | if (g_set_object (&self->voice, voice)) 259 | { 260 | g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VOICE]); 261 | } 262 | } 263 | 264 | /** 265 | * spiel_utterance_get_language: (get-property language) 266 | * @self: a `SpielUtterance` 267 | * 268 | * Gets the language used in this utterance. 269 | * 270 | * Returns: (transfer none): the language 271 | * 272 | * Since: 1.0 273 | */ 274 | const char * 275 | spiel_utterance_get_language (SpielUtterance *self) 276 | { 277 | g_return_val_if_fail (SPIEL_IS_UTTERANCE (self), NULL); 278 | 279 | return self->language; 280 | } 281 | 282 | /** 283 | * spiel_utterance_set_language: (set-property language) 284 | * @self: a `SpielUtterance` 285 | * @language: the language to assign to this utterance 286 | * 287 | * Sets the language of this utterance 288 | * 289 | * Since: 1.0 290 | */ 291 | void 292 | spiel_utterance_set_language (SpielUtterance *self, const char *language) 293 | { 294 | g_return_if_fail (SPIEL_IS_UTTERANCE (self)); 295 | 296 | g_free (self->language); 297 | self->language = g_strdup (language); 298 | g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LANGUAGE]); 299 | } 300 | 301 | /** 302 | * spiel_utterance_get_is_ssml: (get-property is-ssml) 303 | * @self: a `SpielUtterance` 304 | * 305 | * Gets whether the current utterance an SSML snippet. 306 | * 307 | * Returns: %TRUE if the utterance text is SSML 308 | * 309 | * Since: 1.0 310 | */ 311 | gboolean 312 | spiel_utterance_get_is_ssml (SpielUtterance *self) 313 | { 314 | g_return_val_if_fail (SPIEL_IS_UTTERANCE (self), FALSE); 315 | 316 | return self->is_ssml; 317 | } 318 | 319 | /** 320 | * spiel_utterance_set_is_ssml: (set-property is-ssml) 321 | * @self: a `SpielUtterance` 322 | * @is_ssml: whether the utterance text is an SSML snippet 323 | * 324 | * Indicates whether this utterance should be interpreted as SSML. 325 | * 326 | * Since: 1.0 327 | */ 328 | void 329 | spiel_utterance_set_is_ssml (SpielUtterance *self, gboolean is_ssml) 330 | { 331 | g_return_if_fail (SPIEL_IS_UTTERANCE (self)); 332 | 333 | self->is_ssml = is_ssml; 334 | } 335 | 336 | static void 337 | spiel_utterance_finalize (GObject *object) 338 | { 339 | SpielUtterance *self = (SpielUtterance *) object; 340 | 341 | g_free (self->text); 342 | g_free (self->language); 343 | g_clear_object (&(self->voice)); 344 | 345 | G_OBJECT_CLASS (spiel_utterance_parent_class)->finalize (object); 346 | } 347 | 348 | static void 349 | spiel_utterance_get_property (GObject *object, 350 | guint prop_id, 351 | GValue *value, 352 | GParamSpec *pspec) 353 | { 354 | SpielUtterance *self = SPIEL_UTTERANCE (object); 355 | 356 | switch (prop_id) 357 | { 358 | case PROP_TEXT: 359 | g_value_set_string (value, spiel_utterance_get_text (self)); 360 | break; 361 | case PROP_PITCH: 362 | g_value_set_double (value, spiel_utterance_get_pitch (self)); 363 | break; 364 | case PROP_RATE: 365 | g_value_set_double (value, spiel_utterance_get_rate (self)); 366 | break; 367 | case PROP_VOLUME: 368 | g_value_set_double (value, spiel_utterance_get_volume (self)); 369 | break; 370 | case PROP_VOICE: 371 | g_value_set_object (value, spiel_utterance_get_voice (self)); 372 | break; 373 | case PROP_LANGUAGE: 374 | g_value_set_string (value, spiel_utterance_get_language (self)); 375 | break; 376 | case PROP_IS_SSML: 377 | g_value_set_boolean (value, spiel_utterance_get_is_ssml (self)); 378 | break; 379 | default: 380 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 381 | } 382 | } 383 | 384 | static void 385 | spiel_utterance_set_property (GObject *object, 386 | guint prop_id, 387 | const GValue *value, 388 | GParamSpec *pspec) 389 | { 390 | SpielUtterance *self = SPIEL_UTTERANCE (object); 391 | 392 | switch (prop_id) 393 | { 394 | case PROP_TEXT: 395 | spiel_utterance_set_text (self, g_value_get_string (value)); 396 | break; 397 | case PROP_PITCH: 398 | spiel_utterance_set_pitch (self, g_value_get_double (value)); 399 | break; 400 | case PROP_RATE: 401 | spiel_utterance_set_rate (self, g_value_get_double (value)); 402 | break; 403 | case PROP_VOLUME: 404 | spiel_utterance_set_volume (self, g_value_get_double (value)); 405 | break; 406 | case PROP_VOICE: 407 | spiel_utterance_set_voice (self, g_value_dup_object (value)); 408 | break; 409 | case PROP_LANGUAGE: 410 | spiel_utterance_set_language (self, g_value_get_string (value)); 411 | break; 412 | case PROP_IS_SSML: 413 | spiel_utterance_set_is_ssml (self, g_value_get_boolean (value)); 414 | break; 415 | default: 416 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 417 | } 418 | } 419 | 420 | static void 421 | spiel_utterance_class_init (SpielUtteranceClass *klass) 422 | { 423 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 424 | 425 | object_class->finalize = spiel_utterance_finalize; 426 | object_class->get_property = spiel_utterance_get_property; 427 | object_class->set_property = spiel_utterance_set_property; 428 | 429 | /** 430 | * SpielUtterance:text: (getter get_text) (setter set_text) 431 | * 432 | * The utterance text that will be spoken. 433 | * 434 | * Since: 1.0 435 | */ 436 | properties[PROP_TEXT] = 437 | g_param_spec_string ("text", NULL, NULL, NULL /* default value */, 438 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 439 | 440 | /** 441 | * SpielUtterance:pitch: (getter get_pitch) (setter set_pitch) 442 | * 443 | * The pitch at which the utterance will be spoken. 444 | * 445 | * Since: 1.0 446 | */ 447 | properties[PROP_PITCH] = g_param_spec_double ( 448 | "pitch", NULL, NULL, 0, 2, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 449 | 450 | /** 451 | * SpielUtterance:rate: (getter get_rate) (setter set_rate) 452 | * 453 | * The speed at which the utterance will be spoken. 454 | * 455 | * Since: 1.0 456 | */ 457 | properties[PROP_RATE] = 458 | g_param_spec_double ("rate", NULL, NULL, 0.1, 10, 1, 459 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 460 | 461 | /** 462 | * SpielUtterance:volume: (getter get_volume) (setter set_volume) 463 | * 464 | * The volume at which the utterance will be spoken. 465 | * 466 | * Since: 1.0 467 | */ 468 | properties[PROP_VOLUME] = 469 | g_param_spec_double ("volume", NULL, NULL, 0, 1, 1, 470 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 471 | 472 | /** 473 | * SpielUtterance:voice: (getter get_voice) (setter set_voice) 474 | * 475 | * The voice with which the utterance will be spoken. 476 | * 477 | * Since: 1.0 478 | */ 479 | properties[PROP_VOICE] = 480 | g_param_spec_object ("voice", NULL, NULL, SPIEL_TYPE_VOICE, 481 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 482 | 483 | /** 484 | * SpielUtterance:language: (getter get_language) (setter set_language) 485 | * 486 | * The utterance language. If no voice is set this language will be used to 487 | * select the best matching voice. 488 | * 489 | * Since: 1.0 490 | */ 491 | properties[PROP_LANGUAGE] = 492 | g_param_spec_string ("language", NULL, NULL, NULL /* default value */, 493 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 494 | 495 | /** 496 | * SpielUtterance:is-ssml: (getter get_is_ssml) (setter set_is_ssml) 497 | * 498 | * Whether the utterance's text should be interpreted as an SSML snippet. 499 | * 500 | * Since: 1.0 501 | */ 502 | properties[PROP_IS_SSML] = g_param_spec_boolean ( 503 | "is-ssml", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 504 | 505 | g_object_class_install_properties (object_class, N_PROPS, properties); 506 | } 507 | 508 | static void 509 | spiel_utterance_init (SpielUtterance *self) 510 | { 511 | self->text = NULL; 512 | self->rate = 1; 513 | self->volume = 1; 514 | self->pitch = 1; 515 | self->voice = NULL; 516 | self->language = NULL; 517 | self->is_ssml = FALSE; 518 | } 519 | -------------------------------------------------------------------------------- /libspiel/spiel-collect-providers.c: -------------------------------------------------------------------------------- 1 | /* spiel-collect-providers.c 2 | * 3 | * Copyright (C) 2024 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #include "spiel-collect-providers.h" 20 | 21 | #include "spiel-provider-private.h" 22 | #include "spiel-provider-proxy.h" 23 | 24 | typedef struct 25 | { 26 | GCancellable *cancellable; 27 | GHashTable *providers; 28 | GList *providers_to_process; 29 | char *provider_name; 30 | } _CollectProvidersClosure; 31 | 32 | static void 33 | _collect_providers_closure_destroy (gpointer data) 34 | { 35 | _CollectProvidersClosure *closure = data; 36 | g_clear_object (&closure->cancellable); 37 | g_hash_table_unref (closure->providers); 38 | if (closure->providers_to_process) 39 | { 40 | g_list_free (closure->providers_to_process); 41 | } 42 | 43 | if (closure->provider_name) 44 | { 45 | g_free (closure->provider_name); 46 | } 47 | 48 | g_slice_free (_CollectProvidersClosure, closure); 49 | } 50 | 51 | static void _on_list_activatable_names (GObject *source, 52 | GAsyncResult *result, 53 | gpointer user_data); 54 | 55 | static gboolean _collect_provider_names (GObject *source, 56 | GAsyncResult *result, 57 | GTask *task, 58 | gboolean activatable); 59 | 60 | static void _on_provider_created (GObject *source, 61 | GAsyncResult *result, 62 | gpointer user_data); 63 | 64 | static void _create_next_provider (_CollectProvidersClosure *closure, 65 | GTask *task); 66 | 67 | static void _create_provider (_CollectProvidersClosure *closure, GTask *task); 68 | 69 | static void 70 | _on_list_names (GObject *source, GAsyncResult *result, gpointer user_data); 71 | 72 | static void _spiel_collect_providers (GDBusConnection *connection, 73 | GCancellable *cancellable, 74 | const char *provider_name, 75 | GAsyncReadyCallback callback, 76 | gpointer user_data); 77 | 78 | void 79 | spiel_collect_providers (GDBusConnection *connection, 80 | GCancellable *cancellable, 81 | GAsyncReadyCallback callback, 82 | gpointer user_data) 83 | { 84 | _spiel_collect_providers (connection, cancellable, NULL, callback, user_data); 85 | } 86 | 87 | void 88 | spiel_collect_provider (GDBusConnection *connection, 89 | GCancellable *cancellable, 90 | const char *provider_name, 91 | GAsyncReadyCallback callback, 92 | gpointer user_data) 93 | { 94 | _spiel_collect_providers (connection, cancellable, provider_name, callback, 95 | user_data); 96 | } 97 | 98 | static void 99 | _spiel_collect_providers (GDBusConnection *connection, 100 | GCancellable *cancellable, 101 | const char *provider_name, 102 | GAsyncReadyCallback callback, 103 | gpointer user_data) 104 | { 105 | GTask *task = g_task_new (connection, cancellable, callback, user_data); 106 | _CollectProvidersClosure *closure = g_slice_new0 (_CollectProvidersClosure); 107 | 108 | closure->providers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 109 | (GDestroyNotify) g_object_unref); 110 | closure->cancellable = cancellable ? g_object_ref (cancellable) : NULL; 111 | 112 | if (provider_name) 113 | { 114 | closure->provider_name = g_strdup (provider_name); 115 | } 116 | 117 | g_task_set_task_data (task, closure, 118 | (GDestroyNotify) _collect_providers_closure_destroy); 119 | g_dbus_connection_call (connection, "org.freedesktop.DBus", 120 | "/org/freedesktop/DBus", "org.freedesktop.DBus", 121 | "ListActivatableNames", NULL, NULL, 122 | G_DBUS_CALL_FLAGS_NONE, -1, closure->cancellable, 123 | _on_list_activatable_names, task); 124 | } 125 | 126 | static void 127 | _on_list_activatable_names (GObject *source, 128 | GAsyncResult *result, 129 | gpointer user_data) 130 | { 131 | GTask *task = user_data; 132 | _CollectProvidersClosure *closure = g_task_get_task_data (task); 133 | GDBusConnection *connection = g_task_get_source_object (task); 134 | 135 | if (!_collect_provider_names (source, result, user_data, TRUE)) 136 | { 137 | return; 138 | } 139 | 140 | g_dbus_connection_call (connection, "org.freedesktop.DBus", 141 | "/org/freedesktop/DBus", "org.freedesktop.DBus", 142 | "ListNames", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, 143 | closure->cancellable, _on_list_names, task); 144 | } 145 | 146 | static void 147 | _on_list_names (GObject *source, GAsyncResult *result, gpointer user_data) 148 | { 149 | GTask *task = user_data; 150 | _CollectProvidersClosure *closure = g_task_get_task_data (task); 151 | 152 | if (!_collect_provider_names (source, result, user_data, FALSE)) 153 | { 154 | return; 155 | } 156 | 157 | closure->providers_to_process = g_hash_table_get_keys (closure->providers); 158 | 159 | if (!closure->providers_to_process) 160 | { 161 | g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, 162 | "No voice provider found"); 163 | g_object_unref (task); 164 | } 165 | else 166 | { 167 | _create_provider (closure, task); 168 | } 169 | } 170 | 171 | static void 172 | _create_next_provider (_CollectProvidersClosure *closure, GTask *task) 173 | { 174 | GList *next_provider = closure->providers_to_process; 175 | closure->providers_to_process = 176 | g_list_remove_link (closure->providers_to_process, next_provider); 177 | g_list_free (next_provider); 178 | 179 | if (closure->providers_to_process) 180 | { 181 | _create_provider (closure, task); 182 | return; 183 | } 184 | 185 | // All done, lets return from the task 186 | if (closure->provider_name) 187 | { 188 | // If a provider name is specified we return a single result. 189 | SpielProvider *provider = 190 | g_hash_table_lookup (closure->providers, closure->provider_name); 191 | g_task_return_pointer (task, g_object_ref (provider), 192 | (GDestroyNotify) g_object_unref); 193 | g_hash_table_remove (closure->providers, closure->provider_name); 194 | g_assert_cmpint (g_hash_table_size (closure->providers), ==, 0); 195 | g_object_unref (task); 196 | } 197 | else 198 | { 199 | // If no provider name was specified we return a hash table of 200 | // results. 201 | g_task_return_pointer (task, g_hash_table_ref (closure->providers), 202 | (GDestroyNotify) g_hash_table_unref); 203 | g_object_unref (task); 204 | } 205 | } 206 | 207 | static char * 208 | _object_path_from_service_name (const char *service_name) 209 | { 210 | char **split_name = g_strsplit (service_name, ".", 0); 211 | g_autofree char *partial_path = g_strjoinv ("/", split_name); 212 | char *obj_path = g_strdup_printf ("/%s", partial_path); 213 | g_strfreev (split_name); 214 | return obj_path; 215 | } 216 | 217 | static void 218 | _create_provider (_CollectProvidersClosure *closure, GTask *task) 219 | { 220 | GList *next_provider = closure->providers_to_process; 221 | const char *service_name = next_provider->data; 222 | g_autofree char *obj_path = _object_path_from_service_name (service_name); 223 | 224 | spiel_provider_proxy_proxy_new_for_bus (G_BUS_TYPE_SESSION, 0, service_name, 225 | obj_path, closure->cancellable, 226 | _on_provider_created, task); 227 | } 228 | 229 | static void 230 | _on_provider_created (GObject *source, GAsyncResult *result, gpointer user_data) 231 | { 232 | GTask *task = user_data; 233 | _CollectProvidersClosure *closure = g_task_get_task_data (task); 234 | const char *service_name = closure->providers_to_process->data; 235 | g_autoptr (GError) error = NULL; 236 | g_autoptr (SpielProviderProxy) provider_proxy = 237 | spiel_provider_proxy_proxy_new_for_bus_finish (result, &error); 238 | 239 | if (error != NULL) 240 | { 241 | if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) 242 | { 243 | // If we are cancelled, return from this task. 244 | g_task_return_error (task, error); 245 | return; 246 | } 247 | else 248 | { 249 | // Otherwise, just warn and move on to the next provider. 250 | g_warning ("Error creating proxy for '%s': %s\n", service_name, 251 | error->message); 252 | } 253 | } 254 | else 255 | { 256 | SpielProvider *provider = 257 | g_hash_table_lookup (closure->providers, service_name); 258 | 259 | g_assert (provider); 260 | g_assert (g_str_equal ( 261 | g_dbus_proxy_get_name (G_DBUS_PROXY (provider_proxy)), service_name)); 262 | 263 | if (provider) 264 | { 265 | spiel_provider_set_proxy (provider, provider_proxy); 266 | } 267 | } 268 | 269 | _create_next_provider (closure, task); 270 | } 271 | 272 | static gboolean 273 | _collect_provider_names (GObject *source, 274 | GAsyncResult *result, 275 | GTask *task, 276 | gboolean activatable) 277 | { 278 | _CollectProvidersClosure *closure = g_task_get_task_data (task); 279 | GError *error = NULL; 280 | GDBusConnection *bus = G_DBUS_CONNECTION (source); 281 | g_autoptr (GVariant) real_ret = NULL; 282 | GVariantIter iter; 283 | GVariant *service = NULL; 284 | g_autoptr (GVariant) ret = 285 | g_dbus_connection_call_finish (bus, result, &error); 286 | if (error != NULL) 287 | { 288 | g_task_return_error (task, error); 289 | g_object_unref (task); 290 | return FALSE; 291 | } 292 | 293 | real_ret = g_variant_get_child_value (ret, 0); 294 | 295 | g_variant_iter_init (&iter, real_ret); 296 | while ((service = g_variant_iter_next_value (&iter))) 297 | { 298 | const char *service_name = g_variant_get_string (service, NULL); 299 | if (g_str_has_suffix (service_name, PROVIDER_SUFFIX) && 300 | (!closure->provider_name || 301 | g_str_equal (closure->provider_name, service_name))) 302 | { 303 | SpielProvider *provider = 304 | g_hash_table_lookup (closure->providers, service_name); 305 | if (!provider) 306 | { 307 | provider = spiel_provider_new (); 308 | g_hash_table_insert (closure->providers, g_strdup (service_name), 309 | provider); 310 | } 311 | if (activatable) 312 | { 313 | spiel_provider_set_is_activatable (provider, TRUE); 314 | } 315 | } 316 | g_variant_unref (service); 317 | } 318 | 319 | return TRUE; 320 | } 321 | 322 | GHashTable * 323 | spiel_collect_providers_finish (GAsyncResult *res, GError **error) 324 | { 325 | return g_task_propagate_pointer (G_TASK (res), error); 326 | } 327 | 328 | SpielProvider * 329 | spiel_collect_provider_finish (GAsyncResult *res, GError **error) 330 | { 331 | return g_task_propagate_pointer (G_TASK (res), error); 332 | } 333 | 334 | GHashTable * 335 | spiel_collect_providers_sync (GDBusConnection *connection, 336 | GCancellable *cancellable, 337 | GError **error) 338 | { 339 | g_autoptr (GHashTable) providers = g_hash_table_new_full ( 340 | g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_object_unref); 341 | const char *list_name_methods[] = { "ListActivatableNames", "ListNames", 342 | NULL }; 343 | for (const char **method = list_name_methods; *method; method++) 344 | { 345 | g_autoptr (GVariant) real_ret = NULL; 346 | GVariantIter iter; 347 | const char *service_name = NULL; 348 | g_autoptr (GVariant) ret = g_dbus_connection_call_sync ( 349 | connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", 350 | "org.freedesktop.DBus", *method, NULL, NULL, G_DBUS_CALL_FLAGS_NONE, 351 | -1, NULL, error); 352 | if (error && *error) 353 | { 354 | g_warning ("Error calling list (%s): %s\n", *method, 355 | (*error)->message); 356 | return NULL; 357 | } 358 | 359 | real_ret = g_variant_get_child_value (ret, 0); 360 | 361 | g_variant_iter_init (&iter, real_ret); 362 | while (g_variant_iter_loop (&iter, "s", &service_name) && 363 | !g_cancellable_is_cancelled (cancellable)) 364 | { 365 | g_autofree char *obj_path = NULL; 366 | SpielProvider *provider = NULL; 367 | g_autoptr (SpielProviderProxy) provider_proxy = NULL; 368 | g_autoptr (GError) err = NULL; 369 | 370 | if (!g_str_has_suffix (service_name, PROVIDER_SUFFIX) || 371 | g_hash_table_contains (providers, service_name)) 372 | { 373 | continue; 374 | } 375 | obj_path = _object_path_from_service_name (service_name); 376 | provider_proxy = spiel_provider_proxy_proxy_new_sync ( 377 | connection, 0, service_name, obj_path, cancellable, &err); 378 | 379 | if (err) 380 | { 381 | g_warning ("Error creating proxy for '%s': %s\n", service_name, 382 | err->message); 383 | continue; 384 | } 385 | 386 | provider = spiel_provider_new (); 387 | spiel_provider_set_proxy (provider, provider_proxy); 388 | spiel_provider_set_is_activatable ( 389 | provider, g_str_equal (*method, "ListActivatableNames")); 390 | g_hash_table_insert (providers, g_strdup (service_name), provider); 391 | } 392 | } 393 | 394 | return g_steal_pointer (&providers); 395 | } -------------------------------------------------------------------------------- /libspiel/spiel-provider.c: -------------------------------------------------------------------------------- 1 | /* spiel-provider.c 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #include "spiel.h" 20 | 21 | #include "spiel-collect-providers.h" 22 | #include "spiel-provider-private.h" 23 | #include "spiel-provider.h" 24 | 25 | /** 26 | * SpielProvider: 27 | * 28 | * Represents a provider speech backend. 29 | * 30 | * Since: 1.0 31 | */ 32 | 33 | struct _SpielProvider 34 | { 35 | GObject parent_instance; 36 | SpielProviderProxy *provider_proxy; 37 | gboolean is_activatable; 38 | GListStore *voices; 39 | GHashTable *voices_hashset; 40 | }; 41 | 42 | G_DEFINE_FINAL_TYPE (SpielProvider, spiel_provider, G_TYPE_OBJECT) 43 | 44 | enum 45 | { 46 | PROP_0, 47 | PROP_WELL_KNOWN_NAME, 48 | PROP_NAME, 49 | PROP_VOICES, 50 | PROP_IDENTIFIER, 51 | N_PROPS 52 | }; 53 | 54 | static GParamSpec *properties[N_PROPS]; 55 | 56 | static gboolean handle_voices_changed (SpielProviderProxy *provider_proxy, 57 | GParamSpec *spec, 58 | gpointer user_data); 59 | 60 | static void _spiel_provider_update_voices (SpielProvider *self); 61 | 62 | /*< private > 63 | * spiel_provider_new: (constructor) 64 | * 65 | * Creates a new [class@Spiel.Provider]. 66 | * 67 | * Returns: (transfer full): The new `SpielProvider`. 68 | */ 69 | SpielProvider * 70 | spiel_provider_new (void) 71 | { 72 | return g_object_new (SPIEL_TYPE_PROVIDER, NULL); 73 | } 74 | 75 | /*< private > 76 | * spiel_provider_set_proxy: 77 | * @self: a `SpielProvider` 78 | * @provider_proxy: a `SpielProviderProxy` 79 | * 80 | * Sets the internal D-Bus proxy. 81 | */ 82 | void 83 | spiel_provider_set_proxy (SpielProvider *self, 84 | SpielProviderProxy *provider_proxy) 85 | { 86 | g_return_if_fail (SPIEL_IS_PROVIDER (self)); 87 | g_assert (!self->provider_proxy); 88 | 89 | if (self->provider_proxy != NULL) 90 | { 91 | g_signal_handlers_disconnect_by_func (self->provider_proxy, 92 | handle_voices_changed, self); 93 | g_clear_object (&self->provider_proxy); 94 | } 95 | 96 | if (g_set_object (&self->provider_proxy, provider_proxy)) 97 | { 98 | _spiel_provider_update_voices (self); 99 | g_signal_connect (self->provider_proxy, "notify::voices", 100 | G_CALLBACK (handle_voices_changed), self); 101 | } 102 | } 103 | 104 | /*< private > 105 | * spiel_provider_get_proxy: (skip) 106 | * @self: a `SpielProvider` 107 | * 108 | * Gets the internal D-Bus proxy. 109 | * 110 | * Returns: (transfer none): a `SpielProviderProxy` 111 | */ 112 | SpielProviderProxy * 113 | spiel_provider_get_proxy (SpielProvider *self) 114 | { 115 | g_return_val_if_fail (SPIEL_IS_PROVIDER (self), NULL); 116 | 117 | return self->provider_proxy; 118 | } 119 | 120 | /*< private > 121 | * spiel_provider_get_voice_by_id: 122 | * @self: a `SpielProvider` 123 | * @voice_id: (not nullable): a voice ID 124 | * 125 | * Lookup a `SpielVoice` by ID. 126 | * 127 | * Returns: (transfer none) (nullable): a `SpielProviderProxy` 128 | */ 129 | SpielVoice * 130 | spiel_provider_get_voice_by_id (SpielProvider *self, const char *voice_id) 131 | { 132 | guint voices_count = 0; 133 | 134 | g_return_val_if_fail (SPIEL_IS_PROVIDER (self), NULL); 135 | g_return_val_if_fail (voice_id != NULL, NULL); 136 | 137 | voices_count = g_list_model_get_n_items (G_LIST_MODEL (self->voices)); 138 | 139 | for (guint i = 0; i < voices_count; i++) 140 | { 141 | g_autoptr (SpielVoice) voice = SPIEL_VOICE ( 142 | g_list_model_get_object (G_LIST_MODEL (self->voices), i)); 143 | if (g_str_equal (spiel_voice_get_identifier (voice), voice_id)) 144 | { 145 | return g_steal_pointer (&voice); 146 | } 147 | } 148 | return NULL; 149 | } 150 | 151 | /** 152 | * spiel_provider_get_name: (get-property name) 153 | * @self: a `SpielProvider` 154 | * 155 | * Gets a human readable name of this provider 156 | * 157 | * Returns: (transfer none): the human readable name. 158 | * 159 | * Since: 1.0 160 | */ 161 | const char * 162 | spiel_provider_get_name (SpielProvider *self) 163 | { 164 | g_return_val_if_fail (SPIEL_IS_PROVIDER (self), NULL); 165 | g_return_val_if_fail (self->provider_proxy, NULL); 166 | 167 | return spiel_provider_proxy_get_name (self->provider_proxy); 168 | } 169 | 170 | /** 171 | * spiel_provider_get_well_known_name: (get-property well-known-name) 172 | * @self: a `SpielProvider` 173 | * 174 | * Gets the provider's D-Bus well known name. 175 | * 176 | * Returns: (transfer none): the well known name. 177 | * 178 | * Deprecated: 1.0.4: Use spiel_provider_get_identifier() instead 179 | */ 180 | G_DEPRECATED 181 | const char * 182 | spiel_provider_get_well_known_name (SpielProvider *self) 183 | { 184 | return spiel_provider_get_identifier (self); 185 | } 186 | 187 | /** 188 | * spiel_provider_get_identifier: (get-property identifier) 189 | * @self: a `SpielProvider` 190 | * 191 | * Gets the provider's unique identifier. 192 | * 193 | * Returns: (transfer none): the identifier. 194 | * 195 | * Since: 1.0.4 196 | */ 197 | const char * 198 | spiel_provider_get_identifier (SpielProvider *self) 199 | { 200 | g_return_val_if_fail (SPIEL_IS_PROVIDER (self), NULL); 201 | g_return_val_if_fail (self->provider_proxy, NULL); 202 | 203 | return g_dbus_proxy_get_name (G_DBUS_PROXY (self->provider_proxy)); 204 | } 205 | 206 | /** 207 | * spiel_provider_get_voices: (get-property voices) 208 | * @self: a `SpielProvider` 209 | * 210 | * Gets the provider's voices. 211 | * 212 | * Returns: (transfer none): A list of available voices 213 | * 214 | * Since: 1.0 215 | */ 216 | GListModel * 217 | spiel_provider_get_voices (SpielProvider *self) 218 | { 219 | g_return_val_if_fail (SPIEL_IS_PROVIDER (self), NULL); 220 | 221 | return G_LIST_MODEL (self->voices); 222 | } 223 | 224 | /*< private > 225 | * spiel_provider_set_is_activatable: 226 | * @self: a `SpielProvider` 227 | * @is_activatable: %TRUE if activatable 228 | * 229 | * Sets whether the provider supports D-Bus activation. 230 | */ 231 | void 232 | spiel_provider_set_is_activatable (SpielProvider *self, gboolean is_activatable) 233 | { 234 | g_return_if_fail (SPIEL_IS_PROVIDER (self)); 235 | 236 | self->is_activatable = is_activatable; 237 | } 238 | 239 | /*< private > 240 | * spiel_provider_get_is_activatable: 241 | * @self: a `SpielProvider` 242 | * 243 | * Gets whether the provider supports D-Bus activation. 244 | * 245 | * Returns: %TRUE if activatable 246 | */ 247 | gboolean 248 | spiel_provider_get_is_activatable (SpielProvider *self) 249 | { 250 | g_return_val_if_fail (SPIEL_IS_PROVIDER (self), FALSE); 251 | 252 | return self->is_activatable; 253 | } 254 | 255 | static GSList * 256 | _create_provider_voices (SpielProvider *self) 257 | { 258 | GSList *voices_slist = NULL; 259 | GVariant *voices = spiel_provider_proxy_get_voices (self->provider_proxy); 260 | gsize voices_count = voices ? g_variant_n_children (voices) : 0; 261 | 262 | for (gsize i = 0; i < voices_count; i++) 263 | { 264 | const char *name = NULL; 265 | const char *identifier = NULL; 266 | const char *output_format = NULL; 267 | g_autofree char **languages = NULL; 268 | guint64 features; 269 | SpielVoice *voice; 270 | 271 | g_variant_get_child (voices, i, "(&s&s&st^a&s)", &name, &identifier, 272 | &output_format, &features, &languages); 273 | if (features >> 32) 274 | { 275 | g_warning ("Voice features past 32 bits are ignored in %s (%s)", 276 | identifier, spiel_provider_get_identifier (self)); 277 | } 278 | voice = g_object_new (SPIEL_TYPE_VOICE, "name", name, "identifier", 279 | identifier, "languages", languages, "provider", 280 | self, "features", features, NULL); 281 | spiel_voice_set_output_format (voice, output_format); 282 | 283 | voices_slist = g_slist_prepend (voices_slist, voice); 284 | } 285 | 286 | return voices_slist; 287 | } 288 | 289 | static void 290 | _spiel_provider_update_voices (SpielProvider *self) 291 | { 292 | GSList *new_voices = NULL; 293 | g_autoptr (GHashTable) new_voices_hashset = NULL; 294 | 295 | g_return_if_fail (self->provider_proxy); 296 | 297 | new_voices = _create_provider_voices (self); 298 | if (g_hash_table_size (self->voices_hashset) > 0) 299 | { 300 | // We are adding voices to an already populated provider_proxy, store 301 | // new voices in a hashset for easy purge of ones that were removed. 302 | new_voices_hashset = g_hash_table_new ((GHashFunc) spiel_voice_hash, 303 | (GCompareFunc) spiel_voice_equal); 304 | } 305 | 306 | if (new_voices) 307 | { 308 | for (GSList *item = new_voices; item; item = item->next) 309 | { 310 | SpielVoice *voice = item->data; 311 | if (!g_hash_table_contains (self->voices_hashset, voice)) 312 | { 313 | g_hash_table_insert (self->voices_hashset, g_object_ref (voice), 314 | NULL); 315 | g_list_store_insert_sorted ( 316 | self->voices, voice, (GCompareDataFunc) spiel_voice_compare, 317 | NULL); 318 | } 319 | if (new_voices_hashset) 320 | { 321 | g_hash_table_insert (new_voices_hashset, voice, NULL); 322 | } 323 | } 324 | } 325 | 326 | if (new_voices_hashset) 327 | { 328 | GHashTableIter voices_iter; 329 | SpielVoice *old_voice; 330 | g_hash_table_iter_init (&voices_iter, self->voices_hashset); 331 | while ( 332 | g_hash_table_iter_next (&voices_iter, (gpointer *) &old_voice, NULL)) 333 | { 334 | if (!g_hash_table_contains (new_voices_hashset, old_voice)) 335 | { 336 | guint position = 0; 337 | if (g_list_store_find (self->voices, old_voice, &position)) 338 | { 339 | g_list_store_remove (self->voices, position); 340 | } 341 | g_hash_table_iter_remove (&voices_iter); 342 | } 343 | } 344 | } 345 | 346 | g_slist_free_full (new_voices, (GDestroyNotify) g_object_unref); 347 | } 348 | 349 | static gboolean 350 | handle_voices_changed (SpielProviderProxy *provider_proxy, 351 | GParamSpec *spec, 352 | gpointer user_data) 353 | { 354 | SpielProvider *self = user_data; 355 | g_autofree char *name_owner = 356 | g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->provider_proxy)); 357 | 358 | if (name_owner == NULL && self->is_activatable) 359 | { 360 | // Got a change notification because an activatable service left the bus. 361 | // Its voices are still valid, though. 362 | return TRUE; 363 | } 364 | 365 | _spiel_provider_update_voices (self); 366 | 367 | return TRUE; 368 | } 369 | 370 | /*< private > 371 | * spiel_provider_compare: 372 | * @self: (not nullable): a `SpielProvider` 373 | * @other: (not nullable): a `SpielProvider` to compare with @self 374 | * @user_data: user-defined callback data 375 | * 376 | * Compares the two [class@Spiel.Provider] values and returns a negative integer 377 | * if the first value comes before the second, 0 if they are equal, or a 378 | * positive integer if the first value comes after the second. 379 | * 380 | * Returns: an integer indicating order 381 | */ 382 | gint 383 | spiel_provider_compare (SpielProvider *self, 384 | SpielProvider *other, 385 | gpointer user_data) 386 | { 387 | g_return_val_if_fail (SPIEL_IS_PROVIDER (self), 0); 388 | g_return_val_if_fail (SPIEL_IS_PROVIDER (other), 0); 389 | 390 | return g_strcmp0 (spiel_provider_get_identifier (self), 391 | spiel_provider_get_identifier (other)); 392 | } 393 | 394 | static void 395 | spiel_provider_finalize (GObject *object) 396 | { 397 | SpielProvider *self = (SpielProvider *) object; 398 | 399 | if (self->provider_proxy != NULL) 400 | { 401 | g_signal_handlers_disconnect_by_func (self->provider_proxy, 402 | handle_voices_changed, self); 403 | g_clear_object (&self->provider_proxy); 404 | } 405 | 406 | g_clear_object (&(self->voices)); 407 | 408 | G_OBJECT_CLASS (spiel_provider_parent_class)->finalize (object); 409 | } 410 | 411 | static void 412 | spiel_provider_get_property (GObject *object, 413 | guint prop_id, 414 | GValue *value, 415 | GParamSpec *pspec) 416 | { 417 | SpielProvider *self = SPIEL_PROVIDER (object); 418 | 419 | switch (prop_id) 420 | { 421 | case PROP_NAME: 422 | g_value_set_string (value, spiel_provider_get_name (self)); 423 | break; 424 | case PROP_WELL_KNOWN_NAME: 425 | g_value_set_string (value, spiel_provider_get_identifier (self)); 426 | break; 427 | case PROP_VOICES: 428 | g_value_set_object (value, spiel_provider_get_voices (self)); 429 | break; 430 | case PROP_IDENTIFIER: 431 | g_value_set_string (value, spiel_provider_get_identifier (self)); 432 | break; 433 | default: 434 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 435 | } 436 | } 437 | 438 | static void 439 | spiel_provider_class_init (SpielProviderClass *klass) 440 | { 441 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 442 | 443 | object_class->finalize = spiel_provider_finalize; 444 | object_class->get_property = spiel_provider_get_property; 445 | 446 | /** 447 | * SpielProvider:name: (getter get_name) 448 | * 449 | * The provider's human readable name 450 | * 451 | * Since: 1.0 452 | */ 453 | properties[PROP_NAME] = 454 | g_param_spec_string ("name", NULL, NULL, NULL /* default value */, 455 | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); 456 | 457 | /** 458 | * SpielProvider:well-known-name: (getter get_well_known_name) 459 | * 460 | * The provider's D-Bus well known name. 461 | * 462 | * Deprecated: 1.0.4: Use #SpielProvider:identifier instead. 463 | */ 464 | properties[PROP_WELL_KNOWN_NAME] = g_param_spec_string ( 465 | "well-known-name", NULL, NULL, NULL /* default value */, 466 | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_DEPRECATED); 467 | 468 | /** 469 | * SpielProvider:voices: (getter get_voices) 470 | * 471 | * The list of available [class@Spiel.Voice]s that are provided. 472 | * 473 | * Since: 1.0 474 | */ 475 | properties[PROP_VOICES] = 476 | g_param_spec_object ("voices", NULL, NULL, G_TYPE_LIST_MODEL, 477 | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); 478 | 479 | /** 480 | * SpielProvider:identifier: (getter get_identifier) 481 | * 482 | * The provider's unique identifer. 483 | * 484 | * Since: 1.0.4 485 | */ 486 | properties[PROP_IDENTIFIER] = 487 | g_param_spec_string ("identifier", NULL, NULL, NULL /* default value */, 488 | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); 489 | 490 | g_object_class_install_properties (object_class, N_PROPS, properties); 491 | } 492 | 493 | static void 494 | spiel_provider_init (SpielProvider *self) 495 | { 496 | self->provider_proxy = NULL; 497 | self->is_activatable = FALSE; 498 | self->voices = g_list_store_new (SPIEL_TYPE_VOICE); 499 | self->voices_hashset = g_hash_table_new ((GHashFunc) spiel_voice_hash, 500 | (GCompareFunc) spiel_voice_equal); 501 | } 502 | -------------------------------------------------------------------------------- /libspiel/spiel-registry.c: -------------------------------------------------------------------------------- 1 | /* spiel-registry.c 2 | * 3 | * Copyright (C) 2023 Eitan Isaacson 4 | * 5 | * This file is free software; you can redistribute it and/or modify it under 6 | * the terms of the GNU Lesser General Public License as published by the Free 7 | * Software Foundation; either version 2.1 of the License, or (at your option) 8 | * any later version. 9 | * 10 | * This file is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program. If not, see . 17 | */ 18 | 19 | #include "spiel.h" 20 | 21 | #include "spiel-collect-providers.h" 22 | #include "spiel-provider-private.h" 23 | #include "spiel-registry.h" 24 | #include "spiel-voices-list-model.h" 25 | 26 | #include 27 | #include 28 | 29 | #define GSETTINGS_SCHEMA "org.monotonous.libspiel" 30 | 31 | struct _SpielRegistry 32 | { 33 | GObject parent_instance; 34 | GDBusConnection *connection; 35 | guint subscription_ids[2]; 36 | GListStore *providers; 37 | SpielVoicesListModel *voices; 38 | GSettings *settings; 39 | }; 40 | 41 | static void initable_iface_init (GInitableIface *initable_iface); 42 | static void 43 | async_initable_iface_init (GAsyncInitableIface *async_initable_iface); 44 | 45 | G_DEFINE_FINAL_TYPE_WITH_CODE ( 46 | SpielRegistry, 47 | spiel_registry, 48 | G_TYPE_OBJECT, 49 | G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init) 50 | G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, 51 | async_initable_iface_init)) 52 | 53 | static SpielRegistry *sRegistry = NULL; 54 | static GSList *sPendingTasks = NULL; 55 | 56 | enum 57 | { 58 | PROVIDER_DIED, 59 | LAST_SIGNAL 60 | }; 61 | 62 | static guint registry_signals[LAST_SIGNAL] = { 0 }; 63 | 64 | static SpielProvider * 65 | _get_provider_by_name (GListStore *providers, 66 | const char *provider_name, 67 | guint *position) 68 | { 69 | guint providers_count = g_list_model_get_n_items (G_LIST_MODEL (providers)); 70 | 71 | for (guint i = 0; i < providers_count; i++) 72 | { 73 | g_autoptr (SpielProvider) provider = SPIEL_PROVIDER ( 74 | g_list_model_get_object (G_LIST_MODEL (providers), i)); 75 | if (g_str_equal (provider_name, spiel_provider_get_identifier (provider))) 76 | { 77 | if (position) 78 | { 79 | *position = i; 80 | } 81 | return provider; 82 | } 83 | } 84 | 85 | return NULL; 86 | } 87 | 88 | static void 89 | _insert_providers (const char *provider_name, 90 | SpielProvider *new_provider, 91 | SpielRegistry *self) 92 | { 93 | SpielProvider *provider = 94 | _get_provider_by_name (self->providers, provider_name, NULL); 95 | 96 | if (!provider) 97 | { 98 | g_list_store_insert_sorted (self->providers, new_provider, 99 | (GCompareDataFunc) spiel_provider_compare, 100 | NULL); 101 | } 102 | else 103 | { 104 | spiel_provider_set_is_activatable ( 105 | provider, spiel_provider_get_is_activatable (new_provider)); 106 | } 107 | } 108 | 109 | static void 110 | _on_providers_updated (GObject *source, GAsyncResult *res, SpielRegistry *self) 111 | { 112 | g_autoptr (GError) err = NULL; 113 | g_autoptr (GHashTable) new_providers = 114 | spiel_collect_providers_finish (res, &err); 115 | guint providers_count = 0; 116 | 117 | if (err != NULL) 118 | { 119 | g_warning ("Error updating providers: %s\n", err->message); 120 | return; 121 | } 122 | 123 | g_hash_table_foreach (new_providers, (GHFunc) _insert_providers, self); 124 | 125 | providers_count = g_list_model_get_n_items (G_LIST_MODEL (self->providers)); 126 | 127 | for (gint i = providers_count - 1; i >= 0; i--) 128 | { 129 | g_autoptr (SpielProvider) provider = SPIEL_PROVIDER ( 130 | g_list_model_get_object (G_LIST_MODEL (self->providers), i)); 131 | if (!g_hash_table_contains (new_providers, 132 | spiel_provider_get_identifier (provider))) 133 | { 134 | g_list_store_remove (self->providers, i); 135 | } 136 | } 137 | } 138 | 139 | static void 140 | _on_new_provider_collected (GObject *source, 141 | GAsyncResult *res, 142 | SpielRegistry *self) 143 | { 144 | g_autoptr (GError) err = NULL; 145 | g_autoptr (SpielProvider) provider = 146 | spiel_collect_provider_finish (res, &err); 147 | const char *provider_name; 148 | 149 | if (err != NULL) 150 | { 151 | g_warning ("Error collecting provider: %s\n", err->message); 152 | return; 153 | } 154 | 155 | provider_name = spiel_provider_get_identifier (provider); 156 | _insert_providers (provider_name, provider, self); 157 | } 158 | 159 | static void 160 | _maybe_activatable_providers_changed (GDBusConnection *connection, 161 | const gchar *sender_name, 162 | const gchar *object_path, 163 | const gchar *interface_name, 164 | const gchar *signal_name, 165 | GVariant *parameters, 166 | gpointer user_data) 167 | { 168 | SpielRegistry *self = user_data; 169 | 170 | // No arguments given, so update the whole providers cache. 171 | spiel_collect_providers (connection, NULL, 172 | (GAsyncReadyCallback) _on_providers_updated, self); 173 | } 174 | 175 | static void 176 | _maybe_running_providers_changed (GDBusConnection *connection, 177 | const gchar *sender_name, 178 | const gchar *object_path, 179 | const gchar *interface_name, 180 | const gchar *signal_name, 181 | GVariant *parameters, 182 | gpointer user_data) 183 | { 184 | SpielRegistry *self = user_data; 185 | const char *service_name; 186 | const char *old_owner; 187 | const char *new_owner; 188 | g_variant_get (parameters, "(&s&s&s)", &service_name, &old_owner, &new_owner); 189 | if (g_str_has_suffix (service_name, PROVIDER_SUFFIX)) 190 | { 191 | gboolean provider_removed = strlen (new_owner) == 0; 192 | guint position = 0; 193 | SpielProvider *provider = 194 | _get_provider_by_name (self->providers, service_name, &position); 195 | if (provider_removed) 196 | { 197 | if (provider && !spiel_provider_get_is_activatable (provider)) 198 | { 199 | g_list_store_remove (self->providers, position); 200 | } 201 | 202 | g_signal_emit (self, registry_signals[PROVIDER_DIED], 0, 203 | service_name); 204 | } 205 | else if (!provider) 206 | { 207 | spiel_collect_provider ( 208 | connection, NULL, service_name, 209 | (GAsyncReadyCallback) _on_new_provider_collected, self); 210 | } 211 | } 212 | } 213 | 214 | static void 215 | _subscribe_to_activatable_services_changed (SpielRegistry *self) 216 | { 217 | self->subscription_ids[0] = g_dbus_connection_signal_subscribe ( 218 | self->connection, "org.freedesktop.DBus", "org.freedesktop.DBus", 219 | "ActivatableServicesChanged", "/org/freedesktop/DBus", NULL, 220 | G_DBUS_SIGNAL_FLAGS_NONE, _maybe_activatable_providers_changed, self, 221 | NULL); 222 | 223 | self->subscription_ids[1] = g_dbus_connection_signal_subscribe ( 224 | self->connection, "org.freedesktop.DBus", "org.freedesktop.DBus", 225 | "NameOwnerChanged", "/org/freedesktop/DBus", NULL, 226 | G_DBUS_SIGNAL_FLAGS_NONE, _maybe_running_providers_changed, 227 | g_object_ref (self), g_object_unref); 228 | } 229 | 230 | static void 231 | _on_providers_collected (GObject *source, GAsyncResult *res, gpointer user_data) 232 | { 233 | GTask *top_task = user_data; 234 | g_autoptr (GError) err = NULL; 235 | SpielRegistry *self = g_task_get_source_object (top_task); 236 | g_autoptr (GHashTable) providers = spiel_collect_providers_finish (res, &err); 237 | g_assert (sPendingTasks->data == top_task); 238 | 239 | if (err != NULL) 240 | { 241 | g_warning ("Error retrieving providers: %s\n", err->message); 242 | while (sPendingTasks) 243 | { 244 | GTask *task = sPendingTasks->data; 245 | g_task_return_error (task, g_error_copy (err)); 246 | g_object_unref (task); 247 | sPendingTasks = g_slist_delete_link (sPendingTasks, sPendingTasks); 248 | } 249 | return; 250 | } 251 | 252 | g_hash_table_foreach (providers, (GHFunc) _insert_providers, self); 253 | 254 | _subscribe_to_activatable_services_changed (self); 255 | 256 | while (sPendingTasks) 257 | { 258 | GTask *task = sPendingTasks->data; 259 | g_task_return_boolean (task, TRUE); 260 | g_object_unref (task); 261 | sPendingTasks = g_slist_delete_link (sPendingTasks, sPendingTasks); 262 | } 263 | } 264 | 265 | static void 266 | _on_bus_get (GObject *source, GAsyncResult *result, gpointer user_data) 267 | { 268 | GTask *task = user_data; 269 | GCancellable *cancellable = g_task_get_task_data (task); 270 | SpielRegistry *self = g_task_get_source_object (task); 271 | GError *error = NULL; 272 | 273 | self->connection = g_bus_get_finish (result, &error); 274 | if (error != NULL) 275 | { 276 | g_task_return_error (task, error); 277 | g_object_unref (task); 278 | return; 279 | } 280 | 281 | spiel_collect_providers (self->connection, cancellable, 282 | _on_providers_collected, task); 283 | } 284 | 285 | void 286 | spiel_registry_get (GCancellable *cancellable, 287 | GAsyncReadyCallback callback, 288 | gpointer user_data) 289 | { 290 | g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); 291 | 292 | if (sRegistry != NULL) 293 | { 294 | GTask *task = g_task_new (g_object_ref (sRegistry), cancellable, callback, 295 | user_data); 296 | g_task_return_boolean (task, TRUE); 297 | g_object_unref (task); 298 | } 299 | else if (sPendingTasks) 300 | { 301 | GObject *source_object = g_task_get_source_object (sPendingTasks->data); 302 | GTask *task = g_task_new (g_object_ref (source_object), cancellable, 303 | callback, user_data); 304 | sPendingTasks = g_slist_append (sPendingTasks, task); 305 | } 306 | else 307 | { 308 | g_async_initable_new_async (SPIEL_TYPE_REGISTRY, G_PRIORITY_DEFAULT, 309 | cancellable, callback, user_data, NULL); 310 | } 311 | } 312 | 313 | SpielRegistry * 314 | spiel_registry_get_finish (GAsyncResult *result, GError **error) 315 | { 316 | GObject *object; 317 | g_autoptr (GObject) source_object = g_async_result_get_source_object (result); 318 | g_assert (source_object != NULL); 319 | 320 | g_return_val_if_fail (G_IS_ASYNC_INITABLE (source_object), NULL); 321 | g_return_val_if_fail (error == NULL || *error == NULL, NULL); 322 | 323 | object = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), 324 | result, error); 325 | if (object != NULL) 326 | { 327 | if (sRegistry == NULL) 328 | { 329 | gst_init_check (NULL, NULL, error); 330 | sRegistry = SPIEL_REGISTRY (object); 331 | } 332 | g_assert (sRegistry == SPIEL_REGISTRY (object)); 333 | return SPIEL_REGISTRY (object); 334 | } 335 | else 336 | { 337 | return NULL; 338 | } 339 | } 340 | 341 | SpielRegistry * 342 | spiel_registry_get_sync (GCancellable *cancellable, GError **error) 343 | { 344 | g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), 345 | NULL); 346 | g_return_val_if_fail (error == NULL || *error == NULL, NULL); 347 | 348 | if (sRegistry == NULL) 349 | { 350 | gst_init_check (NULL, NULL, error); 351 | sRegistry = 352 | g_initable_new (SPIEL_TYPE_REGISTRY, cancellable, error, NULL); 353 | } 354 | else 355 | { 356 | g_object_ref (sRegistry); 357 | } 358 | 359 | return sRegistry; 360 | } 361 | 362 | static GSettings * 363 | _settings_new (void) 364 | { 365 | GSettingsSchema *schema = NULL; 366 | GSettingsSchemaSource *source = g_settings_schema_source_get_default (); 367 | GSettings *settings = NULL; 368 | 369 | schema = g_settings_schema_source_lookup (source, GSETTINGS_SCHEMA, TRUE); 370 | if (!schema) 371 | { 372 | g_debug ("libspiel settings schema is not installed"); 373 | return NULL; 374 | } 375 | 376 | settings = g_settings_new (GSETTINGS_SCHEMA); 377 | g_settings_schema_unref (schema); 378 | 379 | return settings; 380 | } 381 | 382 | static void 383 | async_initable_init_async (GAsyncInitable *initable, 384 | gint io_priority, 385 | GCancellable *cancellable, 386 | GAsyncReadyCallback callback, 387 | gpointer user_data) 388 | { 389 | GTask *task = g_task_new (initable, cancellable, callback, user_data); 390 | SpielRegistry *self = SPIEL_REGISTRY (initable); 391 | 392 | g_assert (!sPendingTasks); 393 | sPendingTasks = g_slist_append (sPendingTasks, task); 394 | 395 | self->providers = g_list_store_new (SPIEL_TYPE_PROVIDER); 396 | self->voices = spiel_voices_list_model_new (G_LIST_MODEL (self->providers)); 397 | self->settings = _settings_new (); 398 | 399 | if (cancellable != NULL) 400 | { 401 | g_task_set_task_data (task, g_object_ref (cancellable), g_object_unref); 402 | } 403 | g_bus_get (G_BUS_TYPE_SESSION, cancellable, _on_bus_get, task); 404 | } 405 | 406 | static gboolean 407 | async_initable_init_finish (GAsyncInitable *initable, 408 | GAsyncResult *res, 409 | GError **error) 410 | { 411 | g_return_val_if_fail (g_task_is_valid (res, initable), FALSE); 412 | 413 | return g_task_propagate_boolean (G_TASK (res), error); 414 | } 415 | 416 | static void 417 | spiel_registry_finalize (GObject *object) 418 | { 419 | SpielRegistry *self = (SpielRegistry *) object; 420 | 421 | g_clear_object (&self->providers); 422 | g_clear_object (&self->voices); 423 | g_clear_object (&self->settings); 424 | if (self->connection) 425 | { 426 | g_dbus_connection_signal_unsubscribe (self->connection, 427 | self->subscription_ids[0]); 428 | g_dbus_connection_signal_unsubscribe (self->connection, 429 | self->subscription_ids[1]); 430 | g_clear_object (&self->connection); 431 | } 432 | 433 | G_OBJECT_CLASS (spiel_registry_parent_class)->finalize (object); 434 | sRegistry = NULL; 435 | } 436 | 437 | static gboolean 438 | initable_init (GInitable *initable, GCancellable *cancellable, GError **error) 439 | { 440 | SpielRegistry *self = SPIEL_REGISTRY (initable); 441 | g_autoptr (GHashTable) providers = NULL; 442 | GDBusConnection *bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error); 443 | 444 | if (error && *error != NULL) 445 | { 446 | g_warning ("Error retrieving session bus: %s\n", (*error)->message); 447 | return FALSE; 448 | } 449 | 450 | providers = spiel_collect_providers_sync (bus, cancellable, error); 451 | 452 | if (error && *error != NULL) 453 | { 454 | g_warning ("Error retrieving providers: %s\n", (*error)->message); 455 | return FALSE; 456 | } 457 | 458 | if (providers) 459 | { 460 | g_hash_table_foreach (providers, (GHFunc) _insert_providers, self); 461 | } 462 | 463 | self->connection = g_object_ref (bus); 464 | 465 | _subscribe_to_activatable_services_changed (self); 466 | 467 | return TRUE; 468 | } 469 | 470 | static void 471 | spiel_registry_init (SpielRegistry *self) 472 | { 473 | self->providers = g_list_store_new (SPIEL_TYPE_PROVIDER); 474 | self->voices = spiel_voices_list_model_new (G_LIST_MODEL (self->providers)); 475 | self->settings = _settings_new (); 476 | } 477 | 478 | static void 479 | spiel_registry_class_init (SpielRegistryClass *klass) 480 | { 481 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 482 | 483 | object_class->finalize = spiel_registry_finalize; 484 | 485 | registry_signals[PROVIDER_DIED] = g_signal_new ( 486 | "provider-died", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, 0, NULL, 487 | NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); 488 | } 489 | 490 | static void 491 | async_initable_iface_init (GAsyncInitableIface *async_initable_iface) 492 | { 493 | async_initable_iface->init_async = async_initable_init_async; 494 | async_initable_iface->init_finish = async_initable_init_finish; 495 | } 496 | 497 | static void 498 | initable_iface_init (GInitableIface *initable_iface) 499 | { 500 | initable_iface->init = initable_init; 501 | } 502 | 503 | /* Public API */ 504 | 505 | SpielProviderProxy * 506 | spiel_registry_get_provider_for_voice (SpielRegistry *self, SpielVoice *voice) 507 | { 508 | g_autoptr (SpielProvider) voice_provider = NULL; 509 | 510 | g_return_val_if_fail (SPIEL_IS_REGISTRY (self), NULL); 511 | g_return_val_if_fail (SPIEL_IS_VOICE (voice), NULL); 512 | 513 | voice_provider = spiel_voice_get_provider (voice); 514 | g_return_val_if_fail (SPIEL_IS_PROVIDER (voice_provider), NULL); 515 | 516 | return spiel_provider_get_proxy (voice_provider); 517 | } 518 | 519 | static SpielVoice * 520 | _get_voice_from_provider_and_name (SpielRegistry *self, 521 | const char *provider_name, 522 | const char *voice_id) 523 | { 524 | SpielProvider *provider = 525 | _get_provider_by_name (self->providers, provider_name, NULL); 526 | g_return_val_if_fail (provider, NULL); 527 | 528 | return spiel_provider_get_voice_by_id (provider, voice_id); 529 | } 530 | 531 | static SpielVoice * 532 | _get_fallback_voice (SpielRegistry *self, const char *language) 533 | { 534 | if (language) 535 | { 536 | guint voices_count = 537 | g_list_model_get_n_items (G_LIST_MODEL (self->voices)); 538 | 539 | for (guint i = 0; i < voices_count; i++) 540 | { 541 | SpielVoice *voice = SPIEL_VOICE ( 542 | g_list_model_get_object (G_LIST_MODEL (self->voices), i)); 543 | if (g_strv_contains (spiel_voice_get_languages (voice), language)) 544 | { 545 | return voice; 546 | } 547 | g_object_unref (voice); // Just want to borrow a ref. 548 | } 549 | } 550 | 551 | return g_list_model_get_item ((GListModel *) self->voices, 0); 552 | } 553 | 554 | SpielVoice * 555 | spiel_registry_get_voice_for_utterance (SpielRegistry *self, 556 | SpielUtterance *utterance) 557 | { 558 | g_autofree char *provider_name = NULL; 559 | g_autofree char *voice_id = NULL; 560 | const char *language = NULL; 561 | SpielVoice *voice = NULL; 562 | 563 | g_return_val_if_fail (SPIEL_IS_REGISTRY (self), NULL); 564 | g_return_val_if_fail (SPIEL_IS_UTTERANCE (utterance), NULL); 565 | 566 | voice = spiel_utterance_get_voice (utterance); 567 | if (voice) 568 | { 569 | return voice; 570 | } 571 | 572 | language = spiel_utterance_get_language (utterance); 573 | if (language && self->settings) 574 | { 575 | g_autoptr (GVariant) mapping = 576 | g_settings_get_value (self->settings, "language-voice-mapping"); 577 | g_autofree char *_lang = g_strdup (language); 578 | char *found = _lang + g_utf8_strlen (_lang, -1); 579 | gssize boundary = -1; 580 | 581 | do 582 | { 583 | *found = 0; 584 | g_variant_lookup (mapping, _lang, "(ss)", &provider_name, &voice_id); 585 | if (provider_name) 586 | { 587 | break; 588 | } 589 | found = g_utf8_strrchr (_lang, boundary, '-'); 590 | boundary = found - _lang - 1; 591 | } 592 | while (found); 593 | } 594 | 595 | if (!provider_name && self->settings) 596 | { 597 | g_settings_get (self->settings, "default-voice", "m(ss)", NULL, 598 | &provider_name, &voice_id); 599 | } 600 | 601 | if (provider_name) 602 | { 603 | g_assert (voice_id != NULL); 604 | voice = _get_voice_from_provider_and_name (self, provider_name, voice_id); 605 | } 606 | 607 | if (voice) 608 | { 609 | return voice; 610 | } 611 | 612 | return _get_fallback_voice (self, language); 613 | } 614 | 615 | GListModel * 616 | spiel_registry_get_voices (SpielRegistry *self) 617 | { 618 | g_return_val_if_fail (SPIEL_IS_REGISTRY (self), NULL); 619 | 620 | return G_LIST_MODEL (self->voices); 621 | } 622 | 623 | GListModel * 624 | spiel_registry_get_providers (SpielRegistry *self) 625 | { 626 | g_return_val_if_fail (SPIEL_IS_REGISTRY (self), NULL); 627 | 628 | return G_LIST_MODEL (self->providers); 629 | } 630 | --------------------------------------------------------------------------------