diff --git a/Package.swift b/Package.swift index 3966a4b..c726c7c 100644 --- a/Package.swift +++ b/Package.swift @@ -57,13 +57,22 @@ func commonHeaderSearchPath(prefix: String = "") -> [CSetting] { } } +func commonCSettings(prefix: String = "") -> [CSetting] { + + return [ .define("ANDROID"), + .define("TARGET_OS_IPHONE", .when(platforms: [.iOS])), + .define("TARGET_OS_MAC", .when(platforms: [.macOS]))] + + boostHeaders(prefix: prefix + "RHVoice/") + + commonHeaderSearchPath(prefix: prefix) +} + let package = Package( name: "RHVoice", platforms: [ - .macOS(.v10_13), - .iOS(.v12), - .tvOS(.v12), - .watchOS(.v4) + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v5) ], products: [ .library( @@ -121,39 +130,24 @@ let package = Package( .define("RHVOICE"), .define("PACKAGE", to: "\"RHVoice\""), .define("DATA_PATH", to: "\"\""), - .define("CONFIG_PATH", to: "\"\""), - .define("ANDROID"), - .define("TARGET_OS_IPHONE", .when(platforms: [.iOS])), - .define("TARGET_OS_MAC", .when(platforms: [.macOS])) + .define("CONFIG_PATH", to: "\"\"") ] - + boostHeaders(prefix: "RHVoice/") - + commonHeaderSearchPath() + + commonCSettings() ), .target(name: "RHVoiceObjC", dependencies: [ .target(name: "RHVoice") ], - path: "Sources", - exclude: [ - "RHVoiceSwift" - ], - sources: [ - "CoreLib", - "RHVoice", - "Utils" - ], - publicHeadersPath: "RHVoiceObjC/PublicHeaders/", + path: "Sources/RHVoiceObjC", + publicHeadersPath: "PublicHeaders/", cSettings: [ - .headerSearchPath("RHVoiceObjC/Logger"), - .headerSearchPath("RHVoiceObjC/PrivateHeaders"), + .headerSearchPath("Logger"), + .headerSearchPath("PrivateHeaders"), .headerSearchPath("Utils"), .headerSearchPath("CoreLib"), - .headerSearchPath("Mock"), - .define("ANDROID"), - .define("TARGET_OS_IPHONE", .when(platforms: [.iOS])), - .define("TARGET_OS_MAC", .when(platforms: [.macOS])) + .headerSearchPath("../Mock") ] - + commonHeaderSearchPath(prefix: "../RHVoice/") + + commonCSettings(prefix: "../../RHVoice/") , linkerSettings: [ .linkedFramework("AVFAudio") @@ -161,26 +155,46 @@ let package = Package( ), .target(name: "RHVoiceSwift", dependencies: [ - .target(name: "RHVoice") - ], - path: "Sources", - sources: [ - "RHVoiceSwift" + .target(name: "RHVoice"), + .target(name: "PlayerLib") ], + path: "Sources/RHVoiceSwift", cSettings: ([ - .headerSearchPath("Mock"), - .define("ANDROID"), - .define("TARGET_OS_IPHONE", .when(platforms: [.iOS])), - .define("TARGET_OS_MAC", .when(platforms: [.macOS])) + .headerSearchPath("../Mock") ] - + boostHeaders(prefix: "../RHVoice/RHVoice/") - + commonHeaderSearchPath(prefix: "../RHVoice/") - + + commonCSettings(prefix: "../../RHVoice/") ), swiftSettings: [ .interoperabilityMode(.Cxx) ] ), + .target(name: "PlayerLib", + dependencies: [ + .target(name: "RHVoice") + ], + path: "Sources/PlayerLib", + cSettings: ([ + .headerSearchPath("../Mock") + ] + + commonCSettings(prefix: "../../RHVoice/") + ) + ), + .executableTarget( + name: "RHVoiceSwiftSample", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .target(name: "RHVoiceSwift") + ], + path: "Sources/RHVoiceSwiftSample", + cSettings: ([ + .headerSearchPath("../Mock") + ] + + commonCSettings(prefix: "../../RHVoice/") + ), + swiftSettings: [ + .interoperabilityMode(.Cxx) + ] + ), /// Plugin to copy languages and voices data files .executableTarget( name: "PackDataExecutable", diff --git a/Sources/PlayerLib/FilePlaybackStream.cpp b/Sources/PlayerLib/FilePlaybackStream.cpp new file mode 100644 index 0000000..31c7da0 --- /dev/null +++ b/Sources/PlayerLib/FilePlaybackStream.cpp @@ -0,0 +1,128 @@ +// +// RHVoiceWrapper.cpp +// +// +// Created by Ihor Shevchuk on 01.02.2023. +// +// Copyright (C) 2022 Olga Yakovleva + +#include + +#include +#include +#include +#include +#include +#include + +#include "core/engine.hpp" +#include "core/document.hpp" +#include "core/client.hpp" +#include "audio.hpp" + +#include "FilePlaybackStream.h" + +namespace PlayerLib +{ + class FilePlaybackStream::Impl + { + public: + Impl(const std::string &path) + { + if (!path.empty()) + { + stream.set_backend(RHVoice::audio::backend_file); + stream.set_device(path); + } + } + + ~Impl() = default; + + bool play_speech(const short *samples, std::size_t count) + { + try + { + if (!stream.is_open()) + { + stream.open(); + } + stream.write(samples, count); + return true; + } + catch (...) + { + stream.close(); + return false; + } + } + + void finish() + { + if (stream.is_open()) + { + stream.drain(); + } + } + + bool set_sample_rate(int sample_rate) + { + try + { + if (stream.is_open() && (stream.get_sample_rate() != sample_rate)) + { + stream.close(); + } + stream.set_sample_rate(sample_rate); + return true; + } + catch (...) + { + return false; + } + } + bool set_buffer_size(unsigned int buffer_size) + { + try + { + if (stream.is_open() && (stream.get_buffer_size() != buffer_size)) + { + stream.close(); + } + stream.set_buffer_size(buffer_size); + return true; + } + catch (...) + { + return false; + } + } + + private: + RHVoice::audio::playback_stream stream; + }; + + FilePlaybackStream::FilePlaybackStream(const char *path) : pimpl(new Impl(path)) + { + } + + bool FilePlaybackStream::set_sample_rate(int sample_rate) + { + return pimpl->set_sample_rate(sample_rate); + } + + bool FilePlaybackStream::set_buffer_size(unsigned int buffer_size) + { + return pimpl->set_buffer_size(buffer_size); + } + + bool FilePlaybackStream::play_speech(const short *samples, std::size_t count) + { + return pimpl->play_speech(samples, count); + } + + void FilePlaybackStream::finish() + { + pimpl->finish(); + } + +} diff --git a/Sources/PlayerLib/include/FilePlaybackStream.h b/Sources/PlayerLib/include/FilePlaybackStream.h new file mode 100644 index 0000000..8beef93 --- /dev/null +++ b/Sources/PlayerLib/include/FilePlaybackStream.h @@ -0,0 +1,38 @@ +/* Copyright (C) 2012, 2013, 2018 Olga Yakovleva */ + +/* This program is free software: you can redistribute it and/or modify */ +/* it under the terms of the GNU General Public License as published by */ +/* the Free Software Foundation, either version 2 of the License, or */ +/* (at your option) any later version. */ + +/* This program is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU General Public License for more details. */ + +/* You should have received a copy of the GNU General Public License */ +/* along with this program. If not, see . */ + + +#ifndef FilePlaybackStream_h +#define FilePlaybackStream_h + +#include + +namespace PlayerLib +{ + class FilePlaybackStream + { + public: + FilePlaybackStream(const char *path); + bool play_speech(const short *samples, std::size_t count); + void finish(); + bool set_sample_rate(int sample_rate); + bool set_buffer_size(unsigned int buffer_size); + + private: + class Impl; + std::shared_ptr pimpl; + }; +} +#endif /* FilePlaybackStream_h */ diff --git a/Sources/CoreLib/RHVoiceWrapper.cpp b/Sources/RHVoiceObjC/CoreLib/RHVoiceWrapper.cpp similarity index 100% rename from Sources/CoreLib/RHVoiceWrapper.cpp rename to Sources/RHVoiceObjC/CoreLib/RHVoiceWrapper.cpp diff --git a/Sources/CoreLib/RHVoiceWrapper.h b/Sources/RHVoiceObjC/CoreLib/RHVoiceWrapper.h similarity index 100% rename from Sources/CoreLib/RHVoiceWrapper.h rename to Sources/RHVoiceObjC/CoreLib/RHVoiceWrapper.h diff --git a/Sources/RHVoiceObjC/RHSpeechSynthesisVoice.mm b/Sources/RHVoiceObjC/RHSpeechSynthesisVoice.mm index f777172..f1cf43d 100644 --- a/Sources/RHVoiceObjC/RHSpeechSynthesisVoice.mm +++ b/Sources/RHVoiceObjC/RHSpeechSynthesisVoice.mm @@ -22,7 +22,6 @@ @interface RHSpeechSynthesisVoice() { @property (nonatomic, strong) NSString *dataPath; @property (nonatomic, strong) RHLanguage *language; @property (nonatomic, strong) NSString *countryCode; -@property (nonatomic, strong) NSString *voiceLanguageCode; @property (nonatomic, strong) NSString *identifier; @property (nonatomic, assign) RHSpeechSynthesisVoiceGender gender; @end @@ -31,28 +30,6 @@ @implementation RHSpeechSynthesisVoice #pragma mark - Public -- (NSString *)languageCode { - - NSString *languageCode = [[self language] code]; - - const BOOL isLanguageCodeEmpty = languageCode == nil || [languageCode isEqualToString:@""]; - const BOOL isCountryCodeEmpty = [self voiceLanguageCode] == nil || [[self voiceLanguageCode] isEqualToString:@""]; - - if (isLanguageCodeEmpty && isCountryCodeEmpty) { - return @""; - } - - if(isLanguageCodeEmpty) { - return [self voiceLanguageCode]; - } - - if(isCountryCodeEmpty) { - return languageCode; - } - - return [NSString stringWithFormat:@"%@-%@", languageCode, [self voiceLanguageCode]]; -} - - (RHVersionInfo * __nullable)version { return [[RHVersionInfo alloc] initWith:[[self dataPath] stringByAppendingPathComponent:@"voice.info"]]; } @@ -118,7 +95,6 @@ - (instancetype)initWith:(RHVoice::voice_list::const_iterator)voice_information self.dataPath = STDStringToNSString(voice_information->get_data_path()); self.name = STDStringToNSString(voice_information->get_name()); self.language = [[RHLanguage alloc] initWith:*voice_information->get_language()]; - self.voiceLanguageCode = STDStringToNSString(voice_information->get_alpha2_country_code()); self.identifier = STDStringToNSString(voice_information->get_id()); self.gender = [RHSpeechSynthesisVoice genderFromRHVoiceGender:voice_information->get_gender()]; } diff --git a/Sources/Utils/NSFileManager+Additions.h b/Sources/RHVoiceObjC/Utils/NSFileManager+Additions.h similarity index 100% rename from Sources/Utils/NSFileManager+Additions.h rename to Sources/RHVoiceObjC/Utils/NSFileManager+Additions.h diff --git a/Sources/Utils/NSFileManager+Additions.m b/Sources/RHVoiceObjC/Utils/NSFileManager+Additions.m similarity index 100% rename from Sources/Utils/NSFileManager+Additions.m rename to Sources/RHVoiceObjC/Utils/NSFileManager+Additions.m diff --git a/Sources/Utils/NSString+Additions.h b/Sources/RHVoiceObjC/Utils/NSString+Additions.h similarity index 100% rename from Sources/Utils/NSString+Additions.h rename to Sources/RHVoiceObjC/Utils/NSString+Additions.h diff --git a/Sources/Utils/NSString+Additions.mm b/Sources/RHVoiceObjC/Utils/NSString+Additions.mm similarity index 100% rename from Sources/Utils/NSString+Additions.mm rename to Sources/RHVoiceObjC/Utils/NSString+Additions.mm diff --git a/Sources/Utils/NSString+stdStringAddtitons.h b/Sources/RHVoiceObjC/Utils/NSString+stdStringAddtitons.h similarity index 100% rename from Sources/Utils/NSString+stdStringAddtitons.h rename to Sources/RHVoiceObjC/Utils/NSString+stdStringAddtitons.h diff --git a/Sources/Utils/NSString+stdStringAddtitons.mm b/Sources/RHVoiceObjC/Utils/NSString+stdStringAddtitons.mm similarity index 100% rename from Sources/Utils/NSString+stdStringAddtitons.mm rename to Sources/RHVoiceObjC/Utils/NSString+stdStringAddtitons.mm diff --git a/Sources/RHVoiceSwift/Extensions/FileManager+TemporaryFolder.swift b/Sources/RHVoiceSwift/Extensions/FileManager+TemporaryFolder.swift new file mode 100644 index 0000000..9409e90 --- /dev/null +++ b/Sources/RHVoiceSwift/Extensions/FileManager+TemporaryFolder.swift @@ -0,0 +1,22 @@ +// +// FileManager+TemporaryFolder.swift.swift +// +// +// Created by Ihor Shevchuk on 27.03.2024. +// + +import Foundation + +extension FileManager { + func createTempFolderIfNeeded(at path: String) throws { + if !fileExists(atPath: path) { + try createDirectory(at: URL(fileURLWithPath: path), withIntermediateDirectories: false) + } + } + + func removeTempFolderIfNeeded(at path: String) throws { + if !fileExists(atPath: path) { + try removeItem(atPath: path) + } + } +} diff --git a/Sources/RHVoiceSwift/Extensions/String+TemporaryFiles.swift b/Sources/RHVoiceSwift/Extensions/String+TemporaryFiles.swift new file mode 100644 index 0000000..5586f35 --- /dev/null +++ b/Sources/RHVoiceSwift/Extensions/String+TemporaryFiles.swift @@ -0,0 +1,20 @@ +// +// String+TemporaryFiles.swift +// +// +// Created by Ihor Shevchuk on 27.03.2024. +// + +import Foundation + +extension String { + private static let tempFolderName = "RHVoice" + static var temporaryFolderPath: String { + return NSTemporaryDirectory().appending("\(tempFolderName)") + } + + static func temporaryPath(extesnion: String) -> String { + let uuid = UUID().uuidString + return temporaryFolderPath.appending("/\(uuid).\(extesnion)") + } +} diff --git a/Sources/RHVoiceSwift/Extensions/String.swift b/Sources/RHVoiceSwift/Extensions/String.swift new file mode 100644 index 0000000..aad1855 --- /dev/null +++ b/Sources/RHVoiceSwift/Extensions/String.swift @@ -0,0 +1,35 @@ +// +// String.swift +// +// +// Created by Ihor Shevchuk on 09.03.2024. +// + +import Foundation + +extension String { + + func toPointer() -> UnsafePointer? { + return withCString { pointer in + return pointer + } + } + + func toUnsafePointer() -> UnsafePointer { + + guard let pointer = utf8CString.withUnsafeBufferPointer({ $0.baseAddress }) else { + return UnsafePointer(bitPattern: 0)! + } + return pointer + } + + init(char: UnsafePointer?) { + + guard let char else { + self.init() + return + } + + self.init(cString: char) + } +} diff --git a/Sources/RHVoiceSwift/RHLanguage.swift b/Sources/RHVoiceSwift/RHLanguage.swift new file mode 100644 index 0000000..068a073 --- /dev/null +++ b/Sources/RHVoiceSwift/RHLanguage.swift @@ -0,0 +1,19 @@ +// +// RHLanguage.swift +// +// +// Created by Ihor Shevchuk on 10.03.2024. +// + +import Foundation + +public struct RHLanguage { + internal(set) public var code: String + internal(set) public var country: String + internal(set) public var version: RHVersionInfo + public var voices: [RHSpeechSynthesisVoice] { + return RHSpeechSynthesisVoice.speechVoices.filter { voice in + voice.language.country == country + } + } +} diff --git a/Sources/RHVoiceSwift/RHSpeechSynthesisVoice.swift b/Sources/RHVoiceSwift/RHSpeechSynthesisVoice.swift new file mode 100644 index 0000000..6f0c2cd --- /dev/null +++ b/Sources/RHVoiceSwift/RHSpeechSynthesisVoice.swift @@ -0,0 +1,63 @@ +// +// RHSpeechSynthesisVoice.swift +// +// +// Created by Ihor Shevchuk on 10.03.2024. +// + +import Foundation +import RHVoice + +public struct RHSpeechSynthesisVoice { + + public enum Gender { + case Female + case Male + case Unknown + + init(gender: RHVoice_voice_gender) { + switch gender { + case RHVoice_voice_gender_unknown: + self = .Unknown + case RHVoice_voice_gender_female: + self = .Female + case RHVoice_voice_gender_male: + self = .Male + default: + self = .Unknown + } + } + } + + public var name: String { + String(char: _name) + } + public let language: RHLanguage + public var identifier: String { + String(char: _identifier) + } + public let gender: Gender + + let _name: UnsafePointer? + let _identifier: UnsafePointer? + + static public var speechVoices: [RHSpeechSynthesisVoice] { + return RHSpeechSynthesizer.shared.speechVoices + } + + init(name: UnsafePointer?, language: RHLanguage, identifier: UnsafePointer?, gender: Gender) { + self._name = name + self.language = language + self._identifier = identifier + self.gender = gender + } + + init(voice: RHVoice_voice_info) { + self.init(name: voice.name, + language: RHLanguage(code: String(char: voice.language), + country: String(char: voice.country), + version: RHVersionInfo(format: 0, revision: 0)), + identifier: voice.name, + gender: Gender(gender: voice.gender)) + } +} diff --git a/Sources/RHVoiceSwift/RHSpeechSynthesizer.swift b/Sources/RHVoiceSwift/RHSpeechSynthesizer.swift new file mode 100644 index 0000000..f189892 --- /dev/null +++ b/Sources/RHVoiceSwift/RHSpeechSynthesizer.swift @@ -0,0 +1,252 @@ +// +// RHSpeechSynthesizer.swift +// +// +// Created by Ihor Shevchuk on 09.03.2024. +// + +import Foundation +import RHVoice.RHVoice +import PlayerLib + +#if canImport(AVFoundation) +import AVFoundation +#endif + +public class RHSpeechSynthesizer { + public struct Params { + // TODO: have logger protocol(RHVoiceLoggerProtocol) and add variable of it's type here + public var dataPath: String? + public var configPath: String? + public static var `default`: Params = { + var pathToData = Bundle.main.path(forResource: "RHVoiceData", ofType: nil) + if pathToData == nil || pathToData?.isEmpty == true { + let rhVoiceBundle = Bundle(path: Bundle.main.path(forResource: "RHVoice_RHVoice", ofType: "bundle") ?? "") + pathToData = rhVoiceBundle?.path(forResource: "data", ofType: nil) + } + return Params(dataPath: pathToData, + // TODO: use FileManager.default.currentDirectoryPath here + configPath: "") + + }() + + var rhVoiceParam: RHVoice_init_params { + var result = RHVoice_init_params() + result.data_path = dataPath?.toPointer() + result.config_path = configPath?.toPointer() + return result + } + } + + public var params: Params { + didSet { + createEngine() + } + } + +#if canImport(AVFoundation) + public func speak(utterance: RHSpeechUtterance) async throws { + let path = String.temporaryPath(extesnion: "wav") + await synthesize(utterance: utterance, to: path) + let playerItem = AVPlayerItem(url: URL(fileURLWithPath: path)) + try await playItemAsync(playerItem) + try FileManager.default.removeItem(atPath: path) + } + + public func stopAndCancel() async { + await player?.pause() + player = nil + if let playerContinuation { + playerContinuation.resume() + self.playerContinuation = nil + } + } +#endif + + public func synthesize(utterance: RHSpeechUtterance, to path: String) async { + + fileStream = PlayerLib.FilePlaybackStream(path) + + let context = Unmanaged.passRetained(self).toOpaque() + + var params = utterance.synthParams + + let paramsAddress = withUnsafePointer(to: ¶ms) { pointer in + UnsafePointer(pointer) + } + + if utterance.empty { + return + } + + let text: String = utterance.ssml ?? "" + let message = RHVoice_new_message(rhVoiceEngine, + text, + UInt32(text.count), + RHVoice_message_ssml, + paramsAddress, + context) + + _ = RHVoice_speak(message) + + RHVoice_delete_message(message) + + fileStream = nil + } + + var rhVoiceEngine: RHVoice_tts_engine? + + public static var shared: RHSpeechSynthesizer = { + let instance = RHSpeechSynthesizer(params: .default) + return instance + }() + + private init(params: Params) { + self.params = params +#if canImport(AVFoundation) + try? FileManager.default.createTempFolderIfNeeded(at: String.temporaryFolderPath) +#endif + } + + deinit { + deleteEngine() +#if canImport(AVFoundation) + try? FileManager.default.removeTempFolderIfNeeded(at: String.temporaryFolderPath) +#endif + } + + var fileStream: PlayerLib.FilePlaybackStream? +#if canImport(AVFoundation) + var player: AVPlayer? + var playerContinuation: CheckedContinuation? +#endif +} + +private extension RHSpeechSynthesizer { + func createEngine() { + deleteEngine() + var params = params.rhVoiceParam + params.callbacks.play_speech = { samples, count, context in + guard let context else { + return 0 + } + + let object = Unmanaged.fromOpaque(context).takeUnretainedValue() + return object.received(samples: samples, count: count) + } + + params.callbacks.set_sample_rate = { sampleRate, context in + guard let context else { + return 0 + } + + let object = Unmanaged.fromOpaque(context).takeUnretainedValue() + return object.changed(sampleRate: sampleRate) + } + + params.callbacks.done = { context in + guard let context else { + return + } + + let object = Unmanaged.fromOpaque(context).takeUnretainedValue() + return object.finished() + } + + let address = withUnsafeMutablePointer(to: ¶ms) { pointer in + UnsafeMutablePointer(pointer) + } + + rhVoiceEngine = RHVoice_new_tts_engine(address) + } + + func deleteEngine() { + if let rhVoiceEngine { + RHVoice_delete_tts_engine(rhVoiceEngine) + } + } +} + +private extension RHSpeechSynthesizer { + func changed(sampleRate: Int32) -> Int32 { + return fileStream?.set_sample_rate(sampleRate) == true ? 1 : 0 + } + + func finished() { + fileStream?.finish() + } + + func received(samples: UnsafePointer?, count: UInt32) -> Int32 { + return fileStream?.play_speech(samples, Int(count)) == true ? 1 : 0 + } + +#if canImport(AVFoundation) + @MainActor + func playItemAsync(_ item: AVPlayerItem) async throws { + await stopAndCancel() + player = AVPlayer() + var observerEnd: Any? + var statusObserver: NSKeyValueObservation? + try await withCheckedThrowingContinuation { [weak self] continuation in + + guard let player = self?.player else { + continuation.resume() // TODO: throw an error here + return + } + + observerEnd = NotificationCenter.default.addObserver(forName: AVPlayerItem.didPlayToEndTimeNotification, object: item, queue: nil) { _ in + continuation.resume() + } + + statusObserver = item.observe(\.status, changeHandler: { item, _ in + if let error = item.error { + continuation.resume(throwing: error) + } + }) + + self?.playerContinuation = continuation + + player.replaceCurrentItem(with: item) + player.play() + } + + if let observerEnd { + NotificationCenter.default.removeObserver(observerEnd, name: .AVPlayerItemDidPlayToEndTime, object: item) + } + statusObserver?.invalidate() + self.playerContinuation = nil + await stopAndCancel() + print("finish") + } +#endif + +} + +extension RHSpeechSynthesizer { + + var speechVoices: [RHSpeechSynthesisVoice] { + guard let rhVoiceEngine else { + return [] + } + + let voicesCount = RHVoice_get_number_of_voices(rhVoiceEngine) + if voicesCount == 0 { + return [] + } + + guard let voices = RHVoice_get_voices(rhVoiceEngine) else { + return [] + } + + let voicesSequence = UnsafePointerSequence(start: voices, count: Int(voicesCount)) + + var result: [RHSpeechSynthesisVoice] = [] + + for voice in voicesSequence { + result.append(RHSpeechSynthesisVoice(voice: voice.pointee)) + } + + return result + } + +} diff --git a/Sources/RHVoiceSwift/RHSpeechUtterance.swift b/Sources/RHVoiceSwift/RHSpeechUtterance.swift new file mode 100644 index 0000000..e30eea1 --- /dev/null +++ b/Sources/RHVoiceSwift/RHSpeechUtterance.swift @@ -0,0 +1,50 @@ +// +// RHSpeechUtterance.swift +// +// +// Created by Ihor Shevchuk on 10.03.2024. +// + +import Foundation +import RHVoice + +public struct RHSpeechUtterance { + public enum Quality { + case Min + case Standart + case Max + } + + public let ssml: String? + var empty: Bool { + return self.ssml?.isEmpty == true + || self.ssml == nil + || self.ssml == "" + } + + public var voice: RHSpeechSynthesisVoice? + public var rate: Double = 1.0 + public var volume: Double = 1.0 + public var pitch: Double = 1.0 + public var quality: Quality = .Standart + + var synthParams: RHVoice_synth_params { + var result = RHVoice_synth_params() + result.absolute_pitch = 0.0 + result.absolute_rate = 0.0 + result.absolute_volume = 0.0 + result.relative_rate = rate + result.relative_pitch = pitch + result.relative_volume = volume + result.voice_profile = voice!.identifier.toPointer() + return result + } + + public init(ssml: String?) { + self.ssml = ssml + } + + public init(text: String?) { + self.init(ssml: "\(text ?? "")") + } +} diff --git a/Sources/RHVoiceSwift/RHVersionInfo.swift b/Sources/RHVoiceSwift/RHVersionInfo.swift new file mode 100644 index 0000000..4480fc8 --- /dev/null +++ b/Sources/RHVoiceSwift/RHVersionInfo.swift @@ -0,0 +1,17 @@ +// +// RHVersionInfo.swift +// +// +// Created by Ihor Shevchuk on 10.03.2024. +// + +import Foundation + +public struct RHVersionInfo { + internal(set) public var format: Int + internal(set) public var revision: Int + + public var string: String { + return "\(format).\(revision)" + } +} diff --git a/Sources/RHVoiceSwift/RHVoice.swift b/Sources/RHVoiceSwift/RHVoice.swift deleted file mode 100644 index 93f00f2..0000000 --- a/Sources/RHVoiceSwift/RHVoice.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// RHVoice.swift -// -// -// Created by Ihor Shevchuk on 08.03.2024. -// - -import Foundation -import RHVoice.core.package_client - -let voice_package = RHVoice.pkg.voice_package() diff --git a/Sources/RHVoiceSwift/Utils/UnsafePointerSequence.swift b/Sources/RHVoiceSwift/Utils/UnsafePointerSequence.swift new file mode 100644 index 0000000..5c87fb1 --- /dev/null +++ b/Sources/RHVoiceSwift/Utils/UnsafePointerSequence.swift @@ -0,0 +1,40 @@ +// +// UnsafePointerSequence.swift +// +// +// Created by Ihor Shevchuk on 10.03.2024. +// + +import Foundation + +struct UnsafePointerSequence: Sequence { + private let start: UnsafePointer + private let count: Int + + init(start: UnsafePointer, count: Int) { + self.start = start + self.count = count + } + + func makeIterator() -> UnsafePointerIterator { + return UnsafePointerIterator(start: start, count: count) + } +} + +struct UnsafePointerIterator: IteratorProtocol { + private var current: UnsafePointer + private let end: UnsafePointer + + init(start: UnsafePointer, count: Int) { + self.current = start + self.end = start + count + } + + mutating func next() -> UnsafePointer? { + guard current != end else { + return nil + } + defer { current = current.successor() } + return current + } +} diff --git a/Sources/RHVoiceSwiftSample/RHVoiceSwiftSample.swift b/Sources/RHVoiceSwiftSample/RHVoiceSwiftSample.swift new file mode 100644 index 0000000..285636c --- /dev/null +++ b/Sources/RHVoiceSwiftSample/RHVoiceSwiftSample.swift @@ -0,0 +1,42 @@ +// +// RHVoiceSwiftSample.swift +// +// +// Created by Ihor Shevchuk on 09.03.2024. +// + +import Foundation +import ArgumentParser +import RHVoiceSwift + +@main +struct PackDataExecutable: ParsableCommand { + + var dataPath: String { + let fileUrl = URL(fileURLWithPath: #file) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + + return fileUrl.path + "/RHVoice/RHVoice/data" + } + + func run() throws { + var params = RHVoiceSwift.RHSpeechSynthesizer.Params.default + + params.dataPath = dataPath + + let synthesizer = RHSpeechSynthesizer.shared + synthesizer.params = params + + Task { + let voices = RHSpeechSynthesisVoice.speechVoices + var utterance = RHSpeechUtterance(text: "Hello My name is RHVoice!") + utterance.voice = voices[0] + + try await synthesizer.speak(utterance: utterance) + } + + RunLoop.main.run() + } +}