HarmonyOS鸿蒙Next中arkTS调用lame.js问题

HarmonyOS鸿蒙Next中arkTS调用lame.js问题 想通过lame.js将生成的pcm数据转换为mp3保存到文件,在lame.min.js头部中增加了一个‘export’(lame.all.js在DevEco中提示有几处语法错,不知为何),终于过了语法编译关,但是运行时闪退,DevEco提示下面这句‘Error message:is not callable’:

let mp3encoder:object = new lamejs().Mp3Encoder(1, 8000, 128);


而lame.js原始的js样例是这样的:


mp3encoder = new lamejs.Mp3Encoder(1, 8000, 128);

但这样写DevEco提示‘Property ‘Mp3Encoder’ does not exist on type ‘() => void’. <ArkTSCheck>',故做了修改,虽然编译通过了,但运行时闪退。

也不清楚其他几处修改是否也有错误,请教是否有arkTS调用lame.js的完整例子?包括对象生成,缓冲区数据类型转换和数据复制等等。

谢谢了!


更多关于HarmonyOS鸿蒙Next中arkTS调用lame.js问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

10 回复

arkTS调用.js的例子

【背景知识】

  • ohpm:ohpm作为OpenHarmony三方库的包管理工具,支持OpenHarmony共享包的发布、安装和依赖管理。
  • ohpm convert:将指定ohpm或npm仓库中的某个包或者本地node_modules目录下的包转换成满足ohpm格式要求的HAR包,并保存至当前工作目录,转换后的包将支持上传至ohpm-repo私仓或OpenHarmony三方库中心仓。
  • ohpm-repo私仓搭建工具:ohpm-repo是一个搭建轻量级的ohpm私仓服务的工具。它与ohpm包管理器兼容,并按需缓存所有依赖项,加速私有网络中的安装。
  • OpenHarmony三方库中心仓:用于检索、查看所需OpenHarmony三方库信息,也可管理关于ohpm的个人配置。
  • js-e2e:基于eslint进行封装、配置规则,分析出JS库代码对NodeJS和Web浏览器的内置模块、对象的依赖及兼容ES标准版本。支持检查指定源码目录和指定三方库的兼容性。

【解决方案】

主要分为2步:

  1. 第1步参考文档移植JS/TS三方库检查兼容性并在目标设备(通常为HarmonyOS NEXT手机)上执行用例,确保三方库在目标设备上功能可用。

    • TS三方库通过DevEco Studio检查TS库的语法和兼容性。
    • JS三方库使用js-e2e工具扫描兼容性。

    下图是一个JS三方库扫描样例:扫描到underscore.js上使用了define和self。

    • (1) define函数是AMD模块规范定义的函数,代码合并后可能会被优化掉。
    • (2) self是浏览器的内置对象。 因此无法直接移植。由于JS语法相当灵活,极有可能是防御性代码引入的非兼容对象。开发者可分析源库源代码,对命中对象进行剥离。
  2. 第2步使用ohpm convert把JS/TS转换成HAR包。

具体实现可以参考如下步骤:

  1. 使用ohpm convert转换三方库需要先参考文档移植JS/TS三方库,如果原库为TS库不需要使用js-e2e工具分析,TS库可直接将放到DevEco Studio里面,DevEco Studio会检查是否有语法报错;JS库需要使用js-e2e工具扫描不兼容项,如果依赖NodeJS/Web库,则需要fork源库代码,进行侵入式修改,或者再找其他合适的三方库替代。
  2. 在命令行窗口使用ohpm convert将远程的npm三方库或者本地npm install后node_modules目录转换成HAR包。

执行完成ohpm convert远程转换命令会在执行命令目录下生成convert_xxxxxx的文件夹,使用本地npm install在转换会在node_modules同级目录下会生成convert_xxxxxx的文件夹,生成的HAR包会在文件夹下。

项目中引用HAR文件有三种方式:

  • 直接在项目中新建文件夹,把HAR包复制到文件夹下,在oh-package.json5中使用。
