flutter_tts icon indicating copy to clipboard operation
flutter_tts copied to clipboard

synthesizeToFile does not use provided path

Open hydralien opened this issue 4 years ago • 4 comments

🚀 Feature Requests

it'd be nice if synthesizeToFile would somehow accept a full path to file (or maybe new method could be added to solve this) - otherwise some default directory is used as root and when the "fileName" is provided as e.g. /data/user/0/net.hydralien.pidging/app_flutter/9accce7af07e27e86bb47b7b3d69f6d2c094c1fb.audio, the actual file is attempted to be stored as /storage/emulated/0/Android/data/net.hydralien.pidging/files/data/user/0/net.hydralien.pidging/app_flutter/9accce7af07e27e86bb47b7b3d69f6d2c094c1fb.audio, failing with java.io.FileNotFoundException: open failed: ENOENT (No such file or directory)

Describe the feature

Because Android TTS does not work offline if the requested language is not installed in the system, I'm trying to store the synthesised files offline so they could be played if exist. However it's not clear how to get to them once they're generated (I found some answer to that here: https://stackoverflow.com/questions/65901746/how-to-get-the-absolute-path-of-a-file-in-flutter/65904131 - but that's not quite obvious, and I'm not sure whether it's reliable)

Platforms affected (mark all that apply)

  • [x] :robot: Android

hydralien avatar Jun 15 '21 17:06 hydralien

Thanks for your job. May be kotlin function synthesizeToFile could become :

` private fun synthesizeToFile(text: String, fileName: String) { val fullPath: String val uuid: String = UUID.randomUUID().toString() bundle!!.putString( TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, SYNTHESIZE_TO_FILE_PREFIX + uuid )

    val result: Int =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !fileName.startsWith("/")) {
                val resolver = this.context?.contentResolver
                val contentValues =
                        ContentValues().apply {
                            put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
                            put(MediaStore.MediaColumns.MIME_TYPE, "audio/wav")
                            put(
                                    MediaStore.MediaColumns.RELATIVE_PATH,
                                    Environment.DIRECTORY_MUSIC
                            )
                        }

                val uri =
                        resolver?.insert(
                                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                                contentValues
                        )

                this.parcelFileDescriptor = resolver?.openFileDescriptor(uri!!, "rw")
                fullPath = uri?.path + File.separatorChar + fileName

                tts!!.synthesizeToFile(
                        text,
                        bundle!!,
                        parcelFileDescriptor!!,
                        SYNTHESIZE_TO_FILE_PREFIX + uuid
                )
            } else {
                val musicDir =
                        Environment.getExternalStoragePublicDirectory(
                                Environment.DIRECTORY_MUSIC
                        )

                val file =
                        if (fileName.startsWith("/")) File(fileName)
                        else File(musicDir, fileName)

                fullPath = file.path

                tts!!.synthesizeToFile(text, bundle!!, file!!, SYNTHESIZE_TO_FILE_PREFIX + uuid)
            }

    if (result == TextToSpeech.SUCCESS) {
        Log.d(tag, "Successfully created file : $fullPath")
    } else {
        Log.d(tag, "Failed creating file : $fullPath")
    }
}

`

Nimo11 avatar Mar 22 '24 14:03 Nimo11

@Nimo11 that's a good idea and I'll see if I can implement that. I'll also have to check on how to accomplish the same on iOS.

dlutton avatar Mar 23 '24 00:03 dlutton

Yes that would be very interesting for me thanks

PROMPTIMMO avatar Mar 23 '24 08:03 PROMPTIMMO

You can try this in SwiftFlutterTtsPlugin. It work for me :

private func synthesizeToFile(text: String, fileName: String, result: @escaping FlutterResult) {
    var output: AVAudioFile?

    var failed = false
    let utterance = AVSpeechUtterance(string: text)

    if self.voice != nil {
      utterance.voice = self.voice!
    } else {
      utterance.voice = AVSpeechSynthesisVoice(language: self.language)
    }
    utterance.rate = self.rate
    utterance.volume = self.volume
    utterance.pitchMultiplier = self.pitch
    
    if #available(iOS 13.0, *) {
      self.synthesizer.write(utterance) { (buffer: AVAudioBuffer) in
        guard let pcmBuffer = buffer as? AVAudioPCMBuffer else {
            NSLog("unknow buffer type: \(buffer)")
            failed = true
            return
        }
        print(pcmBuffer.format)
        if pcmBuffer.frameLength == 0 {
            // finished
        } else {
          // append buffer to file
          
          let fileURL = fileName.hasPrefix("/") ?
            URL(string: fileName):
            FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(fileName)
          
        if let fileURL {

          NSLog("Saving utterance to file: \(fileURL.absoluteString)")

          if output == nil {
            do {
              if #available(iOS 17.0, *) {
                guard let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: Double(22050), channels: 1, interleaved: false) else {
                  NSLog("Error creating audio format for iOS 17+")
                  failed = true
                  return
                }
                output = try AVAudioFile(forWriting: fileURL, settings: audioFormat.settings)
              } else {
                output = try AVAudioFile(forWriting: fileURL, settings: pcmBuffer.format.settings, commonFormat: .pcmFormatInt16, interleaved: false)
              }
            } catch {
                NSLog("Error creating AVAudioFile: \(error.localizedDescription)")
                failed = true
                return
            }
          }


          try! output!.write(from: pcmBuffer)
        }}
      }
    } else {
        result("Unsupported iOS version")
    }
    if failed {
        result(0)
    }
    if self.awaitSynthCompletion {
      self.synthResult = result
    } else {
      result(1)
    }
  }

Nimo11 avatar Mar 27 '24 09:03 Nimo11