audio.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import { assetManager, AudioClip, AudioSource, director, Node } from "cc";
  2. const ch_log = chsdk.log;
  3. const ch_storage = chsdk.storage;
  4. /**音频等资源加载方式*/
  5. export enum loadType {
  6. /**未知*/
  7. none = 0,
  8. /**bundle*/
  9. bundle = 1,
  10. /**远程*/
  11. remote = 2,
  12. }
  13. interface AudioConfig {
  14. volume_music: number;
  15. volume_effect: number;
  16. switch_music: boolean;
  17. switch_effect: boolean;
  18. }
  19. /**音频播放控制
  20. * 需要初始化资源加载方式
  21. * loadType.bundle 时需要设置 bundle名
  22. * loadType.remote 测试使用远程的音频需要设置远程地址
  23. */
  24. export default class ch_audio {
  25. private static _instance: ch_audio;
  26. public static getInstance(): ch_audio {
  27. if (!this._instance) this._instance = new ch_audio();
  28. return this._instance;
  29. }
  30. private _volume_music: number = 1;
  31. private _volume_effect: number = 1;
  32. private _switch_music: boolean = true;
  33. private _switch_effect: boolean = true;
  34. private readonly _effect_max: number = 5;
  35. private _effect_index: number = 0;
  36. private _effect_source_pool: AudioSource[] = [];
  37. private _music_source: AudioSource;
  38. private _load_type: loadType = loadType.none;
  39. private _bundle_name: string;
  40. private _remote_url: string;
  41. private _playing_sound: Set<string> = new Set();
  42. private _remote_cache: Map<string, AudioClip> = new Map();
  43. constructor() {
  44. const audio = new Node();
  45. audio.name = '__ch_audio__';
  46. director.getScene().addChild(audio);
  47. director.addPersistRootNode(audio);
  48. this._music_source = this._create(audio);
  49. for (let i = 0; i < this._effect_max; i++) {
  50. this._effect_source_pool.push(this._create(audio));
  51. }
  52. this.load();
  53. }
  54. /**
  55. * 创建音频源
  56. * @param node 节点
  57. * @param volume 音量
  58. * @returns AudioSource 音频源组件
  59. */
  60. private _create(node: Node): AudioSource {
  61. const source = node.addComponent(AudioSource);
  62. source.loop = false;
  63. source.playOnAwake = false;
  64. source.volume = 0.5;
  65. return source;
  66. }
  67. /**初始化*/
  68. init(load_type: loadType, bundle_name: string, remote_url: string): void {
  69. this._load_type = load_type;
  70. this._bundle_name = bundle_name;
  71. this._remote_url = remote_url.endsWith('/') ? remote_url : remote_url + '/';
  72. }
  73. /**切换bundle*/
  74. set_bundle_name(bundle_name: string): void {
  75. this._bundle_name = bundle_name;
  76. }
  77. /**
  78. * 释放通过 [[load]] 或者 [[loadDir]] 加载的声音资源。
  79. * @param sound 声音资源路径
  80. */
  81. release(sound?: string): void {
  82. if (this._load_type == loadType.none) {
  83. ch_log.warn('音频模块未初始化');
  84. } else if (this._load_type == loadType.bundle) {
  85. const bundle = assetManager.getBundle(this._bundle_name);
  86. if (!sound) {
  87. bundle.releaseAll();
  88. } else {
  89. bundle.release(sound, AudioClip);
  90. }
  91. } else if (this._load_type == loadType.remote) {
  92. if (!sound) {
  93. this._remote_cache.forEach((v, k) => {
  94. assetManager.releaseAsset(v);
  95. });
  96. this._remote_cache.clear();
  97. } else {
  98. const path = this._remote_url + sound;
  99. const clip = this._remote_cache.get(path);
  100. if (clip) {
  101. assetManager.releaseAsset(clip);
  102. this._remote_cache.delete(path);
  103. }
  104. }
  105. }
  106. }
  107. /** 保存音乐音效的音量、开关配置数据到本地 */
  108. save() {
  109. const local_data: any = {};
  110. local_data.volume_music = this._volume_music;
  111. local_data.volume_effect = this._volume_effect;
  112. local_data.switch_music = this._switch_music;
  113. local_data.switch_effect = this._switch_effect;
  114. ch_storage.set("ch_audio", local_data);
  115. }
  116. /** 本地加载音乐音效的音量、开关配置数据并设置到游戏中 */
  117. load() {
  118. const local_data = ch_storage.getObject("ch_audio");
  119. if (local_data) {
  120. try {
  121. this.setState(local_data);
  122. }
  123. catch (e) {
  124. this.setStateDefault();
  125. }
  126. }
  127. else {
  128. this.setStateDefault();
  129. }
  130. }
  131. private setState(local_data: AudioConfig) {
  132. this.volumeMusic = local_data.volume_music;
  133. this.volumeEffect = local_data.volume_effect;
  134. this.switchMusic = local_data.switch_music;
  135. this.switchEffect = local_data.switch_effect;
  136. }
  137. private setStateDefault() {
  138. this.volumeMusic = 0.8;
  139. this.volumeEffect = 0.8;
  140. this.switchMusic = true;
  141. this.switchEffect = true;
  142. }
  143. /**
  144. * 获取背景音乐音量
  145. */
  146. get volumeMusic(): number {
  147. return this._volume_music;
  148. }
  149. /**
  150. * 设置背景音乐音量
  151. * @param value 音乐音量值
  152. */
  153. set volumeMusic(value: number) {
  154. this._volume_music = value;
  155. this._music_source.volume = value;
  156. }
  157. /**
  158. * 获取背景音乐开关值
  159. */
  160. get switchMusic(): boolean {
  161. return this._switch_music;
  162. }
  163. /**
  164. * 设置背景音乐开关值
  165. * @param value 开关值
  166. */
  167. set switchMusic(value: boolean) {
  168. this._switch_music = value;
  169. if (value == false) this._music_source.stop();
  170. }
  171. /**
  172. * 获取音效音量
  173. */
  174. get volumeEffect(): number {
  175. return this._volume_effect;
  176. }
  177. /**
  178. * 设置获取音效音量
  179. * @param value 音效音量值
  180. */
  181. set volumeEffect(value: number) {
  182. this._volume_effect = value;
  183. for (let i = 0; i < this._effect_source_pool.length; i++) {
  184. this._effect_source_pool[i].volume = this._volume_effect;
  185. }
  186. }
  187. /**
  188. * 获取音效开关值
  189. */
  190. get switchEffect(): boolean {
  191. return this._switch_effect;
  192. }
  193. /**
  194. * 设置音效开关值
  195. * @param value 音效开关值
  196. */
  197. set switchEffect(value: boolean) {
  198. this._switch_effect = value;
  199. if (value == false) {
  200. for (let i = 0; i < this._effect_source_pool.length; i++) {
  201. this._effect_source_pool[i].stop();
  202. }
  203. }
  204. }
  205. /**
  206. * @en
  207. * play short audio, such as strikes,explosions
  208. * @zh
  209. * 播放短音频,比如 打击音效,爆炸音效等
  210. * @param sound clip or url for the audio
  211. * @param interval 同名字音频限制播放间隔(毫秒) (默认:0不限制 特殊系数:>0 <=1 使用音频时间X此系数)
  212. */
  213. playOneShot(sound: AudioClip | string, interval: number = 0, remote_ext: string = '.mp3') {
  214. if (!this._switch_effect) return;
  215. if (sound instanceof AudioClip) {
  216. this.doPlayOneShot(sound, interval);
  217. }
  218. else {
  219. if (this._load_type == loadType.none) {
  220. ch_log.warn('音频模块未初始化');
  221. } else if (this._load_type == loadType.bundle) {
  222. const bundle = assetManager.getBundle(this._bundle_name);
  223. if (!bundle) {
  224. ch_log.warn(`请确保 bundle${this._bundle_name} 已加载`);
  225. } else {
  226. bundle.load(sound, (err, clip: AudioClip) => {
  227. if (err) {
  228. ch_log.error(err);
  229. }
  230. else {
  231. this.doPlayOneShot(clip, interval);
  232. }
  233. });
  234. }
  235. } else if (this._load_type == loadType.remote) {
  236. const fullPath = this._remote_url + sound + remote_ext;
  237. if (this._remote_cache.has(fullPath)) {
  238. this.doPlayOneShot(this._remote_cache.get(fullPath)!, interval);
  239. return;
  240. }
  241. assetManager.loadRemote(fullPath, (err: Error | null, clip: AudioClip) => {
  242. if (err) {
  243. ch_log.error(err);
  244. } else {
  245. this._remote_cache.set(fullPath, clip);
  246. this.doPlayOneShot(clip, interval);
  247. }
  248. });
  249. }
  250. }
  251. }
  252. private doPlayOneShot(clip: AudioClip, interval: number): void {
  253. const name: string = clip.name;
  254. if (interval > 0) {
  255. if (this._playing_sound.has(name)) return;
  256. this._playing_sound.add(name);
  257. const time = interval <= 1 ? clip.getDuration() * interval * 1000 : interval;
  258. setTimeout(() => { this._playing_sound.delete(name); }, time);
  259. this.getNextEffectSource().playOneShot(clip, this._volume_effect);
  260. } else {
  261. this.getNextEffectSource().playOneShot(clip, this._volume_effect);
  262. }
  263. }
  264. private getNextEffectSource(): AudioSource {
  265. const source = this._effect_source_pool[this._effect_index];
  266. this._effect_index = (this._effect_index + 1) % this._effect_max;
  267. return source;
  268. }
  269. /**
  270. * @en
  271. * play long audio, such as the bg music
  272. * @zh
  273. * 播放长音频,比如 背景音乐
  274. * @param sound clip or url for the sound
  275. */
  276. play(sound: AudioClip | string, remote_ext: string = '.mp3') {
  277. if (!this._switch_music) return;
  278. if (sound instanceof AudioClip) {
  279. this._music_source.loop = true;
  280. this._music_source.stop();
  281. this._music_source.clip = sound;
  282. this._music_source.play();
  283. this._music_source.volume = this._volume_music;
  284. }
  285. else {
  286. if (this._load_type == loadType.none) {
  287. ch_log.warn('音频模块未初始化');
  288. } else if (this._load_type == loadType.bundle) {
  289. const bundle = assetManager.getBundle(this._bundle_name);
  290. if (!bundle) {
  291. ch_log.warn(`请确保 bundle${this._bundle_name} 已加载`);
  292. } else {
  293. bundle.load(sound, (err, clip: AudioClip) => {
  294. if (err) {
  295. ch_log.error(err);
  296. }
  297. else {
  298. this._music_source.loop = true;
  299. this._music_source.stop();
  300. this._music_source.clip = clip;
  301. this._music_source.play();
  302. this._music_source.volume = this._volume_music;
  303. }
  304. });
  305. }
  306. } else if (this._load_type == loadType.remote) {
  307. assetManager.loadRemote(this._remote_url + sound + remote_ext, (err: Error | null, clip: AudioClip) => {
  308. if (err) {
  309. ch_log.error(err);
  310. }
  311. else {
  312. this._music_source.loop = true;
  313. this._music_source.stop();
  314. this._music_source.clip = clip;
  315. this._music_source.play();
  316. this._music_source.volume = this._volume_music;
  317. }
  318. });
  319. }
  320. }
  321. }
  322. /**
  323. * stop the audio play
  324. */
  325. stop() {
  326. this._music_source.stop();
  327. for (let i = 0; i < this._effect_source_pool.length; i++) {
  328. this._effect_source_pool[i].stop();
  329. }
  330. }
  331. /**stop and clean */
  332. clean() {
  333. this._music_source.stop();
  334. this._music_source.clip?.name
  335. this._music_source.clip = null;
  336. for (let i = 0; i < this._effect_source_pool.length; i++) {
  337. this._effect_source_pool[i].stop();
  338. this._effect_source_pool[i].clip = null;
  339. }
  340. }
  341. /**
  342. * pause the audio play
  343. */
  344. pause() {
  345. this._music_source.pause();
  346. }
  347. /**
  348. * resume the audio play
  349. */
  350. resume() {
  351. if (!this._switch_music) return;
  352. this._music_source.play();
  353. }
  354. /** 重播当前音乐 */
  355. public replay_music(): void {
  356. this._music_source.stop();
  357. this._music_source.play();
  358. }
  359. }