"dependencies": {
  "@protobuf-ts/plugin-framework": "file:./entry/src/main/ets/har/@protobuf-ts+plugin-framework.har",
  "@protobuf-ts/runtime": "file:./entry/src/main/ets/har/@protobuf-ts+runtime.har"
}
"dependencies": {
  "@protobuf-ts/plugin-framework": "2.9.6",
  "@protobuf-ts/runtime": "2.9.6"
}
"dependencies": {
  "@protobuf-ts/plugin-framework": "2.9.6",
  "@protobuf-ts/runtime": "2.9.6"
}

依赖的HAR包中会有引用其他HAR包的情况,会出现找不到依赖的报错,有三种方式解决。

  • 在工程级oh-package.json5中使用overrides将依赖树中的依赖替换为本地HAR包。
"overrides": {
  "typescript": "file:./entry/src/main/ets/har/typescript.har",
  "@protobuf-ts/runtime": "file:./entry/src/main/ets/har/@protobuf-ts+runtime.har"
}

上述引用完成后,在.ets文件中引入三方库。

import { WireType } from '@protobuf-ts/runtime';

export class Test {}

【总结】

  • 对于ArkTS中使用TS的三方库可以通过ohpm convert转换成HarmonyOS可使用的HAR包。
  • 对于HAR包中包含的依赖无法获取到时,需要上传依赖的HAR包或者使用overrides。
  • ArkTS中可以引用TS语言开发的三方库,TS文件不能引用ArkTS语言开发的三方库,参考文档
  • 对于移植的JS库需要使用js-e2e工具扫描扫,TS库则需要放入DevEco Studio检查语法。

【常见FAQ】

A:目前使用js-e2e工具扫描会发现有很多不兼容,但是ohpm下载的JS库中也有这些不兼容的参数是为什么? Q:JS库是多平台的,它要用在浏览器上就会有self、window这些,HarmonyOS上面是用不到的,不作调整是因为没有调用相关接口,如果在运行时调用到这些还是会闪退的,不调用就不会闪退。

A:为什么导入JS文件后打开程序会闪退? Q:JS文件如果有不兼容报错,会出现闪退情况,可排查JS文件是否有问题。

更多关于HarmonyOS鸿蒙Next中arkTS调用lame.js问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


用js-e2e检测了一下,近百个变量“没有定义”,看来比较依赖node.js或浏览器。只好放弃这个方案了,谢谢!,

【背景知识】

AudioCapturer是音频采集器,仅支持录制PCM格式音频数据,详情可以参考AudioCapturer开发指导

AVPlayer可以实现音频解码和音频输出功能。可用于直接播放MP3、M4A、WAV等格式的音频文件,不支持直接播放PCM格式文件,详情可以参考AVPlayer开发步骤

AudioRenderer用于播放PCM音频数据,相比AVPlayer而言,可以在输入前添加数据预处理,以实现更灵活的播放功能,详情可以参考AudioRenderer开发步骤

AVPlayer和AudioRenderer存在如下差异:

播放工具 支持格式 播放占用内存 播放PCM数据 优势
AudioRenderer PCM 可直接播放 数据预处理实现更灵活播放、可支持多实例音频播放
AVPlayer MP3/WAV等 需要转码播放 播放封装后的音频占用内存小
OHAudio PCM 可直接播放 适用于依赖Native层实现音频输出的场景

【解决方案】

对于AudioCapturer录制的PCM音频数据,有如下两种方案来进行处理:

方案一:使用AudioRenderer直接播放PCM格式的音频数据

AudioRenderer可以直接播放PCM数据,还可以通过数据预处理来实现更灵活的播放,关键代码如下:

  1. 配置文件路径:
let bufferSize: number = 0;
let path = getContext().cacheDir;
let filePath = path + '/StarWars10s-2C-48000-4SW.wav';
let file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
  1. 读取文件数据:
try {
  fs.readSync(file.fd, buffer, options);
  bufferSize += buffer.byteLength;
  // 系统会判定buffer有效,正常播放。
  return audio.AudioDataCallbackResult.VALID;
} catch (error) {
  console.error('Error reading file:', error);
  // 系统会判定buffer无效,不播放。
  return audio.AudioDataCallbackResult.INVALID;
}
  1. 调用start()方法进行音频渲染
