|
@@ -0,0 +1,274 @@
|
|
|
+//import { Component, OnInit } from '@angular/core';
|
|
|
+import { Component, ElementRef, ViewChild, OnInit } from '@angular/core';
|
|
|
+import { Capacitor } from '@capacitor/core';
|
|
|
+import { Filesystem, Directory } from '@capacitor/filesystem';
|
|
|
+//import { AudioService } from '../../services/audio.service';
|
|
|
+import { Share } from '@capacitor/share';
|
|
|
+import { AlertController } from '@ionic/angular';
|
|
|
+import { AudioService } from 'src/app/services/audio.service';
|
|
|
+@Component({
|
|
|
+ selector: 'app-scream-room',
|
|
|
+ templateUrl: './scream-room.page.html',
|
|
|
+ styleUrls: ['./scream-room.page.scss'],
|
|
|
+ standalone:false,
|
|
|
+})
|
|
|
+export class ScreamRoomPage implements OnInit {
|
|
|
+
|
|
|
+ @ViewChild('visualizerCanvas') visualizerCanvas!: ElementRef<HTMLCanvasElement>;
|
|
|
+
|
|
|
+ isRecording = false;
|
|
|
+ volumeLevel = 0;
|
|
|
+ volumePercentage = 0;
|
|
|
+ waveformImage: string | null = null;
|
|
|
+ private canvasCtx!: CanvasRenderingContext2D;
|
|
|
+ private animationId!: number;
|
|
|
+ private audioChunks: Blob[] = [];
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private audioService: AudioService,
|
|
|
+ private alertController: AlertController
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ ngOnInit() {
|
|
|
+ this.audioService.initAudioContext();
|
|
|
+ }
|
|
|
+
|
|
|
+ ngAfterViewInit() {
|
|
|
+ this.setupVisualizer();
|
|
|
+ }
|
|
|
+
|
|
|
+ private setupVisualizer() {
|
|
|
+ const canvas = this.visualizerCanvas.nativeElement;
|
|
|
+ this.canvasCtx = canvas.getContext('2d')!;
|
|
|
+ canvas.width = canvas.offsetWidth;
|
|
|
+ canvas.height = 200;
|
|
|
+
|
|
|
+ this.drawInitialVisualizer();
|
|
|
+ }
|
|
|
+
|
|
|
+ private drawInitialVisualizer() {
|
|
|
+ const { width, height } = this.visualizerCanvas.nativeElement;
|
|
|
+ this.canvasCtx.clearRect(0, 0, width, height);
|
|
|
+
|
|
|
+ // 绘制初始平静的波浪线
|
|
|
+ this.canvasCtx.lineWidth = 2;
|
|
|
+ this.canvasCtx.strokeStyle = '#3880ff';
|
|
|
+ this.canvasCtx.beginPath();
|
|
|
+
|
|
|
+ for (let x = 0; x < width; x++) {
|
|
|
+ const y = height / 2 + Math.sin(x * 0.05) * 20;
|
|
|
+ if (x === 0) {
|
|
|
+ this.canvasCtx.moveTo(x, y);
|
|
|
+ } else {
|
|
|
+ this.canvasCtx.lineTo(x, y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.canvasCtx.stroke();
|
|
|
+ }
|
|
|
+
|
|
|
+ async startRecording() {
|
|
|
+ try {
|
|
|
+ this.isRecording = true;
|
|
|
+ this.audioChunks = [];
|
|
|
+ await this.audioService.startRecording((volume) => {
|
|
|
+ this.volumeLevel = volume / 100; // 转换为0-1范围
|
|
|
+ this.volumePercentage = Math.round(volume);
|
|
|
+ this.updateVisualizer(volume);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.startVisualizerAnimation();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('录音失败:', error);
|
|
|
+ this.presentAlert('录音失败', '无法访问麦克风,请检查权限设置');
|
|
|
+ this.isRecording = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async stopRecording() {
|
|
|
+ this.isRecording = false;
|
|
|
+ cancelAnimationFrame(this.animationId);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const audioBlob = await this.audioService.stopRecording();
|
|
|
+ this.audioChunks = this.audioService.getRecordedChunks();
|
|
|
+
|
|
|
+ // 生成声波图
|
|
|
+ this.generateWaveformArt();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('停止录音失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private startVisualizerAnimation() {
|
|
|
+ const canvas = this.visualizerCanvas.nativeElement;
|
|
|
+ const { width, height } = canvas;
|
|
|
+
|
|
|
+ const draw = () => {
|
|
|
+ if (!this.isRecording) return;
|
|
|
+
|
|
|
+ this.canvasCtx.clearRect(0, 0, width, height);
|
|
|
+
|
|
|
+ // 根据音量动态绘制
|
|
|
+ this.canvasCtx.lineWidth = 2;
|
|
|
+ this.canvasCtx.strokeStyle = '#ff3860';
|
|
|
+ this.canvasCtx.beginPath();
|
|
|
+
|
|
|
+ const amplitude = this.volumePercentage / 2;
|
|
|
+
|
|
|
+ for (let x = 0; x < width; x++) {
|
|
|
+ const y = height / 2 +
|
|
|
+ Math.sin(x * 0.05 + Date.now() * 0.005) * (20 + amplitude) +
|
|
|
+ Math.sin(x * 0.1) * (10 + amplitude/2);
|
|
|
+
|
|
|
+ if (x === 0) {
|
|
|
+ this.canvasCtx.moveTo(x, y);
|
|
|
+ } else {
|
|
|
+ this.canvasCtx.lineTo(x, y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.canvasCtx.stroke();
|
|
|
+ this.animationId = requestAnimationFrame(draw);
|
|
|
+ };
|
|
|
+
|
|
|
+ this.animationId = requestAnimationFrame(draw);
|
|
|
+ }
|
|
|
+
|
|
|
+ private updateVisualizer(volume: number) {
|
|
|
+ // 实时更新可视化效果
|
|
|
+ // 动画循环中已经处理,这里可以留空或添加额外效果
|
|
|
+ }
|
|
|
+
|
|
|
+ private generateWaveformArt() {
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = 800;
|
|
|
+ canvas.height = 400;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+
|
|
|
+ // 创建渐变背景
|
|
|
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
|
|
|
+ gradient.addColorStop(0, '#ff758c');
|
|
|
+ gradient.addColorStop(1, '#ff7eb3');
|
|
|
+ ctx.fillStyle = gradient;
|
|
|
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
+
|
|
|
+ // 分析音频数据生成波形
|
|
|
+ this.audioService.analyzeAudio(this.audioChunks, (dataArray) => {
|
|
|
+ ctx.lineWidth = 4;
|
|
|
+ ctx.strokeStyle = '#ffffff';
|
|
|
+ ctx.globalCompositeOperation = 'overlay';
|
|
|
+ ctx.beginPath();
|
|
|
+
|
|
|
+ const sliceWidth = canvas.width / dataArray.length;
|
|
|
+ let x = 0;
|
|
|
+
|
|
|
+ for (let i = 0; i < dataArray.length; i++) {
|
|
|
+ const v = dataArray[i] / 255.0;
|
|
|
+ const y = v * canvas.height;
|
|
|
+
|
|
|
+ if (i === 0) {
|
|
|
+ ctx.moveTo(x, y);
|
|
|
+ } else {
|
|
|
+ ctx.lineTo(x, y);
|
|
|
+ }
|
|
|
+
|
|
|
+ x += sliceWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ // 添加一些艺术效果
|
|
|
+ this.addArtisticEffects(ctx, canvas.width, canvas.height, dataArray);
|
|
|
+
|
|
|
+ // 转换为图像URL
|
|
|
+ this.waveformImage = canvas.toDataURL('image/png');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private addArtisticEffects(ctx: CanvasRenderingContext2D, width: number, height: number, dataArray: Uint8Array) {
|
|
|
+ // 添加粒子效果
|
|
|
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
|
|
+ for (let i = 0; i < dataArray.length; i += 10) {
|
|
|
+ const x = (i / dataArray.length) * width;
|
|
|
+ const y = (dataArray[i] / 255.0) * height;
|
|
|
+ const size = (dataArray[i] / 255.0) * 10 + 2;
|
|
|
+
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
|
+ ctx.fill();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加一些随机线条增加艺术感
|
|
|
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ for (let i = 0; i < 20; i++) {
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(Math.random() * width, Math.random() * height);
|
|
|
+ ctx.lineTo(Math.random() * width, Math.random() * height);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async saveArtwork() {
|
|
|
+ if (!this.waveformImage) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 保存到相册
|
|
|
+ const base64Data = this.waveformImage.split(',')[1];
|
|
|
+ const fileName = `scream-art-${new Date().getTime()}.png`;
|
|
|
+
|
|
|
+ await Filesystem.writeFile({
|
|
|
+ path: fileName,
|
|
|
+ data: base64Data,
|
|
|
+ directory: Directory.Documents,
|
|
|
+ recursive: true
|
|
|
+ });
|
|
|
+
|
|
|
+ // 在Android上需要特殊处理才能显示在相册中
|
|
|
+ if (Capacitor.getPlatform() === 'android') {
|
|
|
+ await Filesystem.getUri({
|
|
|
+ directory: Directory.Documents,
|
|
|
+ path: fileName
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ this.presentAlert('保存成功', '你的声波艺术画已保存到相册');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存失败:', error);
|
|
|
+ this.presentAlert('保存失败', '无法保存图片,请重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async shareArtwork() {
|
|
|
+ if (!this.waveformImage) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ await Share.share({
|
|
|
+ title: '我的声波艺术画',
|
|
|
+ text: '看看我用情绪发泄室创作的声波艺术!',
|
|
|
+ url: this.waveformImage,
|
|
|
+ dialogTitle: '分享我的艺术创作'
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('分享失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async presentAlert(header: string, message: string) {
|
|
|
+ const alert = await this.alertController.create({
|
|
|
+ header,
|
|
|
+ message,
|
|
|
+ buttons: ['OK']
|
|
|
+ });
|
|
|
+
|
|
|
+ await alert.present();
|
|
|
+ }
|
|
|
+
|
|
|
+ ngOnDestroy() {
|
|
|
+ if (this.isRecording) {
|
|
|
+ this.audioService.stopRecording();
|
|
|
+ }
|
|
|
+ cancelAnimationFrame(this.animationId);
|
|
|
+ }
|
|
|
+}
|