audioRenderer.start((err: BusinessError) => {
  if (err) {
    console.error(`Renderer start failed, code is ${err.code}, message is ${err.message}`);
  } else {
    console.info('Renderer start success.');
  }
});

具体开发步骤以及完整代码可以参考AudioRenderer的开发步骤

方案二:对PCM数据进行音频转码后使用AVPlayer播放

AVPlayer无法直接播放PCM格式的音频数据,需要将音频数据转码封装成AVPlayer支持的格式。PCM格式数据是裸流,播放占用内存大,使用AVPlayer播放封装后的音频数据会占用更小的内存。

这里以WAV格式为例,WAV格式是一种无损的格式,可以最好地保存音频质量,如果对音频大小或者格式有其他要求,可以参考音频编码媒体数据封装进行其他的音频编码格式转化。

现在将PCM数据转码封装成完整的WAV文件再用AVPlayer播放,参考代码如下:

  1. 定义PCM转WAV的方法,获取源文件路径和目标文件路径,分别写入WAV文件头和PCM数据,参考代码如下:
public pcmToWav(src: string, dest: string) {
  const inFile: fs.File = fs.openSync(src, fs.OpenMode.READ_ONLY);
  const outFile: fs.File = fs.openSync(dest, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  let byteRate = 16 * sampleRate * channel / 8;
  let inFileStat = fs.statSync(inFile.fd)
  // 获取源文件信息,包括文件大小等
  let audioDataSize = inFileStat.size;
  let totalDataLen = audioDataSize + 36;
  console.log('audioDataSize= ', audioDataSize)
  // 1.wav文件头编写
  this.writeWavFileHeader(outFile, audioDataSize, totalDataLen, byteRate);
  // 2.写入pcm数据
  this.writePcmData(inFile, outFile, audioDataSize)
}
  1. 定义写入WAV头部信息的方法,创建一个大小为44字节的缓冲区,用于存储WAV文件的头部信息,再将其写入输出文件,参考代码如下:
private writeString(dv: DataView, offset: number, str: string) {
  for (let i = 0; i < str.length; i++) {
    dv.setUint8(offset + i, str.charCodeAt(i));
  }
}
// 定义写入WAV头文件信息的方法
private writeWavFileHeader(out: fs.File, audioDataSize: number, totalDataLen: number, byteRate: number) {
  const header = new ArrayBuffer(44);
  const dv = new DataView(header);
  const bitsPerSample = 16; // 当前位深是16
  // 写入RIFF块
  this.writeString(dv, 0, 'RIFF');
  dv.setUint32(4, totalDataLen, true);
  this.writeString(dv, 8, 'WAVE');
  // 写入fmt块
  this.writeString(dv, 12, 'fmt ');
  dv.setUint32(16, 16, true); // fmt块大小
  dv.setUint16(20, 1, true); // 格式类别(PCM)
  dv.setUint16(22, channel, true); // 通道数
  dv.setUint32(24, sampleRate, true); // 采样率
  dv.setUint32(28, byteRate, true); // 比特率
  dv.setUint16(32, channel * bitsPerSample / 8, true); // 每个采样点的字节数
  dv.setUint16(34, bitsPerSample, true); // 位深
  // 写入data块
  this.writeString(dv, 36, 'data');
  dv.setUint32(40, audioDataSize, true); // 数据块大小
  console.log('audioDataSize= ', audioDataSize)
  // 将头文件信息写入输出文件
  fs.writeSync(out.fd, new Uint8Array(header).buffer, {
    length: 44
  })
}
  1. 定义读取pcm数据的方法,将PCM数据从输入文件写入输出文件,使用fs.readSync读取输入文件的数据,并写入输出文件,直到读取完毕,参考代码如下:
private writePcmData(inFile: fs.File, outFile: fs.File, audioDataSize: number) {
  // 写入PCM数据
  let readSize = 0
  let data = new ArrayBuffer(audioDataSize);
  let readOptions: ReadOptions = {
    offset: readSize,
    length: audioDataSize
  };
  let readLen = fs.readSync(inFile.fd, data, readOptions);
  while (readLen > 0) {
    readSize += readLen;
    fs.writeSync(outFile.fd, data, { length: readLen });
    readOptions.offset = readSize;
    readLen = fs.readSync(inFile.fd, data, readOptions);
  }
  fs.closeSync(inFile.fd)
  fs.closeSync(outFile.fd)
}
  1. 完成转码后让AVPlayer使用fs文件系统打开沙箱地址获取媒体文件地址并通过dataSrc属性进行播放,AVPlayer的具体开发流程可以参考AVPlayer播放音频完整示例

方案三:使用OHAudio播放格式的PCM音频数据

具体可参考:使用OHAudio开发音频播放功能

【常见FAQ】

Q:AVPlayer支持多种播放格式,为什么转码的时候会优先转为WAV格式?

A:WAV是一种无损的格式,转码方式简单,并且本身的支持度高,可以达到较高的质量要求,而MP3等是有损的格式,会牺牲音频文件质量来换取较小的体积,为尽量保证音频质量,一般会选择转码为WAV格式。

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:BV1S4411E7LY/?p=17

这种方式不行的话可以换一个思路来处理这个需求,直接将这个功能页面做成一个H5,使用ArkWeb来处理传输,当然具体的还要看楼主的需求是怎样的

猜测可能是运行时PCM数据缓冲区类型可能与JavaScript原生类型存在差异,导致内存访问越界。参考官网通过@kit.AudioKit实现PCM转码试试行不行?

import fs from '@ohos.file.fs';

function pcmToWav(pcmPath: string, wavPath: string) {
  // 添加WAV文件头逻辑
  const header = new ArrayBuffer(44);
  // ...写入采样率、声道数等元数据
  fs.writeSync(wavPath, header);
  
  // 追加PCM数据
  const pcmData = fs.readSync(pcmPath);
  fs.appendFile(wavPath, pcmData.buffer);
}

// 使用AVPlayer播放
import media from '@kit.MediaKit';
const avPlayer = new media.AVPlayer();
avPlayer.url = 'file://' + wavPath;
avPlayer.play();

只找到用C语言的工具,但是不太会混合编程。media.AVTranscoder试了一下,似乎只能转视频文件,wav文件会出错。

补充lame.js官方简单调用样例:

var mp3Data = [];

var mp3encoder = new lamejs.Mp3Encoder(1, 44100, 128); //mono 44.1khz encode to 128kbps
var samples = new Int16Array(44100); //one second of silence replace that with your own samples
var mp3Tmp = mp3encoder.encodeBuffer(samples); //encode mp3

//Push encode buffer to mp3Data variable
mp3Data.push(mp3Tmp);

// Get end part of mp3
mp3Tmp = mp3encoder.flush();

// Write last data to the output data, too
// mp3Data contains now the complete mp3Data
mp3Data.push(mp3Tmp);

console.debug(mp3Data);

在HarmonyOS Next中,arkTS通过FFI机制调用lame.js需使用NAPI接口封装。具体步骤:

  1. 编写C++层NAPI模块,将lame.js功能封装为接口;
  2. 使用napi_value转换数据类型;
  3. 在arkTS侧通过import native模块调用。

注意暂不支持直接调用JS库,需通过Native层桥接。确保NDK版本匹配鸿蒙SDK要求,编译生成.so文件部署到应用中。

在HarmonyOS Next中使用arkTS调用lame.js确实需要注意一些兼容性问题。针对您的问题,建议如下解决方案:

  1. 关于lame.js导入问题: 建议使用ES Module方式导入:
import lamejs from './lame.min.js';
  1. 创建Mp3Encoder的正确方式应该是:
const mp3encoder = new lamejs.Mp3Encoder(1, 8000, 128);
  1. 常见问题排查点:
  • 确保lame.js文件已正确放置在项目目录中
  • 检查lame.js是否完整,建议使用官方提供的未压缩版本进行调试
  • 确认arkTS编译器配置中允许使用ES6语法
  1. 数据类型转换示例:
// PCM数据转换示例
const samples = new Int16Array(pcmData);
const mp3Data = mp3encoder.encodeBuffer(samples);

如果仍遇到问题,建议先简化测试用例,逐步验证各个功能模块。arkTS对JS的兼容性较好,但某些ES6+特性可能需要polyfill支持。

回到顶部